Fix reverting from classical jupyter

Fix reverting from classical jupyter

--- src/smc-webapp/editor_jupyter.coffee
+++ src/smc-webapp/editor_jupyter.coffee
@@ -1188,7 +1188,12 @@
             a = redux.getProjectActions(@project_id)
             a.close_file(@filename)
             redux.getTable('account').set(editor_settings: {jupyter_classic : false})
-            a.open_file(path : @filename)
+            # ensure the side effects from changing registered
+            # editors in project_file.coffee finish happening
+            window.setTimeout =>
+                console.log "Opening #{@filename}"
+                a.open_file(path : @filename)
+            , 0

     info: () =>
         t = "
 About Jupyter Notebook
"


sync2 + jupyter: fixing little things; removing old hacks, doing things "righter", etc.

--- src/smc-webapp/editor_jupyter.coffee
+++ src/smc-webapp/editor_jupyter.coffee
@@ -133,7 +133,7 @@

     constructor: (@editor, @filename, opts={}) ->
         opts = @opts = defaults opts,
-            sync_interval   : 1000
+            sync_interval   : 2000
             cursor_interval : 2000
             read_only : false
         window.s = @
@@ -183,12 +183,8 @@
         @con      = @element.find(".smc-jupyter-notebook-connecting")
         @setup () =>
             # TODO: We have to do this stupid thing because in IPython's notebook.js they don't systematically use
-            # set_dirty, sometimes instead just directly seting the flag.  So there's no simple way to know exactly
+            # set_dirty, sometimes instead just directly setting the flag.  So there's no simple way to know exactly
             # when the notebook is dirty. (TODO: fix all this via upstream patches.)
-            # Also, note there are cases where IPython doesn't set the dirty flag
-            # even though the output has changed.   For example, if you type "123" in a cell, run, then
-            # comment out the line and shift-enter again, the empty output doesn't get sync'd out until you do
-            # something else.  If any output appears then the dirty happens.  I guess this is a bug that should be fixed in ipython.

             @_autosync_interval = setInterval(@autosync, @opts.sync_interval)
             @_cursor_interval   = setInterval(@broadcast_cursor_pos, @opts.cursor_interval)
@@ -249,6 +245,8 @@
                     @iframe.css(opacity:1)
                     @save_button.text('Readonly').addClass('disabled')
                     @show()
+                    for c in @nb.get_cells()
+                        c.code_mirror?.setOption('readOnly',true)
                     cb()
                 else
                     @_init_doc(cb)
@@ -308,7 +306,6 @@
         @status("Displaying Jupyter Notebook")
         if @doc.live() == ""
             # set the synchronized string from the visible notebook
-            ## TODO here
             @doc.live(@nb_to_string())
         else
             # set the visible notebook from the synchronized string
@@ -346,7 +343,7 @@
         @status()

     broadcast_cursor_pos: () =>
-        if not @nb? or @readonly
+        if not @nb? or @readonly or not @doc?
             # no point -- reloading or loading or read-only
             return
         # This is an ugly hack to ignore cursor movements resulting from remote changes.
@@ -518,7 +515,6 @@
                         if @opts.read_only
                             $(@frame.document).find("#menubar").remove()
                             $(@frame.document).find("#maintoolbar").remove()
-                            $(@frame.document).on('keydown', (e) -> e.preventDefault())  # helps a little.

                         a = @frame.$("#ipython_notebook").find("a")
                         if a.length == 0
@@ -547,7 +543,8 @@
                             for cmd in ['new', 'open', 'copy', 'rename']
                                 @frame.$("#" + cmd + "_notebook").remove()

-                            @frame.$("#save-notbook").remove()  # typo in ipython-3
+                            @frame.$("#save_checkpoint").remove()
+                            @frame.$("#restore_checkpoint").remove()
                             @frame.$("#save-notebook").remove()  # in case they fix the typo

                             @frame.$(".checkpoint_status").remove()
@@ -562,11 +559,6 @@
                             if not require('./feature').IS_MOBILE
                                 @frame.$("#site").css("padding-left", "20px")

-                            ### -- WARNING: this code below completely breaks everything on Firefox!
-                            @nb._save_checkpoint = @nb.save_checkpoint
-                            @nb.save_checkpoint = @save
-                            ###
-
                             # We have our own auto-save system
                             @nb.set_autosave_interval(0)

@@ -581,19 +573,21 @@
     autosync: () =>
         if @readonly or @_reloading
             return
-        if @nb?.dirty
+        if @nb?.dirty and @nb.dirty != 'clean'
             @dbg("autosync")
-            @nb.dirty = false
+            # nb.dirty is used internally by IPython so we shouldn't change it's truthiness.
+            # However, we still need a way in Sage to know that the notebook isn't dirty anymore.
+            @nb.dirty = 'clean'
             #console.log("causing sync")
             @save_button.removeClass('disabled')
             @sync()

     sync: (cb) =>
-        if @readonly
+        if @readonly or not @doc?
             cb?()
             return
         @editor.activity_indicator(@filename)
-        @save_button.icon_spin(start:true, delay:1500)
+        @save_button.icon_spin(start:true, delay:3000)
         @dbg("sync", "start")
         @doc.sync () =>
             @dbg("sync", "done")
@@ -604,7 +598,7 @@
         return not @save_button.hasClass('disabled')

     save: (cb) =>
-        if not @nb? or @readonly
+        if not @nb? or @readonly or not @doc?
             cb?(); return
         @save_button.icon_spin(start:true, delay:4000)
         @nb.save_notebook?(false)
@@ -616,7 +610,7 @@
     # Set the the visible notebook in the DOM from the synchronized string
     set_nb_from_doc: () =>
         current = @nb_to_string()
-        if not current?
+        if not current? or not @doc?
             return
         if @doc.live() != current
             @set_nb(@doc.live())
@@ -800,46 +794,38 @@
         @nb.delete_cell(index + 1)

         # TODO: If this cell was focused and our cursors were in this cell, we put them back:
-
+

         #console.log("set_cell: done", misc.mswalltime(t))

     # Notebook Doc Format: line 0 is meta information in JSON.
-    # Rest of file has one line for each cell for rest of file, in the following format:
+    # Rest of file has one line for each cell for rest of file, in JSON format.
     #
-    #     cell input text (with newlines replaced) [special unicode character] json object for cell, without input
-    #
-    # We split the line as above so that if/when there are merge conflicts
-    # that result in json corruption, which we then reject, only the *output*
-    # is impacted. The odds of corruption in the output is much less.
-    #
+    remove_images: (cell) =>
+        return # for now
+        if cell.outputs?
+            for out in cell.outputs
+                if out.data?
+                    for k, v of out.data
+                        if k.slice(0,6) == 'image/'
+                            delete out.data[k]
+
+    restore_images: (cell) =>
+        return
+
     cell_to_line: (cell) =>
         cell = misc.copy(cell)
-        source = misc.to_json(cell.source)
-        delete cell['source']
-        line = source + diffsync.MARKERS.output + misc.to_json(cell)
-        #console.log("\n\ncell=", misc.to_json(cell))
-        #console.log("line=", line)
-        return line
+        @remove_images(cell)
+        return misc.to_json(cell)

     line_to_cell: (line) =>
-        v = line.split(diffsync.MARKERS.output)
         try
-            if v[0] == 'undefined'  # backwards incompatibility...
-                source = undefined
-            else
-                source = JSON.parse(v[0])
+            cell = misc.from_json(line)
+            @restore_images(cell)
+            return cell
         catch e
-            console.log("line_to_cell('#{line}') -- source ERROR=", e)
-            return
-        try
-            cell = JSON.parse(v[1])
-            cell.source = source
-        catch e
-            console.log("line_to_cell('#{line}') -- output ERROR=", e)
-        #console.log("\n\nlin=", line)
-        #console.log("cell=", misc.to_json(cell))
-        return cell
+            console.warn("line_to_cell('#{line}') -- source ERROR=", e)
+            return

     # Convert the visible displayed notebook into a textual sync-friendly string
     nb_to_string: () =>
@@ -851,7 +837,7 @@
         doc = misc.to_json({notebook_name:obj.metadata.name})
         for cell in obj.cells
             doc += '\n' + @cell_to_line(cell)
-        @nb.dirty = false
+        @nb.dirty = 'clean' # see comment in autosync
         #@dbg("nb_to_string", "time", misc.mswalltime(tm))
         return doc

@@ -948,28 +934,10 @@
         @iframe?.attr('width', width).maxheight()
         setTimeout((()=>@iframe?.maxheight()), 1)   # set it one time more the next render loop.

-# This isn't used really -- it is called if somehow somebody directly opens the .ipython.syncdoc
-# history file, rather than opening the history directly in an ipython notebook.
-exports.process_history_editor = (cm) ->
-    #console.log("process_history_editor")
-    if cm.findMarksAt({line:0,ch:0}).length == 0
-        # hide document-wide metadata line
-        cm.markText({line:0,ch:0},{line:1,ch:0},{collapsed:true})
-    for i in [1...cm.lineCount()]
-        v = cm.getLine(i)
-        j = v.indexOf(diffsync.MARKERS.output)
-        m = cm.findMarksAt({line:i, ch:j-1})
-        #console.log("existing marks=",m)
-        if m.length == 0
-            #console.log("putting a mark from", {line:i, ch:j}, " to ", {line:i,ch:v.length})
-            cm.markText({line:i, ch:j-1}, {line:i,ch:v.length}, {collapsed:true})
-            cm.markText({line:i, ch:0}, {line:i,ch:1}, {collapsed:true})
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+


Disabling these by default as they break blur in Chrome. We should conditionally enable them.

--- framer/Config.coffee
+++ framer/Config.coffee
@@ -21,8 +21,8 @@
 		#"overflow": "visible"
 		#"z-index": 0
 		"-webkit-box-sizing": "border-box"
-		"-webkit-transform-style": "preserve-3d"
-		"-webkit-backface-visibility": "visible"
+		# "-webkit-transform-style": "preserve-3d"
+		# "-webkit-backface-visibility": "visible"
 		#"-webkit-backface-visibility": ""
 		#"-webkit-perspective": 500
 		# "pointer-events": "none"


Allow collection to be configured

Allow collection to be configured

--- components/user_interests/view.coffee
+++ components/user_interests/view.coffee
@@ -28,7 +28,7 @@
       @persist,
       @collectorProfile } = _.defaults options, @defaults

-    @collection = new UserInterests [], collectorProfile: @collectorProfile
+    @collection ?= new UserInterests [], collectorProfile: @collectorProfile
     @following = new Following [], kind: 'artist'

     @listenTo @collection, 'sync add remove', @renderCollection


state: fix update bug

state: fix update bug

--- src/server.coffee
+++ src/server.coffee
@@ -76,7 +76,7 @@
   setMode: (mode) =>
     @mode = mode

-  setRootFactory: (factory) ->
+  setRootFactory: (factory) =>
     @$rootComponent = factory
       cookies: cookie.parse document.cookie or ''


OHM-68: Added missing required module - manual merge slip through

--- src/server.coffee
+++ src/server.coffee
@@ -26,6 +26,7 @@
 http = require 'http'
 https = require 'https'
 net = require 'net'
+dgram = require 'dgram'
 koaMiddleware = require "./koaMiddleware"
 koaApi = require "./koaApi"
 tlsAuthentication = require "./middleware/tlsAuthentication"


Server: Detect and run subcommands.

Server: Detect and run subcommands.

--- src/server.coffee
+++ src/server.coffee
@@ -1,18 +1,27 @@
 path = require 'path'
 net = require 'net'
 spawn = require('child_process').spawn
+Impromptu = require './impromptu'

 # Create a new server.
 server = net.createServer (connection) ->
-  connection.on 'data', (chunk) ->
-    chunk = chunk.toString().trim()
-    if chunk == 'exit'
+  connection.on 'data', (input) ->
+    input = input.toString().trim()
+    if input == 'exit'
       process.exit()

     # Split the command back into its components.
-    chunks = chunk.split '__IMPROMPTU__'
+    args = input.split '__IMPROMPTU__'
+
+    # Find the most specific function based on the provided commands.
+    command = Impromptu
+    command = command[args.shift()] while args[0] && command[args[0]]
+
+    # Run the command.
+    command.apply this, args unless command == Impromptu
+
     # Output the command and close the connection.
-    connection.end chunks.toString()
+    connection.end()

 # Generate the socket path for this process.
 socketPath = path.resolve __dirname + '/../etc/' + process.pid + '.sock'


Update default value of sockjs_url to point to 0.3 client

--- src/sockjs.coffee
+++ src/sockjs.coffee
@@ -138,7 +138,7 @@
             heartbeat_delay: 25000
             disconnect_delay: 5000
             log: (severity, line) -> console.log(line)
-            sockjs_url: 'http://cdn.sockjs.org/sockjs-0.2.min.js'
+            sockjs_url: 'http://cdn.sockjs.org/sockjs-0.3.min.js'
         if user_options
             utils.objectExtend(@options, user_options)


removed logger

removed logger

--- src/javascript/views/card-stack.coffee
+++ src/javascript/views/card-stack.coffee
@@ -33,8 +33,6 @@
   onCardExit: (cardModel) =>
     @collection.remove(cardModel)
     @addCard()
