refactor(stock-import): Add better method for escaping quotes from SKU

--- src/coffee/stockimport.coffee
+++ src/coffee/stockimport.coffee
@@ -34,7 +34,7 @@
       else throw new Error "Unsupported mode (file extension) for file #{fileName} (use csv or xml)"

   _escapeSku: (sku) ->
-    sku.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
+    JSON.stringify(sku)

   ###
   Elastic.io calls this for each csv row, so each inventory entry will be processed at a time
@@ -69,7 +69,7 @@
         else
           Promise.resolve(msg.body.CHANNEL_ID)

-      @client.inventoryEntries.where("sku=\"#{@_escapeSku(msg.body.SKU)}\"").perPage(1).fetch()
+      @client.inventoryEntries.where("sku=#{@_escapeSku(msg.body.SKU)}").perPage(1).fetch()
       .then (results) =>
         debug 'Existing entries: %j', results
         existingEntries = results.body.results
@@ -305,7 +305,7 @@
       skus = _.map uniqueStocksToProcessBySku, (s) =>
         @_summary.emptySKU++ if _.isEmpty s.sku
         # TODO: query also for channel?
-        "\"#{@_escapeSku(s.sku)}\""
+        "#{@_escapeSku(s.sku)}"
       predicate = "sku in (#{skus.join(', ')})"

       @client.inventoryEntries.all()


Fixes bug in js object expression dsl which caused it to create too many extra expressions

--- javascripts/lib/math/build_expression_from_javascript_object.coffee
+++ javascripts/lib/math/build_expression_from_javascript_object.coffee
@@ -18,7 +18,8 @@
         @blank_builder = @opts.blank_builder || components.blank
         @multiplication_builder = @opts.multiplication_builder || components.multiplication

-      process: (object_to_convert)->
+
+      proc_method = (object_to_convert)->
         return @blank_builder.build() unless object_to_convert
         if object_to_convert instanceof Array
           @convertSubExpression(object_to_convert)
@@ -36,6 +37,8 @@
                   @numberFromString(obj)
                 else throw "STRING NOT IMPLEMENTED"
             when "object" then @convertObject(object_to_convert)
+      process: logger().instrument(name: "Process", fn: proc_method)
+

       matchesNumberRegexp: (str)->
         str.search(/\d+/) != -1
@@ -52,7 +55,8 @@
       convertSubExpression: (parts) ->
         exp = @expression_builder.build()
         for x in parts
-          exp = exp.append(@process(x))
+          converted_part = @process(x)
+          exp = exp.append(converted_part)
         exp

       convertObject: (object)->
@@ -73,21 +77,27 @@
           power: power
         )

-      convertImplicitSubexp: (subexp_raw)->
-        if typeof subexp_raw == "number"
-          @expression_builder.build(expression: [@process(subexp_raw)])
-        else if subexp_raw == null
+      convertImplicitSubexp = (subexp)->
+        processed = @process(subexp)
+        if typeof subexp == "number"
+          @expression_builder.build(expression: [processed])
+        else if subexp == null || subexp == false
           @expression_builder.build(expression: [])
+        else if subexp instanceof Array
+          processed
         else
-          @process(subexp_raw)
+          @expression_builder.build(expression: [processed])

-
+      convertImplicitSubexp: logger().instrument(name: "convertImplicitSubexp", fn: convertImplicitSubexp)

     class_mixer BuildExpressionFromJavascriptObject

     BuildExpressionFromJavascriptObject.buildExpression = ->
       builder = BuildExpressionFromJavascriptObject.build()
       arguments_as_array = Array.prototype.slice.call(arguments, 0)
-      builder.process(arguments_as_array)
+      converted_part = builder.process(arguments_as_array)
+      logger().info("returned converted from process in buildExpression", converted_part.toString(), arguments_as_array)
+      converted_part

     return BuildExpressionFromJavascriptObject
+


The settings page behaves nicer.

The settings page behaves nicer.

--- app/scripts/controllers/settings.coffee
+++ app/scripts/controllers/settings.coffee
@@ -26,6 +26,7 @@
     # Users
     do ->
       # Fetches them.
+      $scope.users = []
       xoApi.call('user.getAll').then (users) ->
         $scope.users = users

@@ -44,6 +45,7 @@
           # Default permission.
           permission: 'none'
         }
+      $scope.addUser()

       # Saves any modifications.
       $scope.saveUsers = ->
@@ -93,6 +95,7 @@
     # Servers
     do ->
       # Fetches them.
+      $scope.servers = []
       xoApi.call('server.getAll').then (servers) ->
         $scope.servers = servers

@@ -108,6 +111,7 @@
           # Fake (unique) identifier needed by Angular.JS
           id: Math.random()
         }
+      $scope.addServer()

       # Saves any modifications.
       $scope.saveServers = ->


Soften the stop threshold a little bit (re #19)

--- app/scripts/directives/headerbar.coffee
+++ app/scripts/directives/headerbar.coffee
@@ -72,6 +72,6 @@
             el = element.parent().find('#violations')
             if el.length == 1
               dy = _scrollToElement(el)
-              if dy == 0
+              if dy <= 4
                 $interval.cancel(scrollInterval)
           100)


increased the default wait timeout

increased the default wait timeout

--- phantom.iced
+++ phantom.iced
@@ -1,7 +1,7 @@
 phantom = require 'phantom'
 module.exports = class Phantom
   constructor: ->
-    @timeout ?= 5000
+    @timeout ?= 10000
   init: (options, cb)->
     options.weakref ?= true
     return cb null if @inited


DataTable/DataForm: Don't output a label if label is set to "".

--- src/elements/DataTable/DataTable.coffee
+++ src/elements/DataTable/DataTable.coffee
@@ -90,7 +90,7 @@
 			if not field.form
 				field.form = {}

-			if not field.form.label
+			if not CUI.util.isString(field.form.label)
 				field.form.label = field.name

 			field_opts.push(field)


teacher is continue endabled should be true

--- src/components/exercise/index.cjsx
+++ src/components/exercise/index.cjsx
@@ -105,7 +105,7 @@
   getTeacherReadOnlyProps: ->
     teacherReadOnlyProps = _.omit(@props, NOT_TEACHER_READ_ONLY_PROPS)
     teacherReadOnlyProps.onContinue = @props.onNextStep
-    teacherReadOnlyProps.isContinueEnabled = false
+    teacherReadOnlyProps.isContinueEnabled = true
     teacherReadOnlyProps.controlButtons = false
     teacherReadOnlyProps.type = 'teacher-review'


:poop:

:poop:

--- src/extensions/snippets/snippets.coffee
+++ src/extensions/snippets/snippets.coffee
@@ -27,7 +27,7 @@
     editor.on 'snippets:expand', (e) =>
       editSession = editor.activeEditSession
       prefix = editSession.getLastCursor().getCurrentWordPrefix()
-      if snippet = @snippetsByExtension[editSession.getFileExtension()][prefix]
+      if snippet = @snippetsByExtension[editSession.getFileExtension()]?[prefix]
         editSession.transact ->
           snippetExpansion = new SnippetExpansion(snippet, editSession)
           editSession.snippetExpansion = snippetExpansion


Remove logging of index

Remove logging of index

--- lib/directory.coffee
+++ lib/directory.coffee
@@ -139,8 +139,6 @@
         index++
         continue

-
-      console.log index
       entry.indexInParentDirectory = index
       index++
       newEntries.push(entry)


Implement Directory::contains

Implement Directory::contains

--- lib/directory.coffee
+++ lib/directory.coffee
@@ -12,6 +12,8 @@

   constructor: ({@name, fullPath, @symlink, @expandedEntries, @isExpanded, @isRoot}) ->
     @path = fullPath
+    @lowerCasePath = @path.toLowerCase() if fs.isCaseInsensitive()
+
     @isRoot ?= false
     @isExpanded ?= false
     @expandedEntries ?= {}
@@ -24,11 +26,22 @@
     if repo?
       @subscribeToRepo(repo)
       @updateStatus(repo)
+    @loadRealPath(repo)

   destroy: ->
     @unwatch()
     @unsubscribe()
     @emit 'destroyed'
+
+  loadRealPath: (repo) ->
+    fs.realpathSync @path, (error, realPath) =>
+      if realPath
+        @realPath = realPath
+        @lowerCaseRealPath = @realPath.toLowerCase() if fs.isCaseInsensitive()
+        @updateStatus(repo) if repo?
+      else
+        @realPath = @path
+        @lowerCaseRealPath = @lowerCasePath

   # Subscribe to the given repo for changes to the Git status of this directory.
   subscribeToRepo: (repo) ->
@@ -69,11 +82,37 @@

     false

+  # Does given full path start with the given prefix?
+  isPathPrefixOf: (prefix, fullPath) ->
+    fullPath.indexOf(prefix) is 0 and fullPath[prefix.length] is path.sep
+
   # Public: Does this directory contain the given path?
   #
   # See atom.Directory::contains for more details.
   contains: (pathToCheck) ->
