insanity += 1

insanity += 1

Freshen up gifsanity with a new source

--- src/scripts/gifsanity.coffee
+++ src/scripts/gifsanity.coffee
@@ -11,6 +11,7 @@
 #   hubot gif me - Returns a random gif from a random blog
 #   hubot food mosh - Returns a random gif from foodmosh.tumblr.com
 #   hubot fluxmachine - Returns a random gif from fluxmachine.tumblr.com
+#   hubot android - Returns a random gif from milosrajkovic.tumblr.com
 #   hubot nic cage me - Returns a random gif from gifolas-cage.tumblr.com
 #
 # Author:
@@ -21,6 +22,7 @@
   "foodmosh.tumblr.com": /(food)( mosh)?( me)?/i
   "fluxmachine.tumblr.com": /(flux)( ?machine)?( me)?/i
   "gifolas-cage.tumblr.com": /(nic )?cage( me)?/i
+  "milosrajkovic.tumblr.com": /(android )( me)?/i
 }

 getGif = (blog, msg) ->


added sadtroll and aligned keys

added sadtroll and aligned keys

--- src/scripts/trollicon.coffee
+++ src/scripts/trollicon.coffee
@@ -3,20 +3,21 @@
 # used resources from : https://github.com/sagargp/trollicons Adium extension

 trollicons = {
-  'gasp' : 'http://i.imgur.com/tYmuZ.png',
-  'challenge': 'http://i.imgur.com/jbKmr.png',
-  'lol' : 'http://i.imgur.com/WjI3L.png',
-  'no' : 'http://i.imgur.com/loC5s.png',
-  'yao' : 'http://i.imgur.com/wTAP3.png',
-  'kidding' : 'http://i.imgur.com/0uCcv.png',
-  'megusta' : 'http://i.imgur.com/QfeUB.png',
-  'isee': 'http://i.imgur.com/M4bcv.png',
-  'fuckyeah' : 'http://i.imgur.com/m7mEZ.png',
-  'problem' : 'http://i.imgur.com/oLlJm.png',
+  'gasp'       : 'http://i.imgur.com/tYmuZ.png',
+  'challenge'  : 'http://i.imgur.com/jbKmr.png',
+  'lol'        : 'http://i.imgur.com/WjI3L.png',
+  'no'         : 'http://i.imgur.com/loC5s.png',
+  'yao'        : 'http://i.imgur.com/wTAP3.png',
+  'kidding'    : 'http://i.imgur.com/0uCcv.png',
+  'megusta'    : 'http://i.imgur.com/QfeUB.png',
+  'isee'       : 'http://i.imgur.com/M4bcv.png',
+  'fuckyeah'   : 'http://i.imgur.com/m7mEZ.png',
+  'problem'    : 'http://i.imgur.com/oLlJm.png',
   'dissapoint' : 'http://i.imgur.com/EwBi7.png',
-  'nothing' : 'http://i.imgur.com/Nwos9.png',
-  'pokerface' : 'http://i.imgur.com/dDjvG.png',
-  'ok' : 'http://i.imgur.com/QRCoI.png'
+  'nothing'    : 'http://i.imgur.com/Nwos9.png',
+  'pokerface'  : 'http://i.imgur.com/dDjvG.png',
+  'ok'         : 'http://i.imgur.com/QRCoI.png',
+  'sadtroll'   : 'http://i.imgur.com/gYsxd.png'
 }

 module.exports = (robot)->


cosmetic changes

cosmetic changes

--- src/scripts/ui-scroll.coffee
+++ src/scripts/ui-scroll.coffee
@@ -77,51 +77,49 @@
 						# Calling linker is the only way I found to get access to the tag name of the template
 						# to prevent the directive scope from pollution a new scope is created and destroyed
 						# right after the repeaterHandler creation is completed
-						linker $scope.$new(),
-							(template) ->
-
-								repeaterType = template[0].localName
-								if repeaterType in ['dl']
-									throw new Error "ui-scroll directive does not support <#{template[0].localName}> as a repeating tag: #{template[0].outerHTML}"
-								repeaterType = 'div' if repeaterType not in ['li', 'tr']
-
-								viewport = if controllers[0] and controllers[0].viewport then controllers[0].viewport else angular.element(window)
-								viewport.css({'overflow-y': 'auto', 'display': 'block'})
-
-								padding = (repeaterType)->
-									switch repeaterType
-										when 'tr'
-											table = angular.element('

')
-											div = table.find('div')
-											result = table.find('tr')
-											result.paddingHeight = -> div.height.apply(div, arguments)
-											result
-										else
-											result = angular.element("<#{repeaterType}></#{repeaterType}>")
-											result.paddingHeight = result.height
-											result
-
-								createPadding = (padding, element, direction) ->
-									element[{top:'before',bottom:'after'}[direction]] padding
-									paddingHeight: -> padding.paddingHeight.apply(padding, arguments)
-									insert: (element) -> padding[{top:'after',bottom:'before'}[direction]] element
-
-								topPadding = createPadding(padding(repeaterType), element, 'top')
-								bottomPadding = createPadding(padding(repeaterType), element, 'bottom')
-
-								$scope.$on '$destroy', () ->
-									template.remove()
-
-								builder =
-									viewport: viewport
-									topPadding: topPadding.paddingHeight
-									bottomPadding: bottomPadding.paddingHeight
-									append: bottomPadding.insert
-									prepend: topPadding.insert
-									bottomDataPos: ->
-										scrollHeight(viewport) - bottomPadding.paddingHeight()
-									topDataPos: ->
-										topPadding.paddingHeight()
+						linker $scope.$new(), (template) ->
+
+							repeaterType = template[0].localName
+							if repeaterType in ['dl']
+								throw new Error "ui-scroll directive does not support <#{template[0].localName}> as a repeating tag: #{template[0].outerHTML}"
+							repeaterType = 'div' if repeaterType not in ['li', 'tr']
+
+							viewport = if controllers[0] and controllers[0].viewport then controllers[0].viewport else angular.element(window)
+							viewport.css({'overflow-y': 'auto', 'display': 'block'})
+
+							padding = (repeaterType)->
+								switch repeaterType
+									when 'tr'
+										table = angular.element('

')
+										div = table.find('div')
+										result = table.find('tr')
+										result.paddingHeight = -> div.height.apply(div, arguments)
+										result
+									else
+										result = angular.element("<#{repeaterType}></#{repeaterType}>")
+										result.paddingHeight = result.height
+										result
+
+							createPadding = (padding, element, direction) ->
+								element[{top:'before',bottom:'after'}[direction]] padding
+								paddingHeight: -> padding.paddingHeight.apply(padding, arguments)
+								insert: (element) -> padding[{top:'after',bottom:'before'}[direction]] element
+
+							topPadding = createPadding(padding(repeaterType), element, 'top')
+							bottomPadding = createPadding(padding(repeaterType), element, 'bottom')
+
+							$scope.$on '$destroy', template.remove
+
+							builder =
+								viewport: viewport
+								topPadding: topPadding.paddingHeight
+								bottomPadding: bottomPadding.paddingHeight
+								append: bottomPadding.insert
+								prepend: topPadding.insert
+								bottomDataPos: ->
+									scrollHeight(viewport) - bottomPadding.paddingHeight()
+								topDataPos: ->
+									topPadding.paddingHeight()

 						viewport = builder.viewport

@@ -383,13 +381,10 @@
 											#log "prepended: requested #{bufferSize} received #{result.length} buffer size #{buffer.length} first #{first} next #{next}"
 										finalize(rid, newItems)

-						resizeAndResizeHandler = ->
+						resizeAndScrollHandler = ->
 							if !$rootScope.$$phase && !isLoading
 								adjustBuffer()
 								$scope.$apply()
-
-						viewport.bind 'resize', resizeAndResizeHandler
-						viewport.bind 'scroll', resizeAndResizeHandler

 						wheelHandler = (event) ->
 							scrollTop = viewport[0].scrollTop
@@ -397,10 +392,11 @@
 							if (scrollTop is 0 and not bof) or (scrollTop is yMax and not eof)
 								event.preventDefault()

+						viewport.bind 'resize', resizeAndScrollHandler
+						viewport.bind 'scroll', resizeAndScrollHandler
 						viewport.bind 'mousewheel', wheelHandler

-						$scope.$watch datasource.revision,
-							-> reload()
+						$scope.$watch datasource.revision, reload

 						if datasource.scope
 							eventListener = datasource.scope.$new()
@@ -411,8 +407,8 @@
 							for item in buffer
 								item.scope.$destroy()
 								item.element.remove()
-							viewport.unbind 'resize', resizeAndResizeHandler
-							viewport.unbind 'scroll', resizeAndResizeHandler
+							viewport.unbind 'resize', resizeAndScrollHandler
+							viewport.unbind 'scroll', resizeAndScrollHandler
 							viewport.unbind 'mousewheel', wheelHandler

 						applyUpdate = (wrapper, newItems) ->
@@ -460,8 +456,10 @@

 						angular.extend(adapterAttr, adapter) if adapterAttr

-						# deprecated since v1.1.0
-						adapter.update = (locator, newItem) ->
+
+						# update events are deprecated since v1.1.0
+
+						doUpdate = (locator, newItem) ->
 							if angular.isFunction locator
 								((wrapper)->
 									locator wrapper.scope
@@ -471,7 +469,7 @@
 									buffer[locator-first-1].scope[itemName] = newItem
 							null

-						adapter.delete = (locator) ->
+						doDelete = (locator) ->
 							if angular.isFunction locator
 								temp = []
 								temp.unshift item for item in buffer
@@ -488,7 +486,7 @@
 							item.scope.$index = first + i for item,i in buffer
 							adjustBuffer()

-						adapter.insert = (locator, item) ->
+						doInsert = (locator, item) ->
 							inserted = []
 							if angular.isFunction locator
 								throw new Error('not implemented - Insert with locator function')
@@ -499,10 +497,9 @@
 							item.scope.$index = first + i for item,i in buffer
 							adjustBuffer(null, inserted)

-
-						eventListener.$on "insert.item", (event, locator, item)-> adapter.insert(locator, item)
-						eventListener.$on "update.items", (event, locator, newItem)-> adapter.update(locator, newItem)
-						eventListener.$on "delete.items", (event, locator)-> adapter.delete(locator)
+						eventListener.$on "insert.item", (event, locator, item)->doInsert(locator, item)
+						eventListener.$on "update.items", (event, locator, newItem)-> doUpdate(locator, newItem)
+						eventListener.$on "delete.items", (event, locator)-> doDelete(locator)

 		])


Use Em.computed.and for message fallthrough logic.

--- app/assets/javascripts/controllers/paper_edit_controller.js.coffee
+++ app/assets/javascripts/controllers/paper_edit_controller.js.coffee
@@ -19,9 +19,7 @@
   showPlaceholder: Em.computed 'isBodyEmpty', 'visualEditor.isCurrentlyEditing', ->
     @get('isBodyEmpty') && !@get('visualEditor.isCurrentlyEditing')

-  statusMessage: ( ->
-    @get('processingMessage') || @get('userEditingMessage') || @get('saveStateMessage')
-  ).property('processingMessage', 'userEditingMessage', 'saveStateMessage')
+  statusMessage: Em.computed.any 'processingMessage', 'userEditingMessage', 'saveStateMessage'

   processingMessage: (->
     if @get('status') is "processing"


code style

code style

--- app/assets/javascripts/uploadcare/widget/multiple-widget.js.coffee
+++ app/assets/javascripts/uploadcare/widget/multiple-widget.js.coffee
@@ -12,21 +12,21 @@
       @currentObject?.promise()

     __setObject: (group) =>
-      unless utils.isFileGroupsEqual @currentObject, group
+      unless utils.isFileGroupsEqual(@currentObject, group)
         super

     __setExternalValue: (value) ->
       @__lastGroupPr = groupPr = utils.valueToGroup(value, @settings)
       @__reset()
-      @template.setStatus 'started'
-      @template.statusText.text t('loadingInfo')
+      @template.setStatus('started')
+      @template.statusText.text(t('loadingInfo'))
       groupPr
         .done (group) =>
           if @__lastGroupPr == groupPr
-            @__setObject group
+            @__setObject(group)
         .fail =>
           if @__lastGroupPr == groupPr
-            @template.error 'createGroup'
+            @template.error('createGroup')

     __onUploadingFailed: (error) ->
       if error is 'createGroup'
@@ -36,6 +36,6 @@
     __handleDirectSelection: (type, data) =>
       files = uploadcare.filesFrom(type, data, @settings)
       if @settings.systemDialog
-        @__setObject uploadcare.FileGroup(files, @settings)
+        @__setObject(uploadcare.FileGroup(files, @settings))
       else
         uploadcare.openDialog(files, @settings).done(@__setObject)


Removes partner access controls

Removes partner access controls

--- api/apps/users/model.coffee
+++ api/apps/users/model.coffee
@@ -32,14 +32,10 @@
         (cb) ->
           request.get("#{ARTSY_URL}/api/v1/me")
             .set('X-Access-Token': accessToken).end cb
-        (cb) ->
-          request.get("#{ARTSY_URL}/api/v1/me/partners")
-            .set('X-Access-Token': accessToken).end cb
       ], (err, results) ->
         return callback err if err
         user = results[0].body
-        partnerIds = _.pluck(results[1].body, '_id')
-        save user, partnerIds, accessToken, callback
+        save user, accessToken, callback

 #
 # Persistance
@@ -59,10 +55,9 @@
     ], (err, results) ->
       return callback err if err
       user = results[0].body