-    console.log @collection
-    @collection.pop()

 module.exports = CardStackView


Style fix

Style fix

--- grammars/csound.cson
+++ grammars/csound.cson
@@ -142,8 +142,7 @@
       {
         name:  'constant.character.placeholder.csound'
         match: '%[#0\\- +]*\\d*(?:\\.\\d+)?[diuoxXfFeEgGaAcs]'
-      }
-      {
+      },{
         name:  'constant.character.escape.csound'
         match: '%%'
       }


Add: Extract enum, lenum, set of enum, set of lenum attributes from product.

--- lib/enum-validator.coffee
+++ lib/enum-validator.coffee
@@ -16,10 +16,43 @@
       productTypeEnumMap: {}

   validateProduct: (product, productType) =>
+    # fetch all enum attributes of all variants of the product.
+    # check if the product type exists in the cache
+      # if not then extract all enum attributes from product type and put in cache map
+    # foreach attribute
+      # check if attribute exists in the enum map
+        # if not then reject the product
+      # check if slugified version of attribute value exists as a key in enum map
+        # if not then create an update action
+        # add the slugified attribute value as key and orginal value as label
+        # if type is of lenum, then add slugified value as key and original value for all languages as label
     # to validate a product enums we need:
       # product type of that product
       # attribute names of type enum or lenum or set of enum or set of lenum
       #
     Promise.resolve()

+  _fetchEnumAttributesFromProduct: (product) ->
+    enumAttributes = @_fetchEmumAttributesFromVariant(product.masterVariant)
+    if product.variants and not _.isEmpty(product.variants)
+      for variant in product.variants
+        enumAttributes = enumAttributes.concat(@_fetchEmumAttributesFromVariant(variant))
+    enumAttributes
+
+
+  _fetchEmumAttributesFromVariant: (variant) ->
+    _.filter(variant.attributes, @_enumFilterPredicate)
+      .concat(_.filter(variant.attributes, @_enumSetFilterPredicate))
+      .concat(_.filter(variant.attributes, @_lenumSetFilterPredicate))
+
+
+  _enumFilterPredicate: (attribute) ->
+    attribute.type.name is 'enum' or attribute.type.name is 'lenum'
+
+  _enumSetFilterPredicate: (attribute) ->
+    attribute.type.name is 'set' and attribute.type.name.elementType is 'enum'
+
+  _lenumSetFilterPredicate: (attribute) ->
+    attribute.type.name is 'set' and attribute.type.name.elementType is 'lenum'
+
 module.exports = EnumValidator


Update: returns map of productTypeId to list of update actions.

--- lib/enum-validator.coffee
+++ lib/enum-validator.coffee
@@ -9,7 +9,7 @@

   constructor: (@logger) ->
     @_resetCache()
-    if @logger then @logger.info "Enum Validator initialized."
+    debug "Enum Validator initialized."

   _resetCache: ->
     @_cache =
@@ -21,7 +21,10 @@
       enumAttributes = @_fetchEnumAttributesFromProduct(product, resolvedProductType)
       @_validateEnums(enumAttributes, resolvedProductType)
       .then (updateActions) ->
-        resolve(updateActions)
+        update =
+          productTypeId: resolvedProductType.id
+          actions: updateActions
+        resolve(update)

   _validateEnums: (enumAttributes, productType) =>
     new Promise (resolve, reject) =>


Drop i scroll.

Drop i scroll.

--- js/flight.coffee
+++ js/flight.coffee
@@ -1,4 +1,5 @@
 class Flight
+  # Private
   version: '0.0.1'
   isTransitioning: false
   startPage: ''
@@ -7,8 +8,6 @@
   pageHistory: [""]
   os: {}
   hideUrlBar: false
-  useScroller: false
-  iScrollInstance: null
   # options
   transitionAnimation: true
   speed: '0.4s'
@@ -19,9 +18,7 @@
       if options.transitionAnimation?
         @transitionAnimation = options.transitionAnimation
       if options.speed?
-        @speed = options.speed
-      if options.useScroller?
-        @useScroller = options.useScroller
+        @speed = options.speed

     # see what device we're using
     @detectUserAgent()
@@ -33,7 +30,7 @@

   # Goes to page, transitionAnimation defines if transition happens or not
   goTo: (targetPage, options) =>
-
+
     if typeof targetPage is "string"
       @targetPage = document.querySelector targetPage
     else if targetPage
@@ -46,13 +43,9 @@
       @startPage = [window.location.hash];
       @currentPage = targetPage
       @addClass @currentPage, 'visible'
-
-      if @useScroller is true
-        @setScroller @currentPage
-
-      return
-
-    # if already transitioning then return
+      return
+
+    # if already transitioning then return
     if @isTransitioning is true then return else @isTransitioning = true

     # set curent panel
@@ -150,10 +143,7 @@
     @currentPage.style.display = "none"
     @addClass @targetPage, 'visible'
     @currentPage.removeEventListener "webkitTransitionEnd", @finishTransition, false
-
-    if @useScroller is true
-      @setScroller(@targetPage)
-
+
    finishSlideFromBottom: =>
     @removeClass @currentPage, 'visible'
     @addClass @targetPage, 'visible'
@@ -183,13 +173,6 @@
     else
       throw new Error "#flight or .content not found."

-  setScroller: (wrapper) =>
-
-      if @iScrollInstance? then @iScrollInstance.destroy(); @iScrollInstance = null
-
-      window.setTimeout =>
-        @iScrollInstance = new iScroll(wrapper.getElementsByClassName('wrapper')[0].id)
-      , 0
   # detects user agent being used
   detectUserAgent: ->
     userAgent = window.navigator.userAgent


Scroll to the top on page transition.

Scroll to the top on page transition.

--- js/flight.coffee
+++ js/flight.coffee
@@ -61,6 +61,8 @@
               @pageHistory.pop()

     @currentPanel = document.getElementsByClassName('visible')[0]
+
+    window.scrollTo 0, 1

     if @transitionAnimation is true
       switch transitionType


Set current panel in goToPage function to accommodate change of context.

--- js/flight.coffee
+++ js/flight.coffee
@@ -33,6 +33,8 @@

     @targetPanel = document.querySelector(targetPanel)
     transtionType = @currentPanel.getAttribute("data-transition")
+
+    @currentPanel = document.getElementsByClassName('visible')[0]

     if @transitionAnimation is true


Sort launches by launch_date_unix and skip fake payload capsule reuse

--- js/script.coffee
+++ js/script.coffee
@@ -55,6 +55,10 @@
 	return

 cb_spacex = (r) ->
+	# Sort by launch_date_unix
+	r.sort (obj1, obj2) => obj1.launch_date_unix - obj2.launch_date_unix
+	# for item in temporals
+	# 	console.log item.flight_number, item.launch_date_unix
 	table = document.createElement 'table'
 	tfoot = document.createElement 'tfoot'
 	tbody = document.createElement 'tbody'
@@ -71,7 +75,7 @@
 	for p in r
 		if p.launch_date_unix
 			core_reused = if p.reused then '*' else ''
-			cap_reused = if p.reuse.capsule then '*' else ''
+			# cap_reused = if p.reused.capsule then '*' else ''
 			t_minus = p.launch_date_unix - (new_date / 1000)
 			t_positive = if t_minus > 0 then true else false
 			timezone = -1 * new Date().getTimezoneOffset() / 60
@@ -147,10 +151,14 @@
 			# CELL 6
 			#
 			body += "#{launch_time}#{tz}#{launch_time_local}#{offset}	"
-			if (p.reuse.core and p.rocket.first_stage.cores[0].core_serial) or p.rocket.first_stage.cores[0].flight > 1 then core_reflown.push p.rocket.first_stage.cores[0].core_serial
-			if p.reuse.side_core1 then core_reflown.push p.rocket.first_stage.cores[1].core_serial
-			if p.reuse.side_core2 then core_reflown.push p.rocket.first_stage.cores[2].core_serial
-			if p.rocket.second_stage.payloads[0].reused then cap_reflown.push p.rocket.second_stage.payloads[0].cap_serial
+			if (p.rocket.first_stage.cores[0].reused and p.rocket.first_stage.cores[0].core_serial) or p.rocket.first_stage.cores[0].flight > 1 then core_reflown.push p.rocket.first_stage.cores[0].core_serial
+			if p.rocket.first_stage.cores.length > 1
+				if p.rocket.first_stage.cores[1].reused then core_reflown.push p.rocket.first_stage.cores[1].core_serial
+				if p.rocket.first_stage.cores[2].reused then core_reflown.push p.rocket.first_stage.cores[2].core_serial
+			# Check capsule reused
+			if p.rocket.second_stage.payloads[0].reused && p.rocket.second_stage.payloads[0].cap_serial
+				cap_reflown.push p.rocket.second_stage.payloads[0].cap_serial
+				console.log p.rocket.second_stage.payloads[0]
 			row.innerHTML = body
 			tbody.appendChild row
 	table.appendChild tbody
@@ -159,7 +167,6 @@
 	for core_id in core_reflown
 		# loadJSON "{{ site.spacex_api }}?core_serial=#{core_id}&date=#{new_date}", cb_reflow
 		loadJSON "{{ site.spacex_api }}?core_serial=#{core_id.split(".")[0]}&date=#{new_date}", cb_reflow
-	console.log cap_reflown
 	for cap_id in cap_reflown
 		loadJSON "{{ site.spacex_api }}?cap_serial=#{cap_id}&date=#{new_date}", cb_reflow
 	return


Cleanup.

Cleanup.

--- js2coffee.coffee
+++ js2coffee.coffee
@@ -958,8 +958,8 @@
       space [ "[", node.elements.map(@walk), "]" ]
     else
       @indent (indent) =>
-        prefix = [ @indent() ]
-        contents = prependAll(node.elements.map((e) => newline @walk(e)), prefix)
+        elements = node.elements.map (e) => newline @walk(e)
+        contents = prependAll(elements, @indent())
         [ "[\n", contents, indent, "]" ]

   ObjectExpression: (node, ctx) ->


Update -> promise resolution fixed.

Update -> promise resolution fixed.

--- lib/product-import.coffee
+++ lib/product-import.coffee
@@ -198,10 +198,10 @@
         resolve(product)

   _ensureProductTypesInMemory: (products) =>
-    Promise.all [
-      _.map products, (product) =>
-        @_ensureProductTypeInMemory(product.productType.id)
-    ]
+    Promise.map products, (product) =>
+      @_ensureProductTypeInMemory(product.productType.id)
+    , {concurrency: 1}
+

   _ensureProductTypeInMemory: (productTypeId) =>
     @logger.info "ensuring product type id: #{productTypeId}"
@@ -223,23 +223,18 @@
     enumUpdateActions

   _updateProductType: (enumUpdateActions) =>
-    new Promise (resolve) =>
-      if _.isEmpty(enumUpdateActions)
-        resolve()
-      else
-        debug "Updating product type(s): #{_.keys(enumUpdateActions)}"
-        Promise.all [
-          for productTypeId in _.keys(enumUpdateActions)
-            updateRequest =
-              version: @_cache.productType[productTypeId].version
-              actions: enumUpdateActions[productTypeId]
-            @client.productTypes.byId(@_cache.productType[productTypeId].id).update(updateRequest)
-            .then (updatedProductType) =>
-              @_cache.productType[productTypeId] = updatedProductType.body
-              @_summary.productTypeUpdated++
-        ]
-        .then ->
-          resolve()
+    if _.isEmpty(enumUpdateActions)
+      Promise.resolve()
+    else
+      debug "Updating product type(s): #{_.keys(enumUpdateActions)}"
+      Promise.map _.keys(enumUpdateActions), (productTypeId) =>
+        updateRequest =
+          version: @_cache.productType[productTypeId].version
+          actions: enumUpdateActions[productTypeId]
+        @client.productTypes.byId(@_cache.productType[productTypeId].id).update(updateRequest)
+        .then (updatedProductType) =>
+          @_cache.productType[productTypeId] = updatedProductType.body
+          @_summary.productTypeUpdated++

   _updateEnumUpdateActions: (enumUpdateActions, updateActions) ->
@@ -249,16 +244,15 @@
       enumUpdateActions[updateActions.productTypeId] = updateActions.actions

   _fetchSameForAllAttributesOfProductType: (productType) =>
-    new Promise (resolve) =>
-      if @_cache.productType["#{productType.id}_sameForAllAttributes"]
-        resolve(@_cache.productType["#{productType.id}_sameForAllAttributes"])
-      else
-        @_resolveReference(@client.productTypes, 'productType', productType, "name=\"#{productType?.id}\"")
-        .then (productTypeId) =>
-          sameValueAttributes = _.where(@_cache.productType[productType.id].attributes, {attributeConstraint: "SameForAll"})
-          sameValueAttributeNames = _.pluck(sameValueAttributes, 'name')
-          @_cache.productType["#{productType.id}_sameForAllAttributes"] = sameValueAttributeNames
-          resolve(sameValueAttributeNames)
+    if @_cache.productType["#{productType.id}_sameForAllAttributes"]
+      Promise.resolve(@_cache.productType["#{productType.id}_sameForAllAttributes"])
+    else
+      @_resolveReference(@client.productTypes, 'productType', productType, "name=\"#{productType?.id}\"")
+      .then =>
+        sameValueAttributes = _.where(@_cache.productType[productType.id].attributes, {attributeConstraint: "SameForAll"})
+        sameValueAttributeNames = _.pluck(sameValueAttributes, 'name')
+        @_cache.productType["#{productType.id}_sameForAllAttributes"] = sameValueAttributeNames
+        Promise.resolve(sameValueAttributeNames)

   _ensureVariantDefaults: (variant = {}) ->
     variantDefaults =