-    # @directory.contains(pathToCheck)
+    return false unless pathToCheck
+
+    # Normalize forward slashes to back slashes on windows
+    pathToCheck = pathToCheck.replace(/\//g, '\\') if process.platform is 'win32'
+
+    if fs.isCaseInsensitive()
+      directoryPath = @lowerCasePath
+      pathToCheck = pathToCheck.toLowerCase()
+    else
+      directoryPath = @path
+
+    return true if @isPathPrefixOf(directoryPath, pathToCheck)
+
+    # Check real path
+    if @realPath
+      if fs.isCaseInsensitive()
+        directoryPath = @lowerCaseRealPath
+      else
+        directoryPath = @realPath
+
+      return @isPathPrefixOf(directoryPath, pathToCheck)
+
+    false

   # Public: Stop watching this directory for changes.
   unwatch: ->


Rename method

Rename method

--- src/persistence.coffee
+++ src/persistence.coffee
@@ -200,7 +200,7 @@
   #after importMembersFromTeam
   # 14. messages and messageData need to reload after saveBroadcastAlert
   # 15. assignment needs to reload after saveMemberAssignment
-  # 16. assignment needs to reload after removeMemberAssignment
+  # 16. assignment needs to reload after deleteMemberAssignment

   # Load related records when a member is created
   wrapSave sdk, 'saveMember', (member) ->
@@ -748,9 +748,9 @@
         sdk.loadAssignments({id: result.assignmentId})
         ).callback callback

-  wrapMethod sdk, 'removeMemberAssignment', (removeMemberAssignment) ->
+  wrapMethod sdk, 'deleteMemberAssignment', (deleteMemberAssignment) ->
     (memberAssignment, callback) ->
-      removeMemberAssignment.call(this, memberAssignment).then((result) ->
+      deleteMemberAssignment.call(this, memberAssignment).then((result) ->
         sdk.loadAssignments({id: memberAssignment.assignmentId}).then -> result
         ).callback callback


Added group alert

Added group alert

--- src/philips-hue.coffee
+++ src/philips-hue.coffee
@@ -188,6 +188,13 @@
       return handleError msg, err if err
       robot.logger.debug status

+  robot.respond /hue @(\w+) alert/i, (msg) ->
+    [group] = msg.match[1]
+    state = lightState.create.alertShort()
+    api.setGroupLightState group, state, (err, status) ->
+      return handleError msg, err if err
+      robot.logger.debug status
+
   robot.respond /hue turn light (\d+) (on|off)/i, (msg) ->
     [light, state] = msg.match[1..2]
     msg.send "Setting light #{light} to #{state}"


Fix fixture graph

Fix fixture graph

--- src/mochacompat.coffee
+++ src/mochacompat.coffee
@@ -110,8 +110,8 @@
         data: """
         # @runtime fbp-spec-mocha

-        INPORT=run.IN:TEST
-        OUTPORT=run.ERROR:ERROR
+        INPORT=runTest.IN:TEST
+        OUTPORT=runTest.ERROR:ERROR

         runTest(mocha/LoadTestCase) OUT -> IN verifyResult(mocha/CheckResults)
         """


Trying to fix bug.

Trying to fix bug.

--- src/models/user.coffee
+++ src/models/user.coffee
@@ -177,8 +177,8 @@
 		.skip opts.skip or 0
 		.exec (err, _docs) =>
 			return cb(err) if err
-			docs = _.filter(_docs, (i) -> i) # prevent null from .limit
-			console.log('inboxes', _.pluck(docs, 'resource'))
+			docs = _.filter(_docs, (i) -> i and i.resource)
+			console.log('inboxes', docs)

 			###
 			# Get oldest post date


vm.stop() improvements

vm.stop() improvements

- The `force` parameter is optional and default to `false`.
- If `force` is `true`, always do a hard shutdown.
- Throw `INVALID_PARAMS` when a clean shutdown failed due to missing PV drivers.

--- src/api/vm.coffee
+++ src/api/vm.coffee
@@ -553,21 +553,26 @@

   xapi = @getXAPI VM

-  try
-    # Attempts a clean shutdown.
+  # Hard shutdown
+  if force
+    $wait xapi.call 'VM.hard_shutdown', VM.ref
+    return true
+
+  # Clean shutdown
+  try
     $wait xapi.call 'VM.clean_shutdown', VM.ref
   catch error
-    throw error unless error[0] is 'VM_MISSING_PV_DRIVERS'
-
-    @throw 'INVALID_PARAMS' unless force
-
-    $wait xapi.call 'VM.hard_shutdown', VM.ref
+    if error[0] is 'VM_MISSING_PV_DRIVERS'
+      # TODO: Improve reporting: this message is unclear.
+      @throw 'INVALID_PARAMS'
+    else
+      throw error

   return true
 exports.stop.permission = 'admin'
 exports.stop.params = {
   id: { type: 'string' }
-  force: { type: 'boolean' }
+  force: { type: 'boolean', optional: true }
 }

 # revert a snapshot to its parent VM


fix permissions issues

fix permissions issues


Avoid markers x selections loop when toggling findOptions.

--- lib/find-view.coffee
+++ lib/find-view.coffee
@@ -448,10 +448,8 @@
       optionButton.removeClass 'selected'

   anyMarkersAreSelected: =>
-    selections = @model.getEditor()?.getSelections() or []
-    _.any selections, (selection) =>
-      _.any @model.markers or [], (marker) =>
-        _.isEqual(marker.getBufferRange(), selection.getBufferRange())
+    @model.getEditor().getSelectedBufferRanges().some (selectedRange) =>
+      @model.findMarker(selectedRange)

   toggleRegexOption: =>
     @search(useRegex: not @model.getFindOptions().useRegex)


Oneline the list comprehension

Oneline the list comprehension

--- lib/find-view.coffee
+++ lib/find-view.coffee
@@ -297,8 +297,7 @@

   selectAllMarkers: =>
     return unless @markers?.length > 0
-    ranges = for marker in @markers
-      marker.getBufferRange()
+    ranges = (marker.getBufferRange() for marker in @markers)
     scrollMarker = @markers[@firstMarkerIndexAfterCursor()]
     editor = @findModel.getEditor()
     editor.setSelectedBufferRanges(ranges, flash: true)


Refactor coffee

Refactor coffee

--- source/assets/javascripts/app.js.coffee
+++ source/assets/javascripts/app.js.coffee
@@ -8,6 +8,7 @@
 wishedSpan = $('#wished span')
 wishedClose = $('.close')
 wishedBox = $('.wished-box')
+defaultSpeed = 250

 emailIsValid = (email) ->
   pattern = new RegExp(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i)
@@ -38,10 +39,10 @@
     if email != '' and emailIsValid(email)
       e.stopPropagation()
       $('.message').html('
Thank you! Check your email soon.
')
-      setTimeout hideWishedBox, 2500
+      setTimeout hideWishedBox, 10*defaultSpeed
     else
       $('.message').html('
Please provide email in valid format!
')
-      setTimeout clearMessage, 2500
+      setTimeout clearMessage, 10*defaultSpeed
     false

 refreshWished = (wished) ->
@@ -162,22 +163,19 @@
       color: '#ff0'
     }
   }
-
   restartDelay = 3000
-
   buttons = null
-
+  moveToPosition = (element, position, scale) ->
+    element.attr { transform: "translate(#{position}) scale(#{scale})" }
   Snap.load '/assets/images/buttons.svg', (canvas) ->
     buttons = canvas.select 'g#Buttons'
     snap.append buttons
-
     $.each config, (name, item) ->
       group = buttons.select 'g#'+item.id
-      group.attr { transform: zero }
-      buttons.selectAll('g#'+item.id+' > g').attr { transform: 'translate('+item.initial+') scale('+scale.initial+')' }
-      buttons.select('g#'+item.id+'-title').attr { transform: 'translate('+item.initial+') scale('+scale.initial+')' }
+      moveToPosition group, zero, ''
+      moveToPosition buttons.selectAll('g#'+item.id+' > g'), item.initial, scale.initial
+      moveToPosition buttons.select('g#'+item.id+'-title'), item.initial, scale.initial
       moveButton item, 0
-
   moveButton = (item, index) ->
     index = 0 if index > 11
     elementToMove = buttons.selectAll('g#'+item.id+' > g')[index]
@@ -190,25 +188,24 @@
     elementToMove.animate { transform: 'translate('+item.final+') scale('+scale.final+')' }, item.speed, mina.bounce, ->
       timeout = setTimeout restart, restartDelay
       elementToMove.mouseover ->
+        elementToMove.unmouseover()
         clearTimeout timeout
-        title.attr { transform: 'translate('+item.final+') scale('+scale.final+')' }
+        moveToPosition title, item.final, scale.final
         wish = title.select('#wish')
         wish.click ->
           addToWished(item.name)
           refreshWished(currentWished())
           renderWished()
-          title.attr { transform: 'translate('+item.initial+') scale('+scale.initial+')' }
-          setTimeout restart, 250
+          moveToPosition title, item.initial, scale.initial
+          setTimeout restart, defaultSpeed
           wish.unclick()
         title.click ->
-          title.attr { transform: 'translate('+item.initial+') scale('+scale.initial+')' }
-          setTimeout restart, 250
+          moveToPosition title, item.initial, scale.initial
+          setTimeout restart, defaultSpeed
           title.unclick()
-        elementToMove.unmouseover()
-  returnButtons = (element, item, callback) ->
-    element.unmouseover
-    element.animate { transform: 'translate('+item.initial+') scale('+scale.initial+')' }, item.speed/2, mina.easeout, ->
-      setTimeout callback, 1500 - item.speed
+    returnButtons = (element, item, callback) ->
+      element.animate { transform: 'translate('+item.initial+') scale('+scale.initial+')' }, item.speed/2, mina.easeout, ->
+        setTimeout callback, 6*defaultSpeed - item.speed

 if $('#show-room').size() > 0
@@ -216,7 +213,6 @@

 $(window).resize ->
   showSlideShow()
-

 if $('section.fonts').size() > 0
   family = (el) ->
@@ -225,7 +221,6 @@
     el.parent().parent().siblings('.styles').children('h4')
   stylesGr = (el) ->
     el.parent().parent().siblings('.styles')
-
   $('.apperance .minus').on 'click', ->
     familySize = (parseInt(family($(this)).css('font-size'))*0.875)+'px'
     family($(this)).css({ 'font-size': familySize })
@@ -243,10 +238,10 @@
       curStylesGr.removeClass('visible')
       hide = ->
         curStylesGr.hide()
-      setTimeout hide, 750
+      setTimeout hide, 3*defaultSpeed
     else
       $(this).addClass('open')
-      stylesGr($(this)).show 150, ->
+      stylesGr($(this)).show defaultSpeed, ->
         $.scrollTo($(this), 'max')
         $(this).addClass('visible')
   $('.fonts').css { opacity: 1 }
@@ -254,17 +249,14 @@
 if $('section.fonts#styles').size() > 0
   style = (el) ->
     el.parent().parent().siblings('h4').children('input')
-
   $('.apperance .minus').on 'click', ->
     styleSize = (parseInt(style($(this)).first().css('font-size'))*0.75)
     $(this).siblings('.size').html(parseInt(styleSize)+'pt')
     style($(this)).css({ 'font-size': styleSize+'px' })
-
   $('.apperance .plus').on 'click', ->
     styleSize = (parseInt(style($(this)).first().css('font-size'))*1.25)
     $(this).siblings('.size').html(parseInt(styleSize)+'pt')
     style($(this)).css({ 'font-size': styleSize+'px' })
-
   fixHeader = true
   $(window).on 'scroll', ->
     topBound = $('header.main').height()
@@ -278,8 +270,6 @@
       $('.font-header header').removeClass('fixed')
       $('#styles').removeClass('with-bumper')
       fixHeader = true
-
-
   $('.sections a.scroll').on 'click', (e) ->
     offset = parseInt $(this).data('offset')
     console.log offset
@@ -290,13 +280,13 @@
       $('.font-header header').addClass('fixed')
       $('#styles').addClass('with-bumper')
       fixHeader = false
-      $(window).scrollTo $($(this).attr('href')), { duration: 300, offset: -offset }
+      $(window).scrollTo $($(this).attr('href')), { duration: defaultSpeed, offset: -offset }
     else
       $('.font-header').removeClass('fixed')
       $('.font-header header').removeClass('fixed')
       $('#styles').removeClass('with-bumper')
       fixHeader = true
-      $(window).scrollTo 0, { duration: 500 }
+      $(window).scrollTo 0, { duration: 2*defaultSpeed }
     $('.sections a.active').removeClass('active')
     $(this).addClass('active')
     e.stopPropagation()
@@ -307,7 +297,7 @@
 $('header.main').on 'mouseleave', ->
   hideMenu = ->
       $('header.main nav').removeClass('visible')
-  setTimeout hideMenu, 500
+  setTimeout hideMenu, 2*defaultSpeed

 $('.fonts input.tester').on 'change', ->
   $(this).parent('h3').siblings('.styles').children('h4').html($(this).val())
@@ -335,7 +325,7 @@
     else
       $(this).parent().siblings('.tools').children('.apperance').children('.name').addClass('visible')
   $('.wish').addClass('pushed')
-  $('#styles .styles').show 150, ->
+  $('#styles .styles').show defaultSpeed, ->
     $('#styles .styles').addClass('visible')
     showWish = ->
       $('.wish').removeClass('pushed')
@@ -346,7 +336,7 @@
           if !inWished($(button).data('name'))
             $(button).removeClass('pushed')
         refreshWished currentWished()
-    setTimeout showWish, 750
+    setTimeout showWish, 3*defaultSpeed
   detailsActive = ->
     $('a.active').removeClass 'active'
     $('a.details').addClass 'active'
@@ -383,7 +373,6 @@
         stylesActive()
   }