-      partnerIds = (ac.property._id for ac in results[1].body)
-      save user, partnerIds, accessToken, callback
+      save user, accessToken, callback

-save = (user, partnerIds, accessToken, callback) ->
+save = (user, accessToken, callback) ->
   async.parallel [
     (cb) ->
       bcrypt.hash accessToken, SALT, cb
@@ -79,7 +74,6 @@
       name: user.name
       email: user.email
       type: user.type
-      access_to_partner_ids: partnerIds
       profile_handle: profile.id
       profile_id: profile._id
       profile_icon_url: _.first(_.values(profile.icon?.image_urls))


Fixing comment (doh)

Fixing comment (doh)

--- popupmodal.coffee
+++ popupmodal.coffee
@@ -62,7 +62,7 @@
       else
         popup

-    # Wait for out animation to complete if nowait is false
+    # Wait for out animation to complete and call back if needed
     Timer.setTimeout (->
       _popups.set shown
       cb && cb()


Some documentation

Some documentation

--- src/parser.coffee
+++ src/parser.coffee
@@ -104,7 +104,11 @@

     @addMarkup block, opts.bounds, opts.depth

-  # flagToRemove
+  # flagToRemove, used for removing the placeholders that
+  # are placed when round-tripping empty sockets, so that, e.g. in CoffeeScript mode
+  # a + (empty socket) -> a + `` -> a + (empty socket).
+  #
+  # These are done by placing blocks around that text and then removing that block from the tree.
   flagToRemove: (bounds, depth) ->
     block = new model.Block()

@@ -287,6 +291,27 @@

   handleButton: (text, command, oldblock) ->
     return text
+
+  # applyMarkup
+  # -----------
+  #
+  # The parser adapter will throw out markup in arbitrary orders. `applyMarkup`'s job is to assemble
+  # a parse tree from this markup. `applyMarkup` also cleans up any leftover text that lies outside blocks or
+  # sockets by passing them through a comment-handling procedure.
+  #
+  # `applyMarkup` applies the generated markup by sorting it, then walking from the beginning to the end of the
+  # document, creating `TextToken`s and inserting the markup between them. It also keeps a stack
+  # of which containers it is currently in, for error detection and comment handling.
+  #
+  # Whenever the container is currently an `Indent` or a `Document` and we get some text, we handle it as a comment.
+  # If it contains block-comment tokens, like /* or */, we immediately make comment blocks around them, amalgamating multiline comments
+  # into single blocks. We do this by scanning forward and just toggling on or off a bit that says whether we're inside
+  # a block comment, refusing to put any markup while we're inside one.
+  #
+  # When outside a block-comment, we pass free text to the mode's comment parser. This comment parser
+  # will return a set of text ranges to put in sockets, usually the place where freeform text can be put in the comment.
+  # For instance, `//hello` in JavaScript should return (2, 6) to put a socket around 'hello'. In C, this is used
+  # to handle preprocessor directives.

   applyMarkup: (opts) ->
     # For convenience, will we


Add setter for Player#lookPos

Add setter for Player#lookPos

--- src/player.coffee
+++ src/player.coffee
@@ -17,6 +17,7 @@
 		Object.defineProperty @, 'lookPos',
 			enumerable: no
 			get: => @_lookPos ? @creature
+			set: (pos) => @_lookPos = pos

 		@_lookPos = null


Minor debug statement.

Minor debug statement.

--- src/player.coffee
+++ src/player.coffee
@@ -153,6 +153,7 @@
       else if @velocity.x isnt 0 and @nextAction isnt 'stop'
         @nextAction = 'stop'
         @_justTurned = yes
+        @debug 'turn', @velocity.x

       @direction = direction
       @sprite.scale.x = direction


payment: subscription: remove uncalled method

(it was probably redundant)

--- workers/social/lib/social/models/payment/subscription.coffee
+++ workers/social/lib/social/models/payment/subscription.coffee
@@ -18,7 +18,6 @@
     sharedMethods   :
       static        : [
         'fetchUserSubscriptions'
-        'fetchUserSubscriptionsWithPlan'
         'checkUserSubscription'
       ]
       instance      : [
@@ -75,21 +74,6 @@
           queue.fin()
       dash queue, -> callback null, subscriptions

-  @fetchUserSubscriptionsWithPlan = secure (client, callback)->
-    @fetchUserSubscriptions client, (err, subs)->
-      return callback err      if err
-      return callback null, [] unless subs
-
-      planCode = $in: (sub.planCode for sub in subs)
-      JPaymentPlan = require './plan'
-      JPaymentPlan.some { planCode }, {}, (err, plans)->
-        return callback err  if err
-        planMap = {}
-        planMap[plan.planCode] = plan  for plan in plans
-        sub.plan = planMap[sub.planCode]  for sub in subs
-
-        callback null, subs
-
   @checkUserSubscription = secure ({connection:{delegate}}, planCode, callback)->
     throw Error 'reimplement this!'


No need for jQuery UI in package config panel

--- src/app/package-config-panel.coffee
+++ src/app/package-config-panel.coffee
@@ -2,14 +2,6 @@
 {$$} = require 'space-pen'
 $ = require 'jquery'
 _ = require 'underscore'
-
-window.jQuery = $
-require 'jqueryui-browser/ui/jquery.ui.core'
-require 'jqueryui-browser/ui/jquery.ui.widget'
-require 'jqueryui-browser/ui/jquery.ui.mouse'
-require 'jqueryui-browser/ui/jquery.ui.sortable'
-require 'jqueryui-browser/ui/jquery.ui.draggable'
-delete window.jQuery

 module.exports =
 class PackageConfigPanel extends ConfigPanel


Bump time-out up to 20 seconds for image load.

--- protractor-conf.coffee
+++ protractor-conf.coffee
@@ -31,8 +31,8 @@

   # Pass options into mocha.
   mochaOpts:
-    # Allow five seconds per test rather than the default two seconds.
-    timeout: 15000
+    # Allow 20 seconds per test rather than the default two seconds.
+    timeout: 20000

     # The spec reporter has nice checkmarks.
     reporter: 'spec'


Fix wrong rejection of uploads from Windows.

--- src/record.coffee
+++ src/record.coffee
@@ -18,7 +18,7 @@
   @createFromRequest: (req, callback) ->
     form = new formidable.IncomingForm()
     form.parse req, (error, fields, files) ->
-      unless files.upload_file_minidump?.name is 'minidump.dmp'
+      unless files.upload_file_minidump?.name?
         return callback new Error('Invalid breakpad upload')

       record = new Record


Optimize HLS stream index by memoizing index records per segment

--- src/streammachine/outputs/live_streaming.coffee
+++ src/streammachine/outputs/live_streaming.coffee
@@ -77,12 +77,13 @@
             """

             for seg in @stream.hls_segmenter.segments
-                @opts.res.write """
+                seg_record = seg.index_record ||= """
                 #EXTINF:#{seg.duration},#{@stream.StreamTitle}
                 #EXT-X-PROGRAM-DATE-TIME:#{tz(seg.ts,"%FT%T.%3N%:z")}
-                http://#{@stream.opts.host}/#{@stream.key}/ts/#{seg.id}.#{@stream.opts.format}?session=#{session_id}
+                http://#{@stream.opts.host}/#{@stream.key}/ts/#{seg.id}.#{@stream.opts.format}
+                """

-                """
+                @opts.res.write seg_record + ( session_id && "?session_id=#{session_id}" ) + "\n"

             @opts.res.end()


Enable embedded division filter view.

Enable embedded division filter view.

--- src/router.coffee
+++ src/router.coffee
@@ -60,7 +60,9 @@
             unit

         renderUnitsWithFilter: (params) ->
-            @listenToOnce @appState.units, 'sync', @drawUnits
+            console.trace()
+            @listenToOnce @appState.units, 'sync', =>
+                @drawUnits @appState.units
             units =  @appState.units
             params = @_parseParameters params
             key = 'division'


Runner: Fix jobs entry not being cleared in situations without results

--- src/runner.coffee
+++ src/runner.coffee
@@ -161,6 +161,7 @@
         details.stdout = p.stdout
         details.stderr = p.stderr
       details.screenshots = job.screenshots
+      delete @jobs[job.id]
       return callback err, sol, details if err
       return callback job.error, sol, details
     @jobs[job.id] = job
@@ -211,7 +212,6 @@
         err = new Error 'Neither solution nor error was provided' if not (out.error or out.solution)
         job.error = err
         job.process.stop()
-        delete @jobs[job.id]
         response.writeHead 204, {}
         response.end()


Fixed incorrect variable access

Fixed incorrect variable access

--- src/scribe.coffee
+++ src/scribe.coffee
@@ -79,6 +79,6 @@
         .sortBy (replacement) ->
           replacement.length * -1
         .each (placeholder) ->
-          sorted[placeholder] = replace[placeholder]
+          sorted[placeholder] = replacements[placeholder]

       sorted


ListView: minor code styling

ListView: minor code styling

--- src/components/list/listview.coffee
+++ src/components/list/listview.coffee
@@ -60,16 +60,16 @@
   removeItem: (itemInstance, itemData, index) ->

     if index?
-      @emit 'ItemIsBeingDestroyed', { view : @items[index], index : index }
-      item = @items.splice index,1
+      @emit 'ItemIsBeingDestroyed', { view : @items[index], index }
+      item = @items.splice index, 1
       item[0].destroy()
       return
     else
       # fix it w/ indexOf
-      for item,i in @items
+      for item, i in @items
         if itemInstance is item or itemData is item.getData()
           @emit 'ItemIsBeingDestroyed', { view : item, index : i }
-          @items.splice i,1
+          @items.splice i, 1
           item.destroy()
           return


Use Meteor.absoluteUrl()

Use Meteor.absoluteUrl()

--- src/server.coffee
+++ src/server.coffee
@@ -44,7 +44,7 @@
     emailConfig = @config().email
     user = args.user
     event = args.event
-    url = process.env.ROOT_URL
+    url = Meteor.absoluteUrl()
     Email.send
       to: user.emails[0].address
       from: emailConfig.fromAddress


fixing small issues

fixing small issues

--- src/scripts/modules/media/body/body.coffee
+++ src/scripts/modules/media/body/body.coffee
@@ -189,6 +189,6 @@
       @render() # Re-render body view to cleanup aloha issues

     addRelNoFollowToOutsideLinks: (temp) ->
-      temp.find('a').each (i,el) =>
+      temp.find('a').each (i,el) ->
         $el = $(el)
-        $(el).attr 'rel', 'no-follow'  if $(el).attr('href').match(regex)
+        $el.attr 'rel', 'nofollow'  if $el.attr('href').match(regex)


Wrap into promise. Refs. #7.

Wrap into promise. Refs. #7.

--- src/spec/integration/streaming.spec.coffee
+++ src/spec/integration/streaming.spec.coffee
@@ -6,13 +6,16 @@
 Promise = require 'bluebird'

 cleanup = (logger, apiClient) ->
-  apiClient.client.categories.all().fetch()
-  .then (result) ->
-    logger.info "Cleaning categories: #{_.size result.body.results}"
-    Promise.all _.map result.body.results, (cat) ->
-      apiClient.delete cat
-    .then (results) ->
-      Promise.resolve()
+  new Promise (resolve, reject) ->
+    apiClient.client.categories.all().fetch()
+    .then (result) ->
+      logger.info "Cleaning categories: #{_.size result.body.results}"
+      Promise.all _.map result.body.results, (cat) ->
+        apiClient.delete cat
+      .then (results) ->
+        resolve results
+    .catch (err)
+      reject err

 describe 'Streaming', ->
   beforeEach (done) ->


Fixed grunt.log typo, fixes build error with static config.xml

--- src/tasks/build/base/compile_config.coffee
+++ src/tasks/build/base/compile_config.coffee
@@ -12,7 +12,7 @@
     dest = path.join phonegapPath, 'www', 'config.xml'

     if grunt.util.kindOf(configXml) == 'string'
-      grunt.logc.writeln "Copying static #{configXml}"
+      grunt.log.writeln "Copying static #{configXml}"
       cp configXml, dest, (err) ->
         if fn then fn(err)


Copy dlls during build task

Copy dlls during build task

--- build/tasks/build-task.coffee
+++ build/tasks/build-task.coffee
@@ -86,6 +86,10 @@
         unless /.+\.plist/.test(sourcePath)
           grunt.file.copy(sourcePath, path.resolve(appDir, '..', subDirectory, filename))

+    if process.platform is 'win32'
+      cp path.join('resources', 'win', 'msvcp100.dll'), shellAppDir
+      cp path.join('resources', 'win', 'msvcr100.dll'), shellAppDir
+
     dependencies = ['compile', "generate-license:save"]
     dependencies.push('copy-info-plist') if process.platform is 'darwin'
     dependencies.push('set-exe-icon') if process.platform is 'win32'


incude other plan types as upgradeable plans

--- client/angular/components/group/current_plan_button/current_plan_button.coffee
+++ client/angular/components/group/current_plan_button/current_plan_button.coffee
@@ -5,7 +5,7 @@
   replace: true
   controller: ['$scope', ($scope) ->
     $scope.plan = $scope.group.subscriptionPlan
-    $scope.show = $scope.plan == 'trial'
+    $scope.show = _.includes(['was-gift', 'was-paid', 'trial'], 'trial')
     $scope.upgradeUrl = AppConfig.baseUrl+'upgrade'
   ]
   template: '


fixed typeof object bug

fixed typeof object bug

--- src/viz/types/helpers/graph/includes/svg.coffee
+++ src/viz/types/helpers/graph/includes/svg.coffee
@@ -1,5 +1,6 @@
-mix      = require "../../../../../color/mix.coffee"
-textwrap = require "../../../../../textwrap/textwrap.coffee"
+mix         = require "../../../../../color/mix.coffee"
+textwrap    = require "../../../../../textwrap/textwrap.coffee"
+validObject = require "../../../../../object/validate.coffee"

 module.exports = (vars) ->

@@ -281,11 +282,11 @@
       lineData = []

       for line in vars[axis].lines.value
-        d = if typeof line is "object" then line.position else line
+        d = if validObject(line) then line.position else line
         unless isNaN(d)
           d = parseFloat(d)
           if d > domain[0] and d < domain[1]
-            d = unless typeof line is "object" then {"position": d} else line
+            d = unless validObject(line) then {"position": d} else line
             d.coords =
               line: vars[axis].scale.viz(d.position)
             lineData.push d


:bug: Fix some typos

:bug: Fix some typos

--- lib/atom-hack.coffee
+++ lib/atom-hack.coffee
@@ -11,7 +11,7 @@
     @Hack
   provideLinter:->
     Path = require 'path'
-    Linter = {
+    LinterHack = {
       grammarScopes: ['source.hack', 'source.html.hack']
       scope: 'project'
       lintOnFly: false
@@ -30,13 +30,13 @@
             Traces.push(
               type: 'Trace',
               text: Message.descr,
-              filePath: Linter.normalizePath(Message.path),
+              filePath: LinterHack.normalizePath(Message.path),
               range: [[Message.line - 1,Message.start - 1 ],[Message.line - 1 ,Message.end]]
             )
           ToReturn.push(
             type: 'Error',
             text: First.descr,
-            filePath: Linter.normalizePath(First.path),
+            filePath: LinterHack.normalizePath(First.path),
             range: [[First.line - 1, First.start - 1 ],[First.line - 1,First.end]]
             trace: Traces
           )
@@ -55,7 +55,7 @@
               Content = JSON.parse(Data.stderr)
             catch error then return Resolve([])# Ignore weird errors for now
             if Content.passed then Resolve([])
-            else Resolve(Linter.formatErrors(Content))
+            else Resolve(LinterHack.formatErrors(Content))
           , (Error)->
             Resolve([])
             atom.notifications.addError Error.toString(), {dismissible: true}


optimize

optimize

--- template/base/api/blueprints/create.coffee
+++ template/base/api/blueprints/create.coffee
@@ -46,9 +46,5 @@
           Model.introduce newInstance
         Model.publishCreate newInstance, not req.options.mirror and req

-      # Send JSONP-friendly response if it's supported
-      # (HTTP 201: Created)
-
-      # Change the id for user.email
       res.status 201
       res.ok newInstance


Log error stack

Log error stack

--- lib/file-view.coffee
+++ lib/file-view.coffee
@@ -22,11 +22,11 @@
         else
           temp.mkdir 'atom-', (error, tempDirPath) ->
             if error?
-              console.error("Error creating temp directory: #{tempDirPath}")
+              console.error("Error creating temp directory: #{tempDirPath}", error.stack ? error)
             else
               tempFilePath = path.join(tempDirPath, path.basename(archivePath), entry.getName())
               fsUtils.writeAsync tempFilePath, contents, (error) ->
                 if error?
-                  console.error("Error writing to #{tempFilePath}")
+                  console.error("Error writing to #{tempFilePath}", error.stack ? error)
                 else
                   rootView.open(tempFilePath)


Fix focus on button clicks

Fix focus on button clicks

--- lib/find-view.coffee
+++ lib/find-view.coffee
@@ -116,8 +116,8 @@

   handleFindEvents: ->
     @findEditor.getEditor().on 'contents-modified', => @liveSearch()
-    @nextButton.on 'click', => @findNext()
-    @previousButton.on 'click', => @findPrevious()
+    @nextButton.on 'click', => @findNext(true)
+    @previousButton.on 'click', => @findPrevious(true)
     atom.workspaceView.command 'find-and-replace:find-next', => @findNext(true)
     atom.workspaceView.command 'find-and-replace:find-previous', => @findPrevious(true)
     atom.workspaceView.command 'find-and-replace:use-selection-as-find-pattern', @setSelectionAsFindPattern


Capitalize message onExit of git write command

--- lib/git-write.coffee
+++ lib/git-write.coffee
@@ -14,7 +14,7 @@
     stderr: (data) ->
       new StatusView(type: 'alert', message: data.toString())
     exit: (data) ->
-      new StatusView(type: 'success', message: "added #{toStage}")
+      new StatusView(type: 'success', message: "Added #{toStage}")
   })

 module.exports = gitWrite


calculate elapsed time per frame to keep movement uniform

--- scrolling/game-controller.coffee
+++ scrolling/game-controller.coffee
@@ -13,7 +13,16 @@
             @nextAnimationFrame()
             requestAnimationFrame browserFrameHook

+        @lastTimestamp = Date.now()
         requestAnimationFrame browserFrameHook

     nextAnimationFrame: ->
+        elapsed = @elapsedSinceLastFrame()
+
         @renderer.render @stage
+
+    elapsedSinceLastFrame: ->
+        now = Date.now()
+        elapsed = now - @lastTimestamp
+        @lastTimestamp = now
+        elapsed


don't unreserve property accessors

don't unreserve property accessors

fixes #95

--- lib/js2coffee.coffee
+++ lib/js2coffee.coffee
@@ -124,13 +124,19 @@
   # Any object identifier like a variable name.

   'identifier': (n) ->
-    unreserve n.value.toString()
+    if n.property_accessor
+      n.value.toString()
+    else
+      unreserve n.value.toString()

   'number': (n) ->
     "#{n.src()}"

   'id': (n) ->
-    unreserve n
+    if n.property_accessor
+      n
+    else
+      unreserve n

   # `id_param`
   # Function parameters. Belongs to `list`.
@@ -357,7 +363,9 @@
     # *If called as `x.prototype`, it should use double colons (`x::`).*

     left  = @build n.left()
-    right = @build n.right()
+    right_obj = n.right()
+    right_obj.property_accessor = true
+    right = @build right_obj

     if n.isThis and n.isPrototype
       "@::"


Extract functions in foundation extras

Extract functions in foundation extras

In foundation extras we were assigning functions to local variables, but
in the rest of the CoffeeScript files we use them as object properties.

--- app/assets/javascripts/foundation_extras.js.coffee
+++ app/assets/javascripts/foundation_extras.js.coffee
@@ -1,24 +1,21 @@
 App.FoundationExtras =
+  clearSticky: ->
+    $("[data-sticky]").foundation("destroy") if $("[data-sticky]").length
+
+  mobile_ui_init: ->
+    $(window).trigger "load.zf.sticky"
+
+  desktop_ui_init: ->
+    $(window).trigger "init.zf.sticky"

   initialize: ->
     $(document).foundation()
     $(window).trigger "resize"
-
-    clearSticky = ->
-      $("[data-sticky]").foundation("destroy") if $("[data-sticky]").length
-
-    $(document).on("page:before-unload", clearSticky)
-
-    window.addEventListener("popstate", clearSticky, false)
-
-    mobile_ui_init = ->
-      $(window).trigger "load.zf.sticky"
-
-    desktop_ui_init = ->
-      $(window).trigger "init.zf.sticky"
+    $(document).on("page:before-unload", this.clearSticky)
+    window.addEventListener("popstate", this.clearSticky, false)

     $ ->
       if $(window).width() < 620
-        do mobile_ui_init
+        App.FoundationExtras.mobile_ui_init()
       else
-        do desktop_ui_init
+        App.FoundationExtras.desktop_ui_init()


auth redis straight after connect

auth redis straight after connect

--- server/code/model/dataset.coffee
+++ server/code/model/dataset.coffee
@@ -33,11 +33,10 @@
   @dbClass: zDbDataset
   @redisClient: redis.createClient 6379, process.env.REDIS_SERVER

-  Dataset.redisClient.on 'connect', ->
-    if /production|staging/.test process.env.NODE_ENV
-      Dataset.redisClient.auth process.env.REDIS_PASSWORD, (err) ->
-        if err?
-          console.warn 'Redis auth error: ', err
+  if /production|staging/.test process.env.NODE_ENV
+    Dataset.redisClient.auth process.env.REDIS_PASSWORD, (err) ->
+      if err?
+        console.warn 'Redis auth error: ', err

   validate: ->


Social: remove duplicate params definition

--- server/lib/server/helpers.coffee
+++ server/lib/server/helpers.coffee
@@ -103,7 +103,6 @@
   isCustomPreview = req.cookies["custom-partials-preview-mode"]
   {generateFakeClient}   = require "./client"

-  {params} = req
   generateFakeClient req, res, (err, client)->
     if err or not client
       console.error err
@@ -118,7 +117,7 @@
       fn                    = if state then loggedIn else loggedOut
       options = { client, account,
                   bongoModels, params,
-                  isCustomPreview, params}
+                  isCustomPreview }

       fn.kodingHome options, (err, subPage)->
         return next()  if err


Update test for attachment editor changes

--- test/src/system/custom_element_test.coffee
+++ test/src/system/custom_element_test.coffee
@@ -277,12 +277,13 @@
         insertImageAttachment()
         after 20, ->
           clickElement element.querySelector("figure"), ->
-            clickElement element.querySelector("figcaption"), ->
-              defer ->
-                assert.equal document.activeElement, element.querySelector("textarea")
-                assert.equal blurEventCount, 1
-                assert.equal focusEventCount, 1
-                done()
+            textarea = element.querySelector("textarea")
+            textarea.focus()
+            defer ->
+              assert.equal document.activeElement, textarea
+              assert.equal blurEventCount, 1
+              assert.equal focusEventCount, 1
+              done()

   # Selenium doesn't seem to focus windows properly in some browsers (FF 47 on OS X)
   # so skip this test when unfocused pending a better solution.


Bug fixes for the status bar

Bug fixes for the status bar

--- lib/php-debug.coffee
+++ lib/php-debug.coffee
@@ -221,7 +221,8 @@

   deactivate: ->
     @unifiedView?.setConnected(false)
-    @statusView.destroy()
+    @statusView?.destroy()
+    @statusView = null
     @unifiedView?.destroy()
     @subscriptions.dispose()

@@ -331,7 +332,7 @@

     if !@getUnifiedView().isVisible()
       @getUnifiedView().setVisible(true)
-      @statusView.setActive(true)
+      @statusView?.setActive(true)
       if !@dbgp.listening()
         @dbgp.listen()

@@ -339,7 +340,7 @@

     else
       @getUnifiedView().setVisible(false)
-      @statusView.setActive(false)
+      @statusView?.setActive(false)
       @dbgp.close()

   addWatch: ->


add fading gradient to y axis; todo: marks coordinates not aligned with axes, lightcurve, etc.

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -65,27 +65,31 @@
     # console.log '--------------------------------------------------------'

     if xClick < 80
-      @ctx.fillStyle = '#FC4542'
       # draw triangle
       w = 10
       s = w*Math.tan(60)
+
       @ctx.beginPath()
       @ctx.moveTo(w,yClick)
       @ctx.lineTo(0,yClick+s)
       @ctx.lineTo(0,yClick-s)
+      @ctx.fillStyle = '#FC4542'
       @ctx.fill()
-      @ctx.font = '10pt Arial'
-      @ctx.textAlign = 'left'
+
+      @ctx.beginPath()
+      @ctx.moveTo(w,yClick)
       @ctx.lineWidth = 1
-      @ctx.strokeStyle = 'rgba(252,69,66,0.5)'
-      @ctx.beginPath()
+      @ctx.strokeStyle = 'rgba(252,69,66,0.9)'
       @ctx.moveTo( 60, yClick )
       @ctx.lineTo( @canvas.width, yClick )
       @ctx.moveTo( 0, yClick )
       @ctx.lineTo( 10, yClick )
+      @ctx.stroke()
+
+      @ctx.font = '10pt Arial'
+      @ctx.textAlign = 'left'
       @ctx.fillText( @toDataYCoord((-yClick+@canvas.height)).toFixed(4), 15, yClick+5 ) # don't forget to flip y-axis values
-      @ctx.stroke()
-
+
   removeOutliers: (nsigma) ->
     mean = @mean(@data.y)
     std = @std(@data.y)
@@ -184,10 +188,6 @@
     val = classifier.el.find('#ui-slider').val()
     zoomRanges = classifier.zoomRanges
     zoomLevel  = classifier.zoomLevel
-
-    # draw axes
-    @drawXTickMarks(xMin, xMax)
-    @drawYTickMarks(yMin, yMax)

     # plot points
     for i in [0...@dataLength]
@@ -198,12 +198,23 @@
       @ctx.fillRect(x+@leftPadding,y,2,2)
     if @marks
       for mark in @marks.all
-        scaledMin = ((mark.dataXMinRel - xMin) / (xMax - xMin)) * @canvas.width
-        scaledMax = ((mark.dataXMaxRel - xMin) / (xMax - xMin)) * @canvas.width
+        scaledMin = ((mark.dataXMinRel - xMin) / (xMax - xMin)) * (@canvas.width-@leftPadding)
+        scaledMax = ((mark.dataXMaxRel - xMin) / (xMax - xMin)) * (@canvas.width-@leftPadding)
         mark.element.style.width = (scaledMax-scaledMin) + "px"
         mark.element.style.left = (scaledMin) + "px"
         mark.save(scaledMin, scaledMax)
     # @highlightCurve(10,14) # test
+
+    gradient = @ctx.createLinearGradient(0,0,60,0);
+    gradient.addColorStop(0,'rgba(0,0,0,1.0)');
+    gradient.addColorStop(1,'rgba(0,0,0,0.0');
+    @ctx.fillStyle = gradient
+    @ctx.fillRect(0,0,60,@canvas.height)
+
+    # draw axes
+    @drawXTickMarks(xMin, xMax)
+    @drawYTickMarks(yMin, yMax)
+
     @scale = (@largestX - @smallestX) / (@xMax - @xMin)
     return

@@ -212,8 +223,8 @@
     tickMinorLength = 5
     tickMajorLength = 10
     tickWidth = 1
-    tickColor = '#fc4541'#323232'
-    textColor = '#fc4541'#323232'
+    tickColor = '#323232'
+    textColor = '#323232'
     textSpacing = 15

     # determine intervals
@@ -314,8 +325,8 @@
     tickMinorLength = 5
     tickMajorLength = 10
     tickWidth = 1
-    tickColor = '#fc454'#323232' #'rgba(200,20,20,1)'
-    textColor = '#fc4541'#'#323232'
+    tickColor = '#323232' #'rgba(200,20,20,1)'
+    textColor = '#323232'
     textSpacing = 15
     majorTickInterval = 2
     minorTickInterval = 1
@@ -547,6 +558,7 @@
     @save(leftXPos, markRightX)

   save: (markLeftX, markRightX) ->
+
     #canvas coords
     @canvasXMin = markLeftX
     @canvasXMax = markRightX


mark handles at 50% height

mark handles at 50% height

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -84,10 +84,10 @@

-

+

-

+

     """


updating plan footer specs

updating plan footer specs

--- test/components/task-plan/footer.spec.coffee
+++ test/components/task-plan/footer.spec.coffee
@@ -1,6 +1,3 @@
-{expect} = require 'chai'
-
-React = require 'react'
 _ = require 'underscore'
 moment = require 'moment'

@@ -9,6 +6,7 @@

 PlanFooter = require '../../../src/components/task-plan/footer'
 {Testing, sinon, expect, _, React} = require '../helpers/component-testing'
+{ExtendBasePlan, PlanRenderHelper} = require '../helpers/task-plan'

 two_days_ago = (new Date(Date.now() - 1000 * 3600 * 24 * 2)).toString()
 yesterday = (new Date(Date.now() - 1000 * 3600 * 24)).toString()
@@ -16,46 +14,21 @@
 dayAfter = (new Date(tomorrow + 1000 * 3600 * 24)).toString()

-extendBasePlan = (props, taskingProps = {}) ->
-  baseModel =
-    id: '111'
-    title: 'Test Title'
-    settings:
-      page_ids: ['1']
+NEW_READING = ExtendBasePlan({type: 'reading', id: "_CREATING_1"})
+UNPUBLISHED_READING = ExtendBasePlan({type: 'reading'})
+PUBLISHED_READING = ExtendBasePlan({type: 'reading', published_at: yesterday})
+PAST_DUE_PUBLISHED_READING = ExtendBasePlan({type: 'reading', published_at: two_days_ago}, {opens_at: two_days_ago, due_at: yesterday})
+VISIBLE_READING = ExtendBasePlan({type: 'reading', published_at: yesterday}, {opens_at: yesterday})
+VISIBLE_HW = ExtendBasePlan({type: 'homework', published_at: yesterday}, {opens_at: yesterday})

-  baseTaskingPlan =
-    opens_at: tomorrow
-    due_at: dayAfter
-
-  baseModel = _.extend({}, baseModel, props)
-
-  if taskingProps?
-    baseTaskingPlan = _.extend({}, baseTaskingPlan, taskingProps)
-
-    baseModel.tasking_plans = []
-    baseModel.tasking_plans.push(baseTaskingPlan)
-
-  baseModel
-
-NEW_READING = extendBasePlan({type: 'reading', id: "_CREATING_1"})
-UNPUBLISHED_READING = extendBasePlan({type: 'reading'})
-PUBLISHED_READING = extendBasePlan({type: 'reading', published_at: yesterday})
-PAST_DUE_PUBLISHED_READING = extendBasePlan({type: 'reading', published_at: two_days_ago}, {opens_at: two_days_ago, due_at: yesterday})
-VISIBLE_READING = extendBasePlan({type: 'reading', published_at: yesterday}, {opens_at: yesterday})
-VISIBLE_HW = extendBasePlan({type: 'homework', published_at: yesterday}, {opens_at: yesterday})
-
-NEW_HW = extendBasePlan({type: 'homework', id: "_CREATING_1"})
+NEW_HW = ExtendBasePlan({type: 'homework', id: "_CREATING_1"})
 HW_WITH_EXERCISES = ({
   type: 'homework',
   settings: {
     exercise_ids: ['1']
   }
 })
-VISIBLE_HW = extendBasePlan({
-  type: 'homework',
-  published_at: yesterday}, {
-  opens_at: yesterday
-})
+

 # Stub the function, TODO - bring in helper
 getBackToCalendarParams = ->
@@ -63,12 +36,7 @@
   params:
     date: moment(TimeStore.getNow()).format('YYYY-MM-DD')

-helper = (model) ->
-  {id} = model
-  # Load the plan into the store
-  TaskPlanActions.loaded(model, id)
-  Testing.renderComponent( PlanFooter, props: {id, courseId: "1", getBackToCalendarParams} )
-
+helper = (model) -> PlanRenderHelper(model, PlanFooter, {getBackToCalendarParams})

 describe 'Task Plan Footer', ->
   beforeEach ->


Fix coffelint

Fix coffelint

--- view/coffee/create_diagram.coffee
+++ view/coffee/create_diagram.coffee
@@ -24,7 +24,9 @@
             s.name = "#{s.line.name} #{s.station.name}"
           @setAutoComplete($('.autoCompleteStation'))
       setAutoComplete: (elem) ->
-        elem.typeahead({hint: true, highlight: true}, {name: 'stations', display: 'name', source: stationMatcher(@stations)})
+        design = {hint: true, highlight: true}
+        config = {name: 'stations', display: 'name', source: stationMatcher(@stations)}
+        elem.typeahead(design, config)
       pushPattern: ->
         start = parseTime(@pattern.start)
         end = parseTime(@pattern.end)


Bugfix: project completion percentage

Bugfix: project completion percentage

--- app/project-statistics.cjsx
+++ app/project-statistics.cjsx
@@ -30,7 +30,5 @@
     </div>

   completion: ->
-    retired = @props.project.retired_subjects_count / @props.project.subjects_count
-    activeCount = @props.project.subjects_count - @props.project.retired_subjects_count
-    active = (@props.project.classifications_count / (activeCount * @props.workflow.retirement.options.count))
-    ((retired + active) * 100).toFixed(0)+    fraction = (@props.project.classifications_count / (@props.project.subjects_count * @props.workflow.retirement.options.count))
+    (fraction * 100).toFixed(0)


Improved extend method

Improved extend method

--- lib/somewhere.coffee
+++ lib/somewhere.coffee
@@ -73,8 +73,7 @@
     return false for key, val of attrs when attrs[key] isnt obj[key]
     true

-_extend = (obj) ->
-  args = Array::slice.call(arguments, 1)
+_extend = (obj, args...) ->
   args.forEach (source) ->
     obj[method] = source[method] for method of source when hasOwnProperty.call source, method
   obj


API call

API call

--- lib/spark-ide.coffee
+++ lib/spark-ide.coffee
@@ -170,3 +170,14 @@
     # TODO: Implement compiling
     if !SettingsHelper.isLoggedIn()
       return
+
+    if !!@compileCloudPromise
+      return
+
+    ApiClient = require './vendor/ApiClient'
+    client = new ApiClient SettingsHelper.get('apiUrl'), SettingsHelper.get('access_token')
+    @compileCloudPromise = client.compileCode []
+    @compileCloudPromise.done (e) =>
+      # Handle success
+    , (e) =>
+      # Handle errors


Add a little bit more delay for this intermittently failing test.

--- tests/batman/view/loop_rendering_test.coffee
+++ tests/batman/view/loop_rendering_test.coffee
@@ -72,8 +72,9 @@
   objects = new Batman.Set({id: 1, name: 'foo'}, {id: 2, name: 'bar'})
   helpers.render source, {objects}, (node, view) ->
     objects.add({id: 0, name: 'zero'})
-    delay =>
-      deepEqual getPs(view), ['zero', 'foo', 'bar']
+    delay ->
+      delay ->
+        deepEqual getPs(view), ['zero', 'foo', 'bar']

 asyncTest 'it should allow data-context definitions on inner nodes', ->
   source = '

'


Update lib/tree-view.coffee

Update lib/tree-view.coffee

Co-Authored-By: 50Wliu <3785021d15294facd37a666b363c2a7f4291f2ce@users.noreply.github.com>

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -857,7 +857,7 @@

   moveConflictingEntry: (initialPath, newPath, newDirectoryPath) =>
     try
-      if fs.isFileSync(initialPath)
+      unless fs.isDirectorySync(initialPath)
         chosen = atom.confirm
           message: "'#{path.relative(newDirectoryPath, newPath)}' already exists"
           detailedMessage: 'Do you want to replace it?'


Move away from deprecated Electron require syntax

Required for atom/atom#10983

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -1,5 +1,5 @@
 path = require 'path'
-shell = require 'shell'
+{shell} = require 'electron'

 _ = require 'underscore-plus'
 {BufferedProcess, CompositeDisposable} = require 'atom'


Terminate pending state after fulfillment of opened item promise

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -205,12 +205,12 @@
       when 1
         @selectEntry(entry)
         if entry instanceof FileView
-          atom.workspace.open(entry.getPath(), pending: true)
+          @openedItem = atom.workspace.open(entry.getPath(), pending: true)
         else if entry instanceof DirectoryView
           entry.toggleExpansion(isRecursive)
       when 2
         if entry instanceof FileView
-          atom.workspace.getActivePaneItem().terminatePendingState?()
+          @openedItem.then((item) -> item.terminatePendingState?())
           @unfocus()
         else if entry instanceof DirectoryView
           entry.toggleExpansion(isRecursive)


Update uber-juno.coffee

Update uber-juno.coffee

--- lib/uber-juno.coffee
+++ lib/uber-juno.coffee
@@ -20,6 +20,7 @@
     'julia-client.enableToolBar': true
     'julia-client.launchOnStartup': true
     'editor.scrollPastEnd': true
+    'autocomplete-plus.confirmCompletion': "tab"

   configSetup: ->
     for k, v of @defaultConfig


Comment out test I can't figure out for now

--- tests/mocha/apollos/user/login/f1/_.coffee
+++ tests/mocha/apollos/user/login/f1/_.coffee
@@ -40,6 +40,9 @@
           done()

     else
+      ###
+      TODO: Not sure how this should be tested
       it "should make an OAuth request syncronously", ->
         success = Apollos.user.login.f1 "bob@example.org", "password123"
         assert.isBoolean success
+      ###


Rename error to error_object

Rename error to error_object

--- app/assets/javascripts/tastes_bitter/javascript_errors.coffee
+++ app/assets/javascripts/tastes_bitter/javascript_errors.coffee
@@ -1,4 +1,6 @@
-window.onerror = (message, file_or_page, line_number, column_number, error) ->
+# `column_number` and `error_object` aren't supported across all browsers, so
+# they are given a default value of `null`.
+window.onerror = (message, file_or_page, line_number, column_number = null, error_object = null) ->
   xhr = null
   form = null

@@ -9,7 +11,7 @@
   form.append 'column_number', column_number
   form.append 'user_agent', navigator.userAgent
   form.append 'current_page', window.location.href
-  form.append 'stack_trace', error.stack
+  form.append 'stack_trace', error_object?.stack

   xhr?.abort()
   xhr = new XMLHttpRequest


Fix bug when scale_color is null

Fix bug when scale_color is null

Uncaught TypeError: Cannot read property 'domain' of null

--- app/frontend/javascripts/views/visualization-legend.js.coffee
+++ app/frontend/javascripts/views/visualization-legend.js.coffee
@@ -10,7 +10,7 @@

   render: ( scale_size, scale_color ) ->

-    if !@parameters.showLegend or ((!scale_color or scale_color.domain().length <= 1 ) and (!scale_size or scale_size.domain()[0] == scale_size.domain()[1]))
+    if !@parameters.showLegend or scale_color == null or ((!scale_color or scale_color.domain().length <= 1 ) and (!scale_size or scale_size.domain()[0] == scale_size.domain()[1]))
       @$el.hide()
       return


abort all requests when user cancel upload

--- app/assets/javascripts/uploadcare/files/object.js.coffee
+++ app/assets/javascripts/uploadcare/files/object.js.coffee
@@ -25,6 +25,10 @@
       else
         @multipartUpload()

+    __autoAbort: (xhr) ->
+      @__uploadDf.always xhr.abort
+      xhr
+
     directUpload: ->
       formData = new FormData()
       formData.append('UPLOADCARE_PUB_KEY', @settings.publicKey)
@@ -32,7 +36,7 @@
         formData.append('UPLOADCARE_STORE', '1')
       formData.append('file', @__file)

-      $.ajax
+      @__autoAbort $.ajax
         xhr: =>
           # Naked XHR for progress tracking
           xhr = $.ajaxSettings.xhr()
@@ -83,7 +87,7 @@
       if @settings.autostore
         data.UPLOADCARE_STORE = '1'

-      utils.jsonp(
+      @__autoAbort utils.jsonp(
         "#{@settings.urlBase}/multipart/start/?jsonerrors=1", 'POST', data
       )

@@ -105,7 +109,7 @@
           progress.push(0)
           blob = @__file.slice(blobOffset, blobOffset + blobSize)
           blobOffset += blobSize
-          $.ajax
+          @__autoAbort $.ajax
             xhr: =>
               # Naked XHR for progress tracking
               xhr = $.ajaxSettings.xhr()
@@ -126,6 +130,7 @@
       data =
         UPLOADCARE_PUB_KEY: @settings.publicKey
         uuid: uuid
-      utils.jsonp(
+
+      @__autoAbort utils.jsonp(
         "#{@settings.urlBase}/multipart/complete/?jsonerrors=1", "POST", data
       )


Project documentation

Project documentation

--- lib/layervault/nodes/project.coffee
+++ lib/layervault/nodes/project.coffee
@@ -2,16 +2,39 @@
 File = require './file'
 Folder = require './folder'

+# Represents a single LayerVault Project. Projects are always tied to a top-level
+# Folder in the LayerVault tree.
 module.exports = class Project extends Node
   relations:
     files: File
     folders: Folder

+  # Retrieves information about this project.
+  #
+  # @param [Function] cb The finished callback.
   get: (cb) -> @api.get @nodePath, {}, @buildRelations(cb)
+
+  # Creates a project (and folder) at the root of the organization.
+  #
+  # @param [Function] cb The finished callback.
   create: (cb) -> @api.post(@nodePath, {}, cb.bind(@))
+
+  # Deletes this project (and folder).
+  #
+  # @param [Function] cb The finished callback.
   delete: (cb) -> @api.delete(@nodePath, {}, cb.bind(@))

+  # Moves/renames this project (and folder).
+  #
+  # @param [String] to The new name for the project.
+  # @param [Function] cb The finished callback.
   move: (to, cb) -> @api.post("#{@nodePath}/move", { to: to }, cb.bind(@))
+
+  # @see Project#move
   rename: @move

+  # Changes the color of the folder. Possible values include: white, green, orange, and red.
+  #
+  # @param [String] color The new color.
+  # @param [Function] cb The finished callback.
   changeColor: (color, cb) -> @api.put("#{@nodePath}/color", { color: color }, cb.bind(@))


We're importing file, not file.write.

We're importing file, not file.write.

--- application/chrome/content/wesabe/canvas/snapshot.coffee
+++ application/chrome/content/wesabe/canvas/snapshot.coffee
@@ -3,7 +3,7 @@
 TYPE = 'image/png'

 writeToFile = (window, path) ->
-  write path, serializeCanvas(canvasWithContentsOfWindow(window))
+  file.write path, serializeCanvas(canvasWithContentsOfWindow(window))

   return true


Remove trailing comma

Remove trailing comma

--- lib/project-manager-add-view.coffee
+++ lib/project-manager-add-view.coffee
@@ -31,7 +31,7 @@
     @hide() if project.title

   hide: =>
-    atom.commands.dispatch(atom.views.getView(atom.workspace), 'focus');
+    atom.commands.dispatch(atom.views.getView(atom.workspace), 'focus')
     @panel.hide()

   show: =>


Added some trimming so that return is removed

--- lib/services/php-file-parser.coffee
+++ lib/services/php-file-parser.coffee
@@ -280,6 +280,8 @@
       element = element.replace /^\s+|\s+$/g, ""
       if element[0] == '{' or element[0] == '(' or element[0] == '['
         element = element.substring(1)
+      else if element.indexOf('return ') == 0
+        element = element.substring('return '.length)

       elements[key] = element


isInFunction - Fixed Atom and language-php quirks.

--- lib/services/php-file-parser.coffee
+++ lib/services/php-file-parser.coffee
@@ -188,6 +188,7 @@
     closedBlocks = 0

     result = false
+    isInClosure = false

     # for each row
     while row != -1
@@ -195,6 +196,7 @@

       character = 0
       lineLength = line.length
+      lastChain = null

       # Scan the entire line, fetching the scope for each character position as one line can contain both a scope start
       # and end such as "} elseif (true) {". Here the scope descriptor will differ for different character positions on
@@ -203,13 +205,27 @@
         # Get chain of all scopes
         chain = editor.scopeDescriptorForBufferPosition([row, character]).getScopeChain()

-        # }
-        if chain.indexOf("scope.end") != -1
-          closedBlocks++
-        # {
-        else if chain.indexOf("scope.begin") != -1
-          openedBlocks++
-
+        # NOTE: Atom quirk: both line.length and line.length - 1 return the same scope descriptor, BUT you can't skip
+        # scanning line.length as sometimes line.length - 1 does not return a scope descriptor at all.
+        if not (character == line.length and chain == lastChain)
+          # }
+          if chain.indexOf("scope.end") != -1
+            closedBlocks++
+          # {
+          else if chain.indexOf("scope.begin") != -1
+            openedBlocks++
+
+        # NOTE: atom/language-php quirk, when you open a closure definition, it's opening brace does NOT have the
+        # 'scope.begin' class. See also https://github.com/atom/language-php/issues/98 .
+        if chain.indexOf('.meta.function.closure.php') != -1
+          if not isInClosure
+            isInClosure = true
+            openedBlocks++
+
+        else
+          isInClosure = false
+
+        lastChain = chain
         character++

       # Get chain of all scopes


Fix #183 disable raw websocket url when websocket option is false

--- src/sockjs.coffee
+++ src/sockjs.coffee
@@ -75,14 +75,15 @@
         t = (s) => [p('/([^/.]+)/([^/.]+)' + s), 'server', 'session']
         opts_filters = (options_filter='xhr_options') ->
             return ['h_sid', 'xhr_cors', 'cache_for', options_filter, 'expose']
-        dispatcher = [
+        prefix_dispatcher = [
             ['GET',     p(''), ['welcome_screen']],
             ['GET',     p('/iframe[0-9-.a-z_]*.html'), ['iframe', 'cache_for', 'expose']],
             ['OPTIONS', p('/info'), opts_filters('info_options')],
             ['GET',     p('/info'), ['xhr_cors', 'h_no_cache', 'info', 'expose']],
             ['OPTIONS', p('/chunking_test'), opts_filters()],
-            ['POST',    p('/chunking_test'), ['xhr_cors', 'expect_xhr', 'chunking_test']],
-            ['GET',     p('/websocket'),   ['raw_websocket']],
+            ['POST',    p('/chunking_test'), ['xhr_cors', 'expect_xhr', 'chunking_test']]
+        ]
+        transport_dispatcher = [
             ['GET',     t('/jsonp'), ['h_sid', 'h_no_cache', 'jsonp']],
             ['POST',    t('/jsonp_send'), ['h_sid', 'h_no_cache', 'expect_form', 'jsonp_send']],
             ['POST',    t('/xhr'), ['h_sid', 'h_no_cache', 'xhr_cors', 'xhr_poll']],
@@ -97,13 +98,17 @@

         # TODO: remove this code on next major release
         if options.websocket
-            dispatcher.push(
+            prefix_dispatcher.push(
+                ['GET', p('/websocket'), ['raw_websocket']])
+            transport_dispatcher.push(
                 ['GET', t('/websocket'), ['sockjs_websocket']])
         else
             # modify urls to return 404
-            dispatcher.push(
+            prefix_dispatcher.push(
+                ['GET', p('/websocket'), ['cache_for', 'disabled_transport']])
+            transport_dispatcher.push(
                 ['GET', t('/websocket'), ['cache_for', 'disabled_transport']])
-        return dispatcher
+        return prefix_dispatcher.concat(transport_dispatcher)

 class Listener
     constructor: (@options, emit) ->


remove v_symmetry, h_symmetry from PlotCanvas. NOTE: these are completely ignored in BokehJS

--- bokehjs/src/coffee/models/plots/plot_canvas.coffee
+++ bokehjs/src/coffee/models/plots/plot_canvas.coffee
@@ -706,9 +706,6 @@
       plot_height:       [ p.Number,   600                    ]
       title:             [ p.Any                              ] # TODO: p.Either(p.Instance(Title), p.String)
       title_location:    [ p.Location, 'above'                ]
-
-      h_symmetry:        [ p.Bool,     true                   ]
-      v_symmetry:        [ p.Bool,     false                  ]

       above:             [ p.Array,    []                     ]
       below:             [ p.Array,    []                     ]


Debugging.

Debugging.

--- src/tucker.coffee
+++ src/tucker.coffee
@@ -92,6 +92,7 @@
   robot.hear /tucker/i, (res) ->
     message = res.message.text
     name = message.search(robot.name)
+    rn = robot.name
     re = new RegExp robot.name, 'i'
     robot.logger.info "name match = #{name}"
     robot.logger.info "robot name is = #{rn}"


Change optional require format to make webpack happier

--- src/yayson.coffee
+++ src/yayson.coffee
@@ -1,18 +1,11 @@
-
-tryRequire = (dep) ->
-  try
-    require dep
-  catch
-    undefined
-
 @window ||= {}

 Q = @window.Q
 _ = @window._

-Q ||= tryRequire 'q'
-_ ||= tryRequire 'lodash/dist/lodash.underscore'
-_ ||= tryRequire 'underscore'
+Q ||= (try require? 'q')
+_ ||= (try require? 'lodash/dist/lodash.underscore')
+_ ||= (try require? 'underscore')

 utils = require('./yayson/utils')(_, Q)

@@ -32,3 +25,5 @@
   Presenter: presenter({adapter})
   Adapter: Adapter

+
+


Update hooks pattern for speed-up tests

Update hooks pattern for speed-up tests

--- test/unit/add-hooks-test.coffee
+++ test/unit/add-hooks-test.coffee
@@ -33,7 +33,7 @@

   describe 'with valid pattern', () ->

-    pattern = './**/*_hooks.*'
+    pattern = './test/**/*_hooks.*'

     it 'should return files', (done)->
       mute (unmute) ->


Fix unique key warning in classifier

Fix unique key warning in classifier

--- app/classifier/index.cjsx
+++ app/classifier/index.cjsx
@@ -223,13 +223,15 @@
             disableInterventionFunction={@disableIntervention}
           />}

-          {persistentHooksBeforeTask.map (HookComponent) =>
-            <HookComponent {...taskHookProps} />}
+          {persistentHooksBeforeTask.map (HookComponent, i) =>
+            key = i + Math.random()
+            <HookComponent key={key} {...taskHookProps} />}

           <TaskComponent autoFocus={true} taskTypes={tasks} workflow={@props.workflow} task={task} preferences={@props.preferences} annotation={annotation} onChange={@handleAnnotationChange.bind this, classification} />

-          {persistentHooksAfterTask.map (HookComponent) =>
-            <HookComponent {...taskHookProps} />}
+          {persistentHooksAfterTask.map (HookComponent, i) =>
+            key = i + Math.random()
+            <HookComponent key={key} {...taskHookProps} />}


Only request for UPP for workflow assignment projects (#4649)

--- app/classifier/index.cjsx
+++ app/classifier/index.cjsx
@@ -115,8 +115,8 @@
     @maybeRequestUserProjectPreferences()
     @props.onComplete?()

-  maybeRequestUserProjectPreferences: ->
-    if classificationsThisSession % RELOAD_UPP_EVERY is 0
+  maybeRequestUserProjectPreferences: ->
+    if classificationsThisSession % RELOAD_UPP_EVERY is 0 and @props.project?.experimental_tools.includes('workflow assignment')
       @props.requestUserProjectPreferences(@props.project, @props.user)

   maybeLaunchMiniCourse: ->


Add Slideshare.net driver

Add Slideshare.net driver

--- coffee/app.coffee
+++ coffee/app.coffee
@@ -189,9 +189,9 @@
     # Based on url, load corresponding remote driver
     setupRemote: () ->
       @driver = switch
-        when @url.indexOf('://speakerdeck.com/') > 1 then new SpeakerdeskRemote
-        when @url.indexOf('://slideshare.net/') > 1  then new SlideshareRemote
-        when @url.indexOf('://www.scribd.com/') > 1  then new ScribdRemote
+        when @url.indexOf('speakerdeck.com/') > 1 then new SpeakerdeskRemote
+        when @url.indexOf('slideshare.net/') > 1  then new SlideshareRemote
+        when @url.indexOf('scribd.com/') > 1  then new ScribdRemote
         else new RabbitRemote

     getCurrentSlide: () ->
@@ -229,12 +229,18 @@
   class SpeakerdeskRemote extends RemoteControlDriver
     constructor: () ->
       super
+      # The player is put inside a iframe so, let get its contents
       @container = $('.speakerdeck-iframe ').contents()
       console? && console.log @container

+    getCurrentSlideNumber: () ->
+      $('.goToSlideLabel > input', @container).val()
+
     getCurrentSlideScreenshot: () ->
       $('#player-content-wrapper > #slide_image', @container).prop('src')
-
+
+    first: () ->
+    last: () ->
     next: () ->
       $('.overnav > .next', @container)[0].click()
     previous: () ->
@@ -243,16 +249,23 @@
   class SlideshareRemote extends RemoteControlDriver
     constructor: () ->
       super
-      @container = $('.speakerdeck-iframe ').contents()
+      @container = $('#svPlayerId')
       console? && console.log @container
+
+    getCurrentSlideNumber: () ->
+      $('.goToSlideLabel > input', @container).val()

     getCurrentSlideScreenshot: () ->
-      $('#player-content-wrapper > #slide_image', @container).prop('src')
-
+      $('.slide_container > .slide').eq(this.getCurrentSlideNumber()).find('img').prop('src')
+
+    first: () ->
+      $('.nav > .btnFirst', @container)[0].click()
+    last: () ->
+      $('.nav > .btnLast', @container)[0].click()
     next: () ->
-      $('.overnav > .next', @container)[0].click()
+      $('.nav > .btnNext', @container)[0].click()
     previous: () ->
-      $('.overnav > .prev', @container)[0].click()
+      $('.nav > .btnPrevious', @container)[0].click()

   class ScribdRemote extends RemoteControlDriver
   class RabbitRemote extends RemoteControlDriver


Agent pie: start/end angle pie transition work

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -231,7 +231,7 @@
         #        b = myInterpolate(t)
         #        return arc b
         #)
-        path.transition().duration(2000).attrTween('d', (d,i) ->
+        path.transition().duration(1000).attrTween('d', (d,i) ->
             s0 = undefined
             e0 = undefined
             if lastPieData[i]


Remove variable check that's always true.

--- community/browser/app/scripts/services/GraphStyle.coffee
+++ community/browser/app/scripts/services/GraphStyle.coffee
@@ -169,7 +169,6 @@
           @persist()
         if not rule.props.caption?
           default_caption = @getDefaultCaption(item)
-          return unless default_caption.caption?
           angular.extend(rule.props, default_caption)
           @rules.push(rule)
           @persist()


pricing: `VMProductForm` forwards "Cancel" event of upgrade form

--- client/Environments/views/vms/vmproductform.coffee
+++ client/Environments/views/vms/vmproductform.coffee
@@ -76,6 +76,8 @@
       else
         @collectData { plan }

+    @forwardEvent upgradeForm, "Cancel"
+
     @addForm 'upgrade', upgradeForm, ['plan', 'subscription']

     packChoiceForm = @createPackChoiceForm()


Client: Activity: Encoded channel name

Client: Activity: Encoded channel name

--- client/activity/lib/util/prepareThreadTitle.coffee
+++ client/activity/lib/util/prepareThreadTitle.coffee
@@ -2,13 +2,14 @@
 whoami                = require 'app/util/whoami'
 getParticipantOrigins = require 'activity/util/getParticipantOrigins'
 makeProfileText       = require 'activity/util/makeProfileText'
+Encoder               = require 'htmlencode'

 module.exports = prepareThreadTitle = (channel) ->

   name = channel.get 'name'

   if name
-    return {name}
+    return {Encoder.htmlDecode name}

   preview = channel.get 'participantsPreview'
   count   = channel.get 'participantCount'


client: load invoices with payment info for admins

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/redux/dispatchInitialActions.coffee
+++ client/app/lib/redux/dispatchInitialActions.coffee
@@ -7,6 +7,7 @@

 { load: loadPaymentInfo } = require 'app/redux/modules/payment/info'
 { load: loadSubscription } = require 'app/redux/modules/payment/subscription'
+{ load: loadInvoices } = require 'app/redux/modules/payment/invoices'

 loadAccount = ({ dispatch, getState }) ->
   dispatch {
@@ -29,7 +30,7 @@
 ensurePaymentDetails = ({ dispatch, getState }) ->

   if isAdmin()
-  then dispatch(loadPaymentInfo())
+  then dispatch(loadPaymentInfo()).then -> dispatch(loadInvoices())
   else dispatch(loadSubscription())


StackTemplateListItem: minor code fix

StackTemplateListItem: minor code fix

--- client/app/lib/stacks/stacktemplatelistitem.coffee
+++ client/app/lib/stacks/stacktemplatelistitem.coffee
@@ -128,8 +128,6 @@

     super

-    return @menu
-

   pistachio: ->


add beamer icon hiding to shift report modal; fixes #310

--- client/views/common/shiftReport/shiftReport.coffee
+++ client/views/common/shiftReport/shiftReport.coffee
@@ -51,6 +51,10 @@
 				$('.skipping').addClass('animated').removeClass('skipping')

 			Meteor.call 'initReport', FlowRouter.getQueryParam('showShiftReport'), FlowRouter.getQueryParam('reportTeamId')
+
+Template.shiftReport.onRendered ->
+
+	$('#beamerSelector').addClass('hidden')

 Template.shiftReport.onDestroyed ->


support other protocols

support other protocols

--- src/plugins/scaleApp.strophe.coffee
+++ src/plugins/scaleApp.strophe.coffee
@@ -9,22 +9,24 @@

 mediator = new scaleApp.Mediator

-ID           = "xmpp"
-DEFAULT_PATH = "http-bind/"
-DEFAULT_PORT = 5280
-CACHE_PREFIX = "scaleApp.#{ID}.cache."
+ID               = "xmpp"
+DEFAULT_PATH     = "http-bind/"
+DEFAULT_PROTOCOL = "http"
+DEFAULT_PORT     = 5280
+CACHE_PREFIX     = "scaleApp.#{ID}.cache."

 # Variable that holds the Strophe connection object
 connection = null

 # Object that holds all connection sepecific options.
 connection_options =
-  host: document.domain
-  port: DEFAULT_PORT
-  path: DEFAULT_PATH
-  jid: null
-  sid: null
-  rid: null
+  host:     document.domain
+  port:     DEFAULT_PORT
+  path:     DEFAULT_PATH
+  protocol: DEFAULT_PROTOCOL
+  jid:      null
+  sid:      null
+  rid:      null

 # Generates an appropriate bosh url for the connection.
 #
@@ -46,35 +48,35 @@

     # case 1
     if opt.host and opt.port and opt.path
-      return "http://#{ opt.host }:#{ opt.port }/#{ opt.path }"
+      return "#{ opt.protocol }://#{ opt.host }:#{ opt.port }/#{ opt.path }"

     # case 2
     if opt.host and opt.port and not opt.path
-      return "http://#{ opt.host }:#{ opt.port }/#{ DEFAULT_PATH }"
+      return "#{ opt.protocol }://#{ opt.host }:#{ opt.port }/#{ DEFAULT_PATH }"

     # case 3
     if opt.host and not opt.port and opt.path
-      return "http://#{ opt.host }/#{ opt.path }"
+      return "#{ opt.protocol }://#{ opt.host }/#{ opt.path }"

     # case 4
     if opt.host and not opt.port and not opt.path
-      return "http://#{ opt.host }/#{ DEFAULT_PATH }"
+      return "#{ opt.protocol }://#{ opt.host }/#{ DEFAULT_PATH }"

     # case 5
     if not opt.host and opt.port and opt.path
-      return "http://#{ domain }:#{ opt.port }/#{ opt.path }"
+      return "#{ opt.protocol }://#{ domain }:#{ opt.port }/#{ opt.path }"

     # case 6
     if not opt.host and opt.port and not opt.path
-      return "http://#{ domain }:#{ opt.port }/#{ DEFAULT_PATH }"
+      return "#{ opt.protocol }://#{ domain }:#{ opt.port }/#{ DEFAULT_PATH }"

     # case 7
     if not opt.host and not opt.port and opt.path
-      return "http://#{ domain }/#{ opt.path }"
+      return "#{ opt.protocol }://#{ domain }/#{ opt.path }"

     # case 8
     if not opt.host and not opt.port and not opt.path
-      return "http://#{ domain }/#{ DEFAULT_PATH }"
+      return "#{ opt.protocol }://#{ domain }/#{ DEFAULT_PATH }"

   # fallback
   "http://#{ domain }/#{ DEFAULT_PATH }"
@@ -83,9 +85,10 @@
 create_connection_obj = ->

   new Strophe.Connection get_bosh_url
-    path: connection_options.path
-    host: connection_options.host
-    port: connection_options.port
+    path:     connection_options.path
+    host:     connection_options.host
+    port:     connection_options.port
+    protocol: connection_options.protocol

 key2cache   = (k) -> "#{CACHE_PREFIX}#{k}"

@@ -94,7 +97,7 @@
     for k in ["jid", "sid", "rid"] when connection[k]?
       sessionStorage[key2cache k] = connection[k]

-    for k in ["host", "port", "path"] when connection_options[k]?
+    for k in ["host", "port", "path", "protocol"] when connection_options[k]?
       sessionStorage[key2cache k] = connection_options[k]

 clearData = -> sessionStorage?.clear()
@@ -211,9 +214,10 @@
 login = (jid, pw, opt) ->

   if opt?
-    connection_options.path = opt.path if opt.path
-    connection_options.port = opt.port if opt.port
-    connection_options.host = opt.host if opt.host
+    connection_options.path     = opt.path     if opt.path
+    connection_options.port     = opt.port     if opt.port
+    connection_options.host     = opt.host     if opt.host
+    connection_options.protocol = opt.protocol if opt.protocol

   connection = create_connection_obj()
   connection.connect jid, pw, on_connection_change
@@ -243,7 +247,7 @@
 # Loads the connection data from local storage.
 restoreData = ->
   if sessionStorage?
-    for k in [ "jid", "sid", "rid", "host", "port", "path" ]
+    for k in [ "jid", "sid", "rid", "host", "port", "path", "protocol" ]
       j = key2cache k
       connection_options[k] = sessionStorage[j]  if sessionStorage[j]


Try, try again.

Try, try again.

--- common/lib/xmodule/xmodule/js/src/html/edit.coffee
+++ common/lib/xmodule/xmodule/js/src/html/edit.coffee
@@ -50,7 +50,7 @@
     })

     @showingVisualEditor = true
-    @element.on('click', '.editor-tabs .tab', @onSwitchEditor)
+    @element.on('click', '.editor-tabs .tab', this, @onSwitchEditor)

   @setupTinyMCE: (ed) ->
     ed.addButton('wrapAsCode', {
@@ -71,16 +71,18 @@
     e.preventDefault();

     if not $(e.currentTarget).hasClass('current')
+      element = e.data.element
+
       $(e.currentTarget).addClass('current')
-      $('table.mceToolbar', @element).toggleClass(HTMLEditingDescriptor.isInactiveClass)
+      $(element).find('table.mceToolbar').toggleClass(HTMLEditingDescriptor.isInactiveClass)
       $(@advanced_editor.getWrapperElement()).toggleClass(HTMLEditingDescriptor.isInactiveClass)

-      visualEditor = @getVisualEditor()
+      visualEditor = @getVisualEditor(element)
       if $(e.currentTarget).attr('data-tab') is 'visual'
-        $('.html-tab', @element).removeClass('current')
+        $(element).find('.html-tab').removeClass('current')
         @showVisualEditor(visualEditor)
       else
-        $('.visual-tab', @element).removeClass('current')
+        $(element).find('.visual-tab').removeClass('current')
         @showAdvancedEditor(visualEditor)

   # Show the Advanced (codemirror) Editor. Pulled out as a helper method for unit testing.
@@ -105,17 +107,17 @@
   focusVisualEditor: (visualEditor) ->
     visualEditor.focus()

-  getVisualEditor: ->
+  getVisualEditor: (element) ->
     ###
     Returns the instance of TinyMCE.
     This is different from the textarea that exists in the HTML template (@tiny_mce_textarea.
     ###
-    return tinyMCE.get($('.tiny-mce', this.element).attr('id'))
+    return tinyMCE.get($(element).find('.tiny-mce').attr('id'))

   save: ->
     @element.off('click', '.editor-tabs .tab', @onSwitchEditor)
     text = @advanced_editor.getValue()
-    visualEditor = @getVisualEditor()
+    visualEditor = @getVisualEditor(@element)
     if @showingVisualEditor and visualEditor.isDirty()
       text = visualEditor.getContent({no_events: 1})
     data: text


sidebar: request machines from compute storage

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/sidebar/view.coffee
+++ client/app/lib/sidebar/view.coffee
@@ -7,7 +7,7 @@
 Container = require './container'

 ConnectedContainer = connectCompute({
-  storage: ['stacks', 'templates']
+  storage: ['stacks', 'templates', 'machines']
 })(Container)

 module.exports = class SidebarView extends ReactView


Environments: Added "Update init script" menu to vm item and let user to update vm init script with an editor.

--- client/Environments/views/scene/environmentmachineitem.coffee
+++ client/Environments/views/scene/environmentmachineitem.coffee
@@ -24,25 +24,25 @@
     vmName = @getData().hostnameAlias
     vmAlwaysOnSwitch = new VMAlwaysOnToggleButtonView null, {vmName}
     items =
-      customView4        : vmAlwaysOnSwitch
-      'Re-initialize VM' :
-        disabled         : KD.isGuest()
-        callback         : ->
+      customView4         : vmAlwaysOnSwitch
+      'Re-initialize VM'  :
+        disabled          : KD.isGuest()
+        callback          : ->
           KD.getSingleton("vmController").reinitialize vmName
           @destroy()
-      'Open VM Terminal' :
-        callback         : =>
+      'Open VM Terminal'  :
+        callback          : =>
           @openTerminal()
           @destroy()
-        separator        : yes
-      'Delete'           :
-        disabled         : KD.isGuest()
-        separator        : yes
-        action           : 'delete'
-      'Unfocus'          :
-        separator        : yes
-        action           : 'unfocus'
-      customView3        : colorSelection
+        separator         : yes
+      'Update init script':
+        separator         : yes
+        callback          : @bound "showInitScriptEditor"
+      'Delete'            :
+        disabled          : KD.isGuest()
+        separator         : yes
+        action            : 'delete'
+      customView3         : colorSelection

     return items

@@ -53,6 +53,22 @@

   confirmDestroy:->
     KD.getSingleton('vmController').remove @getData().hostnameAlias, @bound "destroy"
+
+  showInitScriptEditor: ->
+    modal =  new EditorModal
+      editor              :
+        title             : "VM Init Script Editor"
+        content           : @data.meta?.initScript or ""
+        saveMessage       : "VM init script saved"
+        saveFailedMessage : "Couldn't save VM init script"
+        saveCallback      : (script, modal) =>
+          KD.remote.api.JVM.updateInitScript @data.hostnameAlias, script, (err, res) =>
+            if err
+              modal.emit "SaveFailed"
+            else
+              modal.emit "Saved"
+              @data.meta or= {}
+              @data.meta.initScript = Encoder.htmlEncode modal.editor.getValue()

   pistachio:->
     title = @getData().hostnameAlias


Client: Activity: Test: Updated ChannelThreadHeader component include style

--- client/activity/lib/components/channelthreadpane/index.coffee
+++ client/activity/lib/components/channelthreadpane/index.coffee
@@ -65,13 +65,12 @@
     return  unless thread = @state.channelThread

     if not isKoding()
-      <ChannelThreadHeader
+      <ChannelThreadHeader.Container
         className="ChannelThreadPane-header"
         thread={thread}
         onInvitePeople={@bound 'invitePeople'}
         onLeaveChannel={@bound 'leaveChannel'}
-        onShowNotificationSettings={@bound 'showNotificationSettingsModal'}>
-      </ChannelThreadHeader>
+        onShowNotificationSettings={@bound 'showNotificationSettingsModal'} />

   renderPaneByTypeConstant: (thread) ->


activity: make sure threads are created before comparing them

--- client/activity/lib/components/channelthreadpane/index.coffee
+++ client/activity/lib/components/channelthreadpane/index.coffee
@@ -102,8 +102,10 @@

   componentWillUpdate: (nextProps, nextState) ->

+    return  unless @state.channelThread and nextState.channelThread
+
     channelId          = @state.channelThread.get 'channelId'
-    nextStateChannelId = nextState.channelThread?.get 'channelId'
+    nextStateChannelId = nextState.channelThread.get 'channelId'

     return @setState editingPurpose: no  if channelId isnt nextStateChannelId


DownloadLink: Fixed user join date.

DownloadLink: Fixed user join date.

--- client/app/Applications/StartTab.kdapplication/AppView.coffee
+++ client/app/Applications/StartTab.kdapplication/AppView.coffee
@@ -54,7 +54,7 @@
       cssClass : 'file-container'

     @downloadFilesLink = new KDCustomHTMLView
-    userJoinDate       = KD.whoami().meta.createdAt
+    userJoinDate       = new Date(KD.whoami().meta.createdAt).getTime()
     oldKodingDownDate  = 1374267600000

     if userJoinDate < oldKodingDownDate


add pokes to breeds helper

add pokes to breeds helper

--- app/helpers/breeds.coffee
+++ app/helpers/breeds.coffee
@@ -1,9 +1,13 @@
 `import bulbasaur from './breeds/bulbasaur'`
+`import ivysaur from './breeds/ivysaur'`
 `import charmander from './breeds/charmander'`
+`import charmeleon from './breeds/charmeleon'`

 breeds = {
     bulbasaur
+    ivysaur
     charmander
+    charmeleon
 }

 `export default breeds`


Run copy-i18n-tags (#6217)

Run copy-i18n-tags (#6217)

Co-authored-by: CoCo CI <5df1af3d7600b97ee69552caffeccf9dfd29ab8f@codecombat.com>


update

update

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -2,7 +2,7 @@

   new_home:
     title: "扣哒世界|CodeCombat中国|AI世青赛|少儿编程等级认证"
-    meta_keywords: "扣哒世界, 青少年编程, 少儿编程, CodeCombat官网,CodeCombat中文,游戏化编程,AI世青赛,信息学奥赛,奥佳睿,CodeCombat中国"
+    meta_keywords: "扣哒世界, 青少年编程, 少儿编程, 信息学奥赛, CodeCombat官网,CodeCombat中文,游戏化编程,AI世青赛,信息学奥赛,奥佳睿,CodeCombat中国"
     meta_description: "扣哒世界，CodeCombat中国唯一官方网站，支持Python, C++, JavaScript, 美国大学理事会（AP)认证，AI世青赛全球独家技术平台，支持中美信息学奥赛(NOI, CSP-J/S, USACO)、电子学会考试和国际青少年编程认证。"
     meta_og_url: "https://koudashijie.com"
     built_for_teachers_title: "一个以教师为中心的课程体系"


removed commented out code

removed commented out code

--- client/main.cjsx
+++ client/main.cjsx
@@ -111,6 +111,4 @@
   reducers = combineReducers (Object.assign (require '/imports/redux/reducers/index.coffee'), router: routerReducer)
   store = createStore reducers, init, composeEnhancers ((require '/imports/redux/middleware/index.coffee') ddp)

-
-  # render <WrappedWrapper />, document.getElementById 'render-target'
   render routes(store, {}), document.getElementById 'render-target'


refactor openlinkinput

refactor openlinkinput

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -146,7 +146,7 @@
 		left = ( startEl.offset().left + endEl.offset().left ) / 2
 		$('#toolpad').css 'top', top - 35
 		$('#toolpad').css 'left', left - 90
-		@update()
+
 		@hideTooltip()
 		@status.showToolpad = true
 	hideToolpad: ->
@@ -155,6 +155,8 @@
 		@lastLinkElement = null
 		@linkRange = null
 		@status.showToolpad = false
+	openLinkInput: ->
+		$('#toolpad li:first-child').addClass 'toggled'
 	cancelLinkInput: ->
 		$('#toolpad li:first-child').removeClass 'toggled'

@@ -198,7 +200,7 @@
 					@showToolpadOverSelection 'toolpad'
 					@restoreSelection 'toolpad'
 		$('#toolpad').delegate 'li:first-child', 'click', (e)->
-			$(@).addClass 'toggled'
+			self.openLinkInput()
 			if self.linkRange
 				a = document.createElement 'a'
 				a.setAttribute 'href', '#'


Added todos.

Added todos.

--- app/server/methods.coffee
+++ app/server/methods.coffee
@@ -1,25 +1,32 @@
+# TODO #1: Add MAIL_URL to the settings.json file.
+#
+# TODO #2: Let's initialize this global variable in some other place.
+# TODO #2: This file is only for server-side methods.
 Meteor.startup () ->
   process.env.MAIL_URL = Meteor.settings["mail_url"]

 Meteor.methods
   sendCompanyEmail: (doc) ->
-    console.log 'doc => ', doc
     check doc, schemas.Invitation
+    @unblock()
     doc.emails.forEach (item) =>
+      # TODO #4: It would be good make it in a bit more asynchronous way ;)
+      # TODO #4: We could add callback into the insert function.
+      # TODO #4: In this callback we should send email if there wasn't errors.
       invitationId = Invitations.insert
         companyId: doc.companyId
         message: doc.message or ""
         emails: [item]
-
-      @unblock()
-
       Email.send
         to: item
+        # TODO #3: Make this option configurable as well.
         from: 'stepan_ua@i.ua'
         subject: 'Invitation to ' + doc.companyId
         text: "#{Meteor.absoluteUrl()}invite/#{invitationId}"

   invitationStatus: (id) ->
     check id, String
+    # TODO #5: Pls check if user has enough rights to activate this invitation.
     Invitations.update id,
-      $set: isActive: true+      $set: isActive: true
+    # TODO #6: Don't forget to deactivate all other invitations of this user.


save options file

save options file

--- electron/ui/app.coffee
+++ electron/ui/app.coffee
@@ -8,6 +8,7 @@
   PATH = require 'path'

   TEMPORARY_FILE = 'temporary.swp.ds'
+  OPTIONS_FILE = 'options.json'
   PROTOCOLS = [
    'https://'
    'http://'
@@ -28,6 +29,9 @@
     @resources = {}
     @_editorChanged = false
     @content = ''
+    @options =
+     file: ''
+     folder: ''
     @editor = new Editor
      openUrl: @on.openUrl
      onChanged: @on.editorChanged
@@ -35,7 +39,7 @@
    load: (callback) ->
     IPC.on 'userDataPath', (e, path) =>
      @_userDataPath = path
-     console.log path
+     #TODO load options, temporary
      callback()
     IPC.send 'getUserDataPath'

@@ -122,7 +126,10 @@
      path: file
     @content = "#{FS.readFileSync file}"
     @editor.setText @content
-    @_editorChanged = false
+    @_editorChanged = true
+    @on.saveTemporary()
+    @options.file = @file.path
+    @saveOptions()
     @elems.save.style.display = 'inline-block'
     @elems.saveName.textContent = "#{@file.name}"

@@ -141,6 +148,15 @@
      lines[i] = line.trimRight()

     lines.join '\n'
+
+   saveOptions: ->
+    FS.writeFile (PATH.join @_userDataPath, OPTIONS_FILE),
+     (JSON.stringify @options)
+     (err) ->
+      if err?
+       console.error err
+      else
+       console.log 'options file saved'

    @listen 'saveTemporary', ->
     return if not @file?


Adding DomainTools to reputation url links (#70)

--- src/scripts/reputation-links.coffee
+++ src/scripts/reputation-links.coffee
@@ -33,10 +33,11 @@
     target_url = msg.match[1].toLowerCase()

     reputation_links = """#{target_url} URL Reputation:
-    - Robtext:    https://pop.robtex.com/#{target_url}.html
-    - CentralOps: http://centralops.net/co/DomainDossier.aspx?addr=#{target_url}&dom_whois=true&dom_dns=true&net_whois=true
-    - URLVoid:    http://www.urlvoid.com/scan/#{target_url}/
-    - HE:         http://bgp.he.net/dns/#{target_url}#_whois
+    - Robtext:     https://pop.robtex.com/#{target_url}.html
+    - CentralOps:  http://centralops.net/co/DomainDossier.aspx?addr=#{target_url}&dom_whois=true&dom_dns=true&net_whois=true
+    - URLVoid:     http://www.urlvoid.com/scan/#{target_url}/
+    - HE:          http://bgp.he.net/dns/#{target_url}#_whois
+    - DomainTools: http://whois.domaintools.com/#{target_url}
     """

     msg.send reputation_links


hotfix for #2851

hotfix for #2851

--- src/smc-webapp/sagews/sagews.coffee
+++ src/smc-webapp/sagews/sagews.coffee
@@ -1592,8 +1592,11 @@
                         elt = $("

")
                         elt.data('uuid',val.uuid)
                         output.append(elt)
-                        require.ensure [], () =>   # only load 3d library if needed
-                            require('./3d').render_3d_scene
+                        # TODO this is a temporary fix -- uncomment it again and remove the if true line once "require.ensure" works again
+                        #require.ensure [], () =>   # only load 3d library if needed
+                        if true
+                            {render_3d_scene} = require('./3d')
+                            render_3d_scene
                                 url     : target
                                 element : elt
                                 cb      : (err, obj) =>


fix https://github.com/sagemathinc/cocalc/issues/3459 -- "sagews: get_all_cells → Cannot read property 'lineCount' of undefined "

--- src/smc-webapp/sagews/sagews.coffee
+++ src/smc-webapp/sagews/sagews.coffee
@@ -266,6 +266,8 @@

     get_all_cells: =>
         cm = @focused_codemirror()
+        if not cm?
+            return []
         cells = []
         top = undefined
         process_line = (n) =>


Update meta overrides

Update meta overrides

--- models/mixins/meta_overrides.coffee
+++ models/mixins/meta_overrides.coffee
@@ -34,3 +34,43 @@
   'zaria-forman':
     title: 'Zaria Forman - Prints & paintings for sale (with prices) on Artsy'
     description: 'Find the best collection of Zaria Forman art, including paintings & prints for sale. Explore her biography, current shows, and articles. Zaria Forman’s pristine, photorealist paintings'
+
+  'vik-muniz':
+    title: 'Vik Muniz - 89 Artworks, Biography & Shows on Artsy'
+    description: 'Browse the best of Vik Minuz, including his biography, artwork & prints for sale, upcoming shows, and exclusive Vik Muniz articles.'
+
+  'blek-le-rat':
+    title: 'Blek Le Rat - 35 Artworks, Biography & Shows on Artsy'
+    description: 'Browse the best of Blek le Rat, including his works & prints for sale, exclusive articles, and related artists. Blek Le Rat counts the infamous Banksy among his many admirers.'
+
+  'john-currin':
+    title: 'John Currin - 50 Artworks, Biography & Shows on Artsy'
+    description: 'Browse the best of John Currin, including paintings & other artworks for sale, his biography, the latest shows & events, and exclusive John Currin articles.'
+
+  'ai-weiwei':
+    title: 'Ai Weiwei - 91 Artworks, Biography & Shows on Artsy'
+    description: 'BrowseFind the best of Ai Weiwei art, including his sculptures, installations & more. Read about Ai Weiwei & browse his latest shows & exclusive articles on Artsy.'
+
+  'jim-dine':
+    title: 'Jim Dine - 196 Artworks, Biography & Shows on Artsy'
+    description: 'Find the best of Jim Dine - his artworks (including Hearts & Robes) & prints for sale, current shows & more. Emerging as a pioneer of New York’s Happenings of the 1960s, Jim Dine...'
+
+  'alex-katz':
+    title: 'Alex Katz - 188 Artworks, Biography & Shows on Artsy'
+    description: 'Browse the best of Alex Katz, including his paintings, prints for sale, exclusive articles, upcoming shows & more. New York School painter Alex Katz developed his highly stylized aesthetic in reaction to...'
+
+  'richard-serra':
+    title: 'Richard Serra - 148 Artworks, Biography & Shows on Artsy'
+    description: 'Browse the best of Richard Serra, including his artwork & prints for sale, upcoming shows, his biography, and exclusive Richard Serra articles. The monumental sculptures of Richard Serra...'
+
+  'andy-warhol':
+    title: 'Andy Warhol Art - 662 Artworks, Biography & Shows on Artsy'
+    description: 'Browse the best of Andy Warhol art, including paintings and artwork for sale (with prices), upcoming shows, and exclusive Andy Warhol articles.'
+
+  'tauba-auerbach':
+    title: 'Tauba Auerbach - Her Artworks, Bio & Shows on Artsy'
+    description: 'Browse the best of Tauba Auerbach, including her artwork & prints for sale, upcoming shows, biography & exclusive Tauba Auerbach articles. Tauba Auerbach is best known for her Op art-inflected paintings that play with perceptions of space.'
+
+  'lucien-smith':
+    title: 'Lucien Smith - 31 Artworks, Biography & Shows on Artsy'
+    description: 'Browse the best of Lucien Smith, including his Rain paintings series, upcoming shows, biography & exclusive Lucien Smith articles. Lucien Smith creates work that traverses a spectrum of styles...'


fallback tab fix

fallback tab fix

--- templates:tabs.coffee
+++ templates:tabs.coffee
@@ -50,10 +50,6 @@
           if activeTab?.onRender?
             activeTab.onRender();

-        # If no active tab specified, default to the first tab
-        if self._activeTab.get()?.slug is null
-          self._activeTab.set(tabs[0].slug)
-
       helpers = {
         activeTab: (slug) ->
           if Template.instance()._activeTab.get()?.slug is slug


Default value of targetSubjectID Set targetSubjectID to current subject ID for new classifications. Override it if a subjectID param is set in the URL.

--- src/classify-page.coffee
+++ src/classify-page.coffee
@@ -113,7 +113,7 @@
     result.promise()

   onActivate: (e) ->
-    @targetSubjectID = e.originalEvent.detail.subjectID
+    @targetSubjectID = e.originalEvent.detail.subjectID if e.originalEvent.detail.subjectID.length
     if @classification?
       unless @targetSubjectID is @classification?.subject.zooniverse_id
         @getNextSubject()
@@ -164,6 +164,8 @@
     super
     if subject.zooniverse_id is @targetSubjectID
       @classification.set 'chosen_subject', true
+
+    @targetSubjectID = subject.zooniverse_id

   loadSubject: (subject, callback) ->
     args = arguments


more saucelabs nonsense

more saucelabs nonsense

--- test/app_tests.coffee
+++ test/app_tests.coffee
@@ -7,7 +7,7 @@

 SHIFT_ENTER = wd.SPECIAL_KEYS.Shift + wd.SPECIAL_KEYS.Return + wd.SPECIAL_KEYS.NULL
 command_key = (browser) ->
-  if browser.capabilities.platform == 'MAC'
+  if browser.capabilities.platform == 'MAC' or browser.capabilities.platform == 'Mac OS X'
     wd.SPECIAL_KEYS.Command
   else
     wd.SPECIAL_KEYS.Control


Added Needed ApiGet dependency in app.coffee.

--- app/scripts/controllers/app.coffee
+++ app/scripts/controllers/app.coffee
@@ -1,7 +1,7 @@
 'use strict'

 angular.module('appApp.controllers')
-  .controller 'AppCtrl', ['$scope', ($scope) ->
+  .controller 'AppCtrl', ['$scope', 'ApiGet', ($scope, ApiGet) ->
     # init root ctrl vars to share data between controllers
     $scope.reps = {}
     $scope.reps_list = []


Fix missing `` ` `` - #2926

Fix missing `` ` `` - #2926

--- src/coffee-script.coffee
+++ src/coffee-script.coffee
@@ -24,7 +24,7 @@
 # options that can be passed to `SourceMap#generate` may also be passed here.
 #
 # This returns a javascript string, unless `options.sourceMap` is passed,
-# in which case this returns a `{js, v3SourceMap, sourceMap}
+# in which case this returns a `{js, v3SourceMap, sourceMap}`
 # object, where sourceMap is a sourcemap.coffee#SourceMap object, handy for doing programatic
 # lookups.
 exports.compile = compile = (code, options = {}) ->


Update zh-HANS.coffee (#4001)

Update zh-HANS.coffee (#4001)

Complete the remaining translations

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -787,7 +787,7 @@
     elliot_title: "客户经理"
     elliot_blurb: "读心者"
     lisa_title: "市场开发代表" # {change}
-#    lisa_blurb: "A gritty one"
+    lisa_blurb: "坚韧不拔的人"
     sean_title: "领土经理"
     retrostyle_title: "插画师"
     retrostyle_blurb: "复古风格的游戏"
@@ -1485,6 +1485,7 @@
     cs1: "计算机科学的介绍"
     cs2: "计算机科学 2"
     cs3: "计算机科学 3"
+    cs4: "计算机科学 4"
     cs1_syntax_python: "课程 1 Python 语法指南"
     cs1_syntax_python_desc: "参考常见的 Python 语法，学生将学习计算科学的介绍."
     cs1_syntax_javascript: "课程 1 JavaScript 语法指南"
@@ -1501,6 +1502,8 @@
     cs2_curriculum_desc: "课程 2 的范围、顺序、课程计划和活动等."
     cs3_curriculum: "计算机科学 3 - 课程指南"
     cs3_curriculum_desc: "课程 3 的范围、顺序、课程计划和活动等."
+    cs4_curriculum: "计算机科学 4 - 课程指南"
+    cs4_curriculum_desc: "课程 4 的范围、顺序、课程计划和活动等."
     cs1_pairprogramming: "组队编程活动"
     cs1_pairprogramming_desc: "让两个学生一起组队编程练习，这将有助于他们互相更好的学习与沟通."
     unlock_resources: "解锁更多资源!"
@@ -1516,20 +1519,20 @@
     class_average: "班级平均"
     not_assigned: "尚未分配以下课程"
     playtime_axis: "游戏时间(秒)"
-#    levels_axis: "Levels in"
-#    student_state: "How is"
-#    student_state_2: "doing?"
-#    student_good: "is doing well in"
+    levels_axis: "关卡来自于"
+    student_state: "看看"
+    student_state_2: "的情况?"
+    student_good: "做得很好，关卡"
     student_good_detail: "这个学生的课程进度."
     student_warn: "可能需要一些帮助"
     student_warn_detail: "这个学生可能需要一些帮助，在本课程中介绍的新概念."
-#    student_great: "is doing great in"
+    student_great: "做得太好了，关卡"
     student_great_detail: "这个学生可能是一个很好的候选人，帮助其他学生通过这门课程."

   sharing:
     game: "游戏"
     webpage: "网页"
-#    your_students_preview: "Your students will click here to see their finished projects! Unavailable in teacher preview."
+    your_students_preview: "您的学生可以点击这里查看他们的完成项目，在老师预览中不可用."
     unavailable: "教师预览中不提供链接共享."
     share_game: "分享此游戏"
     share_web: "分享此网页"
@@ -1960,17 +1963,17 @@
     vectors: "向量"
     while_loops: "While 循环"
     recursion: "递归"
-#    basic_html: "Basic HTML" # TODO: these web-dev concepts will change, don't need to translate
-#    basic_css: "Basic CSS"
-#    basic_web_scripting: "Basic Web Scripting"
-#    intermediate_html: "Intermediate HTML"
-#    intermediate_css: "Intermediate CSS"
-#    intermediate_web_scripting: "Intermediate Web Scripting"
-#    advanced_html: "Advanced HTML"
-#    advanced_css: "Advanced CSS"
-#    advanced_web_scripting: "Advanced Web Scripting"
-#    jquery: "jQuery"
-#    bootstrap: "Bootstrap"
+    basic_html: "基础 HTML" # TODO: these web-dev concepts will change, don't need to translate
+    basic_css: "基础 CSS"
+    basic_web_scripting: "基础 Web 脚本"
+    intermediate_html: "中级 HTML"
+    intermediate_css: "中级 CSS"
+    intermediate_web_scripting: "中级 Web 脚本"
+    advanced_html: "高级 HTML"
+    advanced_css: "高级 CSS"
+    advanced_web_scripting: "高级 Web 脚本"
+    jquery: "jQuery"
+    bootstrap: "Bootstrap"

   delta:
     added: "被添加"


Remove unused action groups

Remove unused action groups

--- src/coffee/import.coffee
+++ src/coffee/import.coffee
@@ -236,9 +236,6 @@
       { type: 'references', group: 'white' }
       { type: 'attributes', group: 'white' }
       { type: 'variants', group: 'white' }
-      { type: 'metaTitle', group: 'white' }
-      { type: 'metaDescription', group: 'white' }
-      { type: 'metaKeywords', group: 'white' }
       { type: 'categories', group: 'white' }
     ]
     if header.has(CONS.HEADER_PRICES)


Whitespace cleanup.

Whitespace cleanup.

--- test/core/util.coffee
+++ test/core/util.coffee
@@ -77,7 +77,3 @@
       styles = Epoch.Util.getComputedStyle(div)
       for k, v of overrideStyles
         assert.equal styles[k], v, "ComputedStyles['#{k}'] should be '#{v}'"
-
-
-
-


Fixed call to hubot-test-helper.

Fixed call to hubot-test-helper.

--- test/dice_test.coffee
+++ test/dice_test.coffee
@@ -1,4 +1,4 @@
-Helper = require('../node_modules/hubot-test-helper/src/index.coffee')
+Helper = require('hubot-tese-helper')

 helper = new Helper('../scripts/dice.coffee')


switch tests to ical (to be removed)

switch tests to ical (to be removed)

--- test/ical_test.coffee
+++ test/ical_test.coffee
@@ -7,7 +7,7 @@
 clientDS = new Client 'http://localhost:9101'
 helpers = require './helpers'

-{ICalParser, VCalendar, VAlarm, VTodo, VEvent} = require '../lib/ical_helpers'
+{ICalParser, VCalendar, VAlarm, VTodo, VEvent} = require 'cozy-ical'

 expectedContent = """
     BEGIN:VCALENDAR


fix remove slack logic

fix remove slack logic

--- vue/src/shared/services/group_service.coffee
+++ vue/src/shared/services/group_service.coffee
@@ -58,8 +58,7 @@
     install_slack:
       name: 'install_slack.modal_title'
       canPerform: ->
-        AbilityService.canAdministerGroup(group) &&
-        !Session.user().identityFor('slack')
+        AbilityService.canAdministerGroup(group) && !Session.user().identityFor('slack')
       perform: ->
         openModal
           component: 'InstallSlackModal'
@@ -67,8 +66,7 @@
     remove_slack:
       name: 'install_slack.remove_slack'
       canPerform: ->
-        AbilityService.canAdministerGroup(group) &&
-        !Session.user().identityFor('slack')
+        AbilityService.canAdministerGroup(group) && Session.user().identityFor('slack')
       perform: ->
         openModal
           component: 'ConfirmModal'


Adds an option to output prettified JSON in the Handlebars helper

--- src/utils/handlebars-helpers.coffee
+++ src/utils/handlebars-helpers.coffee
@@ -18,7 +18,7 @@
      * Fallback Helper for falsy values.
      *
      *      user.picture = undefined
-     *
+     *
      *
      *     =>
      *
@@ -104,8 +104,11 @@
      * @param  {Object} obj Any javascript object
      * @return {String}     A String dump of the object
     ###
-    HandlebarsHelpers.json = (obj)->
-      JSON.stringify obj
+    HandlebarsHelpers.json = (obj, pretty)->
+      if (pretty)
+        JSON.stringify obj, null, 2
+      else
+        JSON.stringify obj

     ###*


test charmap altering

test charmap altering

--- test/slug.test.coffee
+++ test/slug.test.coffee
@@ -131,3 +131,9 @@
     it 'should allow forcing lowercase slugs', ->
         [slug('FOO Bar baZ').toLowerCase()].should.eql ['foo-bar-baz']
         [slug('FOO Bar baZ', replacement:'_').toLowerCase()].should.eql ['foo_bar_baz']
+
+    it 'should allow altering the charmap', ->
+        charmap = {
+            'f': 'ph', 'o':'0', 'b':'8', 'a':'4', 'r':'2', 'z':'5'
+        }
+        [slug("foo bar baz", {charmap}).toUpperCase()].should.eql ['PH00-842-845']


appstorage: defensive check in `AppStorage#fetchStorage` callback

--- client/Main/AppStorage.coffee
+++ client/Main/AppStorage.coffee
@@ -29,7 +29,7 @@

     @reset()
     @fetchStorage (storage)->
-      callback  if storage[group]?[key] then storage[group][key]
+      callback  if storage?[group]?[key] then storage[group][key]
     , force

   getValue: (key, group = 'bucket')->
@@ -46,7 +46,7 @@
     @_storageData[group][key] = value

     @fetchStorage (storage)->
-      storage.update {
+      storage?.update {
         $set: pack
       }, -> callback?()

@@ -85,4 +85,4 @@
     return storage

 # Let people can use AppStorage
-KD.classes.AppStorage = AppStorage+KD.classes.AppStorage = AppStorage