:memo: Add code documentation

:memo: Add code documentation

--- lib/tabs-to-spaces.coffee
+++ lib/tabs-to-spaces.coffee
@@ -2,12 +2,17 @@
 # Copyright (c) 2014 by Lifted Studios. All Rights Reserved.
 #

+# Handles the interface between Atom and the Tabs to Spaces package.
 class TabsToSpaces
   spaces = null

+  # Activates the package.
   activate: ->
     atom.workspaceView.command 'tabs-to-spaces:convert', => @convert()

+  # Converts all leading tabs to spaces in the current editor window.
+  #
+  # Sets the cursor position to the top of the file when complete.
   convert: ->
     editor = atom.workspace.getActiveEditor()
     return unless editor?
@@ -19,6 +24,10 @@
       @replaceTabsWithSpaces(selection) for selection in editor.getSelections()
     editor.setCursorBufferPosition([0, 0])

+  # Replaces all leading tabs with spaces in the given selection.
+  #
+  # @private
+  # @param [Selection] selection Selection within which to replace tabs with spaces.
   replaceTabsWithSpaces: (selection) ->
     text = selection.getText()
     newText = text.replace /^\t+/, ''
@@ -26,6 +35,10 @@
     selection.insertText(indentation + newText)
     selection.clear()

+  # Sets the number of spaces to replace a single tab character with.
+  #
+  # @private
+  # @param [String] spcs Text to replace a single tab character with.
   setSpaces: (spcs) ->
     spaces = spcs


fix session

fix session

--- src/server/middleware/user_info.coffee
+++ src/server/middleware/user_info.coffee
@@ -8,8 +8,8 @@
   return next() unless req.session.user_id
   Cache.get "user:#{req.session.user_id}", (cache_user) ->
     if cache_user
-      console.log "cache", cache_user
       res.locals.USER = cache_user[0]
+      console.log "hay cache, seteo res locals", res.locals.USER
       next()
     else
       User.connect (client) ->
@@ -18,6 +18,7 @@
             user_data = user[0]
             Cache.set "user:#{req.session.user_id}", user_data, (cache_user) ->
               res.locals.USER = user_data
+              console.log "no hay cache, seteo res locals", res.locals.USER
               next()
           else
             Cache.del "user:#{req.session.user_id}", () ->


less confusing way to fix has a ward rule operands to be shown in ruleset editor

--- static/coffee/flows/controllers.coffee
+++ static/coffee/flows/controllers.coffee
@@ -946,15 +946,8 @@
       # lets rip out the delta parameter and use it as our test instead
       rule.test._base = rule.test.test.slice(15, -1)

-
-    # set operands for has ward rule
-    else if rule.test.type == "ward"
-      rule.test =
-        state: rule.test.state
-        district: rule.test.district
-
     # set the operands
-    else if rule.test.type != "between"
+    else if rule.test.type != "between" and rule.test.type != "ward"

       if rule.test.test and rule._config.localized
         rule.test._base = rule.test.test[flow.base_language]


Remove --rpm-auto-add-directories from rpm package

--- tasks/pack.coffee
+++ tasks/pack.coffee
@@ -178,7 +178,7 @@
         '--name'
         manifest.name
         '--force' # Overwrite existing files
-        '--rpm-auto-add-directories'
+        # '--rpm-auto-add-directories'
         if args.verbose then '--verbose' else null
         '--after-install'
         './build/resources/linux/after-install.sh'
@@ -246,31 +246,31 @@
         fpmArgs.splice depsInsertIndex, 0, dep
         fpmArgs.splice depsInsertIndex, 0, '--depends'

-      # Exclude folders from rpm
-      if target == 'rpm'
-        excludedFolders = [
-          '/usr/share/icons/hicolor'
-          '/usr/share/icons/hicolor/16x16'
-          '/usr/share/icons/hicolor/16x16/apps'
-          '/usr/share/icons/hicolor/24x24'
-          '/usr/share/icons/hicolor/24x24/apps'
-          '/usr/share/icons/hicolor/32x32'
-          '/usr/share/icons/hicolor/32x32/apps'
-          '/usr/share/icons/hicolor/48x48'
-          '/usr/share/icons/hicolor/48x48/apps'
-          '/usr/share/icons/hicolor/64x64'
-          '/usr/share/icons/hicolor/64x64/apps'
-          '/usr/share/icons/hicolor/128x128'
-          '/usr/share/icons/hicolor/128x128/apps'
-          '/usr/share/icons/hicolor/256x256'
-          '/usr/share/icons/hicolor/256x256/apps'
-          '/usr/share/icons/hicolor/512x512'
-          '/usr/share/icons/hicolor/512x512/apps'
-        ]
-
-        for excludedFolder in excludedFolders
-          fpmArgs.splice depsInsertIndex, 0, excludedFolder
-          fpmArgs.splice depsInsertIndex, 0, '--exclude'
+      # # Exclude folders from rpm
+      # if target == 'rpm'
+      #   excludedFolders = [
+      #     '/usr/share/icons/hicolor'
+      #     '/usr/share/icons/hicolor/16x16'
+      #     '/usr/share/icons/hicolor/16x16/apps'
+      #     '/usr/share/icons/hicolor/24x24'
+      #     '/usr/share/icons/hicolor/24x24/apps'
+      #     '/usr/share/icons/hicolor/32x32'
+      #     '/usr/share/icons/hicolor/32x32/apps'
+      #     '/usr/share/icons/hicolor/48x48'
+      #     '/usr/share/icons/hicolor/48x48/apps'
+      #     '/usr/share/icons/hicolor/64x64'
+      #     '/usr/share/icons/hicolor/64x64/apps'
+      #     '/usr/share/icons/hicolor/128x128'
+      #     '/usr/share/icons/hicolor/128x128/apps'
+      #     '/usr/share/icons/hicolor/256x256'
+      #     '/usr/share/icons/hicolor/256x256/apps'
+      #     '/usr/share/icons/hicolor/512x512'
+      #     '/usr/share/icons/hicolor/512x512/apps'
+      #   ]
+      #
+      #   for excludedFolder in excludedFolders
+      #     fpmArgs.splice depsInsertIndex, 0, excludedFolder
+      #     fpmArgs.splice depsInsertIndex, 0, '--exclude'

       async.series [
         # Update package.json


Fix makensis argument path

Fix makensis argument path

--- tasks/pack.coffee
+++ tasks/pack.coffee
@@ -418,7 +418,7 @@
         .then callback, callback

     # Run makensis
-    applySpawn (process.env.MAKENSIS_PATH or 'makensis.exe'), ['./build/resources/win/installer.nsi']
+    applySpawn (process.env.MAKENSIS_PATH or 'makensis.exe'), ['build/resources/win/installer.nsi']
   ], done

 # Create the win32 portable zip


also select a leg when its expanded graph bar is clicked

--- web/js/views/route_graph_view.coffee
+++ web/js/views/route_graph_view.coffee
@@ -11,6 +11,7 @@

     events:
       'click .leg-info': 'selectLeg'
+      'click .leg-bar':  'selectLegWhenExpanded'

     initialize: (routes: routes, routeParams: routeParams, index: index) ->
       @routes = routes
@@ -22,6 +23,9 @@
     render: ->
       @$el.html template(legs: @_legData())
       this
+
+    selectLegWhenExpanded: (e) =>
+      if @expanded then @selectLeg(e)

     selectLeg: (e) =>
       idx = $(e.target).closest('[data-leg]').data('leg')


Popup - allow displaying jQuery element in popup. Popup option to align popup on right instead of left.

--- lib/Popup.coffee
+++ lib/Popup.coffee
@@ -39,6 +39,8 @@
   if ST.View && (display instanceof ST.View)
     display.load()
     popup.append display.element()
+  else if display instanceof jQuery
+    popup.append display
   else
     ul = $ "

"
     popup.append ul
@@ -58,9 +60,10 @@
           li.append a
           ul.append li

-  popup.click (e) -> e.stopPropagation()
+  popup.click (e) ->
+    e.stopPropagation()

-  style = if offset.left < $(window).width() - 150
+  style = if (offset.left < $(window).width() - 150) && !options.right
     "left: #{Math.round offset.left}px"
   else
     "right: #{Math.round($(window).width() - offset.left - element.outerWidth())}px"
@@ -72,12 +75,13 @@
 # items   Items for menu, see #popup
 # open    Callback function to execute before opening the popup menu
 # close   Callback function to execute after the popup menu is closed
-jQuery.fn.popup = (items, open, close) ->
+jQuery.fn.popup = (items, open, close, options) ->
   id = popupIDs++
   @click (e) ->
+    e.preventDefault()
     e.stopPropagation()
     element = this
-    options = {}
+    options = $.extend({}, options);
     options.close = -> close.call element if close
     if popupID == id
       closePopup()


Highlights definition

Highlights definition

--- lib/godef.coffee
+++ lib/godef.coffee
@@ -24,7 +24,7 @@
     @emit 'reset', @editor

   # new pattern as per http://blog.atom.io/2014/09/16/new-event-subscription-api.html
-  # (so far unable to get event-kit subscriptions to work)
+  # (but so far unable to get event-kit subscriptions to work)
   onDidComplete: (callback) ->
     @on @didCompleteNotification, callback

@@ -36,7 +36,8 @@
     @reset @editor
     done = (err, messages) =>
       @dispatch.resetAndDisplayMessages @editor, messages
-    @gotoDefinitionForWord  @wordAtCursor(), done
+    {word, range} = @wordAtCursor()
+    @gotoDefinitionForWord word, done

   gotoDefinitionForWord: (word, callback = ->) ->
     message = {}
@@ -60,10 +61,11 @@
         targetFilePath = outputs[0]
         if targetFilePath == @editor.getPath()
           @editor.setCursorBufferPosition [col, line]
-          # @editor.markBufferRange([[1,1], ])
+          @highlightWordAtCursor()
           @emit @didCompleteNotification, @editor, false
         else
           atom.workspace.open(targetFilePath, {initialLine:line, initialColumn:col}).then (e) =>
+            @highlightWordAtCursor(atom.workspace.getActiveEditor())
             @emit @didCompleteNotification, @editor, false
       else # godef can't find def
         message =
@@ -82,7 +84,20 @@

     @dispatch.executor.exec(cmd, cwd, env, done, args)

-  wordAtCursor: ->
+  wordAtCursor: (editor = @editor) ->
     options =
       wordRegex: /[\w+\.]*/
-    return @editor.getWordUnderCursor(options)
+    cursor = editor.getCursor()
+    # TODO this is probably a furphy. It's the range of godef's output that's needed
+    range = cursor.getCurrentWordBufferRange(options)
+    word = @editor.getTextInBufferRange(range)
+    return {word: word, range: range}
+
+  highlightWordAtCursor: (editor = @editor) ->
+    {word, range} = @wordAtCursor(editor)
+    highlightMarker = editor.markBufferRange(range, {invalidate:'inside'})
+    highlightDecoration = editor.decorateMarker(highlightMarker, {type:'highlight', class:'goplus-godef-highlight'})
+    cursor = editor.getCursor()
+    # TODO how
+    subscription = cursor.onDidChangePosition ->
+      highlightMarker.destroy()


leftover

leftover

--- lib/input.coffee
+++ lib/input.coffee
@@ -86,8 +86,7 @@

   confirm: ->
     result =
-      text: "\\s*desc"
-      # text: @editor.getText()
+      text: @editor.getText()
       isRegExp: @regExp
     RegExpStateByProvider[@provider.name] = @regExp
     @provider = null


Fix cache

Fix cache

--- lib/proxy.coffee
+++ lib/proxy.coffee
@@ -96,7 +96,7 @@
             res.writeHead 200,
               'Server': 'Apache'
               'Content-Length': data.length
-              'Cache-Control': 'max-age=2592000, public'
+              'Cache-Control': 'no-cache'
               'Content-Type': mime.lookup cacheFile
             res.end data
           # Enable retry for game api


Fix the debug

Fix the debug

--- lib/queue.coffee
+++ lib/queue.coffee
@@ -1,4 +1,4 @@
-debug = require('debug')('czirho:bus')
+debug = require('debug')('czirho:queue')

 class Queue
   constructor: (options={}) ->


Hating this.

Hating this.

--- lib/setup.coffee
+++ lib/setup.coffee
@@ -66,6 +66,10 @@

   console.log 'Mounting middleware...'

