[#154456146] Add willOpenFile event to CFM Client

Event sent before requesting document content.
Needed in CODAP to display splash screen at the
beginning of a wait, rather than in the middle.

--- src/code/client.coffee
+++ src/code/client.coffee
@@ -221,6 +221,7 @@

   openFile: (metadata, callback = null) ->
     if metadata?.provider?.can 'load', metadata
+      @_event 'willOpenFile', {op: "openFile"}
       metadata.provider.load metadata, (err, content) =>
         return @alert(err, => @ready()) if err
         # should wait to close current file until client signals open is complete
@@ -268,6 +269,7 @@
     reader.readAsText file

   openLocalFile: (file, callback=null) ->
+    @_event 'willOpenFile', {op: "openLocalFile"}
     @readLocalFile file, (data) =>
       content = cloudContentFactory.createEnvelopedCloudContent data.content
       metadata = new CloudMetadata
@@ -281,6 +283,7 @@
       @importData data, callback

   openSharedContent: (id) ->
+    @_event 'willOpenFile', {op: "openSharedContent"}
     @state.shareProvider?.loadSharedContent id, (err, content, metadata) =>
       return @alert(err, => @ready()) if err
       @_fileOpened content, metadata, {overwritable: false, openedContent: content.clone()}
@@ -298,6 +301,7 @@
     # trigger authorize() from confirmation dialog to avoid popup blockers
     @confirm tr("~CONFIRM.AUTHORIZE_OPEN"), =>
       provider.authorize =>
+        @_event 'willOpenFile', {op: "confirmAuthorizeAndOpen"}
         provider.openSaved providerParams, (err, content, metadata) =>
           return @alert(err) if err
           @_fileOpened content, metadata, {openedContent: content.clone()}, @_getHashParams metadata
@@ -309,6 +313,7 @@
       provider.authorized (authorized) =>
         # we can open the document without authorization in some cases
         if authorized or not provider.isAuthorizationRequired()
+          @_event 'willOpenFile', {op: "openProviderFile"}
           provider.openSaved providerParams, (err, content, metadata) =>
             return @alert(err, => @ready()) if err
             @_fileOpened content, metadata, {openedContent: content.clone()}, @_getHashParams metadata
@@ -320,6 +325,7 @@

   openUrlFile: (url) ->
     @urlProvider.openFileFromUrl url, (err, content, metadata) =>
+      @_event 'willOpenFile', {op: "openUrlFile"}
       return @alert(err, => @ready()) if err
       @_fileOpened content, metadata, {openedContent: content.clone()}, @_getHashParams metadata

@@ -433,6 +439,7 @@
       callback "Unable to temporarily save copied file"

   openCopiedFile: (copyParams) ->
+    @_event 'willOpenFile', {op: "openCopiedFile"}
     try
       key = "cfm-copy::#{copyParams}"
       copied = JSON.parse window.localStorage.getItem key
@@ -572,6 +579,7 @@
       @rename @state.metadata, newName, callback

   revertToLastOpened: (callback = null) ->
+    @_event 'willOpenFile', {op: "revertToLastOpened"}
     if @state.openedContent? and @state.metadata
       @_fileOpened @state.openedContent, @state.metadata, {openedContent: @state.openedContent.clone()}


Clampable#clampToBounds

Clampable#clampToBounds

--- src/clampable.coffee
+++ src/clampable.coffee
@@ -26,7 +26,7 @@
   ###*
   @name clampToBounds
   @methodOf Clampable#
-  @param {Rectangle} [bounds] The bounds to clamp the object's position within.
+  @param {Rectangle} [bounds] The bounds to clamp the object's position within. Defaults to the
   ###
   clampToBounds: (bounds) ->
     bounds ||= Rectangle x: 0, y: 0, width: App.width, height: App.height


Clampable#clampToBounds

Clampable#clampToBounds

--- src/clampable.coffee
+++ src/clampable.coffee
@@ -9,7 +9,8 @@
   clamp: (data) ->
     Object.extend(I.clampData, data)

-
+  ###*
+###
   clampToBounds: (bounds) ->
     bounds ||= Rectangle x: 0, y: 0, width: App.width, height: App.height


Clampable#clampToBounds

Clampable#clampToBounds

--- src/clampable.coffee
+++ src/clampable.coffee
@@ -1,5 +1,5 @@
 ###*
-The Clampable module provides a method to tween object properties.
+The Clampable module provides h to tween object properties.

 @name Clampable
 @module


follow the backend changes

follow the backend changes

--- app/scripts/routes/dashboard_route.coffee
+++ app/scripts/routes/dashboard_route.coffee
@@ -3,9 +3,9 @@
     type = @routeName.split('.')[1]

     if type == 'index'
-      $.getJSON('/api/redis')
+      $.getJSON('/api/info')
     else
-      $.getJSON('/api/redis/' + type)
+      $.getJSON('/api/info/' + type)

 WingTsun.DashboardIndexRoute       = WingTsun.DashboardBaseRoute.extend()
 WingTsun.DashboardServerRoute      = WingTsun.DashboardBaseRoute.extend()


formatting

formatting


corr_w_scatter: I had reversed the colors

--- corr_w_scatter/corr_w_scatter.coffee
+++ corr_w_scatter/corr_w_scatter.coffee
@@ -38,7 +38,7 @@

   corXscale = d3.scale.ordinal().domain(d3.range(nvar)).rangeBands([0, w])
   corYscale = d3.scale.ordinal().domain(d3.range(nvar)).rangeBands([h, 0])
-  corZscale = d3.scale.linear().domain([-1, 0, 1]).range(["crimson", "white", "darkslateblue"])
+  corZscale = d3.scale.linear().domain([-1, 0, 1]).range(["darkslateblue", "white", "crimson"])

   # create list with correlations
   corr = []


Set SVG attributes as objects.

Set SVG attributes as objects.

--- app/static/coffee/production-chart.coffee
+++ app/static/coffee/production-chart.coffee
@@ -37,68 +37,74 @@
   vis = d3.select("#paired-line-chart")
     .data([values])
     .append("svg:svg")
-      .attr("width", w + p * 2)
-      .attr("height", h + p * 2)
-      .attr("viewBox", "0 0 " + (w + p * 2) + " " + (h + p * 2))
-      .attr("preserveAspectRatio", "xMidYMid meet")
+      .attr
+        "width": w + p * 2
+        "height": h + p * 2
+        "viewBox": "0 0 " + (w + p * 2) + " " + (h + p * 2)
+        "preserveAspectRatio": "xMidYMid meet"

-  grid = vis.append("svg:g").attr('class', 'grid')
-  xaxis = grid.append("svg:g").attr('class', 'x-axis')
-  yaxis = grid.append("svg:g").attr('class', 'y-axis')
+  grid = vis.append("svg:g").attr 'class', 'grid'
+  xaxis = grid.append("svg:g").attr 'class', 'x-axis'
+  yaxis = grid.append("svg:g").attr 'class', 'y-axis'

   xaxis.append("svg:g").attr('class', 'lines')
     .selectAll('line')
     .data(x.ticks(15))
     .enter()
       .append("svg:line")
-        .attr("x1", x)
-        .attr("x2", x)
-        .attr("y1", 0)
-        .attr("y2", h)
+        .attr
+          "x1": x
+          "x2": x
+          "y1": 0
+          "y2": h

   yaxis.append("svg:g").attr('class', 'lines')
     .selectAll('line')
     .data(y.ticks(10))
     .enter()
       .append("svg:line")
-        .attr("x1", 0)
-        .attr("x2", w)
-        .attr("y1", y)
-        .attr("y2", y)
+        .attr
+          "x1": 0
+          "x2": w
+          "y1": y
+          "y2": y

   xaxis.append("svg:g").attr('class', 'labels')
     .selectAll('text')
     .data(x.ticks(15))
     .enter()
       .append("svg:text")
-        .attr("x", x)
-        .attr("y", h + 15)
-        .attr("dy", ".71em")
-        .attr("text-anchor", "middle")
         .text(x.tickFormat(10))
         .text(String)
+        .attr
+          "x": x
+          "y": h + 15
+          "dy": ".71em"
+          "text-anchor": "middle"

   yaxis.append("svg:g").attr('class', 'labels')
     .selectAll('text')
     .data(y.ticks(10))
     .enter()
       .append("svg:text")
-        .attr("x", "8em")
-        .attr("y", y)
-        .attr("dx", "-.35em")
-        .attr("dy", ".35em")
-        .attr("text-anchor", "end")
         .text(y.tickFormat(5))
+        .attr
+          "x": "8em"
+          "y": y
+          "dx": "-.35em"
+          "dy": ".35em"
+          "text-anchor": "end"

   seriesChart = (vis, column, color = '#000') ->
     vis.append("svg:path")
-      .attr("class", "line")
-      .attr("fill", "none")
-      .attr("stroke", color)
-      .attr("stroke-width", 2)
-      .attr("d", d3.svg.line()
-        .x((d) -> x d.x)
-        .y((d) -> y d[column]))
+      .attr
+        "class": "line"
+        "fill": "none"
+        "stroke": color
+        "stroke-width": 2
+        "d": d3.svg.line()
+          .x((d) -> x d.x)
+          .y((d) -> y d[column])

   seriesChart(vis, 'res', color4)  # Reserves
   seriesChart(vis, 'zlog', color3) # Log estimate
@@ -112,16 +118,18 @@
     y = index * lineHeight
     if color
       legend.append("svg:rect")
-        .attr("x", 0)
-        .attr("y", y)
-        .attr("fill", color)
-        .attr("stroke", color)
-        .attr("height", 2)
-        .attr("width", markerWidth)
+        .attr
+          "x": 0
+          "y": y
+          "fill": color
+          "stroke": color
+          "height": 2
+          "width": markerWidth
     legend.append("svg:text")
-      .attr("x", markerWidth + 10)
-      .attr("y", y + 5)
       .text(text)
+      .attr
+        "x": markerWidth + 10
+        "y": y + 5

   legendData = [
     ["World production", color2],


Removed extra click handler

Removed extra click handler

--- packages/emberella/lib/views/autocomplete.coffee
+++ packages/emberella/lib/views/autocomplete.coffee
@@ -112,9 +112,6 @@
     e.preventDefault() #Keeps focus intact

   mouseUp: (e) ->
-    @dispatch('selectMember', @, true)
-
-  click: (e) ->
     @dispatch('selectMember', @, true)

 ###############################################################################


Fixed the progression graph for users with no solves.

--- picoCTF-web/web/coffee/progression_graphs.coffee
+++ picoCTF-web/web/coffee/progression_graphs.coffee
@@ -4,7 +4,7 @@
   _.first($(selector))

 topTeamsGraphOptions = {
-  title: "Top Team Score Progression",
+  title: "Top Score Progression",
   legend: {
     position: "top"
   },
@@ -20,7 +20,7 @@
 }

 teamGraphOptions = {
-  title: "Team Score Progression",
+  title: "Score Progression",
   legend: {
     position: "none"
   },
@@ -129,9 +129,8 @@
   .done (data) ->
     apiCall "GET", "/api/time", {}
     .done (timedata) ->
-
       if data.status == 1
-          if data.data.length > 0
+          if data.data.length > 0 and data.data[0].length > 0

             graphData = [
               ["Time", "Score", {role: "tooltip"}]
@@ -145,6 +144,6 @@
             chart = new google.visualization.SteppedAreaChart(div)
             chart.draw(packagedData, teamGraphOptions)
           else
-            $(container_selector).hide()
+            $(selector).html("
You have not solved any enabled problems.
")
         else
           $(container_selector).hide()


Fix leak

Fix leak

--- admin/static/coffee/models.coffee
+++ admin/static/coffee/models.coffee
@@ -1,7 +1,8 @@
 #Models for Backbone.js
 class Namespace extends Backbone.Model
     initialize: ->
-        @load_key_distr()
+        # TODO: magic number + uniformize setInterval and setTImeout
+        setInterval @load_key_distr(), 5000

         # Add a computed shards property for convenience and metadata
         @.set 'computed_shards', new DataUtils.Shards [],@
@@ -22,8 +23,6 @@
                     distr_keys.push(key)
                 _.sortBy(distr_keys, _.identity)
                 @set('key_distr_sorted', distr_keys)
-                # TODO: magic number
-                window.setTimeout @load_key_distr, 5000
     sorted_key_distr_keys: =>
         keys = @get('key_distr_sorted')
         if keys?


javaScript, css and html must be enabled by default to pull from bower

--- generators/app/index.coffee
+++ generators/app/index.coffee
@@ -44,12 +44,8 @@
 		,
 			name    : 'scriptLanguages'
 			type    : 'checkbox'
-			message : 'What scripting languages would you like?'
+			message : 'What scripting languages in addition to JavaScript would you like?'
 			choices: [
-				value   : 'javaScript'
-				name    : 'JavaScript'
-				checked : true
-			,
 				value   : 'coffeeScript'
 				name    : 'CoffeeScript'
 				checked : true
@@ -69,11 +65,15 @@
 		,
 			name    : 'styleLanguages'
 			type    : 'checkbox'
-			message : 'What styling languages would you like?'
+			message : 'What styling languages in addition to CSS would you like?'
 			choices: [
+				value   : 'css'
+				name    : 'CSS'
+				checked : true
+			,
 				value   : 'less'
 				name    : 'Less'
-				checked : true
+				checked : false
 			,
 				value   : 'sass'
 				name    : 'Sass'
@@ -82,15 +82,19 @@
 		,
 			name    : 'templateLanguages'
 			type    : 'checkbox'
-			message : 'What templating languages would you like?'
+			message : 'What templating languages in addition to HTML would you like?'
 			choices: [
+				value   : 'html'
+				name    : 'HTML'
+				checked : true
+			,
 				value   : 'haml'
 				name    : 'Haml'
 				checked : false
 			,
 				value   : 'jade'
 				name    : 'Jade'
-				checked : true
+				checked : false
 			,
 				value   : 'makdown'
 				name    : 'Markdown'


Do not rely on properties order in scope tests

--- test/scope.coffee
+++ test/scope.coffee
@@ -35,7 +35,7 @@

 test "loop variable should be accessible after for-of loop", ->
   d = (x for x of {1:'a',2:'b'})
-  eq x, '2'
+  ok x in ['1','2']

 test "loop variable should be accessible after for-in loop", ->
   d = (x for x in [1,2])


Reorganize tests.

Reorganize tests.

--- test/sente.coffee
+++ test/sente.coffee
@@ -1,21 +1,20 @@
-should  = require('chai').should()
+should = require('chai').should()
+sente  = require '../'

-sente = require '../'
+class Renderable extends sente.Component
+  observes:
+    tick: ->
+
+class WatchA extends sente.Component
+  observes:
+    'change:a': (value) ->
+
+class Character extends sente.Entity
+  components: [Renderable, WatchA]

 describe 'Component', ->
   it 'should respond to events', ->
-    class Renderable extends sente.Component
-      observes:
-        tick: ->
-          # render self
-
   it 'should be able to observe changes in an entity', ->
-    class WatchA extends sente.Component
-      observes:
-        'change:a': (value) ->
-          # do somethign with a

 describe 'Entity', ->
   it 'should compose components', ->
-    class Character extends sente.Entity
-      components: [Renderable, WatchA]


Cosmetic

Cosmetic

--- tests/html/src/endtoendtests.coffee
+++ tests/html/src/endtoendtests.coffee
@@ -2,7 +2,7 @@

 factory_body_check = (protocol) ->
     if not SockJS[protocol] or not SockJS[protocol].enabled(client_opts.sockjs_opts)
-        n = " " + protocol + "[unsupported by client]"
+        n = " " + protocol + " [unsupported by client]"
         test n, ->
             log('Unsupported protocol (by client): "' + protocol + '"')
     else


Added check if slider exist and then run

--- app/assets/javascripts/dashboard.js.coffee
+++ app/assets/javascripts/dashboard.js.coffee
@@ -1,41 +1,42 @@
 $ ->
-  slider = document.getElementById('js-age-scroll')
-  noUiSlider.create slider,
-    start: [
-      13
-      19
-    ]
-    step: 1
-    connect: true
-    range:
-      'min': 13
-      'max': 19
-  # Binding change
-  slider.noUiSlider.on 'change', (values, handle, unencoded, tap, positions, noUiSlider) ->
+  if $('#js-age-scroll').length > 0
+    slider = document.getElementById('js-age-scroll')
+    noUiSlider.create slider,
+      start: [
+        13
+        19
+      ]
+      step: 1
+      connect: true
+      range:
+        'min': 13
+        'max': 19
+    # Binding change
+    slider.noUiSlider.on 'change', (values, handle, unencoded, tap, positions, noUiSlider) ->
+      lower = document.getElementById('js-lower-value')
+      higher = document.getElementById('js-higher-value')
+      lower.innerHTML = if values[0] > 18 then '18+' else Math.round(values[0]) + 'j.'
+      higher.innerHTML = if values[1] > 18 then '18+' else Math.round(values[1]) + 'j.'
+
+      window.seekersList.filter (item) ->
+        if values[1] < 19
+          values_1 = values[1]
+        else
+          values_1 = 100
+        if item.values().age >= Math.round(values[0]) and item.values().age <= Math.round(values_1)
+          true
+        else
+          false
+      # Only items with id > 1 are shown in list
+  #    listObj.filter()
+      return
+
+
+    values = slider.noUiSlider.get()
     lower = document.getElementById('js-lower-value')
     higher = document.getElementById('js-higher-value')
     lower.innerHTML = if values[0] > 18 then '18+' else Math.round(values[0]) + 'j.'
     higher.innerHTML = if values[1] > 18 then '18+' else Math.round(values[1]) + 'j.'
-
-    window.seekersList.filter (item) ->
-      if values[1] < 19
-        values_1 = values[1]
-      else
-        values_1 = 100
-      if item.values().age >= Math.round(values[0]) and item.values().age <= Math.round(values_1)
-        true
-      else
-        false
-    # Only items with id > 1 are shown in list
-#    listObj.filter()
-    return
-
-
-  values = slider.noUiSlider.get()
-  lower = document.getElementById('js-lower-value')
-  higher = document.getElementById('js-higher-value')
-  lower.innerHTML = if values[0] > 18 then '18+' else Math.round(values[0]) + 'j.'
-  higher.innerHTML = if values[1] > 18 then '18+' else Math.round(values[1]) + 'j.'

   $(document).on
     click: (e)->
@@ -46,7 +47,6 @@
   $(document).on
     click: (e)->
       $('.js-age-slider-container').addClass('display-none')
-      console.log('clear filter')
       window.seekersList.filter()
     '.js-seeker-delete-icon'


Remove trailing slashes

Remove trailing slashes

--- app/assets/javascripts/user_tabs.js.coffee
+++ app/assets/javascripts/user_tabs.js.coffee
@@ -122,7 +122,9 @@
   setCurrentAction: (action) ->
     # Remove possible actions from URL
     regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$')
-    new_state = @_location.pathname.replace(regExp, '')
+    new_state = @_location.pathname
+    new_state = new_state.replace(/\/+$/, "") # remove trailing slashes
+    new_state = new_state.replace(regExp, '')

     # Append the new action if we're on a tab other than 'activity'
     unless action == @defaultAction


Re-enable submit for completed and failed jobs

--- app/assets/javascripts/workflows.js.coffee
+++ app/assets/javascripts/workflows.js.coffee
@@ -161,7 +161,7 @@
     $("#terminal_button").bind('click', false)

 @update_submit_button = (id, status_char) ->
-  if id? && !status_char?
+  if id? && status_char != "R" && status_char != "Q"
       $("#submit_button").attr("href", Routes.submit_workflow_path(id))
       $("#submit_button").data("method", "PUT")
       $("#submit_button").removeAttr("disabled")


The square-root 2 constant is not needed (yet)

--- src/constants.coffee
+++ src/constants.coffee
@@ -1,7 +1,7 @@
 # Program constants

 # Mathematical constants
-math_sqrt2 = Math.sqrt 2.0
+#math_sqrt2 = Math.sqrt 2.0

 # Application constants
 constants =


Fixed contracts errors with many user defined contracts not readable (cherry picked from commit 19af5fc)

--- src/contracts.coffee
+++ src/contracts.coffee
@@ -424,7 +424,7 @@
           propName + " : " + obj[propName].value?.cname
       , this

-      "{" + props.join(", ") + "}"
+      "{\n  " + props.join(",\n  ") + "\n}"
     else
       name


fix zh-tw translation

fix zh-tw translation

--- def/zh-tw/menu_linux.cson
+++ def/zh-tw/menu_linux.cson
@@ -274,12 +274,12 @@
   "&Packages":
     value: "擴充套件(&P)"
   "&Help":
-    value: "擴充套件(&H)"
+    value: "幫助(&H)"
     submenu:
       "View &Terms of Use":
-        value: "使用條款(&T)"
+        value: "檢視使用條款(&T)"
       "View &License":
-        value: "使用條款(&L)"
+        value: "檢視授權(&L)"
       "&Documentation":
         value: "說明文件(&D)"
       "Frequently Asked Questions":


Fix error in checkForCorruptedDocs

Fix error in checkForCorruptedDocs

--- assets/javascripts/app/db.coffee
+++ assets/javascripts/app/db.coffee
@@ -273,6 +273,10 @@
       for slug in @corruptedDocs
         $.arrayDelete(docs, slug)

+      if docs.length is 0
+        setTimeout(@deleteCorruptedDocs, 0)
+        return
+
       txn = @idbTransaction(db, stores: docs, mode: 'readonly', ignoreError: false)
       txn.oncomplete = =>
         setTimeout(@deleteCorruptedDocs, 0) if @corruptedDocs.length > 0


Prevent empty username suggestion

Prevent empty username suggestion

--- server/methods/getUsernameSuggestion.coffee
+++ server/methods/getUsernameSuggestion.coffee
@@ -18,22 +18,23 @@
 	if Meteor.settings.public.sandstorm
 		usernames.push user.services.sandstorm.preferredHandle

-	if RocketChat.settings.get 'UTF8_Names_Slugify'
-		usernames.push slug user.name
-	else
-		usernames.push user.name
+	if Match.test(user?.name, String)
+		if RocketChat.settings.get 'UTF8_Names_Slugify'
+			usernames.push slug user.name
+		else
+			usernames.push user.name

-	nameParts = user?.name?.split(' ')
-	if nameParts.length > 1
-		first = nameParts[0]
-		last = nameParts[nameParts.length - 1]
+		nameParts = user?.name?.split(' ')
+		if nameParts.length > 1
+			first = nameParts[0]
+			last = nameParts[nameParts.length - 1]

-		if RocketChat.settings.get 'UTF8_Names_Slugify'
-			usernames.push slug first[0] + last
-			usernames.push slug first + last[0]
-		else
-			usernames.push first[0] + last
-			usernames.push first + last[0]
+			if RocketChat.settings.get 'UTF8_Names_Slugify'
+				usernames.push slug first[0] + last
+				usernames.push slug first + last[0]
+			else
+				usernames.push first[0] + last
+				usernames.push first + last[0]

 	if user.profile?.name?
 		if RocketChat.settings.get 'UTF8_Names_Slugify'
@@ -42,17 +43,16 @@
 			usernames.push user.profile.name

 	if user.services?
-		for serviceName, service of user.services
-			if service.name?
-				if RocketChat.settings.get 'UTF8_Names_Slugify'
-					usernames.push slug service.name
-				else
-					usernames.push service.name
-			else if service.username?
-				if RocketChat.settings.get 'UTF8_Names_Slugify'
-					usernames.push slug service.username
-				else
-					usernames.push service.username
+		services = _.map user.services, (service) ->
+			return _.values(_.pick(service, 'name', 'username', 'firstName', 'lastName'))
+
+		services = _.uniq(_.flatten(services))
+
+		for service in services
+			if RocketChat.settings.get 'UTF8_Names_Slugify'
+				usernames.push slug service
+			else
+				usernames.push service

 	if user.emails?.length > 0
 		for email in user.emails when email.address? and email.verified is true
@@ -61,17 +61,21 @@
 		for email in user.emails when email.address? and email.verified is true
 			usernames.push slug email.address.replace(/(.+)@(\w+).+/, '$1.$2')

+	usernames = _.compact(usernames)
+
 	for item in usernames
 		if usernameIsAvaliable item
 			username = item
 			break

-	if usernames[0]? and usernames[0].length > 0
-		index = 0
-		while not username?
-			index++
-			if usernameIsAvaliable usernames[0] + '-' + index
-				username = usernames[0] + '-' + index
+	if usernames.length is 0 or usernames[0].length is 0
+		usernames.push('user')
+
+	index = 0
+	while not username?
+		index++
+		if usernameIsAvaliable usernames[0] + '-' + index
+			username = usernames[0] + '-' + index

 	if usernameIsAvaliable username
 		return username


allowing Embed to inherit from Line

allowing Embed to inherit from Line

--- src/core/line.coffee
+++ src/core/line.coffee
@@ -12,11 +12,13 @@
   @CLASS_NAME : 'ql-line'
   @ID_PREFIX  : 'ql-line-'

-  constructor: (@doc, @node) ->
-    @id = _.uniqueId(Line.ID_PREFIX)
+  constructor: (@doc, @node, base) ->
+    if(typeof base == 'undefined')
+      base = Line
+    @id = _.uniqueId(base.ID_PREFIX)
     @formats = {}
     dom(@node).removeClass('ql-embed')
-    dom(@node).addClass(Line.CLASS_NAME)
+    dom(@node).addClass(base.CLASS_NAME)
     this.rebuild()
     super(@node)


fix bug with pasting rich text into wysiwyg editor. fixes #62

Former-commit-id: 5556f20ac582cb8553d28223bc579aee1e156f76 [formerly 45f85936cfcb6208f406b21362ef082e7c0e73d4] [formerly 71cd472a3e975622b6a84fe1d58c99756180367b [formerly 173ffeab3749b7a6861aa73fa6be851f9bf2ead0]] [formerly 2b7cbb843ef02e0faa9161c70b00d65dc925bef2 [formerly b1bf2e613bf21da17ada7e8099161ea66e78dcd1] [formerly f380b2d5ca596d3e540b845ca0612273efce4fd0 [formerly f380b2d5ca596d3e540b845ca0612273efce4fd0 [formerly 5715f4aa82158e3a38bc26f1d5c98239a7894d56]]]]
Former-commit-id: 82281ed301e42b4805e26d4a8c16f6a7e7968b28 [formerly 491b77d0ac3e956a6141f21dd6613355235e1826] [formerly 7468acffc95cca7589ea301e173ac35ea4066eef [formerly 9fa316b9904db325cdd532696b61fabf22f289f2]]
Former-commit-id: 25e977bc064821d4cfd35dbe4633246e1186bff1 [formerly ed2b6fe89cc2f0b33f84eba24a4cb990c8e48118]
Former-commit-id: 6e9e7ac670f9d79332f400b6c3aec1fd67fccf1b
Former-commit-id: 2b69f9fcb93e02c97d65dc59fae9a97316b6f576

--- @client/form.coffee
+++ @client/form.coffee
@@ -98,10 +98,10 @@
         title: 'Numbered list'
       },{
         className: "ql-bold fa fa-bold",
-        title: 'Bold'
+        title: 'Bold selected text'
       },{
         className: "ql-link fa fa-link",
-        title: 'Link'
+        title: 'Hyperlink selected text'
       },
       # {
       #   className: "ql-image fa fa-image",
@@ -197,17 +197,16 @@

       if source == 'user' && my_data.html.indexOf(' style') > -1
         # strip out any style tags the user may have pasted into the html
+
         removeStyles = (el) ->
           el.removeAttribute 'style'
           if el.childNodes.length > 0
             for child in el.childNodes
               removeStyles child if child.nodeType == 1

-        node = $(my_data.html)[0]
+        node = @editor.root
         removeStyles node
-        @editor.setHTML $(node).html()
-        return # the above line will trigger this text-change event
-               # again, w/o the style html
+        my_data.html = @editor.getHTML()

       save my_data


force an opening proposal to move to the top

--- @client/item.coffee
+++ @client/item.coffee
@@ -846,7 +846,7 @@

   $$.ensureInView el,
     dom_possibly_shifting: true
-    extra_height: if !expanded_state[proposal.key] then 80 else 0
+    extra_height: if !expanded_state[proposal.key] then 4000 else 0
     force: mode == 'crafting'
     callback: =>
       loc = fetch 'location'


Fix display of removal status of invitees

--- assets/js/home/membership.coffee
+++ assets/js/home/membership.coffee
@@ -154,7 +154,7 @@
         role: @change_set.update[email]?.role or invitee.role
         role_changed: @change_set.update[email]?.role?
         new_invitee: false
-        removed: @change_set.remove[invitee.user.email]?
+        removed: @change_set.remove[email]?
       }))

     @$(".has-been-invited-header").toggle(@group.invited_members?.length > 0)


Render batches of buttons

Render batches of buttons

--- js-library/app/js/jail_iframe/classes/icon_buttons.coffee
+++ js-library/app/js/jail_iframe/classes/icon_buttons.coffee
@@ -141,14 +141,20 @@

     @_paragraphButtons.push new ParagraphButton el

+  _addParagraphButtonsBatch: (elements) ->
+    for el in elements[0...10]
+      @_addParagraphButton el
+
+    elementsLeft = elements[10..]
+    setTimeout (=> @_addParagraphButtonsBatch(elementsLeft)), 200
+
   addParagraphButtons: ->
     return unless FactlinkJailRoot.can_haz.paragraph_icons

     for paragraphButton in @_paragraphButtons
       paragraphButton.destroy()

-    for el in $('p, h2, h3, h4, h5, h6')
-      @_addParagraphButton el
+    @_addParagraphButtonsBatch $('p, h2, h3, h4, h5, h6')

 FactlinkJailRoot.loaded_promise.then ->
   paragraphButtons = new FactlinkJailRoot.ParagraphButtons


Use 755 instead of 777

Use 755 instead of 777

--- build/tasks/install-task.coffee
+++ build/tasks/install-task.coffee
@@ -35,4 +35,4 @@
       process.chdir(binDir)
       fs.symlinkSync(path.join('..', '..', 'share', 'atom', 'resources', 'app', 'apm', 'node_modules', '.bin', 'apm'), 'apm')

-      fs.chmodSync(path.join(shareDir, 'atom'), "777")
+      fs.chmodSync(path.join(shareDir, 'atom'), "755")


Improve exponential tests.

Improve exponential tests.

--- test/trier.coffee
+++ test/trier.coffee
@@ -174,24 +174,28 @@
         action = spooks.fn { name: 'action', log, callback: done }
         fail = spooks.fn { name: 'fail', log, callback: done }
         timestamps.push Date.now()
-        trier.when { predicate, action, fail, interval: -32, limit: 3 }
+        trier.when { predicate, action, fail, interval: -32, limit: 4 }

       teardown ->
         log = timestamps = predicate = action = fail = undefined

-      test 'four timestamps were recorded', ->
-        assert.lengthOf timestamps, 4
+      test 'five timestamps were recorded', ->
+        assert.lengthOf timestamps, 5

       test 'first interval is immediate', ->
         assert.isTrue timestamps[1] < timestamps[0] + 16

-      test 'second interval is about 100 ms', ->
+      test 'second interval is about 32 ms', ->
         assert.isTrue timestamps[2] > timestamps[1] + 16
         assert.isTrue timestamps[2] < timestamps[1] + 48

-      test 'third interval is about a second', ->
+      test 'third interval is about 64 ms', ->
         assert.isTrue timestamps[3] > timestamps[2] + 48
         assert.isTrue timestamps[3] < timestamps[2] + 80
+
+      test 'fourth interval is about 128 ms', ->
+        assert.isTrue timestamps[4] > timestamps[3] + 112
+        assert.isTrue timestamps[4] < timestamps[3] + 144

     suite 'until immediately:', ->
       log = predicate = action = fail = context = args = undefined
@@ -344,22 +348,26 @@
         action = spooks.fn { name: 'action', log }
         fail = spooks.fn { name: 'fail', log, callback: done }
         timestamps.push Date.now()
-        trier.until { predicate, action, fail, interval: -32, limit: 3 }
+        trier.until { predicate, action, fail, interval: -32, limit: 4 }

       teardown ->
         log = timestamps = predicate = action = fail = undefined

-      test 'four timestamps were recorded', ->
-        assert.lengthOf timestamps, 4
+      test 'five timestamps were recorded', ->
+        assert.lengthOf timestamps, 5

       test 'first interval is immediate', ->
         assert.isTrue timestamps[1] < timestamps[0] + 16

-      test 'second interval is about 100 ms', ->
+      test 'second interval is about 32 ms', ->
         assert.isTrue timestamps[2] > timestamps[1] + 16
         assert.isTrue timestamps[2] < timestamps[1] + 48

-      test 'third interval is about a second', ->
+      test 'third interval is about 64 ms', ->
         assert.isTrue timestamps[3] > timestamps[2] + 48
         assert.isTrue timestamps[3] < timestamps[2] + 80

+      test 'fourth interval is about 128 ms', ->
+        assert.isTrue timestamps[4] > timestamps[3] + 112
+        assert.isTrue timestamps[4] < timestamps[3] + 144
+


Custom CSS should reset when empty.

Custom CSS should reset when empty.

Fixes #2876.

--- pages/options.coffee
+++ pages/options.coffee
@@ -206,7 +206,7 @@
   grabBackFocus: CheckBoxOption
   searchEngines: TextOption
   searchUrl: NonEmptyTextOption
-  userDefinedLinkHintCss: TextOption
+  userDefinedLinkHintCss: NonEmptyTextOption

 initOptionsPage = ->
   onUpdated = ->


:triangular_ruler: adding support for holes in polygon triangulation

--- src/Canvas.coffee
+++ src/Canvas.coffee
@@ -64,20 +64,44 @@
     point = @_project x, y
     @buffer.setBackground point[0], point[1], color

-  # TODO: support for polygon holes
-  polygon: (points, color) ->
+  polygon: (polylines, color) ->
     vertices = []
-    lastPoint = [-1, -1]
-    for point in points
-      point = @_project point[0], point[1]
-      point[0] = utils.clamp point[0], 0, @width
-      point[1] = utils.clamp point[1], 0, @height
-
-      if point[0] isnt lastPoint[0] or point[1] isnt lastPoint[1]
-        vertices = vertices.concat point[0], point[1]
-        lastPoint = point
-
-    triangles = earcut vertices
+    holes = []
+
+    xs = {}
+    ys = {}
+
+    for points in polylines
+      if vertices.length
+        continue
+        holes.push vertices.length/2
+
+      lastPoint = [-1, -1]
+      for point in points
+        point = @_project point[0], point[1]
+        point[0] = utils.clamp point[0], 0, @width
+        point[1] = utils.clamp point[1], 0, @height
+
+        if point[0] isnt lastPoint[0] or point[1] isnt lastPoint[1]
+          vertices = vertices.concat point[0], point[1]
+
+          xs[point[0]] = ys[point[1]] = true
+          lastPoint = point
+
+      # Check if we actually got a valid polygon after projection and clamping
+      if Object.keys(xs).length is 1 or Object.keys(ys).length is 1
+        if vertices.length
+          # TODO: a line-hole - skip it for now
+          continue
+        else
+          # TODO: a line instead of a polygon - skip it for now
+          return false
+
+    try
+      triangles = earcut vertices, holes
+    catch e
+      return false
+
     extract = (pointId) ->
       [vertices[pointId*2], vertices[pointId*2+1]]


ENHANCE: Add analytics to student learning outcomes tab

--- src/app/projects/partials/directives/project-outcome-alignment.coffee
+++ src/app/projects/partials/directives/project-outcome-alignment.coffee
@@ -3,7 +3,7 @@
 .directive("projectOutcomeAlignment", ->
   restrict: 'E'
   templateUrl: 'projects/partials/templates/project-outcome-alignment.tpl.html'
-  controller: ($scope, $rootScope, $timeout, outcomeService, Unit, alertService, Visualisation) ->
+  controller: ($scope, $rootScope, $timeout, outcomeService, Unit, alertService, analyticsService, Visualisation) ->
     $scope.poaView = {
       activeTab: 'list'
     }
@@ -36,6 +36,8 @@
                 alertService.add("danger", "Failed to get unit progress class statistics", 6000)
               $scope.classStats = {}
       $scope.poaView.activeTab = tab
+      eventName = if tab is 'progress' then "View Learning Progress Tab" else "Reflect on Learning Tab"
+      analyticsService.event "Stuent Feedback View - Learning Outcomes Tab", "Switched Tab", eventName
       $scope.refreshCharts()

     # Default tab
@@ -46,4 +48,4 @@
       $rootScope.$broadcast('ProgressUpdated')
     )

-)+)


Amend: scrub secrets from logs

Amend: scrub secrets from logs

Calls to `getFile` can now include S3 credentials in `opts`, so sanitize before writing to opts to log.

--- app/coffee/FileHandler.coffee
+++ app/coffee/FileHandler.coffee
@@ -7,7 +7,7 @@
 async = require("async")
 ImageOptimiser = require("./ImageOptimiser")

-module.exports =
+module.exports = FileHandler =

 	insertFile: (bucket, key, stream, callback)->
 		convertedKey = KeyBuilder.getConvertedFolderKey key
@@ -23,7 +23,8 @@
 		], callback

 	getFile: (bucket, key, opts = {}, callback)->
-		logger.log bucket:bucket, key:key, opts:opts, "getting file"
+		# In this call, opts can contain credentials
+		logger.log bucket:bucket, key:key, opts:@_scrubSecrets(opts), "getting file"
 		if !opts.format? and !opts.style?
 			@_getStandardFile bucket, key, opts, callback
 		else
@@ -32,7 +33,7 @@
 	_getStandardFile: (bucket, key, opts, callback)->
 		PersistorManager.getFileStream bucket, key, opts, (err, fileStream)->
 			if err?
-				logger.err  bucket:bucket, key:key, opts:opts, "error getting fileStream"
+				logger.err  bucket:bucket, key:key, opts:FileHandler._scrubSecrets(opts), "error getting fileStream"
 			callback err, fileStream

 	_getConvertedFile: (bucket, key, opts, callback)->
@@ -71,7 +72,7 @@
 				return callback(err)
 			done = (err, destPath)->
 				if err?
-					logger.err err:err, bucket:bucket, originalKey:originalKey, opts:opts, "error converting file"
+					logger.err err:err, bucket:bucket, originalKey:originalKey, opts:FileHandler._scrubSecrets(opts), "error converting file"
 					return callback(err)
 				LocalFileWriter.deleteFile originalFsPath, ->
 				callback(err, destPath, originalFsPath)
@@ -98,3 +99,8 @@
 			if err?
 				logger.err  bucket:bucket, project_id:project_id, "error getting size"
 			callback err, size
+
+	_scrubSecrets: (opts)->
+		safe = Object.assign {}, opts
+		delete safe.credentials
+		safe


fix: clicking a module in a book in the picker opens the book in the sidebar instead of workspace

--- scripts/models/content/book.coffee
+++ scripts/models/content/book.coffee
@@ -255,6 +255,8 @@

     getRoot: () -> @
+    # Prevent the whole workspace from loading up in the book sidebar when a Module is clicked in the picker
+    getParent: () -> null

     newNode: (options) ->
       model = options.model


Using crazy new include lookup for non-string modules

--- src/core_object.coffee
+++ src/core_object.coffee
@@ -170,6 +170,8 @@

       send: (name, args...) ->
         self[name](args...)
+
+

     return self
 )()