-
 $('.studio img').on 'mouseenter', ->
   $(this).parent().addClass('hidden')
   $('.nonstop').addClass('visible').on 'mouseleave', ->


Fix spacing - In 2014, we launched a Kickstarter campaign and Michael Tupy was one of our wonderful backers! He sponsored this glyph. You can have a look at his work at http://www.t-2.at. Thank you Michael!

--- src/glyphs/punctuation/braceleft.coffee
+++ src/glyphs/punctuation/braceleft.coffee
@@ -8,17 +8,13 @@
 		['skewX', slant + 'deg']
 	)
 	parameters:
-		spacingLeft: 15 * spacing
-		spacingRight: 5 * spacing
+		spacingLeft: 5 * spacing
+		spacingRight: 15 * spacing
 	tags: [
 		'all',
 		'latin',
 		'punctuation'
 	]
-	anchors:
-		0:
-			x: 0
-			y: 0
 	contours:
 		0:
 			skeleton: true
@@ -148,5 +144,5 @@
 			)
 			transforms: Array(
 				[ 'scaleX', -1 ],
-				[ 'translateX', - contours[0].nodes[5].x + spacingLeft ]
+				[ 'translateX', - contours[0].nodes[5].x - spacingRight + ( spacingRight - spacingLeft ) ]
 			)


added some comments explaining ensure_elem_is_text

--- source/coffee/time-parsing-input.coffee
+++ source/coffee/time-parsing-input.coffee
@@ -54,9 +54,11 @@
     @$elem.after @$hidden_field
     @$elem.attr('name', "#{field_name}_display")

+  # If the element is something like type="number", then replace it with a text
+  # element. Most browsers won't let us just change the type.
   ensure_elem_is_text: ->
     return if @$elem.is('[type=text]')
-    new_elem = @$elem.clone(true)
+    new_elem = @$elem.clone(true) # true copies events and data
     new_elem.attr('type', 'text')
     @$elem.replaceWith(new_elem)
     @$elem = new_elem


:white_check_mark: Add more widgets tests

--- specs/units/widgets/widgets.spec.coffee
+++ specs/units/widgets/widgets.spec.coffee
@@ -9,21 +9,57 @@

     beforeEach ->
       spy = jasmine.createSpy('widget')
-
+      element = @fixture.querySelector('div')
       agt.widgets.define 'dummy', spy

-      agt.widgets 'dummy', '.dummy', on: 'custom:event'
     afterEach ->
       agt.widgets.reset()

-      document.dispatchEvent(agt.domEvent 'custom:event')
+    describe 'without any conditions', ->
+      beforeEach ->
+        agt.widgets 'dummy', '.dummy', on: 'custom:event'

-      element = @fixture.querySelector('div')
-      widget = agt.widgets.widgetsFor(element, 'dummy')
+        document.dispatchEvent(agt.domEvent 'custom:event')

-    it 'calls the widget method', ->
-      expect(spy).toHaveBeenCalled()
-      expect(spy.calls.first().args[0]).toBe(element)
-      expect(spy.calls.first().object).toBe(widget)
+        widget = agt.widgets.widgetsFor(element, 'dummy')

-
+      it 'calls the widget method', ->
+        expect(spy).toHaveBeenCalled()
+        expect(spy.calls.first().args[0]).toBe(element)
+        expect(spy.calls.first().object).toBe(widget)
+
+    describe 'with a if condition', ->
+      describe 'that return true', ->
+        beforeEach ->
+          agt.widgets 'dummy', '.dummy', on: 'custom:event', if: -> true
+
+          document.dispatchEvent(agt.domEvent 'custom:event')
+
+        it 'calls the widget handler', ->
+          expect(spy).toHaveBeenCalled()
+
+      describe 'that return false', ->
+        beforeEach ->
+          agt.widgets 'dummy', '.dummy', on: 'custom:event', if: -> false
+          document.dispatchEvent(agt.domEvent 'custom:event')
+
+        it 'does not call the widget handler', ->
+          expect(spy).not.toHaveBeenCalled()
+
+    describe 'with a unless condition', ->
+      describe 'that return true', ->
+        beforeEach ->
+          agt.widgets 'dummy', '.dummy', on: 'custom:event', unless: -> true
+
+          document.dispatchEvent(agt.domEvent 'custom:event')
+
+        it 'does not call the widget handler', ->
+          expect(spy).not.toHaveBeenCalled()
+
+      describe 'that return false', ->
+        beforeEach ->
+          agt.widgets 'dummy', '.dummy', on: 'custom:event', unless: -> false
+          document.dispatchEvent(agt.domEvent 'custom:event')
+
+        it 'calls the widget handler', ->
+          expect(spy).toHaveBeenCalled()


Removed unnecessary comments

Removed unnecessary comments

--- src/assets.coffee
+++ src/assets.coffee
@@ -280,10 +280,8 @@
       less.Parser.importer = (file, paths, callback) ->
         paths.unshift "."

-        # for (i = 0; i < paths.length; i++)
         i = 0
         while i < paths.length
-        # for i in [0..paths.length]
           try
             pathname = path.join(paths[i], file)
             fs.statSync(pathname)


* step: 期望获得id为"xx"的资源:Xxx,其结果(包含): to use containSubset

--- src/default/chinese/api-lib.coffee
+++ src/default/chinese/api-lib.coffee
@@ -126,13 +126,17 @@
     else
       expect(testScope.result.body).to.be.deep.equal data

-  # 希望获得id为"id"的资源:bottle，其结果为
-  this.define new RegExp('[获取拿得][取得到](?:id|ID|编号)[为是:：]?$string的?资源\\s*'+res4DataRegEx), (id, resource, data)->
-    resource ?= this.resource
-    id = path.join(resource, encodeURIComponent id) if resource
+  # 希望获得id为"id"的资源:bottle，其结果（包含）为
+  this.define new RegExp('[获取拿得][取得到](?:id|ID|编号)[为是:：]?$string的?资源\\s*'+resNameRegEx + '[,，.。]?\\s*[的其]?(?:内容|结果)(包[含括]|[为是等]同?于?)[:：]?$object'), (id, resource, aInclude, data)->
+    resource ?= this.resource
+    id = path.join(resource, encodeURIComponent id) if resource
+    aInclude = aInclude[0] is '包'
     this.api.get id
     .then (res)=>