+  app.use (req, res, next) ->
+    console.log 'Passing through middleware'
+    next()
+
   app.use sharify

   blocker.addToReferrers [
@@ -120,6 +124,10 @@

   console.log 'Mounting apps...'

+  app.use (req, res, next) ->
+    console.log 'Passing through routes'
+    next()
+
   app
     # Normal routing: In order
     .use require '../apps/feed'


Changed the callback to use the fat arrow.. because i want to.

--- lib/suite.coffee
+++ lib/suite.coffee
@@ -45,7 +45,7 @@
       @_run_runners @_after_alls, callback
       return

-    before_alls_callback = (reports) ->
+    before_alls_callback = (reports) =>
       before_alls = []
       item.run test_callback


coffee-script@1.0.0 support with do-keyword, and more globals suppport like __filename

--- lib/zappa.coffee
+++ lib/zappa.coffee
@@ -18,6 +18,9 @@
       app: (name) => @app name
       include: (path) => @include path
       require: require
+      global: global
+      process: process
+      module: module

     @ensure_app 'default' unless @current_app?

@@ -49,6 +52,9 @@
   read: (file) -> fs.readFileSync file, 'utf8'

   run_file: (file, options) ->
+    @locals.__filename = require('path').join(process.cwd(), file)
+    @locals.__dirname = process.cwd()
+    @locals.module.filename = @locals.__filename
     code = if file.match /\.coffee$/ then @read_and_compile file else @read file
     @run code, options

@@ -195,17 +201,19 @@
   client: (arg) ->
     pairs = if typeof arg is 'object' then arg else {default: arg}
     for k, v of pairs
-      code = ";(#{v})();"
-      @http_server.get "/#{k}.js", (req, res) ->
-        res.contentType 'bla.js'
-        res.send code
+      do (k, v) ->
+        code = ";(#{v})();"
+        @http_server.get "/#{k}.js", (req, res) ->
+          res.contentType 'bla.js'
+          res.send code

   style: (arg) ->
     pairs = if typeof arg is 'object' then arg else {default: arg}
     for k, v of pairs
-      @http_server.get "/#{k}.css", (req, res) ->
-        res.contentType 'bla.css'
-        res.send v
+      do (k, v) ->
+        @http_server.get "/#{k}.css", (req, res) ->
+          res.contentType 'bla.css'
+          res.send v

 class RequestHandler
   constructor: (handler, @defs, @helpers, @postrenders, @views, @layouts, @vars) ->
@@ -225,8 +233,9 @@
       @locals[k] = v

     for k, v of @helpers
-      @locals[k] = ->
-        v(@context, @, arguments)
+      do (k, v) ->
+        @locals[k] = ->
+          v(@context, @, arguments)

     @locals.defs = @defs
     @locals.postrenders = @postrenders
@@ -314,8 +323,9 @@
       @locals[k] = v

     for k, v of @app.helpers
-      @locals[k] = ->
-        v(@context, @, arguments)
+      do (k, v) ->
+        @locals[k] = ->
+          v(@context, @, arguments)

     @locals.defs = @app.defs
     @locals.postrenders = @app.postrenders
@@ -407,10 +417,11 @@

 publish_api = (from, to, methods) ->
   for name in methods.split '|'
-    if typeof from[name] is 'function'
-      to[name] = -> from[name].apply from, arguments
-    else
-      to[name] = from[name]
+    do (name) ->
+      if typeof from[name] is 'function'
+        to[name] = -> from[name].apply from, arguments
+      else
+        to[name] = from[name]

 z = new Zappa()


Fix hud

Fix hud

--- modules/player/hud.coffee
+++ modules/player/hud.coffee
@@ -125,7 +125,7 @@
     } if fstr
     reply.attachments.push {
       color: '#42a7f4',
-      footer: "Sent by #{@getDisplayName @bot.User}"
+      footer: "Sent by #{@getDisplayName @bot.User.memberOf(guild)}"
       footer_icon: @bot.User.avatarURL
     }
     return reply
@@ -147,7 +147,7 @@
           thumb_url: item.thumbnail
         }
         {
-          footer: "Sent by #{@getDisplayName @bot.User}"
+          footer: "Sent by #{@getDisplayName @bot.User.memberOf(guild)}"
           footer_icon: @bot.User.avatarURL
         }
       ]
@@ -170,7 +170,7 @@
         {
           color: '#42a7f4'
           text: "Added a playlist of **#{length}** items to the queue!"
-          footer: "Sent by #{@getDisplayName @bot.User}"
+          footer: "Sent by #{@getDisplayName @bot.User.memberOf(guild)}"
           footer_icon: @bot.User.avatarURL
         }
       ]


sort the teams by last deployed

sort the teams by last deployed

--- nodeko.coffee
+++ nodeko.coffee
@@ -127,9 +127,9 @@

 # list teams
 get '/teams', ->
-  Team.all (error, teams) =>
+  Team.all {}, { 'sort': [['lastDeployedAt', -1]] }, (error, teams) =>
     [@teams, @unverifiedTeams] = [[], []]
-    for team in _.shuffle(teams)
+    for team in teams
       if team.members.length == team.invited.length
         @unverifiedTeams.push team
       else


Reduce code and pattern duplication

Reduce code and pattern duplication

--- js/simulation.coffee
+++ js/simulation.coffee
@@ -366,7 +366,7 @@
   )

-card_bind = (type, tag, take_=id) ->
+card_bind = (type, tag, take_func=id) ->
   ### Bind data to a card selection

   Args:
@@ -380,12 +380,12 @@
   sequence(
     (x) -> x.data,
     filter((x) -> x.type is type),
-    take_,
+    take_func,
     select_tag(tag),
   )

-add_card = (addf, type, tag) ->
+add_card = (addf, type, tag, with_div=id, take_func=take(1)) ->
   ### Constuct a data card

   Args:
@@ -397,17 +397,10 @@
     a function to build the card
   ###
   sequence(
-    card_bind(type, tag, take_=take(1)),
+    card_bind(type, tag, take_func=take_func),
+    with_div,
     build_card(addf)
   )
-
-
-card_images = sequence(
-  card_bind("image", "#images"),
-  (x) -> x.append("div").attr("class", "col s4"),
-  build_card(add_image)
-)
-

 update_data = (x) ->
@@ -429,11 +422,6 @@
 )

-prom = (x, i, a) ->
-  view = new vega.View(vega.parse(x))
-  prom_ = view.toImageURL('svg')
-
-
 add_src = (x) ->
   new vega.View(vega.parse(x.datum()))
     .toImageURL("svg")
@@ -442,21 +430,18 @@

 add_chart = (x) ->
   add_src(add_card_image_(x)
-    # .attr("id", "chart")
     .attr("style", "backgroud-color: white;"))