End tutorial on sign in

End tutorial on sign in


Fixed an issue with the meme generator and question marks

--- scripts/memegen.coffee
+++ scripts/memegen.coffee
@@ -45,6 +45,7 @@
 _sanitize = (str) ->
   str = str.replace(/\s+/g, '-')
   str = str.replace(/"/g, '')
+  str = str.replace(/\?/g, '~q')
   str.toLowerCase()

 _queryApi = (msg, parts, handler) ->


changed action to value

changed action to value

--- bokehjs/src/coffee/widget/dropdown.coffee
+++ bokehjs/src/coffee/widget/dropdown.coffee
@@ -42,10 +42,10 @@

     for item in @mget("menu")
       $item = if item?
-        [label, action] = item
-        $a = $('').text(label).data('action', action)
+        [label, value] = item
+        $a = $('').text(label).data('value', value)
         that = this
-        $a.click((e) -> that.change_input($(this).data('action')))
+        $a.click((e) -> that.change_input($(this).data('value')))
         $('

').append($a)
       else
         $divider


Use jQuery.Deferred instead of Promise for IE compatibility

--- app/assets/javascripts/lib/models/chart_list.coffee
+++ app/assets/javascripts/lib/models/chart_list.coffee
@@ -183,21 +183,18 @@

     # The @chart_requests are jqXHR objects returned by $.ajax().
     # Here, we want to wait until all Ajax requests have returned, no matter
-    # their success. But Promise.all (and similar constructs like $.when)
-    # has a fast-failure feature if one of the requests fails.
-    # Hence, we have to wrap the jqXHRs in new Promises which will
+    # their success, but $.when has a fast-failure feature if one
+    # of the requests fails.
+    # Hence, we have to wrap the jqXHRs in new Deferreds which will
     # always return successfully - after all, we are not interested in the
     # success of the requests anyways (since success is handled directly in
     # callbacks to the Ajax calls).
-    Promise.all(@chart_requests.map((request) ->
-      # We pass the Promise's _resolve_ function to the outside so that
-      # we can call it in the request's _always_ callback and thereby resolve
-      # the Promise
-      p_resolve = null
-      promise = new Promise((resolve) -> p_resolve = resolve)
-      request.always -> p_resolve()
-      promise
-    )).then( =>
+    $.when.apply(null, @chart_requests.map((request) ->
+      deferred = $.Deferred()
+      request.always ->
+        deferred.resolve()
+      deferred
+    )).always( =>
       App.call_api()

       for holder_id, chart of @chart_holders


one one message in it's table

one one message in it's table

git-svn-id: 62c02ef84702a3047cfe9caf550d919eac72bfff@386 b2333513-9f29-43eb-bce0-aa7d4792c706

--- src/RowMap.coffee
+++ src/RowMap.coffee
@@ -186,6 +186,7 @@

     _processBlocks: ->
         select = @options.select
+
         for block in ['where', 'group', 'having', 'order', 'limit', 'offset']
             option = @options[block]

@@ -207,6 +208,9 @@
             for opt in option
                 _readFields.call @, opt, select, block

+        if @options.distinct
+            select.distinct()
+
         return

     _getSetColumn: (field)->


Avoid # link when navigating between Chapters

--- app/frontend/javascripts/views/story-info.js.coffee
+++ app/frontend/javascripts/views/story-info.js.coffee
@@ -83,6 +83,7 @@
     @$el.find('.chapters-list li:nth-child('+@index+') a.chapter').trigger 'click'

   onChapterNavigationChildrenClick: (e) ->
+    e.preventDefault()
     e.stopPropagation()
     $(this).parent().trigger 'click'


engine data module

engine data module

--- src/engine_data.coffee
+++ src/engine_data.coffee
@@ -1,7 +1,8 @@
 ###*
 The Data module provides methods to store global and persistent data in the engine.

-    engine.data
+    engine.data.score = 0
+

 @name Data
 @fieldOf Engine


UTC times look good. Bit more testing then will remove the WIP flag

--- scripts/standup.coffee
+++ scripts/standup.coffee
@@ -32,44 +32,29 @@
   # Compares current time to the time of the standup to see if it should be fired.
   standupShouldFire = (standup) ->
     standupTime = standup.time
-    standupDayOfWeek = standup.dayOfWeek
-    utc = standup.utc
-    now = new Date
-
-    currentHours = undefined
-    currentMinutes = undefined
-    currentWeekday = undefined
-    if utc
-      currentUTC = now.getUTC
-      currentHours = now.getUTCHours() + parseInt(utc, 10)
-      currentMinutes = now.getUTCMinutes()
-      if currentHours > 23
-        currentHours -= 23
-      currentWeekday = now.getUTCDay()
-    else
-      currentHours = now.getHours()
-      currentMinutes = now.getMinutes()
-      currentWeekday = now.getDay()
-
-    standupHours = standupTime.split(':')[0]
-    standupMinutes = standupTime.split(':')[1]
-    try
-      standupHours = parseInt(standupHours, 10)
-      standupMinutes = parseInt(standupMinutes, 10)
-      standupDayOfWeek = getDayOfWeek(standupDayOfWeek)
-    catch _error
-      return false
-
-    if standupHours == currentHours and standupMinutes == currentMinutes and (standupDayOfWeek == -1 or standupDayOfWeek == currentWeekday)
-      return true
-    false
+    standupDayOfWeek = getDayOfWeek(standup.dayOfWeek)
+    now = new Date()
+    standupDate = new Date()
+    utcOffset = -standup.utc or (now.getTimezoneOffset() / 60)
+
+    standupHours = parseInt(standupTime.split(":")[0], 10)
+    standupMinutes = parseInt(standupTime.split(":")[1], 10)
+
+    standupDate.setUTCMinutes(standupMinutes)
+    standupDate.setUTCHours(standupHours + utcOffset)
+
+    result = (standupDate.getUTCHours() == now.getUTCHours()) and
+      (standupDate.getUTCMinutes() == now.getUTCMinutes()) and
+      (standupDayOfWeek == -1 or (standupDayOfWeek == standupDate.getDay() == now.getUTCDay()))
+
+    if result then true else false

   # Returns the number of a day of the week from a supplied string. Will only attempt to match the first 3 characters
+  # Sat/Sun currently aren't supported by the cron but are included to ensure indexes are correct
   getDayOfWeek = (day) ->
     if (!day)
-      return -1;
-    days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
-    days.indexOf(day.toLowerCase().substring(0,3))
+      return -1
+    ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].indexOf(day.toLowerCase().substring(0,3))

   # Returns all standups.
   getStandups = ->
@@ -89,7 +74,7 @@
   doStandup = (room) ->
     standups = getStandupsForRoom(room)
     if standups.length > 0
-      #do some magic here to loop through the standups and find the one for right now
+      # Do some magic here to loop through the standups and find the one for right now
       theStandup = standups.filter(standupShouldFire)
       message = "#{PREPEND_MESSAGE} #{_.sample(STANDUP_MESSAGES)} #{theStandup[0].location}"
     else
@@ -115,8 +100,8 @@
       location: location.trim()
     standups.push newStandup
     updateBrain standups
-    displayDate = dayOfWeek || 'weekday'
-    msg.send 'Ok, from now on I\'ll remind this room to do a standup every ' + displayDate + ' at ' + time
+    displayDate = dayOfWeek or 'weekday'
+    msg.send 'Ok, from now on I\'ll remind this room to do a standup every ' + displayDate + ' at ' + time + (if location then location else '')
     return

   # Updates the brain's standup knowledge.
@@ -172,7 +157,7 @@
       standupsText = [ 'Here\'s your standups:' ].concat(_.map(standups, (standup) ->
         text =  'Time: ' + standup.time
         if standup.utc
-          text += ' UTC+' + standup.utc
+          text += ' UTC' + standup.utc
         if standup.location
           text +=', Location: '+ standup.location
         text
@@ -188,7 +173,7 @@
       standupsText = [ 'Here\'s the standups for every room:' ].concat(_.map(standups, (standup) ->
         text =  'Room: ' + standup.room + ', Time: ' + standup.time
         if standup.utc
-          text += ' UTC+' + standup.utc
+          text += ' UTC' + standup.utc
         if standup.location
           text +=', Location: '+ standup.location
         text
@@ -215,7 +200,7 @@
   new cronJob('1 * * * * 1-5', checkStandups, null, true)

   # Global regex should match all possible options
-  robot.respond /(.*)standups? ?(?:([A-z]*)\s?\@\s?)?((?:[01]?[0-9]|2[0-4]):[0-5]?[0-9])?(?: UTC\+(\d\d?))?(.*)/i, (msg) ->
+  robot.respond /(.*)standups? ?(?:([A-z]*)\s?\@\s?)?((?:[01]?[0-9]|2[0-4]):[0-5]?[0-9])?(?: UTC([- +]\d\d?))?(.*)/i, (msg) ->
     action = msg.match[1].trim().toLowerCase()
     dayOfWeek = msg.match[2]
     time = msg.match[3]


Collaboration: Don't resurrect snapshot if the session is just started.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -946,6 +946,8 @@

   resurrectSnapshot: ->

+    return  if @initializedCollaboration
+
     snapshot = @mySnapshot.values()

     if snapshot.length
@@ -1419,6 +1421,8 @@
         'system-message' : 'start'
         collaboration    : yes
     , callback
+
+    @initializedCollaboration = yes

     @rtm.once 'FileCreated', (file) =>
       @loadCollaborationFile file.id


minor: commenting

minor: commenting

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -541,7 +541,11 @@

       if rootPath and not isDefault
         path = rootPath
-
+
+    # options can be an Event instance if the initiator is
+    # a shortcut, and that can have a `path` property
+    # which is an Array. This check is to make sure that the
+    # `path` is always the one we send explicitly here - SY
     path = null  unless typeof path is 'string'

     @activeTabView.emit 'TerminalPaneRequested', { machine, path }


Expose built-in objects defined in ECMAScript5

--- src/aether.coffee
+++ src/aether.coffee
@@ -168,7 +168,30 @@
     # Return a ready-to-execute, instrumented function from the purified code
     # Because JS_WALA normalizes it to define a wrapper function on this, we need to run the wrapper to get our real function out.
     wrapper = new Function ['_aether'], @pure
-    dummyContext = {Math: Math}  # TODO: put real globals in
+    globals = [
+        # Other
+        'eval',
+
+        # Math related
+        'NaN', 'Infinity', 'undefined', 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
+
+        # URI related
+        'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent',
+
+        # Built-in objects
+        'Object', 'Function', 'Array', 'String', 'Boolean', 'Number', 'Date', 'RegExp', 'Math', 'JSON',
+
+        # Error Objects
+        'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError'
+    ]
+
+    dummyContext = {}
+
+    globalRef = global ? window
+
+    for name in globals
+      dummyContext[name] = globalRef[name]
+
     wrapper.call dummyContext, @
     dummyContext[@options.functionName or 'foo']


Remove panes from `AceApplicationTabView` instances quietly

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -221,7 +221,7 @@
       {tabView} = view

       for p in tabView.panes by -1
-        {pane} = tabView.removePane p, yes, (yes if pane instanceof AceApplicationTabView)
+        {pane} = tabView.removePane p, yes, (yes if tabView instanceof AceApplicationTabView)
         panes.push pane

       view.destroy()


Fix umlaut decoding in incoming messages

Incoming messages had umlauts encoded
as HTML entities. Added message decoding
before calling the message handler.

--- src/fleepClient.coffee
+++ src/fleepClient.coffee
@@ -190,9 +190,9 @@

     @robot.logger.info 'Got message: ' + JSON.stringify message

-    # Strip HTML tags
     if messageText isnt null and messageType is 'text'
-      messageText = S(message.message).stripTags().s
+      # Strip HTML tags and decode HTML entities
+      messageText = S(message.message).stripTags().decodeHTMLEntities().s

     # Mark message as read
     @markRead conversationId, messageNumber


Auto scroll to bottom if user send a new message

--- client/views/app/message.coffee
+++ client/views/app/message.coffee
@@ -63,6 +63,9 @@
 						urlNode?.innerHTML = Blaze.toHTMLWithData Template.iframelyBaseWidget, data

 		if not lastNode.nextElementSibling?
-			if view.parentView.parentView.parentView.parentView.parentView.templateInstance().atBottom isnt true
-				newMessage = document.querySelector(".new-message")
-				newMessage.className = "new-message"
+			if lastNode.classList.contains('own') is true
+				view.parentView.parentView.parentView.parentView.parentView.templateInstance().atBottom = true
+			else
+				if view.parentView.parentView.parentView.parentView.parentView.templateInstance().atBottom isnt true
+					newMessage = document.querySelector(".new-message")
+					newMessage.className = "new-message"


include Cooldown as default module

include Cooldown as default module

--- src/game_object.coffee
+++ src/game_object.coffee
@@ -161,7 +161,7 @@
       I.active = false
   }

-  defaultModules = [Bindable, Bounded, Cooldown, Drawable, Durable]
+  defaultModules = [Bindable, Bounded, , Drawable, Durable]
   modules = defaultModules.concat(I.includedModules.invoke('constantize'))
   modules = modules.without(I.excludedModules.invoke('constantize'))


Auth: integrate artifact server into auth worker

--- workers/auth/lib/auth/main.coffee
+++ workers/auth/lib/auth/main.coffee
@@ -18,4 +18,8 @@
   authExchange    : KONFIG.authWorker.authExchange
   authAllExchange : KONFIG.authWorker.authAllExchange
 }
+
+# expose healthcheck and version handlers
+require '../../../artifact'
+
 authWorker.connect()


Ignored echoes that begin with underscore.

--- server/manifest.coffee
+++ server/manifest.coffee
@@ -31,7 +31,7 @@
     echoes = []
     for echoType in echoTypes
       for echo in fs.readdirSync("echoes/#{echoType}")
-        continue if /^\./.test echo
+        continue if /^(\.|_)/.test echo
         echoes.push "#{echoType}/#{echo}"

     callback null, echoes


Add appendIds and fix prependIds Collection methods

--- src/marbles/collection.js.coffee
+++ src/marbles/collection.js.coffee
@@ -137,6 +137,9 @@

     models

+  appendIds: (model_cids...) =>
+    @model_ids = @model_ids.concat(model_cids)
+
   prependJSON: (json, options = {}) =>
     return [] unless json?.length

@@ -162,7 +165,7 @@
     models

   prependIds: (model_cids...) =>
-    @model_cids = model_cids.concat(@model_cids)
+    @model_ids = model_cids.concat(@model_ids)

   first: =>
     return unless cid = @model_ids[0]


commit in cosmo.js

commit in cosmo.js

--- cosmo.coffee
+++ cosmo.coffee
@@ -1,6 +1,7 @@
 window.Cosmo =
   version: '0.1.0'

+# FOOBAR
 # All apps should be an extended instance of this class
 class Cosmo.Router
   # app regions, assumes main content goes in a div#container


fixed allocate

fixed allocate

--- server/src/Game.coffee
+++ server/src/Game.coffee
@@ -4,7 +4,7 @@
 {ExpressionParser, Node} = require './Parser.js'
 {Player} = require './Player.js'
 {Evaluator} = require './Evaluator.js'
-DEBUG = true
+DEBUG = false

 class Game
@@ -152,6 +152,8 @@
       @globalDice = [1, DICEFACESYMBOLS.plus, 2, DICEFACESYMBOLS.minus, 3, 4, 5, 6, 7, 8, 9, 0, DICEFACESYMBOLS.divide, 9, 9, 1, 2, 9, 4, 5, DICEFACESYMBOLS.minus, DICEFACESYMBOLS.plus, 2, 4]
     else
       ops = 0
+      @globalDice = []  #clear the array first
+      #console.log(@globalDice)
       for x in [1..24]  #24 dice rolls
         rand = Math.random()  #get a random number
         if rand < 2/3  #first we decide if the roll yields an operator or a digit
@@ -161,6 +163,7 @@
           ops++ #we keep track of the number of operators generated so that later we can check if there are enough
         @globalDice.push(rand)  #here we add the die to the global dice array
       if (ops < 2) || (ops > 21)  #if there are too few or too many operators, we must roll again
+        #@globalDice = [] #clear the array first
         @allocate()  #do the allocation again


Bug fix: VDIs should be on chosen SRs.

Bug fix: VDIs should be on chosen SRs.

--- src/api/vm.coffee
+++ src/api/vm.coffee
@@ -226,8 +226,8 @@

   yield @getXapi(vm).migrateVm(vm._xapiId, @getXapi(host), host._xapiId, {
     migrationNetworkId: migrationNetwork?._xapiId
-    mapVifsNetworksXapi,
-    mapVdisSrsXapi,
+    mapVifsNetworks: mapVifsNetworksXapi,
+    mapVdisSrs: mapVdisSrsXapi,
   })
   return


Subscribe only to IP events, fixes #480

Subscribe only to IP events, fixes #480

--- src/lib/Network.coffee
+++ src/lib/Network.coffee
@@ -108,6 +108,19 @@
       for ev in @eventBuffer
         @emit ev.type, ev.payload
       @eventBuffer = []
+
+    if event is 'ip'
+      # Emit also the legacy events from IP
+      switch payload.type
+        when 'openBracket'
+          @bufferedEmit 'begingroup', payload
+          return
+        when 'closeBracket'
+          @bufferedEmit 'endgroup', payload
+          return
+        when 'data'
+          @bufferedEmit 'data', payload
+          return

   # ## Loading components
   #
@@ -354,11 +367,6 @@
         data.subgraph = [node.id]
       @bufferedEmit type, data

-    node.component.network.on 'connect', (data) -> emitSub 'connect', data
-    node.component.network.on 'begingroup', (data) -> emitSub 'begingroup', data
-    node.component.network.on 'data', (data) -> emitSub 'data', data
-    node.component.network.on 'endgroup', (data) -> emitSub 'endgroup', data
-    node.component.network.on 'disconnect', (data) -> emitSub 'disconnect', data
     node.component.network.on 'ip', (data) -> emitSub 'ip', data
     node.component.network.on 'process-error', (data) ->
       emitSub 'process-error', data
@@ -372,50 +380,22 @@
         socket: socket
         data: ip.data
         metadata: socket.metadata
-    socket.on 'connect', =>
-      if source and source.component.isLegacy()
-        # Handle activation for legacy components
-        source.component.__openConnections = 0 unless source.component.__openConnections
-        source.component.__openConnections++
-      @bufferedEmit 'connect',
-        id: socket.getId()
-        socket: socket
-        metadata: socket.metadata
-    socket.on 'begingroup', (group) =>
-      @bufferedEmit 'begingroup',
-        id: socket.getId()
-        socket: socket
-        group: group
-        metadata: socket.metadata
-    socket.on 'data', (data) =>
-      @bufferedEmit 'data',
-        id: socket.getId()
-        socket: socket
-        data: data
-        metadata: socket.metadata
-    socket.on 'endgroup', (group) =>
-      @bufferedEmit 'endgroup',
-        id: socket.getId()
-        socket: socket
-        group: group
-        metadata: socket.metadata
-    socket.on 'disconnect', =>
-      @bufferedEmit 'disconnect',
-        id: socket.getId()
-        socket: socket
-        metadata: socket.metadata
-      if source and source.component.isLegacy()
-        # Handle deactivation for legacy components
-        source.component.__openConnections--
-        if source.component.__openConnections < 0
-          source.component.__openConnections = 0
-        if source.component.__openConnections is 0
-          @checkIfFinished()
     socket.on 'error', (event) =>
       if @listeners('process-error').length is 0
         throw event.error if event.id and event.metadata and event.error
         throw event
       @bufferedEmit 'process-error', event
+    return unless source?.component?.isLegacy()
+    # Handle activation for legacy components via connects/disconnects
+    socket.on 'connect', =>
+      source.component.__openConnections = 0 unless source.component.__openConnections
+      source.component.__openConnections++
+    socket.on 'disconnect', =>
+      source.component.__openConnections--
+      if source.component.__openConnections < 0
+        source.component.__openConnections = 0
+      if source.component.__openConnections is 0
+        @checkIfFinished()

   subscribeNode: (node) ->
     node.component.on 'deactivate', (load) =>


Inlined templates instead of requesting them from core

--- app/js/views/templates.coffee
+++ app/js/views/templates.coffee
@@ -3,28 +3,27 @@
 Factlink.templates = {}

 Factlink.templates.getTemplate = (str, callback = ->) ->
-  if Factlink.tmpl[str]?
-    callback Factlink.tmpl[str]
-  else
-    Factlink.el.bind "factlink.tmpl.#{str}", ->
-      callback Factlink.tmpl[str]
+  if str == "create"
+    callback _.template """
+

+
+          Add Factlink
+          Added
+

-    fetchTemplate str unless requesting[str]
+        Loading...
+

+    """
+  else if str == "indicator"
+    callback _.template """
+

+        Show Factlink
+        Loading...
+

+    """
+  else alert "meh"

 Factlink.templates.preload = ->
   Factlink.templates.getTemplate 'indicator'
   Factlink.templates.getTemplate 'create', (template) ->
     Factlink.prepare = new Factlink.Prepare(template)
-
-fetchTemplate = (str) ->
-  requesting[str] = true
-
-  $.ajax
-    url: "#{FactlinkConfig.api}/templates/#{str}"
-    dataType: 'jsonp'
-    crossDomain: true
-    type: 'GET'
-    jsonp: 'callback'
-    success: (data) ->
-      Factlink.tmpl[str] = _.template(data)
-      Factlink.el.trigger "factlink.tmpl.#{str}"


migratePool fallback for dragNdrop migration

--- app/modules/tree/index.coffee
+++ app/modules/tree/index.coffee
@@ -294,7 +294,21 @@
               title: 'VM Migration'
               message: 'Starting your VM migration'
             })
-            xo.vm.migrate(vm, targetHost)
+            (xo.vm.migrate vm, targetHost).catch (error) ->
+              modal.confirm
+                title: 'VM migrate'
+                message: 'This VM can\'t be migrated with Xen Motion to this host because they don\'t share any storage. Do you want to try a Xen Storage Motion?'
+
+              .then ->
+                notify.info {
+                  title: 'VM migration'
+                  message: 'The migration process started'
+                }
+
+                xo.vm.migratePool {
+                  id: vm
+                  target_host_id: targetHost
+                }
       restrict: 'A'
     }
   # A module exports its name.


fix review confirmation

fix review confirmation

--- src/scripts/fgb.coffee
+++ src/scripts/fgb.coffee
@@ -70,6 +70,9 @@
   sendToChat robot, { room, message }
   if resolved
     user = space.getUser username
+    unless user?
+      robot.logger.warning 'hubot-fgb: unknown backlog user: ' + username
+      return
     requests.add room, user, issueKey
     message = "#{issueKey} を誰かにレビュー依頼しないの？"
     sendToChat robot, { room, user, message }


remove mutate from artwork url

remove mutate from artwork url

--- client/apps/edit/components/content/sections/image_collection/components/url_artwork_input.coffee
+++ client/apps/edit/components/content/sections/image_collection/components/url_artwork_input.coffee
@@ -6,7 +6,7 @@

 React = require 'react'
 Artwork = require '../../../../../../../models/artwork.coffee'
-_ = require 'underscore'
+_ = require 'lodash'
 { div, input, button } = React.DOM

 module.exports = React.createClass
@@ -31,7 +31,8 @@
               error: !@state.error
           , 3000)
       success: (artwork) =>
-        newImages = @props.images.concat [artwork.denormalized()]
+        newImages = _.clone(@props.images)
+        newImages.concat [artwork.denormalized()]
         @props.addArtworkFromUrl(newImages)
         @setState isLoading: !@state.isLoading


Make all back buttons go to last page in history.

--- js/flight.coffee
+++ js/flight.coffee
@@ -39,26 +39,29 @@
     if @hideUrlBar is true
       @hideUrlBar()

-  goToPage: (targetPanel,options) ->
-    @targetPanel = targetPanel
-
-    if @pageHistory.length > 1 and @pageHistory[@pageHistory.length - 2] == @targetPanel
-      @pageHistory.pop()
-      @back = true
-    else
-      @pageHistory.push(@targetPanel)
+  # Goes to page with or without transtion
+  goToPage: (options) =>

     if options?
-      if options.back
-        @back = options.back
-
-    @targetPanel = document.querySelector(targetPanel)
-    transtionType = @currentPanel.getAttribute("data-transition")
+      if options.targetPanel?
+        @targetPanel = options.targetPanel
+        @pageHistory.push(@targetPanel)
+        @targetPanel = document.querySelector(options.targetPanel)
+        transitionType = @targetPanel.getAttribute("data-transition")
+      else
+        if options.back
+          @back = options.back
+          if @back is true
+            if @pageHistory.length > 1
+              @targetPanel = @pageHistory[@pageHistory.length - 2]
+              @targetPanel = document.querySelector(@targetPanel)
+              transitionType = @targetPanel.getAttribute("data-transition")
+              @pageHistory.pop()

     @currentPanel = document.getElementsByClassName('visible')[0]

     if @transitionAnimation is true
-      switch transtionType
+      switch transitionType
         when "slide" then @slideTransition()
         when "slideUp" then @slideUp()
         when "slideDown" then @slideDown()
@@ -188,10 +191,13 @@
     window.flight = new Flight();

     $('.back').on 'click', =>
-        flight.goToPage('#panel-1')
+        flight.goToPage({back:true})

     $('.forward').on 'click', =>
-      flight.goToPage('#panel-2');
+      flight.goToPage({targetPanel:'#panel-2'});
+
+    $('#go-page-3').on 'click', =>
+      flight.goToPage({targetPanel:'#panel-3'});

 #if 'ontouchstart' in window
 #  alert 'ontouchstart'


Add no transition display page function.

--- js/flight.coffee
+++ js/flight.coffee
@@ -57,13 +57,15 @@

     @currentPanel = document.getElementsByClassName('visible')[0]

+    if @transitionAnimation is true
+      switch transtionType
+        when "slide" then @slideTransition()
+        when "slideUp" then @slideUp()
+        when "slideDown" then @slideDown()
+
     if @transitionAnimation isnt true
-      @speed = "0s"
-
-    switch transtionType
-      when "slide" then @slideTransition()
-      when "slideUp" then @slideUp()
-      when "slideDown" then @slideDown()
+      @displayPage()
+

   slideTransition: (speed) ->
     unless @targetPanel
@@ -112,6 +114,16 @@
     @back = false
     @isTransitioning = false
     @currentPanel.removeEventListener("webkitTransitionEnd", @finishTransition, false);
+
+   # displays pages when transiton is false
+   displayPage: =>
+    @targetPanel.style.display = "block"
+    @currentPanel.style.display = "block"
+    @targetPanel.style.left = "0%"
+    @currentPanel.style.left = "100%"
+
+    @removeClass(@currentPanel, 'visible')
+    @addClass(@targetPanel, 'visible')

   fitHeightToContent: ->
     flightViewport = document.getElementById('flight')


chameleon--->New---->Project... 解决快速点击Next时，会出现两个选择文件对话框

--- lib/atom-chameleon.coffee
+++ lib/atom-chameleon.coffee
@@ -119,23 +119,26 @@
     pv = new ProgressView("Create Project...")

     Q.Promise (resolve, reject, notify) =>
+      isOpenDialog = false
       projectWizardView.on 'finish', (result) ->
+        if isOpenDialog
+          return
+        isOpenDialog = true
         # composite promise combine result with previous result
         openDirectory(title: 'Select Path')
         .then (destPath) ->
           resolve _.extend(result, path: destPath[0])
+        .finally ->
+          isOpenDialog = false

     # do UI stuffs
     .then (options)->
-      console.log "foreveross:#{options}"
       projectWizardView.destroy()
       pv.attach()
-
       Q(options)

     # create project with options
     .then (options) ->
-
       switch options.template
         when 'simple' then options.repo = "https://github.com/yezhiming/butterfly-starter.git"
         when 'modular' then options.repo = "https://git.oschina.net/cwlay/ModuleManager.git"


remove no-longer-valid TODO

remove no-longer-valid TODO

--- lib/atom-io-client.coffee
+++ lib/atom-io-client.coffee
@@ -107,7 +107,6 @@
       for imagePath in files
         filename = path.basename(imagePath)
         [..., createdOn] = filename.split('-')
-        # TODO don't check expiry if network connection is not avail
         if Date.now() - parseInt(createdOn) < @expiry
           return callback(null, imagePath)
           break


refactored Mincha + Arbit on 2nd day of Rosh hashana -- no functional changes

--- js/mincha.coffee
+++ js/mincha.coffee
@@ -55,8 +55,11 @@
       else
         mincha: round_down_to_5_minutes(sunset.subtract(85, 'minutes'))
     else
-      mincha: round_down_to_5_minutes(sunset.subtract(55, 'minutes')),
-      arbit: (if hebrew_date.isErebShabbat() then null else (show_mosaei_yom_tob(sunset, zmanim); round_down_to_5_minutes(moment(zmanim.set_hakochabim).subtract(10, 'minutes'))))
+      if hebrew_date.isErebShabbat()
+        mincha: round_down_to_5_minutes(sunset.subtract(55, 'minutes')), arbit: null
+      else
+        show_mosaei_yom_tob(sunset, zmanim)
+        mincha: round_down_to_5_minutes(sunset.subtract(55, 'minutes')), arbit: round_down_to_5_minutes(moment(zmanim.set_hakochabim).subtract(10, 'minutes'))
   else if hebrew_date.is9Ab()
     mincha: moment(sunset).subtract(45, 'minutes'), arbit: sunset
   else if hebrew_date.isTaanitEster()


fix dsl exports

fix dsl exports

--- app/dsl.coffee
+++ app/dsl.coffee
@@ -113,4 +113,4 @@
           Components.ExampleComponent value: "#{f} #{object.to_js_string()}", run: true
     true

-module.exports = dsl
+_.extend exports, dsl


KeyValueTable bugfix

KeyValueTable bugfix

--- components/KeyValueTable.coffee
+++ components/KeyValueTable.coffee
@@ -10,7 +10,7 @@

     render: ->
-        keys = _.keys @prop.obj()
+        keys = @prop.obj().getKeys()
         if @prop.sortKeys() then keys.sort J.util.compare

         $$ ('table'),
@@ -21,7 +21,7 @@
                 {}

             for key in keys
-                value = @prop.obj()[key]
+                value = @prop.obj().get(key)

                 $$ ('tr'),
                     key: key
@@ -41,7 +41,7 @@
                         if React.isValidElement value
                             value

-                        else if J.util.isPlainObject value
+                        else if value instanceof J.Dict
                             $$ ('KeyValueTable'),
                                 obj: value


remove commas

remove commas

Signed-off-by: Kalman Olah <9c6f1d21f830c70c1a1dae4ae81fa22e2b8bc9d4@multatronic.com>

--- src/server/game.coffee
+++ src/server/game.coffee
@@ -135,12 +135,12 @@
     ###
     ID.
     ###
-    id: null,
+    id: null

     ###
     Name.
     ###
-    name: null,
+    name: null

     ###
     Constructor.


fixed bug for #67

fixed bug for #67

--- src/lib/options.coffee
+++ src/lib/options.coffee
@@ -350,12 +350,17 @@
       fs.root.getFile 'export.json', create: yes, (fileEntry) ->
         fileEntry.createWriter (fileWriter) ->
           builder = new WebKitBlobBuilder()
+          done    = no
+          builder.append str
           fileWriter.onerror = (error) ->
             log.error error
           fileWriter.onwriteend = ->
-            window.location.href = fileEntry.toURL()
-          builder.append str
-          fileWriter.write builder.getBlob 'application/json'
+            if done
+              window.location.href = fileEntry.toURL()
+            else
+              done = yes
+              fileWriter.write builder.getBlob 'application/json'
+          fileWriter.truncate 0
   # Select all of the templates in the list.
   $('.export_select_all_btn').on 'click', ->
     $('.export_con_list option').attr('selected', 'selected').parent().focus()


Server system working

Server system working

--- server/serversystem/manager.coffee
+++ server/serversystem/manager.coffee
@@ -29,13 +29,13 @@
   srvr.on('auth', ->
     connecting = false
     for plyr in lobby.radiant
-      cmd = "add_radiant_player "+plyr.steam+" "+plyr.name
+      cmd = "add_radiant_player "+plyr.steam+" \""+plyr.name+"\""
       srvr.send cmd
-      console.log "  - "+cmd
+      console.log cmd
     for plyr in lobby.dire
-      cmd = "add_dire_player "+plyr.steam+" "+plyr.name
+      cmd = "add_dire_player "+plyr.steam+" \""+plyr.name+"\""
       srvr.send cmd
-      console.log "  - "+cmd
+      console.log cmd
     console.log "server configured"
     new Fiber(->
       finalizeInstance(serverObj, lobby, instance)


:racehorse: Enumerate the smallest set when subtracting

--- src/set-helpers.coffee
+++ src/set-helpers.coffee
@@ -6,7 +6,10 @@
   true

 subtractSet = (set, valuesToRemove) ->
-  valuesToRemove.forEach (value) -> set.delete(value)
+  if set.size > valuesToRemove.size
+    valuesToRemove.forEach (value) -> set.delete(value)
+  else
+    set.forEach (value) -> set.delete(value) if valuesToRemove.has(value)

 addSet = (set, valuesToAdd) ->
   valuesToAdd.forEach (value) -> set.add(value)


servers: Allow cors in non-prod environments for email validate

--- servers/lib/server/handlers/validateemail.coffee
+++ servers/lib/server/handlers/validateemail.coffee
@@ -7,6 +7,9 @@

 generateCheckEmailCallback = (res, email, JUser) ->
+
+  unless KONFIG.environment is 'production'
+    res.header 'Access-Control-Allow-Origin', 'http://www.koding.com'

   return (err, info) ->
     { isValid : isEmail } = JUser.validateAt 'email', email, yes


Fix smoke test when product name changes

--- services/web/test/smoke/coffee/SmokeTests.coffee
+++ services/web/test/smoke/coffee/SmokeTests.coffee
@@ -94,7 +94,7 @@

 			expect(error, "smoke test: error returned in getting project list").to.not.exist
 			expect(!!stderr.match("200 OK"), "smoke test: response code is not 200 getting project list").to.equal true
-			expect(!!stdout.match("<title>Your Projects - ShareLaTeX, Online LaTeX Editor</title>"), "smoke test: body does not have correct title").to.equal true
+			expect(!!stdout.match("<title>Your Projects - .*, Online LaTeX Editor</title>"), "smoke test: body does not have correct title").to.equal true
 			expect(!!stdout.match("ProjectPageController"), "smoke test: body does not have correct angular controller").to.equal true
 			done()


Let mocha handle exceptions during tests.

--- app/express.coffee
+++ app/express.coffee
@@ -118,7 +118,7 @@
   date = new Date()
   timestamp = '[' + date.toISOString().slice(0, -5) + ']'
   console.error timestamp, "Uncaught exception: " + err
-  process.exit()
+  process.exit() unless config.appMode is 'testing'

 version = pkg.version
 mode    = config.appMode


workers: downcase email before saving to jVerificationToken

--- workers/social/lib/social/models/verificationtoken.coffee
+++ workers/social/lib/social/models/verificationtoken.coffee
@@ -26,6 +26,7 @@
       email       :
         type      : String
         email     : yes
+        set       : (value) -> value.toLowerCase()
       pin         : String
       createdAt   :
         type      : Date


social: group information is no longer needed to fetch bot channel

--- client/app/lib/socialapicontroller.coffee
+++ client/app/lib/socialapicontroller.coffee
@@ -205,7 +205,7 @@

   mapChannel = (channel) ->
     { socialapi } = kd.singletons
-
+
     data  = channel.channel

     # hold state of cache hit
@@ -213,7 +213,7 @@

     # item is initially our data
     item = data
-
+
     # if we find the channel in cache, replace item with it
     if cacheItem = socialapi.retrieveCachedItem item.typeConstant, item.id
       cacheFound = yes
@@ -221,7 +221,7 @@

     item._id                 = data.id
     item.isParticipant       = channel.isParticipant
-    # we only allow name, purpose and payload to be updated
+    # we only allow name, purpose and payload to be updated
     item.payload             = data.payload
     item.name                = data.name
     item.purpose             = data.purpose
@@ -229,10 +229,10 @@
     item.participantsPreview = mapAccounts channel.participantsPreview
     item.unreadCount         = channel.unreadCount
     item.lastMessage         = mapActivity channel.lastMessage  if channel.lastMessage
-
-    unless cacheFound
+
+    unless cacheFound
       channelInstance = new remote.api.SocialChannel item
-    else
+    else
       channelInstance = item

     kd.singletons.socialapi.cacheItem channelInstance
@@ -743,21 +743,19 @@
       doXhrRequest {type: 'POST', endPoint, async: yes}, callback

     fetchBotChannel      : (callback) ->
-      getCurrentGroup (group) ->
-
-        { nickname } = whoami().profile
-        doXhrRequest {
-          type     : 'GET'
-          endPoint : "/api/integration/botchannel"
-        }, (err, response) ->
+
+      doXhrRequest {
+        type     : 'GET'
+        endPoint : "/api/integration/botchannel"
+      }, (err, response) ->
+        return callback err  if err
+
+        { channelId } = response.data
+
+        { socialapi } = kd.singletons
+
+        socialapi.channel.byId {id: channelId}, (err, channel) ->
+
           return callback err  if err

-          {channelId} = response
-
-          { socialapi } = kd.singletons
-
-          socialapi.channel.byId {id: channelId}, (err, channel) ->
-
-            return callback err  if err
-
-            return callback null, mapChannel { channel: channel }
+          return callback null, mapChannel { channel: channel }


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/text_screen.coffee
+++ src/text_screen.coffee
@@ -25,7 +25,7 @@

     @name centerText
     @methodOf TextScreen#
-    @param {GameObject} canvas The object you want to follow
+    @param {Pixie} canvas The object you want to follow
     ###
     centerText: (canvas, text, options={}) ->
       font = options.font || I.font


Bug fix. Assigned users on userstory creation lightbox

--- app/modules/components/assigned/assigned-users-inline.directive.coffee
+++ app/modules/components/assigned/assigned-users-inline.directive.coffee
@@ -67,7 +67,11 @@
                 currentAssignedTo = null
             else if currentAssignedIds.indexOf(currentAssignedTo) == -1 || !currentAssignedTo
                 currentAssignedTo = currentAssignedIds[0]
-            $model.$modelValue.setAttr('assigned_users', currentAssignedIds)
+
+            if (!$model.$modelValue.assigned_users)
+                $model.$modelValue.assigned_users = currentAssignedIds
+            else
+                $model.$modelValue.setAttr('assigned_users', currentAssignedIds)
             $model.$modelValue.assigned_to = currentAssignedTo

         $el.on "click", ".users-dropdown", (event) ->


Removed old fact tabs

Removed old fact tabs

--- core/app/backbone/views/facts/fact_tabs_view.coffee
+++ core/app/backbone/views/facts/fact_tabs_view.coffee
@@ -1,105 +0,0 @@
-class window.FactTabsView extends Backbone.Marionette.Layout
-  template: "facts/fact_tabs"
-
-  events:
-    "click .tab-control .is-tab": "tabClick",
-    "click .tab-control .is-popup": "popupClick",
-    "click .transparent-layer": "closePopup",
-    "click .popup-content .close-popup": "closePopup"
-
-  regions:
-    startConversationRegion: '.popup-content .start-conversation-container'
-    addToChannelRegion: ".popup-content .add-to-channel-form"
-
-  initialize: ->
-    @_currentTab = `undefined`
-    @initFactRelationsViews()
-
-  renderAddToChannel: ->
-    if @addToChannelView == `undefined`
-      @addToChannelView = new AutoCompleteChannelsView
-                               collection: new OwnChannelCollection()
-      _.each @model.getOwnContainingChannels(), (ch) =>
-        @addToChannelView.collection.add ch  if ch.get("type") is "channel"
-
-      @addToChannelView.on "addChannel", (channel) =>
-        @model.addToChannel channel, {}
-
-      @addToChannelView.on "removeChannel", (channel) =>
-        @model.removeFromChannel channel, {}
-        @model.collection.remove @model  if window.currentChannel and currentChannel.get("id") is channel.get("id")
-
-      @addToChannelRegion.show @addToChannelView
-
-  onClose: -> @addToChannelView?.close()
-
-  hideTabs: ->
-    $tabButtons = @$el.find(".tab-control li")
-    $tabButtons.removeClass "active"
-
-    @$(".tab-content > div").hide()
-    @$(".tab-control > li").removeClass "tabOpened"
-    @_currentTab = `undefined`
-
-  showTab: (tab, $tabHandle)->
-    @hideTabs()
-    @_currentTab = tab
-    $tabHandle.addClass "active"
-    @$(".tab-content > ." + tab).show()
-    @$(".tab-control > li").addClass "tabOpened"
-    @handleTabActions tab
-
-  handleTabActions: (tab) ->
-    mp_track "Factlink: Open tab",
-      factlink_id: @model.id
-      type: tab
-
-    switch tab
-      when "doubting", "supporting", "weakening" then @showFactRelations tab
-
-  initFactRelationsViews: ->
-    @supportingFactRelations = new SupportingFactRelations([],fact: @model)
-    @weakeningFactRelations = new WeakeningFactRelations([],fact: @model)
-
-  showFactRelations: (type) ->
-    unless type + "FactRelationsView" of this
-      if type == "doubting"
-        this[type + "FactRelationsView"] = new DoubtingRelationsView()
-      else
-        this[type + "FactRelationsView"] = new FactRelationsView(collection: this[type + "FactRelations"])
-
-      @$("." + type + " .dropdown-container").append this[type + "FactRelationsView"].render().el
-    @$("." + type + " .dropdown-container").show()
-    this[type + "FactRelationsView"].fetch()
-
-  tabClick: (e) ->
-    e.preventDefault()
-    e.stopPropagation()
-    $target = $(e.target).closest("li")
-    tab = $target.attr("class").split(" ")[0]
-    if tab is @_currentTab
-      @hideTabs()
-    else
-      @showTab(tab, $target)
-
-  popupClick: (e) ->
-    e.preventDefault()
-    e.stopPropagation()
-    $target = $(e.target).closest("li")
-    popup = $target.attr("class").split(" ")[0]
-
-    @showPopup(popup)
-
-  showPopup: (popup) ->
-    @$('.popup-content .' + popup + '-container').show()
-
-    @$('.transparent-layer').show()
-
-    switch popup
-      when "start-conversation"
-        @startConversationRegion.show new StartConversationView(model: @model)
-      when "add-to-channel" then @renderAddToChannel()
-
-  closePopup: (e) ->
-    @$('.popup-content > div').hide()
-    @$('.transparent-layer').hide()


Replaced concat tasks by concat

Replaced concat tasks by concat

--- js-library/Gruntfile.coffee
+++ js-library/Gruntfile.coffee
@@ -53,11 +53,6 @@
           'src/js/loader.js'
         ]
         dest: 'dist/factlink.js'
-      'dist/factlink.start_annotating.js':   'src/js/start_annotating.js'
-      'dist/factlink.stop_annotating.js':    'src/js/stop_annotating.js'
-      'dist/factlink.start_highlighting.js': 'src/js/start_highlighting.js'
-      'dist/factlink.stop_highlighting.js':  'src/js/stop_highlighting.js'
-      'dist/easyXDM/easyXDM.js':             'libs/easyXDM.js'
     copy:
       main:
         files: [
@@ -65,6 +60,14 @@
           { src: ['dist/easyXDM/easyXDM.min.js'], dest: 'public/easyXDM.min.js' }
           { src: ['**'],                          dest: 'dist/server/images/', expand: true, cwd: 'src/images/', filter: 'isFile' }
           { src: ['**'],                          dest: 'dist/images/',        expand: true, cwd: 'src/images/', filter: 'isFile' }
+      js_files:
+          {
+            src: ['start_annotating.js', 'stop_annotating.js', 'start_highlighting.js', 'stop_highlighting.js']
+            cwd: 'src/js/'
+            expand: true
+            dest: 'dist/'
+            rename: (dest, src) -> "#{dest}factlink.#{src}"
+          }
         ]
     less:
       development:


Remove patterns from globalExclusions when in inclusions

--- src/path-filter.coffee
+++ src/path-filter.coffee
@@ -27,9 +27,9 @@
     {includeHidden, excludeVcsIgnores} = options
     {inclusions, exclusions, globalExclusions} = @sanitizePaths(options)

-    @inclusions = @createMatchers(inclusions, true)
-    @exclusions = @createMatchers(exclusions, false)
-    @globalExclusions = @createMatchers(globalExclusions, false)
+    @inclusions = @createMatchers(inclusions, {deepMatch: true})
+    @exclusions = @createMatchers(exclusions, {deepMatch: false})
+    @globalExclusions = @createMatchers(globalExclusions, {deepMatch: false, disallowDuplicatesFrom: @inclusions})

     @repo = GitUtils.open(rootPath) if excludeVcsIgnores

@@ -118,11 +118,12 @@
     @exclusions.file.push(matcher)
     @exclusions.directory.push(matcher)

-  createMatchers: (patterns=[], deepMatch) ->
-    addFileMatcher = (matchers, pattern) ->
+  createMatchers: (patterns=[], {deepMatch, disallowDuplicatesFrom}={}) ->
+    addFileMatcher = (matchers, pattern) =>
+      return if disallowDuplicatesFrom? and @containsPattern(disallowDuplicatesFrom, 'file', pattern)
       matchers.file.push(new Minimatch(pattern, PathFilter.MINIMATCH_OPTIONS))

-    addDirectoryMatcher = (matchers, pattern, deepMatch) ->
+    addDirectoryMatcher = (matchers, pattern, deepMatch) =>
       # It is important that we keep two permutations of directory patterns:
       #
       # * 'directory/anotherdir'
@@ -165,6 +166,7 @@
       matchIndex = pattern.search(directoryPattern)
       addDirectoryMatcher(matchers, pattern.slice(0, matchIndex)) if matchIndex > -1

+      return if disallowDuplicatesFrom? and @containsPattern(disallowDuplicatesFrom, 'directory', pattern)
       matchers.directory.push(new Minimatch(pattern, PathFilter.MINIMATCH_OPTIONS))

     pattern = null
@@ -192,3 +194,8 @@
         addFileMatcher(matchers, pattern)

     matchers
+
+  containsPattern: (matchers, fileOrDirectory, pattern) ->
+    for matcher in matchers[fileOrDirectory]
+      return true if matcher.pattern is pattern
+    false


fixing toolbar shown in wrong place on keyboard selection

--- hallo.coffee
+++ hallo.coffee
@@ -228,22 +228,10 @@
         _getToolbarPosition: (event, selection) ->
             return unless event
             if @options.floating
-                if event.originalEvent instanceof MouseEvent
+                if event.originalEvent instanceof KeyboardEvent
+                   return @_getCaretPosition(selection);
+                else if event.originalEvent instanceof MouseEvent
                     return { top: event.pageY, left: event.pageX }
-                else
-                    if jQuery(event.target).attr('contenteditable') == "true"
-                        containerElement = jQuery(event.target)
-                    else
-                        containerElement = jQuery(event.target).parent('[contenteditable]').first()
-
-                    containerPosition = containerElement.position()
-                    switch @options.offset.y
-                        when "top" then offsety = containerPosition.top - @toolbar.outerHeight()
-                        #TODO: "bottom" may break style
-                        when "bottom" then offsety = containerPosition.top + containerElement.outerHeight()
-                        else offsety = containerPosition.top - @options.offset.y
-
-                    return { "top": containerPosition.left - @options.offset.x, "left": offsety }
             else
                 offset = parseFloat @element.css('outline-width') + parseFloat @element.css('outline-offset')
                 top: @element.offset().top - this.toolbar.outerHeight() - offset


Removed AudioContext check

Removed AudioContext check

--- lib/client/metronome.coffee
+++ lib/client/metronome.coffee
@@ -1,6 +1,5 @@
 class @Metronome
   constructor: (audioContext, beatsPerMinute, ticksPerBeat) ->
-    check audioContext, AudioContext
     check beatsPerMinute, Number
     check ticksPerBeat, Number
     @_audioContext = audioContext


Delete server-new.coffee

Delete server-new.coffee

--- server-new.coffee
+++ server-new.coffee
@@ -1,37 +0,0 @@
-express         = require 'express'
-fs                 = require 'fs'
-passport         = require 'passport'
-
-# Load configurations
-env                 = process.env.NODE_ENV || 'development'
-config                 = require('./config/config')[env]
-mongoose         = require 'mongoose'
-
-# db connection
-db = mongoose.connect config.db
-
-# Models
-models_path = __dirname + '/app/models'
-fs.readdirSync(models_path).forEach (file)->
-        require models_path + '/' + file
-
-
-# Passport config
-require('./config/passport')(passport, config)
-
-# Init Express
-app = express()
-
-# Express settings
-require('./config/express')(app, config, passport)
-
-# Routes
-require('./config/routes')(app, passport)
-
-# Start the app by listening on <port>
-port = process.env.PORT || 5000
-app.listen port
-console.log 'Express app started on port ' + port
-
-# expose app
-exports = module.exports = app


Fixed wrong comparison

Fixed wrong comparison

--- server/app.coffee
+++ server/app.coffee
@@ -30,7 +30,7 @@
 app.set('port', parseInt(process.env["PORT"], 100) or config.port or 3000)

 # Live bundle reloading in development
-if env is not "production"
+if env != "production"
   compiler = webpack(webpackConfig)
   compiler.plugin("done", io.onInvalided)
   webpackDevMiddleware = webpackDevMiddleware compiler,


Cache some common modal selectors for external manipulation

--- components/modalize/view.coffee
+++ components/modalize/view.coffee
@@ -38,8 +38,8 @@
     this

   __postRender__: ->
-    @$('.js-modalize-dialog').css @dimensions
-    @$('.js-modalize-body').html @subView.render().$el
+    (@$dialog = @$('.js-modalize-dialog')).css @dimensions
+    (@$body = @$('.js-modalize-body')).html @subView.render().$el
     @__postRendered__ = true
     this


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -36,7 +36,7 @@
       height: 2 * deadzone.y

     if objectCenter.x > centerRect.right
-      I.scroll.x = I.scroll.x.approach()
+      I.scroll.x = I.scroll.x.approach(I.maxVelocity)
     else if objectCenter.x < centerRect.left


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -6,7 +6,7 @@
       width: App.width
       height: App.height
     screen: Rectangle
-      x: App.width / 2
+      x:
       y: App.height / 2
       width: App.width
       height: App.height


flicker before and after fix

flicker before and after fix

--- src/camera.coffee
+++ src/camera.coffee
@@ -46,15 +46,15 @@
       focusOn(object)

     topdown: (object) ->
-      helper = Math.max(I.cameraBounds.width, I.cameraBounds.height) / 4
+      helper = Math.max(I.screen.width, I.screen.height) / 4

       I.deadzone = Point(helper, helper)

       focusOn(object)

     platformer: (object) ->
-      width = I.cameraBounds.width / 8
-      height = I.cameraBounds.height / 3
+      width = I.screen.width / 8
+      height = I.screen.height / 3

       I.deadzone = Point(width, height)


Adding the ability to see index settings in elasticsearch

--- src/scripts/elasticsearch.coffee
+++ src/scripts/elasticsearch.coffee
@@ -131,6 +131,17 @@
         .get() (err, res, body) ->
           msg.send("/code #{body}")

+  showIndexSettings = (msg, alias, index) ->
+    cluster_url = _esAliases[alias]
+
+    if cluster_url == "" || cluster_url == undefined
+      msg.send("Do not recognise the cluster alias: #{alias}")
+    else
+      msg.send("Getting the Index settings for #{index} on #{cluster_url}")
+      msg.http("#{cluster_url}/#{index}/_settings?pretty=true")
+        .get() (err, res, body) ->
+          msg.send("/code #{body}")
+
   showAliases = (msg) ->

     if _esAliases == null
@@ -177,6 +188,13 @@
     clusterHealth msg, msg.match[1], (text) ->
       msg.send text

+  robot.hear /elasticsearch index settings (.*) (.*)/i, (msg) ->
+    if msg.message.user.id is robot.name
+      return
+
+    showIndexSettings msg, msg.match[1], msg.match[2], (text) ->
+      msg.send text
+
   robot.hear /elasticsearch show aliases/i, (msg) ->
     if msg.message.user.id is robot.name
       return


add support for ctrl-a in smart number, fix bug where ctrl-backspace deleted from beginning of highlighted region rather than end

--- app/assets/javascripts/angular/directives/smartNumber.js.coffee
+++ app/assets/javascripts/angular/directives/smartNumber.js.coffee
@@ -5,8 +5,8 @@

     defaultOptions = smartNumberConfig.defaultOptions

-    # 0 = tab, 8 = backspace , 13 = enter, 46 = delete, 37 = left arrow, 39 = right arrow
-    controlKeys = [0,8,13, 46, 37, 39]
+    # 0 = tab, 8 = backspace , 13 = enter, 46 = delete, 37 = left arrow, 39 = right arrow, 65 = A
+    controlKeys = [0,8,13, 46, 37, 39, 65]

     # 37 = left arrow, 39 = right arrow, 9 = enter, 33 = page up, 34 = page down
     inertKeys = [37, 39, 9, 33, 34]
@@ -41,6 +41,9 @@

     isBackspace = (event)->
       event.which == 8
+
+    isLetterA = (event)->
+      event.which == 65

     getOptions = (scope, attrs) ->
         options = angular.copy defaultOptions
@@ -306,7 +309,7 @@

                 if isBackspace(event)
                   # get the string before the selection and figure out how many commas it has
-                  originalCursorPosition = elem[0].selectionStart
+                  originalCursorPosition = elem[0].selectionEnd
                   stringAfterCursor = elem.val().slice(originalCursorPosition, elem.val().length)
                   elem.val resetCommas(stringAfterCursor) # reformat the number in place

@@ -315,6 +318,9 @@

                   # trigger final change event
                   triggerChange(elem)
+
+                if event.which == 65
+                  elem[0].setSelectionRange(0, elem.val().length) # highlight the entire field

               else


add flexibility for adding redactor plugins;

--- app/assets/javascripts/redactor/input-redactor_character.coffee
+++ app/assets/javascripts/redactor/input-redactor_character.coffee
@@ -24,7 +24,12 @@
   _redactor_options: ->
     @_trigger_change = true

-    plugins = []
+    @config.redactorOptions ?= {}
+
+    # workaround plugins & custom configuration to include loft
+    # and optional fixedtoolbar
+    plugins = @config.redactorOptions.plugins || []
+    delete @config.redactorOptions.plugins

     if ! chr.isMobile()
       plugins.push('fixedtoolbar')
@@ -38,8 +43,6 @@
       config.toolbarFixed = false
       # config.toolbarFixedTopOffset = 40

-
-    @config.redactorOptions ?= {}
     $.extend(config, @config.redactorOptions)

     return config


Classroom: Added passed chapters to data for ClassroomCoursesView.

--- client/landing/app/Applications/Classroom.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Classroom.kdapplication/AppView.coffee
@@ -28,8 +28,9 @@
       @addSubView @coursesView = new ClassroomCoursesView
         delegate : this
       ,
-        enrolled : @enrolledCourses
-        related  : @relatedCourses
+        enrolled          : @enrolledCourses
+        related           : @relatedCourses
+        completedChapters : @appStorage.getValue "CompletedChapters"

   createHeader: ->
     @addSubView @header = new KDView


Do not create socket if `io` is not present.

--- src/client.coffee
+++ src/client.coffee
@@ -50,6 +50,7 @@
         action.apply ctx, data

     context.connect = ->
+      return unless io?
       context.socket = io.apply io, arguments

     context.emit = invariate (message,data) ->


Fix proof search

Fix proof search

--- lib/idris-controller.coffee
+++ lib/idris-controller.coffee
@@ -341,7 +341,7 @@
       .subscribe successHandler, @displayErrors

   doProofSearch: ({ target }) =>
-    editor = getEditor()
+    editor = @getEditor()
     @saveFile editor
     uri = editor.getURI()
     line = editor.getLastCursor().getBufferRow()


fix link reference insertion position :two:

--- lib/insert-link-view.coffee
+++ lib/insert-link-view.coffee
@@ -114,19 +114,23 @@

   insertReferenceLink: (text, title, url) ->
     @editor.buffer.beginTransaction()
+
+    # modify selection
     id = require("guid").raw()[0..7]
     @editor.insertText("[#{text}][#{id}]")
+
+    # insert reference
     position = @editor.getCursorBufferPosition()
-    @editor.moveCursorDown(2)
+    @editor.insertNewlineBelow()
+    @editor.moveCursorDown()
+    @editor.insertText("[#{id}]: #{url} \"#{title}\"")
+    @editor.moveCursorDown()
     line = @editor.selectLine()[0].getText().trim()
-    @editor.moveCursorToBeginningOfPreviousParagraph()
-    @editor.insertNewlineAbove()
-    if utils.isReferenceDefinition(line)
-      @editor.moveCursorDown()
-    else
+    unless utils.isReferenceDefinition(line)
+      @editor.moveCursorUp()
       @editor.insertNewlineBelow()
-    @editor.insertText("[#{id}]: #{url} \"#{title}\"")
     @editor.setCursorBufferPosition(position)
+
     @editor.buffer.commitTransaction()

   updateReferenceLink: (text, title, url) ->


Allow keybindings to be copied to the clipboard

Closes #24

--- lib/keybinding-panel.coffee
+++ lib/keybinding-panel.coffee
@@ -9,7 +9,7 @@
       @div class: 'block', =>
         @label 'Filter:'
         @subview 'filter', new EditorView(mini: true)
-      @table =>
+      @table class: 'native-key-bindings', tabindex: -1, =>
         @col class: 'keystroke'
         @col class: 'command'
         @col class: 'source'


Run, Forest!

Run, Forest!

--- server/swf.coffee
+++ server/swf.coffee
@@ -22,8 +22,9 @@
   try
     user = Users.findOne(step.userId, {transform: Transformations.User})
     plan = user.plan()
-    if plan.executionsLimit and user.executions > plan.executionsLimit
-      throw new Meteor.Error(402, "Payment Required", EJSON.stringify({}))
+#    TODO: comment out when we implement the "Choose your plan" step
+#    if plan.executionsLimit and user.executions > plan.executionsLimit
+#      throw new Meteor.Error(402, "Payment Required", EJSON.stringify({}))
     input = step.input(command)
     _.defaults input,
       commandId: command._id


https://github.com/sagemathinc/smc/issues/174 - "projects should ensure that ~/.sage directory exists, even if sage not run"

--- src/smc-project/local_hub.coffee
+++ src/smc-project/local_hub.coffee
@@ -83,10 +83,15 @@

 DATA = path.join(SMC, 'local_hub')

-if not fs.existsSync(SMC)
-    fs.mkdirSync(SMC)
-if not fs.existsSync(DATA)
-    fs.mkdirSync(DATA)
+# See https://github.com/sagemathinc/smc/issues/174 -- some stupid (?)
+# code sometimes assumes this exists, and it's not so hard to just ensure
+# it does, rather than fixing any such code.
+SAGE = path.join(process.env.HOME, '.sage')
+
+for directory in [SMC, DATA, SAGE]
+    if not fs.existsSync(directory)
+        fs.mkdirSync(directory)
+

 CONFPATH = exports.CONFPATH = misc_node.abspath(DATA)


webapp/auth strategies: already loaded in r_account.cjsx, no need to load it here, too

--- src/smc-webapp/landing_page.cjsx
+++ src/smc-webapp/landing_page.cjsx
@@ -10,10 +10,6 @@

 images = ['static/sagepreview/01-worksheet.png', 'static/sagepreview/02-courses.png', 'static/sagepreview/03-latex.png', 'static/sagepreview/05-sky_is_the_limit.png' ]
 # 'static/sagepreview/04-files.png'
-
-$.get window.smc_base_url + "/auth/strategies", (obj, status) ->
-    if status == 'success'
-        redux.getActions('account').setState(strategies : obj)

 $.get window.smc_base_url + "/registration", (obj, status) ->
     if status == 'success'


Warning when no connection on save

Warning when no connection on save

--- lib/models/synced-fs.coffee
+++ lib/models/synced-fs.coffee
@@ -6,6 +6,9 @@
     ipc.send 'register-new-fs-connection', ws_url
     ipc.on 'remote-open-event', (file) ->
       atom.workspace.open(file)
+
+    ipc.on 'connection-state', (state) =>
+      @connectionState = state

     @handleEvents()
     @treeViewEventQueue = []
@@ -18,6 +21,9 @@

       editor.onDidSave =>
         if this.formatFilePath(buffer.file.path).match(/\.atom\/code/)
+          if @connectionState == 'closed'
+            @popupNoConnectionWarning()
+
           @sendSave(editor.project, buffer.file, buffer)
       editor.onDidChangePath =>
         console.log('PATH CHANGED')
@@ -59,6 +65,27 @@
       else
         console.log(e.type)

+  popupNoConnectionWarning: ->
+    noConnectionPopup = document.createElement 'div'
+    noConnectionPopup.setAttribute 'style', 'width: 100%; text-align: center;'
+    noConnectionTextContainer = document.createElement 'div'
+    noConnectionTextContainer.setAttribute 'style', 'margin-bottom: 14px; margin-top: 20px; font-weight: bold; font-size: 14px; color: red;'
+    noConnectionTextContainer.appendChild document.createTextNode "You aren't currently connected to Learn, and local changes won't sync. Please save this file again when you reconnect."
+    noConnectionButtonContainer = document.createElement 'div'
+    noConnectionButton = document.createElement 'input'
+    noConnectionButton.setAttribute 'type', 'submit'
+    noConnectionButton.setAttribute 'value', 'OK'
+    noConnectionButton.setAttribute 'style', 'width: 10%; color: black; margin-bottom: 7px;'
+    noConnectionButtonContainer.setAttribute 'style', 'width: 100%, text-align: center;'
+    noConnectionButtonContainer.appendChild noConnectionButton
+
+    noConnectionPopup.appendChild noConnectionTextContainer
+    noConnectionPopup.appendChild noConnectionButtonContainer
+    panel = atom.workspace.addModalPanel item: noConnectionPopup
+
+    noConnectionButton.addEventListener 'click', (e) =>
+      panel.destroy()
+
   sendSave: (project, file, buffer) ->
     ipc.send 'fs-local-save', JSON.stringify({
       action: 'local_save',


Updating spec to new Jasmine syntax

Updating spec to new Jasmine syntax

--- spec/javascripts/unit/order_cycle_spec.js.coffee
+++ spec/javascripts/unit/order_cycle_spec.js.coffee
@@ -328,7 +328,7 @@
       scope.submit(eventMock,'/admin/order_cycles')
       expect(eventMock.preventDefault).toHaveBeenCalled()
       expect(OrderCycle.update).toHaveBeenCalledWith('/admin/order_cycles')
-      expect(scope.order_cycle_form.$setPristine.calls.length).toEqual 1
+      expect(scope.order_cycle_form.$setPristine.calls.count()).toEqual 1

 describe 'OrderCycle services', ->


fix non-ie specs

fix non-ie specs

--- spec_app/spec/javascripts/up/flow_spec.js.coffee
+++ spec_app/spec/javascripts/up/flow_spec.js.coffee
@@ -8,21 +8,23 @@
     describe 'up.replace', ->

       if up.browser.canPushState()
+
+        beforeEach ->
+
+          affix('.before').text('old-before')
+          affix('.middle').text('old-middle')
+          affix('.after').text('old-after')

-        affix('.before').text('old-before')
-        affix('.middle').text('old-middle')
-        affix('.after').text('old-after')
-
-        @respond = ->
-          jasmine.Ajax.requests.mostRecent().respondWith
-            status: 200
-            contentType: 'text/html'
-            responseText:
-              """
-
new-before

-
new-middle

-
new-after

-              """
+          @respond = ->
+            jasmine.Ajax.requests.mostRecent().respondWith
+              status: 200
+              contentType: 'text/html'
+              responseText:
+                """
+
new-before

+
new-middle

+
new-after

+                """

         it 'replaces the given selector with the same selector from a freshly fetched page', (done) ->
           @request = up.replace('.middle', '/path')