-      expect(res.body).to.be.include data
+      if aInclude
+        expect(res.body).to.be.containSubset data
+      else
+        expect(res.body).to.be.deep.equal data
       return
     # .catch (err)=>
     #   console.log 'err',err


Remove unused props

Remove unused props

Signed-off-by: Max Brunsfeld <78036c9b69b887700d5846f26a788d53b201ffbb@gmail.com>

--- src/lines-component.coffee
+++ src/lines-component.coffee
@@ -36,7 +36,7 @@
     div {className: 'lines', style},
       div className: 'placeholder-text', placeholderText if placeholderText?

-      CursorsComponent {presenter, cursorBlinkPeriod, cursorBlinkResumeDelay}
+      CursorsComponent {presenter}

       HighlightsComponent {
         editor, highlightDecorations, lineHeightInPixels, defaultCharWidth,


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -35,7 +35,6 @@
       width: 2 * deadzone.x
       height: 2 * deadzone.y

-
     I.scroll = Point(
       I.scroll.x.clamp(centerRect.left, centerRect.right)
       I.scroll.y.clamp(centerRect.top, centerRect.bottom)


Fixed comments.

Fixed comments.

--- lib.coffee
+++ lib.coffee
@@ -317,7 +317,7 @@
       if Meteor.isServer or alreadyInSimulation
         try
           # We first insert documents without any optional or in-array references set.
-          # This allows us to have all processors in the database before we start to
+          # This allows us to have all documents in the database before we start to
           # add references so that PeerDB does not complain or even remove documents
           # because of a missing referenced document.
           ids = for doc in docs
@@ -326,7 +326,7 @@

             @insert doc

-          # And now add also the references between processors.
+          # And now add also the references between documents.
           for doc in docs when '_id' of doc
             @update doc._id,
               $set: referencesIncludeProjection doc
@@ -382,7 +382,7 @@
             anyUpdate = false
             callbackClosure.counter = docs.length

-            # And now add also the references between processors.
+            # And now add also the references between documents.
             for doc in docs
               unless '_id' of doc
                 callbackClosure.counter--


Cleanup core extension.

Cleanup core extension.

Factor out _createPluginAPIAdditions.

--- src/js/jquery.extension.hlf.core.coffee
+++ src/js/jquery.extension.hlf.core.coffee
@@ -45,10 +45,10 @@
       if createOptions.baseMixins?
         hlf.applyMixins instance, namespace, createOptions.baseMixins...
       hlf.applyMixin instance, namespace, apiMixins.base
-      otherMixins = _.chain(apiMixins)
-        .filter(mixinFilter, instance)
+      otherMixins = _.chain apiMixins
+        .filter mixinFilter, instance
         .values()
-        .without(apiMixins.base)
+        .without apiMixins.base
         .value()
       hlf.applyMixins instance, namespace, otherMixins...
     if _.isFunction(instance.init) then instance.init()
@@ -56,6 +56,18 @@
     if instance.cls isnt $.noop then $root.addClass instance.cls()

     $root.data instance.attr(), instance
+
+  _createPluginAPIAdditions = (name, namespace) ->
+    evt: _.memoize (name) -> "#{name}#{namespace.toString 'event'}"
+    attr: _.memoize (name) ->
+      name = if name? then "-#{name}" else ''
+      namespace.toString('data') + name
+    cls: if namespace.toString('class') is namespace.toString() then $.noop else
+      _.memoize (name) ->
+        name = if name? then "-#{name}" else ''
+        namespace.toString('class') + name
+    debugLog: if namespace.debug is off then $.noop else
+      -> hlf.debugLog namespace.toString('log'), arguments...

   _.extend hlf,

@@ -91,17 +103,7 @@
       safeName = "#{@toString()}#{name[0].toUpperCase()}#{name[1..]}"
       namespace = createOptions.namespace

-      apiAdditions =
-        evt: _.memoize (name) -> "#{name}#{namespace.toString 'event'}"
-        attr: _.memoize (name) ->
-          name = if name? then "-#{name}" else ''
-          namespace.toString('data') + name
-        cls: if namespace.toString('class') is namespace.toString() then $.noop else
-          _.memoize (name) ->
-            name = if name? then "-#{name}" else ''
-            namespace.toString('class') + name
-        debugLog: if namespace.debug is off then $.noop else
-          -> hlf.debugLog namespace.toString('log'), arguments...
+      apiAdditions = _createPluginAPIAdditions name, namespace

       if createOptions.apiClass?
         apiClass = namespace.apiClass = createOptions.apiClass


fix pr url printout

fix pr url printout

This fixes the "undefined" showing up in place of a proper full html link to the pull request. I've evaluated the json sent by github and verified this exists under the pull_request object.

--- src/formatters/pull-request.coffee
+++ src/formatters/pull-request.coffee
@@ -18,7 +18,7 @@
   #{sender.login} #{verb} Pull Request ##{data.number} on #{repo.full_name}
   Title: #{pullRequest.title}

-  #{data.html_url}
+  #{pullRequest.html_url}
   """

 assignEvent = ({data}) ->
@@ -33,7 +33,7 @@
   #{sender.login} #{verb} #{assignee} to Pull Request ##{data.number} on #{repo.full_name}
   Title: #{pullRequest.title}

-  #{data.html_url}
+  #{pullRequest.html_url}
   """

 labelEvent = ({data}) ->
@@ -50,5 +50,5 @@
   #{sender.login} #{verb} the label #{label} to Pull Request ##{data.number} on #{repo.full_name}
   Title: #{pullRequest.title}

-  #{data.html_url}
+  #{pullRequest.html_url}
   """


re-init the clock

re-init the clock

--- apps/fair_organizer/client/index.coffee
+++ apps/fair_organizer/client/index.coffee
@@ -5,10 +5,10 @@
 Clock = require '../../../components/auction_clock/view.coffee'

 module.exports.init = ->
-  # @fair = new Fair sd.FAIR
+  @fair = new Fair sd.FAIR

-  # @clock = new Clock
-  #   modelName: "Fair"
-  #   model: @fair
-  #   el: $('.fair-organizer-top__countdown__clock')
-  # @clock.start()+  @clock = new Clock
+    modelName: "Fair"
+    model: @fair
+    el: $('.fair-organizer-top__countdown__clock')
+  @clock.start()


dont fail getCaretPosition for ie

dont fail getCaretPosition for ie

--- src/mentions-kinder.coffee
+++ src/mentions-kinder.coffee
@@ -175,7 +175,7 @@
     window.getSelection?().collapse(node, position)

   _getCaretPosition: ->
-    window.getSelection().baseOffset
+    window.getSelection?().baseOffset

   _isCaretInTempMention: ->
     @isAutocompleting() && window.getSelection().baseNode?.parentElement == @_current.$tempMention[0]


Implement specs for parse errors in non-generated statements.

--- spec/compiler.coffee
+++ spec/compiler.coffee
@@ -300,18 +300,18 @@

   describe 'Errors', ->

-    expectError '', 'Preparse error', true
-
-    expectError '', 'VGL parse error', true
+    expectError '@', 'Preparse error'
+
+    expectError '@grid-template simple "";', 'VGL parse error'

     expectError '', 'VGL generated VFL parse error', true

     expectError '', 'VGL generated CCSS parse error', true

-    expectError '', 'VFL parse error', true
+    expectError '@h [];', 'VFL parse error'

     expectError '', 'VFL generated CCSS parse error', true

-    expectError '', 'CCSS conditional parse error', true
-
-    expectError '', 'Constraint parse error', true
+    expectError '@if [target] === 960 {}', 'CCSS conditional parse error'
+
+    expectError '#box[right] === #box2[left];', 'Constraint parse error'


download log files using buildPdfDownloadUrl

--- services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -224,6 +224,12 @@
 				_csrf: window.csrfToken
 			}, {params: params}

+		buildPdfDownloadUrl = (pdfDownloadDomain, path)->
+			if pdfDownloadDomain?
+				return "#{pdfDownloadDomain}#{path}"
+			else
+				return path
+
 		parseCompileResponse = (response) ->

 			# keep last url
@@ -244,11 +250,7 @@
 			$scope.pdf.compileInProgress = false
 			$scope.pdf.autoCompileDisabled = false

-			buildPdfDownloadUrl = (path)->
-				if pdfDownloadDomain?
-					return "#{pdfDownloadDomain}#{path}"
-				else
-					return path
+
 			# make a cache to look up files by name
 			fileByPath = {}
 			if response?.outputFiles?
@@ -267,24 +269,24 @@
 			if response.status == "timedout"
 				$scope.pdf.view = 'errors'
 				$scope.pdf.timedout = true
-				fetchLogs(fileByPath)
+				fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
 			else if response.status == "terminated"
 				$scope.pdf.view = 'errors'
 				$scope.pdf.compileTerminated = true
-				fetchLogs(fileByPath)
+				fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
 			else if response.status in ["validation-fail", "validation-pass"]
 				$scope.pdf.view = 'pdf'
-				$scope.pdf.url = buildPdfDownloadUrl last_pdf_url
+				$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, last_pdf_url
 				$scope.shouldShowLogs = true
 				$scope.pdf.failedCheck = true if response.status is "validation-fail"
 				event_tracking.sendMB "syntax-check-#{response.status}"
-				fetchLogs(fileByPath, { validation: true })
+				fetchLogs(fileByPath, { validation: true, pdfDownloadDomain:pdfDownloadDomain})
 			else if response.status == "exited"
 				$scope.pdf.view = 'pdf'
 				$scope.pdf.compileExited = true
-				$scope.pdf.url = buildPdfDownloadUrl last_pdf_url
+				$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, last_pdf_url
 				$scope.shouldShowLogs = true
-				fetchLogs(fileByPath)
+				fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
 			else if response.status == "autocompile-backoff"
 				if $scope.pdf.isAutoCompileOnLoad # initial autocompile
 					$scope.pdf.view = 'uncompiled'
@@ -300,7 +302,7 @@
 				$scope.pdf.view = 'errors'
 				$scope.pdf.failure = true
 				$scope.shouldShowLogs = true
-				fetchLogs(fileByPath)
+				fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})
 			else if response.status == 'clsi-maintenance'
 				$scope.pdf.view = 'errors'
 				$scope.pdf.clsiMaintenance = true
@@ -320,12 +322,12 @@

 				# define the base url. if the pdf file has a build number, pass it to the clsi in the url
 				if fileByPath['output.pdf']?.url?
-					$scope.pdf.url = buildPdfDownloadUrl fileByPath['output.pdf'].url
+					$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, fileByPath['output.pdf'].url
 				else if fileByPath['output.pdf']?.build?
 					build = fileByPath['output.pdf'].build
-					$scope.pdf.url = buildPdfDownloadUrl "/project/#{$scope.project_id}/build/#{build}/output/output.pdf"
+					$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, "/project/#{$scope.project_id}/build/#{build}/output/output.pdf"
 				else
-					$scope.pdf.url = buildPdfDownloadUrl "/project/#{$scope.project_id}/output/output.pdf"
+					$scope.pdf.url = buildPdfDownloadUrl pdfDownloadDomain, "/project/#{$scope.project_id}/output/output.pdf"
 				# check if we need to bust cache (build id is unique so don't need it in that case)
 				if not fileByPath['output.pdf']?.build?
 					qs.cache_bust = "#{Date.now()}"
@@ -335,7 +337,7 @@
 				qs.popupDownload = true
 				$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)

-				fetchLogs(fileByPath)
+				fetchLogs(fileByPath, {pdfDownloadDomain:pdfDownloadDomain})

 			IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
 			$scope.pdf.outputFiles = []
@@ -376,11 +378,11 @@
 						compileGroup:ide.compileGroup
 						clsiserverid:ide.clsiServerId
 				if file?.url?  # FIXME clean this up when we have file.urls out consistently
-					opts.url = file.url
+					opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, file.url
 				else if file?.build?
-					opts.url = "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}"
+					opts.url = buildPdfDownloadUrl options.pdfDownloadDomain, "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}"
 				else
-					opts.url = "/project/#{$scope.project_id}/output/#{name}"
+					opts.url = buildPdfDownloadUrl options.pdfDownloadDomain,  "/project/#{$scope.project_id}/output/#{name}"
 				# check if we need to bust cache (build id is unique so don't need it in that case)
 				if not file?.build?
 					opts.params.cache_bust = "#{Date.now()}"


Tests: Fix noisy console.log

Tests: Fix noisy console.log

--- spec/examples.coffee
+++ spec/examples.coffee
@@ -26,7 +26,6 @@
   before (done) ->
     tv4.addSchema schema.id, schema
     runner = new fbpspec.runner.Runner runtimeInfo
-    console.log 'before runner is', runner
     runner.connect done
   after (done) ->
     tv4.reset()


clean up workspace commands after the terminal closes

--- lib/term-view.coffee
+++ lib/term-view.coffee
@@ -1,9 +1,10 @@
-util       = require 'util'
-os         = require 'os'
-fs         = require 'fs-plus'
-path       = require 'path'
-debounce   = require 'debounce'
-Terminal   = require 'atom-term.js'
+util = require 'util'
+os = require 'os'
+fs = require 'fs-plus'
+path = require 'path'
+debounce = require 'debounce'
+Terminal = require 'atom-term.js'
+{CompositeDisposable} = require 'atom'
  # see https://github.com/f/atom-term.js/pull/5
  # see https://github.com/f/atom-term.js/pull/4
 window.isMac = window.navigator.userAgent.indexOf('Mac') != -1;
@@ -25,6 +26,7 @@
   constructor: (@opts={})->
     @emitter = new Emitter
     @fakeRow = $("

").css visibility: 'hidden'
+    @disposable = new CompositeDisposable();
     super

   @content: ->
@@ -173,9 +175,9 @@
     @resizeToPane_ = @resizeToPane_.bind this
     @on 'focus', @focus
     $(window).on 'resize', => @resizeToPane_()
-    @disposable = atom.workspace.getActivePane().observeFlexScale => setTimeout (=> @resizeToPane_()), 300
-    atom.commands.add "atom-workspace", "term3:paste", => @paste()
-    atom.commands.add "atom-workspace", "term3:copy", => @copy()
+    @disposable.add atom.workspace.getActivePane().observeFlexScale => setTimeout (=> @resizeToPane_()), 300
+    @disposable.add atom.commands.add "atom-workspace", "term3:paste", => @paste()
+    @disposable.add atom.commands.add "atom-workspace", "term3:copy", => @copy()

   copy: ->
     return unless @term


add proper error

add proper error

--- lib/championify.coffee
+++ lib/championify.coffee
@@ -99,7 +99,7 @@
   ]
   async.each _.flatten(globbed), (item, next) ->
     fs.unlink item, (err) ->
-      return next(new cErrors.FileWriteError("Can\'t unlink file: #{item}").causedBy(err))
+      return next(new cErrors.FileWriteError("Can\'t unlink file: #{item}").causedBy(err)) if err
       next null
   , ->
     hlp.updateProgressBar(2.5) if !deletebtn


update for new settings structure in julia-client

--- lib/uber-juno.coffee
+++ lib/uber-juno.coffee
@@ -16,8 +16,8 @@

   defaultConfig:
     'tool-bar.position': 'Left'
-    'julia-client.enableMenu': true
-    'julia-client.enableToolBar': true
+    'julia-client.uiOptions.enableMenu': true
+    'julia-client.uiOptions.enableToolBar': true
     'editor.scrollPastEnd': true
     'autocomplete-plus.confirmCompletion': "tab always, enter when suggestion explicitly selected"


bad validateURN regexp

bad validateURN regexp

--- lib/validator.coffee
+++ lib/validator.coffee
@@ -75,7 +75,7 @@
 @param urn - the urn string to validate
 ###
 exports.validateURN = (urn) ->
-  /(^urn:[a-zA-Z0-9]{1}[a-zA-Z0-9\-.]+:[a-zA-Z0-9_,=@;!'%/#\(\)\+\-\.\$\*\?]+\/?.+$)/.test(urn)
+  /(^urn:[a-zA-Z0-9]{1}[a-zA-Z0-9\-.]+:[a-zA-Z0-9_,=@;!'%/#\(\)\+\-\.\$\*\?]+(\/.+)?$)/.test(urn)

 ###
 Returns true or false if it is a valid URN with resource following hubiquitus standards


simplify lazyProp definition

simplify lazyProp definition

--- lib/vim-state.coffee
+++ lib/vim-state.coffee
@@ -35,18 +35,19 @@
   @delegatesMethods('flash', 'flashScreenRange', toProperty: 'flashManager')
   @delegatesMethods('subscribe', 'getCount', 'setCount', 'hasCount', 'addToClassList', toProperty: 'operationStack')

-  @defineLazyProperty: (name, fileToLoad) ->
+  @defineLazyProperty: (name, fileToLoad, instantiate=true) ->
     Object.defineProperty @prototype, name,
-      get: -> this["__#{name}"] ?= new (lazyRequire(fileToLoad))(this)
-
-  Object.defineProperty @prototype, 'swrap',
-    get: -> this.__swrap ?= lazyRequire('./selection-wrapper')
-
-  Object.defineProperty @prototype, 'utils',
-    get: -> this.__utils ?= lazyRequire('./utils')
+      get: -> this["__#{name}"] ?= do =>
+        if instantiate
+          new (lazyRequire(fileToLoad))(this)
+        else
+          lazyRequire(fileToLoad)

   getProp: (name) ->
     this[name] if this["__#{name}"]?
+
+  @defineLazyProperty('swrap', './selection-wrapper', false)
+  @defineLazyProperty('utils', './utils', false)

   @lazyProperties =
     mark: './mark-manager'


Fix browser check

Fix browser check

--- app/lib/setup.coffee
+++ app/lib/setup.coffee
@@ -3,9 +3,8 @@

 BrowserCheck = require 'zooniverse/lib/controllers/browser_check'
 checker = new BrowserCheck
-checker.support.ie = 9
-checker.check
-
+checker.support.msie = 9
+checker.check()

 # Load US English by default.
 translate = require 'translate'


use var

use var

--- lib/column-name.coffee
+++ lib/column-name.coffee
@@ -1,4 +1,5 @@

+namingMethod=atom.config.get('tablr.defaultColumnNamingMethod')
 module.exports =
 columnName = (index) ->
   if atom.config.get('tablr.defaultColumnNamingMethod') == 'numeric'


Integrate gitter

Integrate gitter

--- lib/webserver.coffee
+++ lib/webserver.coffee
@@ -1,6 +1,7 @@
 http = require 'http'
 url = require 'url'
 model = require('../lib/model')
+gitter = require '../lib/gitter'

 server = http.createServer()

@@ -22,6 +23,5 @@
     when "/health"
       res.end "ok"
     else
-      res.writeHead 404
-      res.end "not found"
+      gitter.handle req, res
 module.exports = server


Don't offer to edit repeat in template, fix #781

--- src/resources/forms/orbeon/builder/resources/section-grid-repeat/grid-repeat-editor.coffee
+++ src/resources/forms/orbeon/builder/resources/section-grid-repeat/grid-repeat-editor.coffee
@@ -7,13 +7,13 @@
     gridsCache = []; do ->
         Builder.onOffsetMayHaveChanged ->
             gridsCache.length = 0                                                                                # Update gridsCache in-place, as references are kept by other functions
-            _.each ($ 'div.xbl-fr-grid:visible'), (grid) ->
-                grid = $ grid
+            _.each ($ '.fr-grid.fr-editable:visible'), (table) ->
+                table = $(table)
+                grid = table.parent()
                 gridInfo =
                     el: grid
                     offset: Builder.adjustedOffset grid
                     height: f$.outerHeight grid                                                                         # Include grid padding
-                table = f$.children '.fr-grid', gridInfo.el
                 if f$.is '.fr-repeat', table
                     head = f$.find 'thead', table
                     gridInfo.head =


Test that buy button renders correctly for desktop non-auctions

--- src/desktop/apps/artwork/components/commercial/test/template.coffee
+++ src/desktop/apps/artwork/components/commercial/test/template.coffee
@@ -6,7 +6,8 @@
 sinon = require 'sinon'
 Backbone = require 'backbone'
 helpers = require '../helpers.coffee'
-inquireableArtwork = require '../../../test/fixtures/inquireable_artwork.json'
+inquireableArtwork = (require '../../../test/fixtures/inquireable_artwork.json').data.artwork
+acquireableArtwork = (require '../../../test/fixtures/acquireable_artwork.json').data.artwork
 render = (templateName) ->
   filename = path.resolve __dirname, "../index.jade"
   jade.compile(
@@ -14,13 +15,13 @@
     { filename: filename }
   )

-renderArtwork = (artworkOptions = {}, sdOptions = {}) ->
-  artwork = _.clone inquireableArtwork.data.artwork
-  _.extend artwork, artworkOptions
-  sd = _.extend { CLIENT: artwork: artwork }, sdOptions
+renderArtwork = ({ artwork = {}, artworkOptions = {}, sdOptions = {}, templateOptions = {} }) ->
+  artworkConfig = {}
+  _.extend artworkConfig, artwork, artworkOptions
+  sd = _.extend { CLIENT: artwork: artworkConfig }, sdOptions

-  html = render('index')(
-    artwork: artwork
+  config =
+    artwork: artworkConfig
     sd: sd
     asset: (->)
     helpers:
@@ -30,25 +31,56 @@
         artistIds: sinon.stub()
       commercial:
         isWithConsignableArtists: sinon.stub()
-  )
+
+  _.extend config, templateOptions
+
+  html = render('index') config

 describe 'Commercial template', ->

   it 'name and password display for prequalified work', ->
-    html = renderArtwork { partner: is_pre_qualify: true }
+    html = renderArtwork
+      artwork: inquireableArtwork
+      artworkOptions:
+        partner:
+          is_pre_qualify: true
     $ = cheerio.load(html)
     $('input[name=email]').length.should.eql 1

   it 'correctly displays on loan without pricing info', ->
-    html = renderArtwork { availability: 'on loan', sale_message: 'On loan', price: '$5,000' }
+    html = renderArtwork
+      artwork: inquireableArtwork
+      artworkOptions:
+        availability: 'on loan'
+        sale_message: 'On loan'
+        price: '$5,000'
     $ = cheerio.load(html)
     $('.artwork-commercial__sale-message').text().should.eql 'On loan'
     $('.artwork-commercial__shipping-info').length.should.eql 0

   it 'correctly displays permanent collection without pricing info', ->
-    html = renderArtwork { availability: 'permanent collection', price: '$5,000' }
+    html = renderArtwork
+      artwork: inquireableArtwork
+      artworkOptions:
+        availability: 'permanent collection'
+        price: '$5,000'
     $ = cheerio.load(html)
     $('.artwork-commercial__sale-message').text().should.eql 'Permanent collection'
     $('.artwork-commercial__shipping-info').length.should.eql 0

+  it 'shows the buy button when ecommerce and the buy now flow lab feature are enabled', ->
+    html = renderArtwork
+      artwork: acquireableArtwork
+      templateOptions:
+        user:
+          hasLabFeature: (feature) -> feature == "New Buy Now Flow"
+    $ = cheerio.load html
+    $('.js-artwork-acquire-button').length.should.eql 1

+  it 'does not show the buy button when the buy now flow lab feature is disabled', ->
+    html = renderArtwork
+      artwork: acquireableArtwork
+    $ = cheerio.load html
+    $('.js-artwork-acquire-button').length.should.eql 0
+
+


payment: GroupProductCreateForm is generalized

+ inherit from KDModalViewWithFields, not KDModalView (which makes it easy to provide a label for every field)

--- client/landing/app/Applications/Dashboard.kdapplication/views/groupproductcreateform.coffee
+++ client/landing/app/Applications/Dashboard.kdapplication/views/groupproductcreateform.coffee
@@ -1,60 +1,69 @@
-class GroupProductCreateForm extends KDFormView
+class GroupProductCreateForm extends KDFormViewWithFields

   constructor: (options = {}, data) ->

-    super options, data
+    options.isRecurringOptional ?= yes

-    @titleInput = new KDInputView
-      cssClass   : "product-title"
-      placeholder: "Product name"
+    options.callback ?= =>
+      @emit 'CreateRequested', @getProductData()

-    @descriptionInput = new KDInputView
-      cssClass   : "product-description"
-      placeholder: "Product description"
+    options.buttons ?=
+      Create      :
+        cssClass  : "modal-clean-green"
+        type      : "submit"
+      cancel      :
+        cssClass  : "modal-cancel"
+        callback  : =>
+          @emit 'CancelRequested'

-    @subscriptionTypeInput = new KDSelectBox
-      defaultValue  : "recurring"
-      selectOptions : [
-        { title : "Recurring payment", value : 'recurring' }
-        { title : "Single payment"   , value : 'single'    }
-      ]
-      callback: =>
-        if @subscriptionTypeInput.getValue() is 'single'
-        then @perMonth.hide()
-        else @perMonth.show()
+    options.fields ?=

-    @amountInput = new KDInputView
-      cssClass    : "product-price"
-      placeholder : "0.00"
-      change      : ->
+      title             :
+        label           : "Title"
+        placeholder     : options.placeholders?.title
+
+      description       :
+        label           : "Description"
+        placeholder     : options.placeholders?.description or "(optional)"
+
+    if options.isRecurringOptional
+      options.fields.subscriptionType =
+        label           : "Subscription type"
+        itemClass       : KDSelectBox
+        defaultValue    : "recurring"
+        selectOptions   : [
+          { title : "Recurring payment", value : 'recurring' }
+          { title : "Single payment"   , value : 'single'    }
+        ]
+        callback: =>
+          if @inputs.subscriptionType.getValue() is 'single'
+          then @inputs.perMonth.hide()
+          else @inputs.perMonth.show()
+
+    options.fields.amount ?=
+      label           : "Amount"
+      placeholder     : "0.00"
+      change          : ->
         num = parseFloat @getValue()

         @setValue if isNaN num then '' else num.toFixed(2)
+      nextElementFlat :

-    @perMonth = new KDView
-      tagName: 'span'
-      partial: '/ mo'
+        perMonth      :
+          itemClass   : KDCustomHTMLView
+          partial     : "/ mo"
+          cssClass    : 'fr'

-    @submitButton = new KDButtonView
-      cssClass   : "product-button"
-      title      : "Create product"
-      callback   : =>
-        @emit 'ProductCreateRequested', @getProductData()
+    if options.isOverageEnabled
+      options.fields.overageEnabled =
+        label           : "Overage enabled"
+        itemClass       : KDOnOffSwitch

-  viewAppended: JView::viewAppended
-
-  pistachio: ->
-    """
-    {{> @titleInput}}
-    {{> @descriptionInput}}
-    {{> @subscriptionTypeInput}}
-    {{> @amountInput}}
-    {{> @perMonth}}
-    {{> @submitButton}}
-    """
+    super options, data

   getProductData: ->
-    price             : @amountInput.getValue()
-    title             : @titleInput.getValue()
-    description       : @descriptionInput.getValue()
-    subscriptionType  : @subscriptionTypeInput.getValue()
+    { amount, title, description, subscriptionType } = @inputs
+    amount            : amount.getValue()
+    title             : title.getValue()
+    description       : description.getValue()
+    subscriptionType  : subscriptionType?.getValue()


changed default standalone setting

changed default standalone setting

--- src/common.coffee
+++ src/common.coffee
@@ -29,8 +29,8 @@
         cssEnd         : '">'
         keepUninjected : yes
         keepUnpacked   : no
-        wrapOpt        :
-            standalone : no
+        wrapOpt        :
+            standalone : yes
             minify     : yes

     if options?


Fixed bug causing storeId to be ignored and providerId to be reused for temp store and filesystem.

--- src/common.coffee
+++ src/common.coffee
@@ -29,7 +29,7 @@
     stores = []
     createStore = (providerId, storeId, config) ->
       StoreClass = StoreConstructors[providerId]
-      new StoreClass(providerId, config)
+      new StoreClass(storeId, config)

     _tempstore = result._tempstore
     if _tempstore


Submit login via HTTPS in production.

Submit login via HTTPS in production.

--- src/main/webapp/coffeescripts-hidden/landing.coffee
+++ src/main/webapp/coffeescripts-hidden/landing.coffee
@@ -16,6 +16,22 @@
   return [];

 $(document).ready ->
+  window.liftAjax.lift_actualAjaxCall = (data, version, onSuccess, onFailure) ->
+    hostnamePrefix = ""
+
+    # In production, we want all AJAX calls from the landing (which is only login atm)
+    # to be transmitted via HTTPS to protect user passwords.
+    if window.location.hostname == "anchortab.com"
+      hostnamePrefix = "https://anchortab.com"
+
+    jQuery.ajax
+      url : hostnamePrefix + liftAjax.addPageNameAndVersion("/ajax_request/", version),
+      data : data,
+      dataType : "jsonp",
+      success : onSuccess,
+      error : onFailure,
+      crossDomain: true
+
   $('body').on 'click', '.get-started-now', ->
     $targetElem = $("section.plans-and-pricing")
     targetTop = $targetElem.offset().top


Rename onDidLoadAll -> onDidLoadInitialPackages

Signed-off-by: Max Brunsfeld <78036c9b69b887700d5846f26a788d53b201ffbb@gmail.com>

--- src/package-manager.coffee
+++ src/package-manager.coffee
@@ -53,8 +53,12 @@
   # * `callback` {Function}
   #
   # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
+  onDidLoadInitialPackages: (callback) ->
+    @emitter.on 'did-load-initial-packages', callback
+
   onDidLoadAll: (callback) ->
-    @emitter.on 'did-load-all', callback
+    Grim.deprecate("Use `::onDidLoadInitialPackages` instead.")
+    @onDidLoadInitialPackages(callback)

   # Public: Invoke the given callback when all packages have been activated.
   #
@@ -278,7 +282,7 @@
     packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
     @loadPackage(packagePath) for packagePath in packagePaths
     @emit 'loaded'
-    @emitter.emit 'did-load-all'
+    @emitter.emit 'did-load-initial-packages'

   loadPackage: (nameOrPath) ->
     return pack if pack = @getLoadedPackage(nameOrPath)


Fix error when running apm in packages

Fix error when running apm in packages

--- src/package-manager.coffee
+++ src/package-manager.coffee
@@ -134,7 +134,7 @@

     commandName = 'apm'
     commandName += '.cmd' if process.platform is 'win32'
-    apmRoot = path.resolve(__dirname, '..', '..', 'apm')
+    apmRoot = path.join(process.resourcesPath, 'apm')
     @apmPath = path.join(apmRoot, 'bin', commandName)
     unless fs.isFileSync(@apmPath)
       @apmPath = path.join(apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName)


[web.coffee] Check for that content-type _starts-with_ 'text/html'

Exact matching means that content types like 'text/html; charset=UTF-8' are
ignored.  This is undesirable.

--- src/scripts/web.coffee
+++ src/scripts/web.coffee
@@ -36,7 +36,7 @@
           if res.statusCode is 301 or res.statusCode is 302
             httpResponse(res.headers.location)
           else if res.statusCode is 200
-            if res.headers['content-type'] != 'text/html'
+            if res.headers['content-type'].indexOf('text/html') != 0
               return

             handler = new HtmlParser.DefaultHandler()


mp4: Add pause() and resume()

mp4: Add pause() and resume()

--- mp4.coffee
+++ mp4.coffee
@@ -98,6 +98,21 @@
     logger.debug "[mp4:#{@filename}] stop"
     @isStopped = true

+  pause: ->
+    if @isStopped
+      @resume()
+    else
+      @isStopped = true
+      logger.debug "[mp4:#{@filename}] pause"
+      @pausedTime = getCurrentTime()
+
+  resume: ->
+    logger.debug "[mp4:#{@filename}] resume"
+    @isStopped = false
+    @playStartTime += getCurrentTime() - @pausedTime
+    @updateCurrentPlayTime()
+    @queueBufferedSamples()
+
   play: ->
     @currentTime = 0
     @consumedAudioSamples = 0
@@ -117,7 +132,9 @@
     @currentPlayTime = 0
     @playStartTime = getCurrentTime()
     @bufferedAudioSamples = []
+    @queuedAudioSampleIndex = 0
     @bufferedVideoSamples = []
+    @queuedVideoSampleIndex = 0

     @isAudioEOF = false
     @isVideoEOF = false
@@ -168,37 +185,49 @@
     @currentPlayTime = getCurrentTime() - @playStartTime

   queueBufferedAudioSamples: ->
-    audioSample = @bufferedAudioSamples.shift()
+    audioSample = @bufferedAudioSamples[@queuedAudioSampleIndex]
     if not audioSample?  # @bufferedAudioSamples is empty
       return
     timeDiff = audioSample.time - @currentPlayTime
     if timeDiff <= MIN_TIME_DIFF
+      @bufferedAudioSamples.shift()
+      @queuedAudioSampleIndex--
       @emit 'audio_data', audioSample.data, audioSample.pts
       @updateCurrentPlayTime()
     else
       setTimeout =>
-        @emit 'audio_data', audioSample.data, audioSample.pts
-        @updateCurrentPlayTime()
-        @checkAudioBuffer()
+        @queuedAudioSampleIndex--
+        if not @isStopped
+          @bufferedAudioSamples.shift()
+          @emit 'audio_data', audioSample.data, audioSample.pts
+          @updateCurrentPlayTime()
+          @checkAudioBuffer()
       , timeDiff * 1000
+    @queuedAudioSampleIndex++
     @queuedAudioTime = audioSample.time
     if @queuedAudioTime - @currentPlayTime < QUEUE_BUFFER_TIME
       @queueBufferedSamples()

   queueBufferedVideoSamples: ->
-    videoSample = @bufferedVideoSamples.shift()
+    videoSample = @bufferedVideoSamples[@queuedVideoSampleIndex]
     if not videoSample?  # read buffer is empty
       return
     timeDiff = videoSample.time - @currentPlayTime
     if timeDiff <= MIN_TIME_DIFF
+      @bufferedVideoSamples.shift()
+      @queuedVideoSampleIndex--
       @emit 'video_data', videoSample.data, videoSample.pts
       @updateCurrentPlayTime()
     else
       setTimeout =>
-        @emit 'video_data', videoSample.data, videoSample.pts
-        @updateCurrentPlayTime()
-        @checkVideoBuffer()
+        @queuedVideoSampleIndex--
+        if not @isStopped
+          @bufferedVideoSamples.shift()
+          @emit 'video_data', videoSample.data, videoSample.pts
+          @updateCurrentPlayTime()
+          @checkVideoBuffer()
       , timeDiff * 1000
+    @queuedVideoSampleIndex++
     @queuedVideoTime = videoSample.time
     if @queuedVideoTime - @currentPlayTime < QUEUE_BUFFER_TIME
       @queueBufferedSamples()


Remove duplicate destroy

Remove duplicate destroy

Telepath now emits the 'destroy' event

--- src/text-buffer.coffee
+++ src/text-buffer.coffee
@@ -87,7 +87,6 @@
       @file?.off()
       @unsubscribe()
       @alreadyDestroyed = true
-      @emit 'destroyed', this

   isRetained: -> @refcount > 0


revert to using selection ranges instead of selection objects

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -821,12 +821,13 @@
   # Move lines intersection the most recent selection or multiple selections up by one row in screen
   # coordinates.
   moveLineUp: ->
-    selections = @getSelectionsOrderedByBufferPosition()
-
-    if selections[0].getBufferRange().start.row is 0
+    selections = @getSelectedBufferRanges()
+    selections.sort (a, b) -> a.compare(b)
+
+    if selections[0].start.row is 0
       return

-    if selections[selections.length - 1].getBufferRange().start.row is @getLastBufferRow() and @buffer.getLastLine() is ''
+    if selections[selections.length - 1].start.row is @getLastBufferRow() and @buffer.getLastLine() is ''
       return

     @transact =>
@@ -835,23 +836,19 @@
       while selections.length > 0
         # Find selections spanning a contiguous set of lines
         selection = selections.shift()
-        lastSelectionRange = selection.getBufferRange()
         selectionsToMove = [selection]
-        rangesOfSelectionsToMove = [lastSelectionRange]
-
-        while lastSelectionRange.end.row is selections[0]?.getBufferRange().start.row
+
+        while selection.end.row is selections[0]?.start.row
           selection = selections.shift()
-          lastSelectionRange = selection.getBufferRange()
           selectionsToMove.push(selection)
-          rangesOfSelectionsToMove.push(lastSelectionRange)

         # Compute the range spanned by all these selections...
-        linesRangeStart = [selectionsToMove[0].getBufferRange().start.row, 0]
-        if lastSelectionRange.end.row > lastSelectionRange.start.row and lastSelectionRange.end.column is 0
+        linesRangeStart = [selection.start.row, 0]
+        if selection.end.row > selection.start.row and selection.end.column is 0
           # Don't move the last line of a multi-line selection if the selection ends at column 0
-          linesRange = new Range(linesRangeStart, lastSelectionRange.end)
+          linesRange = new Range(linesRangeStart, selection.end)
         else
-          linesRange = new Range(linesRangeStart, [lastSelectionRange.end.row + 1, 0])
+          linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0])

         # If selected line range is preceded by a fold, one line above on screen
         # could be multiple lines in the buffer.
@@ -866,7 +863,7 @@

         # Make sure the inserted text doesn't go into an existing fold
         if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow)
-          rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount(), 0]))
+          rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0]))
           fold.destroy()

         # Delete lines spanned by selection and insert them on the preceding buffer row
@@ -879,8 +876,8 @@
         for rangeToRefold in rangesToRefold
           @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row)

-        for selection, i in selectionsToMove
-          newSelectionRanges.push(rangesOfSelectionsToMove[i].translate([-insertDelta, 0]))
+        for selection in selectionsToMove
+          newSelectionRanges.push(selection.translate([-insertDelta, 0]))

       @setSelectedBufferRanges(newSelectionRanges)


bug that causes moveLineUp to move the wrong selections

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -831,10 +831,6 @@
         selection = selections.shift()
         selectionsToMove = [selection]

-        while selection.end.row is selections[0]?.start.row
-          selection = selections.shift()
-          selectionsToMove.push(selection)
-
         # Compute the range spanned by all these selections...
         linesRangeStart = [selection.start.row, 0]
         if selection.end.row > selection.start.row and selection.end.column is 0


Don't require notification manager in TextEditor

Instead expose getCursorScope and let users outside the text editor
show the notification.

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -79,7 +79,6 @@
     state.displayBuffer = displayBuffer
     state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId)
     state.config = atomEnvironment.config
-    state.notificationManager = atomEnvironment.notifications
     state.packageManager = atomEnvironment.packages
     state.clipboard = atomEnvironment.clipboard
     state.viewRegistry = atomEnvironment.views
@@ -100,12 +99,11 @@
       @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength,
       softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
       @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config,
-      @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
+      @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
       @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd
     } = params

     throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
-    throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager?
     throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager?
     throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard?
     throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry?
@@ -520,7 +518,7 @@
     softTabs = @getSoftTabs()
     newEditor = new TextEditor({
       @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs,
-      suppressCursorCreation: true, @config, @notificationManager, @packageManager,
+      suppressCursorCreation: true, @config, @packageManager,
       @firstVisibleScreenRow, @firstVisibleScreenColumn,
       @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate
     })
@@ -2827,13 +2825,9 @@
       @commentScopeSelector ?= new TextMateScopeSelector('comment.*')
       @commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes)

-  logCursorScope: ->
-    scopeDescriptor = @getLastCursor().getScopeDescriptor()
-    list = scopeDescriptor.scopes.toString().split(',')
-    list = list.map (item) -> "* #{item}"
-    content = "Scopes at Cursor\n#{list.join('\n')}"
-
-    @notificationManager.addInfo(content, dismissable: true)
+  # Get the scope descriptor at the cursor.
+  getCursorScope: ->
+    @getLastCursor().getScopeDescriptor()

   # {Delegates to: DisplayBuffer.tokenForBufferPosition}
   tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition)


Render child controls as txt

Render child controls as txt

--- src/util/render.coffee
+++ src/util/render.coffee
@@ -1,6 +1,10 @@
 my = require '../my'

+render_world = (world) ->
+  world.map_children (child) ->
+      "#{child} "
+
 exports.render = (world) ->
   T = world.T()
-  T.div {id: world}
+  T.div {id: world}, render_world(world)


Actually create the spy instead of just assigning

Otherwise you get some really funky, hard to track down errors like:

  "Attempted to wrap undefined property [object Object] as function"

--- test/components/helpers/component-testing.coffee
+++ test/components/helpers/component-testing.coffee
@@ -14,7 +14,7 @@
 beforeEach ->
   sandbox = sinon.sandbox.create()
   ROUTER  = sandbox.spy()
-  ROUTER.makeHref = sandbox.spy
+  ROUTER.makeHref = sandbox.spy()
   ROUTER.transitionTo = sandbox.spy()

 afterEach ->


whitespace nazi

whitespace nazi

--- workers/social/lib/social/models/graph/graph.coffee
+++ workers/social/lib/social/models/graph/graph.coffee
@@ -1,6 +1,7 @@
-_ = require 'underscore'
-neo4j = require "neo4j-koding"
+_      = require 'underscore'
+neo4j  = require "neo4j-koding"
 {race} = require 'sinkrow'
+
 {Base, ObjectId, race} = require 'bongo'

 module.exports = class Graph


payment: don't pass an undefined selector

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -1577,7 +1577,10 @@
     options ?= {}
     { tag, tags } = options
     if tag    then tags = [tag]
-    if tags   then selector = { tags }
+    selector =
+      if tags
+      then { tags }
+      else {}
     options = targetOptions: options: sort: sortWeight: 1
     switch category
       when 'product'


social: fix console.error typos in scriptblock

--- workers/social/lib/social/render/scriptblock.coffee
+++ workers/social/lib/social/render/scriptblock.coffee
@@ -120,9 +120,9 @@
         queue.fin()
     ->
       client.connection.delegate.fetchUser (err, user) ->
-        console.err err  if err
+        console.error err  if err
         unless user
-          console.err {message: "user not found"}
+          console.error {message: "[scriptblock] user not found"}
         else
           userId = user.getId()
         queue.fin()


Do not refresh the stats for a table if nothing has changed

--- admin/static/coffee/namespaces/namespace.coffee
+++ admin/static/coffee/namespaces/namespace.coffee
@@ -157,31 +157,31 @@
             directory.on 'all', @render
             progress_list.on 'all', @render
             log_initial '(initializing) namespace view: replica'
+            @data = {}

         render: =>
-            namespace_status = DataUtils.get_namespace_status(@model.get('id'))
-
-            json = @model.toJSON()
-            json = _.extend json, namespace_status
+            data = DataUtils.get_namespace_status(@model.get('id'))

             #Compute the total number of keys
-            json.total_keys_available = false
+            data.total_keys_available = false
             if @model.get('key_distr')?
                 # we can't use 'total_keys' because we need to
                 # distinguish between zero and unavailable.
-                json.total_keys_available = true
-                json.total_keys = 0
+                data.total_keys_available = true
+                data.total_keys = 0
                 for key of @model.get('key_distr')
-                    json.total_keys += parseInt @model.get('key_distr')[key]
+                    data.total_keys += parseInt @model.get('key_distr')[key]

-            json.stats_up_to_date = true
+            data.stats_up_to_date = true
             for machine in machines.models
                 if machine.get('stats')? and @model.get('id') of machine.get('stats') and machine.is_reachable
                     if machine.get('stats_up_to_date') is false
-                        json.stats_up_to_date = false
+                        data.stats_up_to_date = false
                         break

-            @.$el.html @template json
+            if not _.isEqual @data, data
+                @data = data
+                @.$el.html @template data

             return @


Add debounce function to taiga utils collection.

--- app/coffee/utils.coffee
+++ app/coffee/utils.coffee
@@ -85,6 +85,10 @@
     return _.str.join(str, coll)

+debounce = (wait, func) ->
+    return _.debounce(func, wait)
+
+
 taiga = @.taiga
 taiga.bindOnce = bindOnce
 taiga.mixOf = mixOf
@@ -95,3 +99,4 @@
 taiga.scopeDefer = scopeDefer
 taiga.toString = toString
 taiga.joinStr = joinStr
+taiga.debounce = debounce


Update fa.coffee (#3989)

Update fa.coffee (#3989)

--- app/locale/fa.coffee
+++ app/locale/fa.coffee
@@ -1,14 +1,14 @@
 module.exports = nativeDescription: "فارسی", englishDescription: "Persian", translation:
 #  new_home:
-#    slogan: "The most engaging game for learning programming."
+    slogan: "بهترین بازی برای یاد گرفتن برنامه نویسی"
 #    classroom_edition: "Classroom Edition:"
 #    learn_to_code: "Learn to code:"
-#    teacher: "Teacher"
-#    student: "Student"
-#    play_now: "Play Now"
-#    im_a_teacher: "I'm a Teacher"
-#    im_a_student: "I'm a Student"
-#    learn_more: "Learn more"
+    teacher: "معلم"
+    student: "دانش آموز"
+    play_now: "بازی کنید"
+    im_a_teacher: "من یک معلم هستم"
+    im_a_student: "من یک دانش آموز هستم"
+    learn_more: "بیشتر بدانید"
 #    classroom_in_a_box: "A classroom in-a-box for teaching computer science."
 #    codecombat_is: "CodeCombat is a platform for students to learn computer science while playing through a real game."
 #    our_courses: "Our courses have been specifically playtested to excel in the classroom, even by teachers with little to no prior programming experience."
@@ -67,7 +67,7 @@
 #    want_coco: "Want CodeCombat at your school?"

   nav:
-    play: "سطوح" # The top nav bar entry where players choose which levels to play
+    play: "بازی کنید" # The top nav bar entry where players choose which levels to play
 #    community: "Community"
 #    courses: "Courses"
     blog: "بلاگ"


Update fa.coffee

Update fa.coffee

--- app/locale/fa.coffee
+++ app/locale/fa.coffee
@@ -345,7 +345,7 @@
 #    unwatch: "Unwatch"
 #    submit_patch: "Submit Patch"
 #    submit_changes: "Submit Changes"
-#    save_changes: "Save Changes"
+#    save_changes: "ذخیره ی تغییرات"
 #    required_field: "required"
 #    valid_phone: "Enter a valid phone number."


Fix: avatar renders on repost in conversation view

--- assets/javascripts/views/post.js.coffee
+++ assets/javascripts/views/post.js.coffee
@@ -32,13 +32,13 @@

   fetchRepost: =>
     if @post?.isRepost()
-      @post.once 'repost:fetch:failed', =>
+      @post.on 'repost:fetch:failed', =>
         @$el.hide()

-      @post.once 'change:repost', =>
+      @post.on 'change:repost', =>
         repost = @post.get('repost')
-        repost.once 'change:profile', => @render()
-        repost.once 'change:disable_repost', => @render()
+        repost.on 'change:profile', => @render()
+        repost.on 'change:disable_repost', => @render()
         @render()
         @$el.show()
       unless @post.get('repost')


Update fr.coffee

Update fr.coffee

Correction

--- app/locale/fr.coffee
+++ app/locale/fr.coffee
@@ -345,7 +345,7 @@
     leaderboard: "classement"
     view_other_solutions: "Voir les autres solutions"
     scores: "Scores"
-    top_solutions: "Meilleurs solutions"
+    top_solutions: "Meilleures solutions"
     day: "Aujourd'hui"
     week: "Cette semaine"
     all: "Tous les temps"