-card_charts = sequence(
-  (x) -> x.data,
-  filter((x) -> x.type is "line"),
+take_data = sequence(
   map(combine_data({{ site.data.charts.plot1d | jsonify }})),
-  map(update_data),
-  select_tag("#images"),
-  (x) -> x.append("div").attr("class", "col s4"),
-  (x) -> x.append("div").attr("class", "card small"),
-  do_(add_chart),
-  add_description
+  map(update_data)
 )
+
+
+add_card_col = (addf, type, take_func) ->
+  with_div = (x) -> x.append("div").attr("class", "col s4")
+  add_card(addf, type, "#images", with_div=with_div, take_func=take_func)

 header(SIM_NAME)
@@ -470,5 +455,5 @@
 results_table(DATA)
 add_card(add_image, "image", "#logo_image")(DATA)
 add_card(add_youtube, "youtube", "#youtube")(DATA)
-card_images(DATA)
-card_charts(DATA)
+add_card_col(add_image, "image", id)(DATA)
+add_card_col(add_chart, "line", take_data)(DATA)


Added about pages (general and LSE SU)

Added about pages (general and LSE SU)

--- routes/about.coffee
+++ routes/about.coffee
@@ -1,2 +1,7 @@
 exports.enactus = (req, res) ->
-	    res.render 'about-enactus'
+	res.locals.page.title = res.locals.page.title + " - About Enactus"
+	res.render 'about/enactus'
+
+exports.lsesu = (req, res) ->
+	res.locals.page.title = res.locals.page.title + " - About Us"
+	res.render 'about/us'


Fixed broken url generation on Windows

Fixed broken url generation on Windows

--- lib/extensions/dynamic_content.coffee
+++ lib/extensions/dynamic_content.coffee
@@ -23,7 +23,7 @@

   set_dynamic_locals: (ctx) ->
     ctx.dynamic_locals.contents = ctx.contents
-
+
     # get an array of folder the content is nested in
     nested_folders = ctx.path.replace(roots.project.rootDir,'').split(path.sep)
     nested_folders.pop()
@@ -65,7 +65,7 @@
     # this provides integration with the layouts extension
     if front_matter.layout
       (new LayoutsExtension).set_layout_locals(fh, front_matter.layout, true)
-      fh.dynamic_locals.url = fh.path.replace(roots.project.rootDir, '').replace(/\..*$/, ".html")
+      fh.dynamic_locals.url = fh.path.replace(roots.project.rootDir, '').replace(/\..*$/, ".html").replace(/\\/g, "/")

     # remove the front matter
     fh.contents = fh.contents.replace(front_matter_string[0], '')


Add Polish

Add Polish

--- overrides.coffee
+++ overrides.coffee
@@ -6,6 +6,7 @@
 languageManager = new LanguageManager
   translations:
     en: label: 'English', strings: enUs
+    pl: label: 'Polski', strings: './translations/pl.json'

 languageManager.on 'change-language', (e, code, strings) ->
   translate.load strings


Add a maximum width for the page image.

Add a maximum width for the page image.

--- overrides.coffee
+++ overrides.coffee
@@ -1,3 +1,5 @@
+MAX_PAGE_WIDTH = 600
+
 require './readymade/overrides.coffee'

 SubjectViewer = require 'zooniverse-readymade/lib/subject-viewer'
@@ -5,7 +7,8 @@
 SubjectViewer::template = require './templates/subject-viewer'

 SubjectViewer::rescale = ()->
-  scale = @markingSurface.el.parentNode.offsetWidth / @maxWidth
+  width = Math.min MAX_PAGE_WIDTH, @markingSurface.el.parentNode.offsetWidth
+  scale = width / @maxWidth
   @markingSurface.maxWidth = @maxWidth
   @markingSurface.maxHeight = @maxHeight
   @markingSurface.svg.attr


Improves development/debugging experience for analytics

--- lib/analytics.coffee
+++ lib/analytics.coffee
@@ -52,19 +52,22 @@
   other: 'Other Events'

 module.exports.track = track =
-  reduce(Object.keys(categories), (memo, kind) ->
+  reduce Object.keys(categories), (memo, kind) ->
     memo[kind] = (description, options = {}) ->
-      # Send google analytics event
-      ga? 'send', {
-        hitType: 'event'
-        eventCategory: options.category || 'UI Interactions'
-        eventAction: description
-        eventLabel: options.label
-        eventValue: options.value
-        nonInteraction: (if options.category in ['Funnel Progressions', 'Impressions', 'Timing'] then 1 else 0)
-      }
+      if NODE_ENV is 'development'
+        console.info "analytics.#{kind}: #{description}", options
+
+      else
+        ga? 'send',
+          hitType: 'event'
+          eventCategory: options.category or 'UI Interactions'
+          eventAction: description
+          eventLabel: options.label
+          eventValue: options.value
+          nonInteraction: if options.category in ['Funnel Progressions', 'Impressions', 'Timing'] then 1 else 0
+
     memo
-  , {})
+  , {}

 module.exports.exception = (errorObj) ->
   @ga? 'send', 'exception', errorObj


Use atom.getActivePackages() API for package names

--- lib/collector.coffee
+++ lib/collector.coffee
@@ -50,8 +50,8 @@
         'unknown'

     # Private
-    getPackages: ->
-      _.keys(atom.activePackages)
+    getPackageNames: ->
+      _.pluck(atom.getActivePackages(), 'name')

     # Public: Returns an object containing all data collected.
     getData: (additionalData) ->


use the real, combined swiss-ephemeris id

--- lib/ephemeris.coffee
+++ lib/ephemeris.coffee
@@ -94,7 +94,8 @@
         for i, group of json
           if i is "1" or i is "2"
             for id, it of group
-              item = ensemble.sid id
+              sid = if i is "2" then "#{10000 + new Number(id)}" else id
+              item = ensemble.sid sid
               [lead, what] = [(if i is "2" then "+" else ""), id]
               if item.get('id') isnt '?'
                 lead = item.get('u').white if item.get('u')?


Redraw the replace box when it is shown. Closes #29

--- lib/find-view.coffee
+++ lib/find-view.coffee
@@ -101,6 +101,7 @@
   showReplace: =>
     @attach()
     @addClass('replace-mode').removeClass('find-mode')
+    @replaceEditor.redraw()
     @replaceEditor.focus()
     @replaceEditor.selectAll()


Add tips for buttons that didnt have tips

--- lib/find-view.coffee
+++ lib/find-view.coffee
@@ -65,6 +65,10 @@
       @caseOptionButton.setTooltip("Match Case", command: 'find-and-replace:toggle-case-option', commandElement: @findEditor)
       @selectionOptionButton.setTooltip("Only In Selection", command: 'find-and-replace:toggle-selection-option', commandElement: @findEditor)

+      @previousButton.setTooltip("Find Previous", command: 'find-and-replace:find-previous', commandElement: @findEditor)
+      @nextButton.setTooltip("Find Next", command: 'find-and-replace:find-next', commandElement: @findEditor)
+
+      @replacePreviousButton.setTooltip("Replace Previous", command: 'find-and-replace:replace-previous', commandElement: @replaceEditor)
       @replaceNextButton.setTooltip("Replace Next", command: 'find-and-replace:replace-next', commandElement: @replaceEditor)
       @replaceAllButton.setTooltip("Replace All", command: 'find-and-replace:replace-all', commandElement: @replaceEditor)
       @tooltipsInitialized = true


derp export the right thing

derp export the right thing

--- lib/jsMegaHal.coffee
+++ lib/jsMegaHal.coffee
@@ -163,4 +163,4 @@

 		parts.join ' '

-module.exports = exports = Quad+module.exports = exports = jsMegaHal


Add rough code that selects entire list

Add rough code that selects entire list

--- lib/list-edit.coffee
+++ lib/list-edit.coffee
@@ -1,4 +1,4 @@
-{CompositeDisposable} = require 'atom'
+{CompositeDisposable, Range} = require 'atom'

 module.exports =
   subscriptions: null
@@ -23,6 +23,16 @@

   select: ->
     console.log 'Executing command list-edit-select'
+    editor = atom.workspace.getActiveTextEditor()
+    # TODO: add null check? (should always exist because we use 'atom-text-editor' command)
+
+    cursorPos = editor.getCursorBufferPosition()
+    console.log 'cursor row ' + cursorPos.row + ' col ' + cursorPos.column
+    bufferRange = @getListRange editor.getBuffer(), cursorPos
+    console.log bufferRange
+
+    if bufferRange?
+      editor.setSelectedBufferRange(bufferRange)

   cut: ->
     console.log 'Executing command list-edit-cut'
@@ -35,3 +45,25 @@

   delete: ->
     console.log 'Executing command list-edit-delete'
+
+  getListRange: (textBuffer, pos) ->
+    bufferText = textBuffer.getText()
+    ix =  textBuffer.characterIndexForPosition(pos)
+
+    listStartIx = null
+    listEndIx = null
+    console.log 'index is ' + ix + ', char is ' + bufferText[ix]
+    for i in [ix-1...0]
+      if bufferText[i] == '['
+        listStartIx = i+1
+        break
+    for i in [ix...bufferText.length-1]
+      if bufferText[i] == ']'
+        listEndIx = i
+        break
+
+    console.log 'list starts at ' + listStartIx
+    if listStartIx? and listEndIx?
+      listStartPos = textBuffer.positionForCharacterIndex listStartIx
+      listEndPos   = textBuffer.positionForCharacterIndex listEndIx
+      new Range(listStartPos, listEndPos)


Update processor.coffee

Update processor.coffee

This is a naive proposal for fixing a bug I found in my research documented at https://github.com/postcss/postcss-import/issues/19#issuecomment-71777987. Also it may be that the bug is with `postcss-import`. But technically this will make the problem go away, I just don't know why it happened in the first place. Very likely @ai can explain the issue and offer a better solution.

--- lib/processor.coffee
+++ lib/processor.coffee
@@ -72,7 +72,7 @@
         notHack = @prefixes.group(decl).down (other) -> other.prop == unprefixed

         if notHack and not @withHackValue(decl)
-          @reduceSpaces(decl) if decl.before.indexOf("\n") > -1
+          @reduceSpaces(decl) if decl.before and decl.before.indexOf("\n") > -1
           rule.remove(i)
           return


Allow using `shuss <port>`.

Allow using `shuss <port>`.

--- lib/shuss-cli.coffee
+++ lib/shuss-cli.coffee
@@ -17,6 +17,7 @@
     @program.parse process.argv

     @_startAction() if @program.args.length == 0
+    @_startAction(@program.args[0]) if @program.args.length == 1

   _loadCliArgs: ()->


documentation

documentation

--- lib/term-view.coffee
+++ lib/term-view.coffee
@@ -6,6 +6,8 @@

 debounce   = require 'debounce'
 Terminal   = require 'atom-term.js'
+ # 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;

 {Task} = require 'atom'


Show notifications for paste errors

Show notifications for paste errors

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -562,36 +562,39 @@
     copiedPaths = if LocalStorage['tree-view:copyPath'] then JSON.parse(LocalStorage['tree-view:copyPath']) else null
     initialPaths = copiedPaths or cutPaths

-    for initialPath in initialPaths ? []
-      initialPathIsDirectory = fs.isDirectorySync(initialPath)
-      if selectedEntry and initialPath and fs.existsSync(initialPath)
-        basePath = selectedEntry.getPath()
-        basePath = path.dirname(basePath) if selectedEntry instanceof FileView
-        newPath = path.join(basePath, path.basename(initialPath))
-
-        if copiedPaths
-          # append a number to the file if an item with the same name exists
-          fileCounter = 0
-          originalNewPath = newPath
-          while fs.existsSync(newPath)
-            if initialPathIsDirectory
-              newPath = "#{originalNewPath}#{fileCounter.toString()}"
+    try
+      for initialPath in initialPaths ? []
+        initialPathIsDirectory = fs.isDirectorySync(initialPath)
+        if selectedEntry and initialPath and fs.existsSync(initialPath)
+          basePath = selectedEntry.getPath()
+          basePath = path.dirname(basePath) if selectedEntry instanceof FileView
+          newPath = path.join(basePath, path.basename(initialPath))
+
+          if copiedPaths
+            # append a number to the file if an item with the same name exists
+            fileCounter = 0
+            originalNewPath = newPath
+            while fs.existsSync(newPath)
+              if initialPathIsDirectory
+                newPath = "#{originalNewPath}#{fileCounter.toString()}"
+              else
+                fileArr = originalNewPath.split('.')
+                newPath = "#{fileArr[0]}#{fileCounter.toString()}.#{fileArr[1]}"
+              fileCounter += 1
+
+            if fs.isDirectorySync(initialPath)
+              # use fs.copy to copy directories since read/write will fail for directories
+              fs.copySync(initialPath, newPath)
             else
-              fileArr = originalNewPath.split('.')
-              newPath = "#{fileArr[0]}#{fileCounter.toString()}.#{fileArr[1]}"
-            fileCounter += 1
-
-          if fs.isDirectorySync(initialPath)
-            # use fs.copy to copy directories since read/write will fail for directories
-            fs.copySync(initialPath, newPath)
-          else
-            # read the old file and write a new one at target location
-            fs.writeFileSync(newPath, fs.readFileSync(initialPath))
-        else if cutPaths
-          # Only move the target if the cut target doesn't exists and if the newPath
-          # is not within the initial path
-          unless fs.existsSync(newPath) or !!newPath.match(new RegExp("^#{initialPath}"))
-            fs.moveSync(initialPath, newPath)
+              # read the old file and write a new one at target location
+              fs.writeFileSync(newPath, fs.readFileSync(initialPath))
+          else if cutPaths
+            # Only move the target if the cut target doesn't exists and if the newPath
+            # is not within the initial path
+            unless fs.existsSync(newPath) or !!newPath.match(new RegExp("^#{initialPath}"))
+              fs.moveSync(initialPath, newPath)
+    catch error
+      atom.notifications.addWarning("Unable to paste paths: #{initialPaths}", detail: error.message)

   add: (isCreatingFile) ->
     selectedEntry = @selectedEntry() ? @roots[0]


We don't need to select DB

We don't need to select DB

--- lib/vie-redis.coffee
+++ lib/vie-redis.coffee
@@ -25,12 +25,6 @@
   if redisOpts.pass
     rd.auth redisOpts.pass, (err) ->
       throw err if err
-  if redisOpts.db
-    rd.select redisOpts.db
-    rd.on 'connect', ->
-      rd.send_anyways true
-      rd.select redisOpts.db
-      rd.send_anyways false
   rd

 exports.createClient = (vie) ->


sort meta done right

sort meta done right

--- lib/view/body.coffee
+++ lib/view/body.coffee
@@ -1,25 +1,29 @@
 Vue = require 'vue'
 _   = require 'lodash'

-# greps all goal indices out, "?2 a ?3 asf fd ?4 ?4" => ['', '2', ' a ', '3', ...]
 parseType = (str) ->
-    tokens = str.split(/\?(\d+)/g)
-    unmarked = tokens.filter (_, i) => i % 2 is 0
-    indices = tokens.filter (_, i) => i % 2 is 1
+    tokens = str.split(/(\?\d+)|(\_\S+)|Set (\_\S+)/g)
+    unmarked = tokens.filter (_, i) => i % 4 is 0
+    goals    = tokens.filter (_, i) => i % 4 is 1
+    metas    = tokens.filter (_, i) => i % 4 is 2
+    sorts    = tokens.filter (_, i) => i % 4 is 3
     unmarked.map (u, i) ->
         unmarked: u
-        index: indices[i]
+        goal: goals[i]
+        meta: metas[i]
+        sort: sorts[i]

-parseBodyRegex = /(?:\?(\d+) \: (.+))|(?:(?:Sort (.+))|(?:(.+) : ([^\[]*))) \[ at ((?:\/[a-zA-Z_\-\s0-9\.]+)+)\.agda\:(\d+)\,(\d+)\-(\d+) \]/
+parseBodyRegex = /(?:(\?\d+) \: (.+))|(?:(?:Sort (.+))|(?:(.+) : ([^\[]*))) \[ at ((?:\/[a-zA-Z_\-\s0-9\.]+)+)\.agda\:(\d+)\,(\d+)\-(\d+) \]/
 parseBody = (str) ->
     result = str.match parseBodyRegex
     if result
         goalTypeSoup = parseType result[2] if result[2]
+        metaTypeSoup = parseType result[5] if result[5]
         goalIndex: result[1]
         goalTypeSoup: goalTypeSoup
         sortIndex: result[3]
         metaIndex: result[4]
-        metaType: result[5]
+        metaTypeSoup: metaTypeSoup
         filepath: result[6]
         lineNo: result[7]
         charStart: result[8]
@@ -34,10 +38,10 @@

{{$value}}

-                    <button class='no-btn text-info' v-on="click: jumpToGoal(goalIndex)">{{goalIndex}}</button> : <template v-repeat="goalTypeSoup">{{unmarked}}<button class='no-btn text-info' v-on="click: jumpToGoal(index)">{{index}}</button></template>
+                    <button class='no-btn text-info' v-on="click: jumpToGoal(goalIndex)">{{goalIndex}}</button> : <template v-repeat="goalTypeSoup">{{unmarked}}<button class='no-btn text-info' v-on="click: jumpToGoal(goal)">{{goal}}</button>{{meta}}<template v-if="sort">Set {{sort}}</template></template>

-                    {{metaIndex}} : {{metaType}}{{filepath}}:{{lineNo}},{{charStart}}-{{charEnd}}
+                    {{metaIndex}} : <template v-repeat="metaTypeSoup">{{unmarked}}<button class='no-btn text-info' v-on="click: jumpToGoal(goal)">{{goal}}</button>{{meta}}<template v-if="sort">Set {{sort}}</template></template>

                     Sort {{sortIndex}}{{filepath}}:{{lineNo}},{{charStart}}-{{charEnd}}


Another bug

Another bug

--- lib/workspace.coffee
+++ lib/workspace.coffee
@@ -32,7 +32,7 @@
       args = for param in params
         if typeof(param) == 'object'
           continue
-        else if obj.hasOwnProperty param
+        else if param of obj
           obj[param]
         else if this.hasOwnProperty param
           this[param]


clean

clean

--- scripts/main.coffee
+++ scripts/main.coffee
@@ -1,4 +1,3 @@
-r = require('redis')
 util = require('./lib/util')
 git = require('./lib/git')

@@ -9,23 +8,3 @@
       git.createPRwithCommitter(title).done((url) ->
         msg.reply url
       )
-
-#    robot.respond /.*/, (msg) ->
-#      msg.reply "test"
-
-#    robot.respond /redis/, (msg) ->
-#      k = "test"
-
-###
-client = r.createClient()
-client.set(k, "test-value")
-client.get(k, (err, reply) ->
-  if err
-    throw err
-  else if reply
-    # msg.reply JSON.parse(reply)
-    msg.reply reply
-  else
-    msg.reply "key: #{k}は見つかりません"
-)
-###


Adding some more Linus quotes to keep things fresh.

--- scripts/wwls.coffee
+++ scripts/wwls.coffee
@@ -1,5 +1,5 @@
 # Description:
-#   What would Linus Say?
+#   What would Linus Say?
 #   Get a random inspirational quote from Linus Torvalds.
 #
 # Commands:
@@ -28,8 +28,17 @@
     "I'm in active competition with them, so I hope they die.",
     "Those particular color choices will make most people decide to pick out their eyes with a fondue fork.",
     "Digging in your eye-sockets with a fondue fork is strictly considered bad for your health, and seve out of nine optometrists are dead set agains the practice.",
-    "In order to avoid a lot of blind users, please apply this patch."]
+    "In order to avoid a lot of blind users, please apply this patch.",
+    "Only wimps use tape backup: real men just upload their important stuff on ftp, and let the rest of the world mirror it.",
+    "Regression testing? What's that? If it compiles, it is good; if it boots up, it is perfect.",
+    "Once you realize that documentation should be laughed at, peed upon, put on fire, and just ridiculed in general, THEN, and only then, have you reached the level where you can safely read it and try to use it to actually implement a driver.",
+    "I'm always right. This time I'm just even more right than usual.",
+    "A lot of people still like Solaris, but I'm in active competition with them, and so I hope they die.",
+    "Standards are paper. I use paper to wipe my butt every day. That's how much that paper is worth.",
+    "Crying that it's an application bug is like crying over the speed of light: you should deal with reality, not what you wish reality was.",
+    "Nvidia, fuck you!",
+    "I am a lazy person, which is why I like open source, for other people to do work for me."]

 module.exports = (robot) ->
   robot.hear /what would linus say/i, (msg) ->
-    msg.send msg.random quotes+    msg.send msg.random quotes


Another fix

Another fix

--- pimatic-smartmeter.coffee
+++ pimatic-smartmeter.coffee
@@ -50,21 +50,21 @@
       P1DataStream = require "./p1meterdata"
       p1datastream = new P1DataStream({portName: @portName})
       p1datastream.on 'data', (data) =>
-        if @actualusage != Number data.currentUsage
-          @actualusage = Number data.currentUsage
-          @emit "Actual usage", Number @actualusage
+        #if @actualusage != Number data.currentUsage
+        @actualusage = Number data.currentUsage
+        @emit "Actual usage", Number @actualusage

-        if @activetariff != Number data.currentTariff
-          @activetariff = Number data.currentTariff
-          @emit "Active tariff", Number @activetariff
+        #if @activetariff != Number data.currentTariff
+        @activetariff = Number data.currentTariff
+        @emit "Active tariff", Number @activetariff

-        if @tariff1totalusage != Number data.tariffOneTotalUsage
-          @tariff1totalusage = Number data.tariffOneTotalUsage
-          @emit "Total usage T1", Number @tariff1totalusage
+        #if @tariff1totalusage != Number data.tariffOneTotalUsage
+        @tariff1totalusage = Number data.tariffOneTotalUsage
+        @emit "Total usage T1", Number @tariff1totalusage

-        if @tariff2totalusage != Number data.tariffTwoTotalUsage
-          @tariff2totalusage = Number data.tariffTwoTotalUsage
-          @emit "Total usage T2", Number @tariff1totalusage
+        #if @tariff2totalusage != Number data.tariffTwoTotalUsage
+        @tariff2totalusage = Number data.tariffTwoTotalUsage
+        @emit "Total usage T2", Number @tariff1totalusage

     getActualusage: -> Promise.resolve @actualusage


Remove conditions for all. Graphs look bad. Need to get into pimatic specifics

--- pimatic-smartmeter.coffee
+++ pimatic-smartmeter.coffee
@@ -56,13 +56,11 @@
         @activetariff = Number data.currentTariff
         @emit "activetariff", Number @activetariff

-        if @tariff1totalusage != Number data.tariffOneTotalUsage
-          @tariff1totalusage = Number data.tariffOneTotalUsage
-          @emit "tariff1totalusage", Number @tariff1totalusage
+        @tariff1totalusage = Number data.tariffOneTotalUsage
+        @emit "tariff1totalusage", Number @tariff1totalusage

-        if @tariff2totalusage != Number data.tariffTwoTotalUsage
-          @tariff2totalusage = Number data.tariffTwoTotalUsage
-          @emit "tariff2totalusage", Number @tariff2totalusage
+        @tariff2totalusage = Number data.tariffTwoTotalUsage
+        @emit "tariff2totalusage", Number @tariff2totalusage

     getActualusage: -> Promise.resolve @actualusage


cleaning up and debuggin

cleaning up and debuggin

--- maproulette/static/js/maproulette.coffee
+++ maproulette/static/js/maproulette.coffee
@@ -13,7 +13,8 @@

 # Challenge related attributes
 tileUrl = "http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
-tileAttrib = '© OpenStreetMap contributors'
+tileAttrib = '©
+OpenStreetMap contributors'

 # Task specific features
 currentChallenge = null
@@ -33,7 +34,8 @@

 # Static strings
 msgMovingOnToTheNextChallenge = 'OK, moving right along...'
-msgZoomInForEdit = """Please zoom in a little so we don't have to load a huge area from the API."""
+msgZoomInForEdit = """Please zoom in a little so we don't have
+to load a huge area from the API."""
 mr_attrib = """

@@ -279,7 +281,7 @@
   mqurl = "http://open.mapquestapi.com/nominatim/v1/reverse?format=json&osm_type=#{type}@osm_id=#{id}"
   msgClose()
   request = $.ajax {url: mqurl}
-  request.success (data) ->
+  request.success (data) ->
     locstr = nomToString(data.address)
     msg locstr
   request.fail(ajaxErrorHandler)
@@ -307,7 +309,7 @@
   ###
   for feature in features
     if feature.properties.selected is true
-        selectedFeature = feature
+      selectedFeature = feature
       geojsonLayer.addData feature
     extent = getExtent(selectedFeature)
     map.fitBounds(extent)
@@ -356,10 +358,11 @@
   near = "#{map.getCenter().lng}|#{map.getCenter().lat}" if not near
   url = "/api/c/challenges/#{challenge}/tasks?near=#{near}"
   request = $.ajax {url: url}
-  request.done (data) ->
-    currentTask = data[0]
-    showTask(data[0])
-  request.fail (ajaxErrorHandler)
+  request.success (data) ->
+    currentTask = data.tasks[0]
+    showTask(currentTask)
+  request.fail (jqXHR, textStatus, errorThrown) ->
+    ajaxErrorHandler(jqXHR, textStatus, errorThrown)

 changeMapLayer = (layerUrl, layerAttrib = tileAttrib) ->
   ###
@@ -394,8 +397,8 @@
   msg msgMovingOnToTheNextChallenge
   setDelay 1, msgClose()
   payload = {
-      "action": action,
-      "editor": editor}
+    "action": action,
+    "editor": editor}
   near = currentTask.center
   challenge = currentChallenge.id
   task_id = currentTask.id
@@ -474,7 +477,7 @@
   # Get the stats for the challenge and display the count of remaining
   # tasks
   ###
-  request = $.ajax {url:  "/api/c/challenges/#{challenge}/stats"}
+  request = $.ajax {url: "/api/c/challenges/#{challenge}/stats"}
   request.done (data) ->
     remaining = data.stats.total - data.stats.done
     $("#counter").text remaining


chore(html minifier): add boolean attrs to ignoreCustomFragments

--- rapid-build.cson
+++ rapid-build.cson
@@ -96,6 +96,7 @@
 					/\s<rba-copy|<\/rba-copy>\s/gi
 					/\s<rba-install|<\/rba-install>\s/gi
 					/<textarea(\s*?.*?)*?\/textarea>/gi # for codemirror
+					/disabled|hidden|multiple|nowrap|open|readonly|required|reversed|selected|sortable/gi # boolean attrs
 				]

 	order:


Cleanup

Cleanup

--- packages/shrub-limiter/limiter.coffee
+++ packages/shrub-limiter/limiter.coffee
@@ -2,8 +2,6 @@
 # # Limits

 Promise = require 'bluebird'
-
-pkgman = require 'pkgman'

 orm = require 'shrub-orm'

@@ -123,9 +121,6 @@
 	accrue: (id, score = 1) ->
 		key = "#{@key}:#{id}"

-		# } Cast score to integer.
-		score = parseInt score, 10
-
 		Limit = orm.collection 'shrub-limit'
 		Limit.findOrCreate(key: key).populateAll().then((limit) =>
 			limit.key = key
@@ -135,7 +130,7 @@

 			return limit

-		).then (limit) -> limit.accrue(score).save()
+		).then (limit) -> limit.accrue(parseInt score).save()

 	# ### .score
 	#
@@ -143,11 +138,10 @@
 	#
 	# * (string) `id` - The ID of the limiter.
 	score: (id) ->
-		key = "#{@key}:#{id}"

 		# } Get all scores for this limiter.
 		Limit = orm.collection 'shrub-limit'
-		Limit.findOne(key: key).populateAll().then (limit) =>
+		Limit.findOne(key: "#{@key}:#{id}").populateAll().then (limit) =>
 			return 0 unless limit?
 			return limit.score() if 0 < limit.ttl @threshold

@@ -160,10 +154,9 @@
 	#
 	# * (string) `id` - The ID of the limiter.
 	ttl: (id) ->
-		key = "#{@key}:#{id}"

 		Limit = orm.collection 'shrub-limit'
-		Limit.findOne(key: key).then (limit) =>
+		Limit.findOne(key: "#{@key}:#{id}").then (limit) =>
 			return 0 unless limit?
 			return ttl if 0 < ttl = limit.ttl @threshold


Adding name to choice model, enforcing it in the create request

--- models/choice.coffee
+++ models/choice.coffee
@@ -6,6 +6,7 @@

 choiceSchema = new Schema
   competition_id: ObjectId
+  name: String

 Choice = mongoose.model('Choice', choiceSchema)
 module.exports = Choice


apparently contestant votes cannot be trusted

--- models/person.coffee
+++ models/person.coffee
@@ -213,11 +213,7 @@
         { 'scores.judge_utility': { $gt: 3 }},
         { 'scores.judge_design': { $gt: 3 }},
         { 'scores.judge_innovation': { $gt: 3 }},
-        { 'scores.judge_completeness': { $gt: 3 }},
-        { 'scores.contestant_utility': { $gt: 4 }},
-        { 'scores.contestant_design': { $gt: 4 }},
-        { 'scores.contestant_innovation': { $gt: 4 }},
-        { 'scores.contestant_completeness': { $gt: 4 }}
+        { 'scores.judge_completeness': { $gt: 3 }}
       ]

     # every third vote should be for something good


Update server development logging

Update server development logging

Provide more details.

--- server_setup.coffee
+++ server_setup.coffee
@@ -27,6 +27,16 @@
     return "\x1b[90m#{req.method} #{req.originalUrl} \x1b[#{color}m#{res.statusCode} \x1b[#{elapsedColor}m#{elapsed}ms\x1b[0m"
   null

+developmentLogging = (tokens, req, res) ->
+  status = res.statusCode
+  color = 32
+  if status >= 500 then color = 31
+  else if status >= 400 then color = 33
+  else if status >= 300 then color = 36
+  elapsed = (new Date()) - req._startTime
+  elapsedColor = if elapsed < 500 then 90 else 31
+  "\x1b[90m#{req.method} #{req.originalUrl} \x1b[#{color}m#{res.statusCode} \x1b[#{elapsedColor}m#{elapsed}ms\x1b[0m"
+
 setupExpressMiddleware = (app) ->
   if config.isProduction
     express.logger.format('prod', productionLogging)
@@ -35,6 +45,7 @@
       return false if req.headers.host is 'codecombat.com'  # CloudFlare will gzip it for us on codecombat.com  # But now it's disabled.
       compressible res.getHeader('Content-Type')
   else
+    express.logger.format('dev', developmentLogging)
     app.use(express.logger('dev'))
   app.use(express.static(path.join(__dirname, 'public')))
   app.use(useragent.express())


Fix watcher for framework

Fix watcher for framework

--- Builder.coffee
+++ Builder.coffee
@@ -74,7 +74,7 @@
     exec cmd, (err, stdout, stderr)->
       console.warn "------------------------ FRAMEWORK COMPILED -------------------------- "
       console.warn " To use watcher for Framework use following command in different tab:  "
-      console.log  " $ #{cmd} "
+      console.log  " $ #{cmd.replace 'compile ', ''} "
       console.warn "------------------------ FRAMEWORK COMPILED -------------------------- "

   buildClient: (options) ->


Fix #82. Actually, no need to call session reload here. Session reload will also trigger the change of mode into the view mode, but we do not want that.


Add a test demonstrating a bracket forwarding issue

--- spec/Scoping.coffee
+++ spec/Scoping.coffee
@@ -282,3 +282,38 @@
       in1.endGroup()
       in1.endGroup()
       in1.disconnect()
+    it 'should forward new-style brackets as expected regardless of sending order', (done) ->
+      expected = [
+        'CONN'
+        '< 1'
+        '< a'
+        'DATA 1bazPc1:2fooPc2:PcMerge'
+        '>'
+        '>'
+        'DISC'
+      ]
+      received = []
+
+      out.on 'connect', ->
+        received.push 'CONN'
+      out.on 'begingroup', (group) ->
+        received.push "< #{group}"
+      out.on 'data', (data) ->
+        received.push "DATA #{data}"
+      out.on 'endgroup', ->
+        received.push '>'
+      out.on 'disconnect', ->
+        received.push 'DISC'
+        chai.expect(received).to.eql expected
+        done()
+
+      in1.connect()
+      in1.beginGroup 1
+      in1.beginGroup 'a'
+      in1.send 'baz'
+      in1.endGroup()
+      in1.endGroup()
+      in1.disconnect()
+      in2.connect()
+      in2.send 'foo'
+      in2.disconnect()


server: static maxAge 4h

server: static maxAge 4h

--- server.coffee
+++ server.coffee
@@ -87,8 +87,8 @@
   }

 if config.ENV is config.ENVS.PROD
-then app.use express['static'](__dirname + '/dist')
-else app.use express['static'](__dirname + '/build')
+then app.use express.static(__dirname + '/dist', {maxAge: '4h'})
+else app.use express.static(__dirname + '/build', {maxAge: '4h'})

 app.use router
 app.use (req, res, next) ->


Refactor.

Refactor.

--- server-ce/settings.coffee
+++ server-ce/settings.coffee
@@ -436,6 +436,7 @@
 	# NOTE: see https://github.com/bergie/passport-saml/blob/master/README.md for docs of `server` options
 	settings.externalAuth = true
 	settings.saml =
+		identityServiceName: process.env["SHARELATEX_SAML_IDENTITY_SERVICE_NAME"]
 		server:
 			# strings
 			entryPoint: process.env["SHARELATEX_SAML_ENTRYPOINT"]
@@ -452,7 +453,6 @@
 			validateInResponseTo: process.env["SHARELATEX_SAML_VALIDATE_IN_RESPONSE_TO"]
 			cacheProvider: process.env["SHARELATEX_SAML_CACHE_PROVIDER"]
 			logoutUrl: process.env["SHARELATEX_SAML_LOGOUT_URL"]
-			additionalLogoutParams: process.env["SHARELATEX_SAML_ADDITIONAL_LOGOUT_PARAMS"]
 			logoutCallbackUrl: process.env["SHARELATEX_SAML_LOGOUT_CALLBACK_URL"]
 			disableRequestedAuthnContext: process.env["SHARELATEX_SAML_DISABLE_REQUESTED_AUTHN_CONTEXT"] == 'true'
 			forceAuthn: process.env["SHARELATEX_SAML_FORCE_AUTHN"] == 'true'
@@ -475,21 +475,33 @@
 				else
 					undefined
 			)
-
-		identityServiceName: process.env["SHARELATEX_SAML_IDENTITY_SERVICE_NAME"]
-
-	if _saml_additionalParams = process.env["SHARELATEX_SAML_ADDITIONAL_PARAMS"]
-		try
-			settings.saml.server.additionalAuthorizeParams = JSON.parse(_saml_additionalParams)
-		catch e
-			console.error "Cannot parse SHARELATEX_SAML_ADDITIONAL_PARAMS"
-
-	if _saml_additionalAuthorizeParams = process.env["SHARELATEX_SAML_ADDITIONAL_AUTHORIZE_PARAMS"]
-		try
-			settings.saml.server.additionalAuthorizeParams = JSON.parse(_saml_additionalAuthorizeParams )
-		catch e
-			console.error "Cannot parse SHARELATEX_SAML_ADDITIONAL_PARAMS"
-
+			additionalParams: (
+				if _saml_additionalParams = process.env["SHARELATEX_SAML_ADDITIONAL_PARAMS"]
+					try
+						JSON.parse(_saml_additionalParams)
+					catch e
+						console.error "Cannot parse SHARELATEX_SAML_ADDITIONAL_PARAMS"
+				else
+					undefined
+			)
+			additionalAuthorizeParams: (
+				if _saml_additionalAuthorizeParams = process.env["SHARELATEX_SAML_ADDITIONAL_AUTHORIZE_PARAMS"]
+					try
+						JSON.parse(_saml_additionalAuthorizeParams )
+					catch e
+						console.error "Cannot parse SHARELATEX_SAML_ADDITIONAL_AUTHORIZE_PARAMS"
+				else
+					undefined
+			)
+			additionalLogoutParams: (
+				if _saml_additionalLogoutParams = process.env["SHARELATEX_SAML_ADDITIONAL_LOGOUT_PARAMS"]
+					try
+						JSON.parse(_saml_additionalLogoutParams )
+					catch e
+						console.error "Cannot parse SHARELATEX_SAML_ADDITIONAL_LOGOUT_PARAMS"
+				else
+					undefined
+			)

 if settings.externalAuth and settings?.nav?.header?
 	results = []


Added VERSION constant and deleted createVector

--- src/DivSugar.coffee
+++ src/DivSugar.coffee
@@ -1,9 +1,10 @@
 DivSugar =
   _initialize: ->
     # initialize constants
+    @VERSION = '0.10'
     @EPSILON = 0.0001
     @DEG_TO_RAD = Math.PI / 180
-    @DEG_TO_RAD = 180 / Math.PI
+    @RAD_TO_DEG = 180 / Math.PI

     # initialize the root task
     @rootTask = null
@@ -74,6 +75,4 @@

   createTask: (args...) -> new @_Task args...

-  createVector: (args...) -> new @_Vector args...
-
 (window.DivSugar = DivSugar)._initialize()


Tweak username-should-not-be-email error message

--- server/models/User.coffee
+++ server/models/User.coffee
@@ -376,7 +376,7 @@
     if not @allowEmailNames # for testing
       filter = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,63}$/i  # https://news.ycombinator.com/item?id=5763990
       if filter.test(name)
-        return next(new errors.UnprocessableEntity('Name may not be an email'))
+        return next(new errors.UnprocessableEntity('Username may not be an email'))

     @set('nameLower', name.toLowerCase())
   else


Add support for JSON bundle server config (#5605)

--- server_config.coco.coffee
+++ server_config.coco.coffee
@@ -4,6 +4,10 @@
 cluster = require 'cluster'

 config = {}
+
+if process.env.COCO_SECRETS_JSON_BUNDLE
+  for k, v of JSON.parse(process.env.COCO_SECRETS_JSON_BUNDLE)
+    process.env[k] = v

 config.clusterID = "#{os.hostname()}"
 if cluster.worker?


The creator says this base URL will work. Lets see what happens!


:thumbsup::oden: Updated at https://danielx.net/editor/

--- autocrop.coffee
+++ autocrop.coffee
@@ -10,20 +10,20 @@
   console.log getContentBounds(imageData)

 getContentBounds = (imageData) ->
-  {width, height} = imageData
-  y = 0
-  x = 0
+  {data, width, height} = imageData

   xMin = Infinity
   yMin = Infinity
   xMax = -Infinity
   yMax = -Infinity

+  y = 0
   while y < height
+    x = 0
     while x < width
-      index = y * width + x + 3
-
-      alpha = imageData[index]
+      index = (y * width + x) * 4 + 3
+
+      alpha = data[index]

       if alpha > 0
         if x < xMin
@@ -38,7 +38,7 @@
       x += 1
     y += 1

-  x: xMin ? 0
-  y: yMin ? 0
-  width: (xMax - xMin) ? 0
-  height: (yMax - yMin) ? 0
+  x: xMin|0
+  y: yMin|0
+  width: (xMax - xMin)|0
+  height: (yMax - yMin)|0


user info clientside length validations

user info clientside length validations

--- client/app/CommonViews/FormViews.coffee
+++ client/app/CommonViews/FormViews.coffee
@@ -63,8 +63,10 @@
       validate      :
         rules       :
           required  : yes
+          maxLength : 25
         messages    :
           required  : "First name is required!"
+          maxLength : "Sorry, maximum 25 chars can be entered for the firstname!"

     @lastName = new KDInputView
       cssClass      : 'lastname editable'
@@ -72,6 +74,11 @@
       name          : 'lastName'
       attributes    :
         size        : Encoder.htmlDecode(profile.lastName).length
+      validate      :
+        rules       :
+          maxLength : 25
+        messages    :
+          maxLength : "Sorry, maximum 25 chars can be entered for the lastname!"

     @nameView = new ProfileTextView
       tagName       : "p"
@@ -155,6 +162,7 @@
       validate      :
         rules       :
           required  : yes
+          maxLength : 500

     @aboutInfo = new PersonalAboutView
       tooltip            :
@@ -222,6 +230,9 @@
       type          : 'text'
       defaultValue  : @memberData.locationTags[0] or 'Earth'
       name          : 'locationTags'
+      validate      :
+        rules       :
+          maxLength : 30

     if @memberData.locationTags.length < 1
       @memberData.locationTags[0] = "Earth"


flux: make KodingReactor work with nuclear-module

--- client/app/lib/flux/base/reactor.coffee
+++ client/app/lib/flux/base/reactor.coffee
@@ -14,7 +14,10 @@
     if Array.isArray storeClasses
       storeClasses = mapWithClassName storeClasses

-    stores = _.mapValues storeClasses, (StoreClass) -> new StoreClass
+    stores = _.mapValues storeClasses, (StoreClass) ->
+      if 'function' is typeof StoreClass
+      then new StoreClass
+      else StoreClass

     # we injected reactor instance to our stores,
     # leave the rest to Nuclear.


Comment out `New Browser` action (temporarily)

--- client/ide/views/tabview/ideview.coffee
+++ client/ide/views/tabview/ideview.coffee
@@ -232,7 +232,7 @@
     items =
       'New File'          : callback : => @createEditor()
       'New Terminal'      : callback : => @createTerminal()
-      'New Browser'       : callback : => @createPreview()
+      # 'New Browser'       : callback : => @createPreview()
       'New Drawing Board' :
         callback          : => @createDrawingBoard()
         separator         : yes


Remove special treatment for items aborting

--- src/checkout.coffee
+++ src/checkout.coffee
@@ -186,13 +186,9 @@
       orderItems: items
       expectedOrderFormSections: expectedOrderFormSections

-    if @_requestingItem isnt undefined
-      @_requestingItem.abort()
-
-    return @_requestingItem = @_updateOrderForm
+    @_updateOrderForm
       url: @_getUpdateItemURL()
       data: JSON.stringify(updateItemsRequest)
-    .done(=> @_requestingItem = undefined)

   # Sends a request to select an available gift
   updateSelectableGifts: (list, selectedGifts, expectedOrderFormSections = @_allOrderFormSections) =>
@@ -201,13 +197,9 @@
       selectedGifts: selectedGifts
       expectedOrderFormSections: expectedOrderFormSections

-    if @_requestingSelectableGifts isnt undefined
-      @_requestingSelectableGifts.abort()
-
-    return @_requestingSelectableGifts = @_updateOrderForm
+    @_updateOrderForm
       url: @_getUpdateSelectableGifts(list)
       data: JSON.stringify(updateSelectableGiftsRequest)
-    .done(=> @_requestingSelectableGifts = undefined)

   # Sends a request to remove items from the OrderForm.
   removeItems: (items, expectedOrderFormSections = @_allOrderFormSections) =>
@@ -216,8 +208,7 @@

   # Sends a request to remove all items from the OrderForm.
   removeAllItems: (expectedOrderFormSections = @_allOrderFormSections)=>
-    orderFormPromise = @getOrderForm(['items'])
-    orderFormPromise.then (orderForm) =>
+    @getOrderForm(['items']).then (orderForm) =>
       items = orderForm.items
       item.quantity = 0 for item in items
       @updateItems(items, expectedOrderFormSections)


cooldown finilazation

cooldown finilazation

--- src/cooldown.coffee
+++ src/cooldown.coffee
@@ -14,7 +14,7 @@
     target ||= 0
     approachBy = 1 unless approachBy?

-    # Set
+    # Set the cooldown d
     I.cooldowns[name] = {
       options.target
       options.approachBy


fix introContent Display name i18n (#343)

--- app/core/utils.ozar.coffee
+++ app/core/utils.ozar.coffee
@@ -222,7 +222,7 @@
         introLevelData = introLevelsContent[c.contentId.python] || {}
       else
         introLevelData = introLevelsContent[c.contentId] || {}
-      c.displayName = introLevelData.displayName || introLevelData.name
+      c.displayName = i18n(introLevelData, 'displayName') || i18n(introLevelData, 'name')
       learningGoals = ((introLevelData.documentation || {}).specificArticles || []).find((a) => a.name == 'Learning Goals')
       if learningGoals
         c.learningGoals = i18n(learningGoals, 'body')


bring up global search prompt from t in addition to s

--- SingularityUI/app/views/globalSearch.coffee
+++ SingularityUI/app/views/globalSearch.coffee
@@ -17,7 +17,7 @@
             focusInput = $(event.target).is @$ 'input[type="search"]'

             modifierKey = event.metaKey or event.shiftKey
-            sPressed = event.keyCode is 83 and not modifierKey
+            sPressed = event.keyCode in [83, 84] and not modifierKey
             escPressed = event.keyCode is 27

             if escPressed and (focusBody or focusInput)


Trim stack trace lines

Trim stack trace lines

--- spec/atom-reporter.coffee
+++ spec/atom-reporter.coffee
@@ -23,6 +23,7 @@
     prefixMatch = line.match(/at \[object Object\]\.<anonymous> \(([^\)]+)\)/)
     lines[index] = "at #{prefixMatch[1]}" if prefixMatch

+  lines = lines.map (line) -> line.trim()
   lines.join('\n')

 module.exports =


Verify that vtexjs.checkout.orderForm object is created

--- spec/checkout-spec.coffee
+++ spec/checkout-spec.coffee
@@ -13,6 +13,9 @@
     expect(vtexjs.checkout.getOrderForm).to.be.a('function')
     expect(vtexjs.checkout._getBaseOrderFormURL()).to.equal(mock.API_URL)

+  it 'should have empty orderform', ->
+    expect(vtexjs.checkout.orderForm).to.not.exist
+
   it 'should get orderform', (done) ->
     xhr = vtexjs.checkout.getOrderForm()
     xhr.done (orderForm) ->
@@ -20,3 +23,6 @@
       done()
     xhr.fail (jqXHR) ->
       done(jqXHR)
+
+  it 'should have orderform', ->
+    expect(vtexjs.checkout.orderForm).to.deep.equal(mock.orderform.empty)


Add spec for links

Add spec for links

--- spec/digester-spec.coffee
+++ spec/digester-spec.coffee
@@ -75,39 +75,73 @@
     expect(json.classes.Something.instanceMethods[0].events.description).toBe 'Method Events'

   describe 'src link generation', ->
-    it 'generates links to github based on repo and version', ->
-      file = """
-        # Public: Some class
-        class Something
-          # Public: this is a function
-          somefunction: ->
-      """
-      json = Parser.generateDigest file,
-        filename: 'file1.coffee'
-        packageJson:
-          name: 'somerepo'
-          repository: 'https://github.com/atom/somerepo.git'
-          version: '2.3.4'
-
-      expect(json).toEqualJson
-        classes:
-          Something:
-            visibility : 'Public'
-            name : 'Something'
-            filename : 'file1.coffee'
-            summary : 'Some class '
-            description : 'Some class '
-            srcUrl: 'https://github.com/atom/somerepo/blob/v2.3.4/file1.coffee#L2'
-            sections : []
-            classMethods : []
-            instanceMethods: [{
+    describe 'when there are multiple packages', ->
+      it 'generates links to github based on repo and version', ->
+        file1 = """
+          # Public: Some class
+          class Something
+            # Public: this is a function
+            somefunction: ->
+        """
+        file2 = """
+          # Public: Another class
+          class Another
+
+            # Public: this is a function
+            anotherfunction: ->
+        """
+        parser = new Parser
+        parser.addFile file1,
+          filename: 'file1.coffee'
+          packageJson:
+            name: 'somerepo'
+            repository: 'https://github.com/atom/somerepo.git'
+            version: '2.3.4'
+        parser.addFile file2,
+          filename: 'file2.coffee'
+          packageJson:
+            name: 'anotherrepo'
+            repository: 'https://github.com/atom/anotherrepo.git'
+            version: '1.2.3'
+
+        json = parser.generateDigest()
+        expect(json.classes.Something.srcUrl).toEqual 'https://github.com/atom/somerepo/blob/v2.3.4/file1.coffee#L2'
+        expect(json.classes.Another.srcUrl).toEqual 'https://github.com/atom/anotherrepo/blob/v1.2.3/file2.coffee#L2'
+
+    describe 'when there is only one package', ->
+      it 'generates links to github based on repo and version', ->
+        file = """
+          # Public: Some class
+          class Something
+            # Public: this is a function
+            somefunction: ->
+        """
+        json = Parser.generateDigest file,
+          filename: 'file1.coffee'
+          packageJson:
+            name: 'somerepo'
+            repository: 'https://github.com/atom/somerepo.git'
+            version: '2.3.4'
+
+        expect(json).toEqualJson
+          classes:
+            Something:
               visibility : 'Public'
-              name : 'somefunction'
-              sectionName : null
-              srcUrl : 'https://github.com/atom/somerepo/blob/v2.3.4/file1.coffee#L4'
-              summary : 'this is a function '
-              description : 'this is a function '
-            }]
+              name : 'Something'
+              filename : 'file1.coffee'
+              summary : 'Some class '
+              description : 'Some class '
+              srcUrl: 'https://github.com/atom/somerepo/blob/v2.3.4/file1.coffee#L2'
+              sections : []
+              classMethods : []
+              instanceMethods: [{
+                visibility : 'Public'
+                name : 'somefunction'
+                sectionName : null
+                srcUrl : 'https://github.com/atom/somerepo/blob/v2.3.4/file1.coffee#L4'
+                summary : 'this is a function '
+                description : 'this is a function '
+              }]

   describe 'class methods', ->
     it 'generates class level methods', ->


kill unused document.coffee for now

kill unused document.coffee for now

--- src/document.coffee
+++ src/document.coffee
@@ -1,26 +0,0 @@
-# Fires these events:
-#   - opened
-#   - saved
-#   - created
-class Document
-  path: null
-  text: null
-  listeners: []
-
-  constructor: (@path, @text) ->
-
-  name: ->
-    _.last @path.split '/' if @path
-
-  save: ->
-    trigger 'saved'
-
-  open = ->
-    trigger 'opened'
-
-  on: (message, listener) ->
-    @listeners.push listener
-
-  trigger: (message, args...) ->
-    _.each @listeners, (listener) ->
-      listener.call args...


Removed static help file

Removed static help file


Simplified the masking function and added some new behavior

--- spec/masked_input_component_spec.cjsx
+++ spec/masked_input_component_spec.cjsx
@@ -21,6 +21,10 @@
   handleKeyPress = null
   handleComplete = null

+  # FIXME:
+  # - up/down arow behavior
+  # - undo?
+
   # TODO:
   # - When to show the placeholder
   # - Should the cursor be set to the first non-mask char?
@@ -35,6 +39,7 @@
     , 0

   simulateKeyPress = (key) ->
+    selection = getInput()._getSelection()
     cursorPos = getInput()._getSelection().begin
     defaultPrevented = false
     TestUtils.Simulate.keyPress domNode,
@@ -43,14 +48,33 @@

     unless defaultPrevented || key.length > 1
       prevVal = getInputValue()
-      newVal = prevVal.substring(0, cursorPos) + key + prevVal.substr(cursorPos)
+      newVal = prevVal.substring(0, selection.begin) + key + prevVal.substr(selection.end)
       getInput()._setSelection(cursorPos + 1)
       TestUtils.Simulate.change domNode,
         target:
           value: newVal

   simulateKeyDown = (key) ->
-    TestUtils.Simulate.keyDown domNode, {key}
+    defaultPrevented = false
+    TestUtils.Simulate.keyDown(domNode,
+      key: key
+      preventDefault: -> defaultPrevented = true
+    )
+
+    unless defaultPrevented
+      {begin, end} = getInput()._getSelection()
+      prevVal = getInputValue()
+      if begin is end
+        if key is 'Backspace'
+          newVal = prevVal.substring(0, begin-1) + prevVal.substr(end)
+        else if key is 'Delete'
+          newVal = prevVal.substring(0, begin) + prevVal.substr(end+1)
+      else
+        newVal = prevVal.substring(0, begin) + prevVal.substr(end)
+
+      TestUtils.Simulate.change domNode,
+        target:
+          value: newVal

   simulatePaste = (content) ->
     cursorPos = getInput()._getSelection()
@@ -77,6 +101,9 @@
           handleChange = null
           handleComplete = null

+        beforeEach ->
+          handleChange.reset()
+
         afterEach ->
           handleChange.reset()
           handleComplete.reset()
@@ -310,6 +337,7 @@
             handleComplete = null

           beforeEach ->
+            handleChange.reset()
             simulatePaste '12345'

           afterEach ->
@@ -345,8 +373,9 @@
             it 'replaces the selected characters', ->
               expect(getInputValue().substring 1, 5).to.equal '6/75'

+            # TODO: Is this the right behavior?
             it 'moves the cursor to the correct position', ->
-              expect(getInput()._getSelection()).to.eql begin: 6, end: 6
+              expect(getInput()._getSelection()).to.eql begin: 4, end: 4

             it 'correctly shifts the mask characters', ->
               expect(getInputValue()).to.equal '16/75/____'
@@ -371,7 +400,7 @@
               expect(getInputValue().substring 1, 5).to.equal '6/75'

             it 'moves the cursor to the correct position', ->
-              expect(getInput()._getSelection()).to.eql begin: 6, end: 6
+              expect(getInput()._getSelection()).to.eql begin: 4, end: 4

             it 'correctly shifts the mask characters', ->
               expect(getInputValue()).to.equal '16/75/____'
@@ -398,6 +427,9 @@
     context "when the mask is 'aaaaaaaa'", ->
       before ->
         mask = 'aaaaaaaa'
+
+      it 'sets the placeholder correctly', ->
+        expect(getInputValue()).to.equal '________'

       describe 'pressing the enter key', ->
         before ->
@@ -569,6 +601,41 @@
       simulateFocus -> done()

     setupTests ->
+      describe 'initial render', ->
+        before ->
+          handleChange = sinon.spy()
+
+        after ->
+          handleChange = null
+
+        afterEach ->
+          handleChange.reset()
+
+        context 'when the initial value is blank', ->
+          it 'calls the onChange callback', ->
+            expect(handleChange).to.have.been.calledOnce
+            expect(handleChange).to.have.been.calledWithExactly target: value: '__/__/____'
+
+        context 'when the initial value matches the placeholder', ->
+          before ->
+            initialVal = '__/__/____'
+
+          after ->
+            initialVal = ''
+
+          it 'does not call the onChange callback', ->
+            expect(handleChange).to.have.not.been.called
+
+        context "when the initial doesn't change when masked", ->
+          before ->
+            initialVal = '1_/__/____'
+
+          after ->
+            initialVal = ''
+
+          it 'does not call the onChange callback', ->
+            expect(handleChange).to.have.not.been.called
+
       describe 'setting an initial value', ->
         before ->
           initialVal = '123456'


Slight change in test numbers for LongInt

--- spec/long_int_spec.coffee
+++ spec/long_int_spec.coffee
@@ -5,26 +5,26 @@
   { LongInt } = pazy

 describe """If a, b and c are long integers with base 10000;
-            a = -1e12 + 501, b = 501 and c = 1e12 - 500""", ->
+            a = -1e12 + 5001, b = 5001 and c = 1e12 - 5000""", ->
   LongInt.digit_size__ 4
-  a = new LongInt -1e12 + 501
-  b = new LongInt 501
-  c = new LongInt 1e12 - 500
+  a = new LongInt -1e12 + 5001
+  b = new LongInt 5001
+  c = new LongInt 1e12 - 5000

-  it "a should print as -999999999499", ->
-    expect(a.toString()).toEqual '-999999999499'
+  it "a should print as -999999994999", ->
+    expect(a.toString()).toEqual '-999999994999'

-  it "a should convert to the number -1e12 + 501", ->
-    expect(a.toNumber()).toBe -1e12 + 501
+  it "a should convert to the number -1e12 + 5001", ->
+    expect(a.toNumber()).toBe -1e12 + 5001

-  it "b should print as 501", ->
-    expect(b.toString()).toEqual '501'
+  it "b should print as 5001", ->
+    expect(b.toString()).toEqual '5001'

-  it "a should convert to the number 501", ->
-    expect(b.toNumber()).toBe 501
+  it "a should convert to the number 5001", ->
+    expect(b.toNumber()).toBe 5001

-  it "c should print as 999999999500", ->
-    expect(c.toString()).toEqual '999999999500'
+  it "c should print as 999999995000", ->
+    expect(c.toString()).toEqual '999999995000'

   it "a should be smaller than b", ->
     expect(a.cmp(b)).toBeLessThan 0
@@ -35,14 +35,14 @@
   it "-a should be larger than b", ->
     expect(a.neg().cmp(b)).toBeGreaterThan 0

-  it "c should convert to the number 1e12 - 500", ->
-    expect(c.toNumber()).toBe 1e12 - 500
+  it "c should convert to the number 1e12 - 5000", ->
+    expect(c.toNumber()).toBe 1e12 - 5000

-  it "a + b should print as -999999998998", ->
-    expect(a.plus(b).toString()).toEqual '-999999998998'
+  it "a + b should print as -999999989998", ->
+    expect(a.plus(b).toString()).toEqual '-999999989998'

-  it "a + b should convert to the number -1e12 + 1002", ->
-    expect(a.plus(b).toNumber()).toBe -1e12 + 1002
+  it "a + b should convert to the number -1e12 + 10002", ->
+    expect(a.plus(b).toNumber()).toBe -1e12 + 10002

   it "a - b should print as -1000000000000", ->
     expect(a.minus(b).toString()).toEqual '-1000000000000'


add variable reveal

add variable reveal

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -15,6 +15,7 @@
     $("#question, #answer").show()
     @current = ""
     @fail = 0
+    @reveal
     update_question(data)
     @data = data

@@ -36,6 +37,7 @@

   randomLetter = () ->
     n = Math.floor(Math.random * @current.length)
+
   revealLetter = () ->
     i = randomLetter()


added utils {}

added utils {}

--- app/assets/javascripts/jblocks.coffee
+++ app/assets/javascripts/jblocks.coffee
@@ -5,6 +5,7 @@
 window.$$ =
 	decls: {}
 	cache: {}
+	utils: {}
 	_id: 0
 	bem_class: '.js_bem'
 	guid: () ->