improve room name escaper and escape room name in log_message function

--- scripts/chat-logger.coffee
+++ scripts/chat-logger.coffee
@@ -71,7 +71,7 @@

 module.exports = (robot) ->
   escape_room = (room) ->
-    room.replace(/[^\w]/, '-', 'g').replace(/-+(.*)-+/, '$1')
+    room.replace(/[^\w]/, '-', 'g').replace(/^-*(.*)-*$/, '$1')

   chat_data = -> robot.brain.data.chat_logger

@@ -108,7 +108,7 @@
       robot.logger.debug "msg without room: #{msg}"
       return

-    room = msg.envelope.room
+    room = escape_room msg.envelope.room
     msg_data =
       nick: msg.envelope.user.name
       type: t


Updated getSelected to assume there are no rows that arent rendered in the collection. Added a getDeselected method

--- ribs.coffee
+++ ribs.coffee
@@ -443,9 +443,13 @@

         getSelected : ->
             return [] unless @$list?
-            selected = @$list.find(".item.selected").map (idx, el) =>
+            @$list.find(".item.selected").map (idx, el) =>
                 @collection.getByCid $(el).data("cid")
-            selected
+
+        getDeselected: ->
+            return [] unless @$list?
+            @$list.find(".item:not(.selected)").map (idx, el) =>
+                @collection.getByCid $(el).data("cid")

         getNumSelected: ->
             return 0 unless @$list?
@@ -637,6 +641,10 @@
             @collection.trigger "deselected"
             @collection?.each @addItem, this

+        get: (id) ->
+            _.find @_subviews, (view) ->
+                view.model.id == id
+
         render: ->
             _.each @_subviews, (view,i) ->
                 view.render()


fallback for getOpForTextChange

fallback for getOpForTextChange

--- src/coffee/client/text_field_handler.coffee
+++ src/coffee/client/text_field_handler.coffee
@@ -61,15 +61,13 @@
         selection = @selection
         text = @baseText

-        #try
-        op = getOpForTextChange selection, text, newSelection, newText
-        Utils.assert (newText == OtText.applyTextOp text, op)
-        #catch
+        try
+            op = getOpForTextChange selection, text, newSelection, newText
+            Utils.assert (newText == OtText.applyTextOp text, op)
+        catch
             # Fallback in case the complicated logic doesn't work
-        #    asStr = (sel) ->
-        #        "[#{("(#{a}, #{b})" for [a,b] in sel).join(", ")}]"
-
-        #    op = OtText.text_diff2 text, newText
+            op = getOpForTextChange2 text, newText
+            Utils.assert (newText == OtText.applyTextOp text, op)

         @selection = newSelection
         @text = newText
@@ -380,4 +378,21 @@
                             (OtText.canonicalized op2)
     return op

+# Like `getOpForTextChange`, but it never fails
+getOpForTextChange2 = (old_text, new_text) ->
+    prefix = 0
+    while prefix < old_text.length and prefix < new_text.length and old_text.charAt(prefix) == new_text.charAt(prefix)
+        prefix++
+
+    suffix = 0
+    while suffix < old_text.length - prefix and suffix < new_text.length - prefix and old_text.charAt(old_text.length - suffix - 1) == new_text.charAt(new_text.length - suffix - 1)
+        suffix++
+
+    return OtText.canonicalized [
+        OtText.take(prefix),
+        OtText.skip(old_text.length - prefix - suffix),
+        OtText.insert(new_text.substring(prefix, new_text.length - suffix)),
+        OtText.take(suffix)
+      ]
+
 window.EditableTextField = EditableTextField


Removed unused data from the drag event.

This extra data had a side effect, on Linux installations of preventing
some data from being transfered to the drop location if the drag
and drop processes are different.

--- lib/tab-bar-view.coffee
+++ lib/tab-bar-view.coffee
@@ -171,7 +171,6 @@

     item = @pane.getItems()[element.index()]
     if item.getPath?
-      event.originalEvent.dataTransfer.setData 'text/uri-list', 'file://' + item.getPath()
       event.originalEvent.dataTransfer.setData 'text/plain', item.getPath() ? ''

       if item.isModified?() and item.getText?


hide fix

hide fix

--- lib/util/tooltip.coffee
+++ lib/util/tooltip.coffee
@@ -33,7 +33,7 @@

   hide_: ->
     @view.classList.add 'dontshow'
-    => @view.style.display = 'none'
+    @view.style.display = 'none'

   hide: ->
     @view.classList.add 'dontshow'


Use our unlocalizeCurrencyFilter to parse unit value field

- More tolerant (can handle `,` or `.` as decimal separator, remove thousands separator) to return a `number`

--- app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee
+++ app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee
@@ -1,4 +1,4 @@
-angular.module("admin.products").controller "variantUnitsCtrl", ($scope, VariantUnitManager, $timeout, UnitPrices) ->
+angular.module("admin.products").controller "variantUnitsCtrl", ($scope, VariantUnitManager, $timeout, UnitPrices, unlocalizeCurrencyFilter) ->

   $scope.unitName = (scale, type) ->
     VariantUnitManager.getUnitName(scale, type)
@@ -23,7 +23,7 @@

   $scope.updateValue = ->
     unit_value_human = angular.element('#unit_value_human').val()
-    $scope.unit_value = unit_value_human * $scope.scale
+    $scope.unit_value = unlocalizeCurrencyFilter(unit_value_human) * $scope.scale

   variant_unit_value = angular.element('#variant_unit_value').val()
   $scope.unit_value_human = variant_unit_value / $scope.scale


#3 исправлен баг с установкой стратегии

#3 исправлен баг с установкой стратегии

--- copy-files.coffee
+++ copy-files.coffee
@@ -7,6 +7,25 @@
 {normalize, basename, dirname, extname, join, existsSync} = path

 class CopyFiles
+  # for internal use
+  STATUS_SUCCESS = 0
+  STATUS_PENDING = 1
+  STATUS_FAIL    = -1
+
+  # for export
+  STATUS_SUCCESS: STATUS_SUCCESS
+  STATUS_FAIL :   STATUS_FAIL
+
+  # for internal use
+  REPLACE       = 0
+  SKIP          = 1
+  REPLACE_OLDER = 2
+
+  # for export
+  REPLACE:       REPLACE
+  SKIP:          SKIP
+  REPLACE_OLDER: REPLACE_OLDER
+
   defaults:
     copyFileTimestamp: yes
     replaceStrategy: REPLACE_OLDER
@@ -14,25 +33,6 @@
     on_complete: null
   numOfOpenFiles: 0
   maxNumOfOpenFiles: 100
-
-  # for internal use
-  STATUS_SUCCESS = 0
-  STATUS_PENDING = 1
-  STATUS_FAIL    = -1
-
-  # for export
-  STATUS_SUCCESS: STATUS_SUCCESS
-  STATUS_FAIL :   STATUS_FAIL
-
-  # for internal use
-  REPLACE       = 0
-  SKIP          = 1
-  REPLACE_OLDER = 2
-
-  # for export
-  REPLACE:       REPLACE
-  SKIP:          SKIP
-  REPLACE_OLDER: REPLACE_OLDER

   constructor: (src, dst, options) ->
     throw "Error: #{src} not found!" unless existsSync(src)


Update atom config

Update atom config

--- .atom/config.cson
+++ .atom/config.cson
@@ -25,4 +25,5 @@
     fontSize: 12
   "file-icons":
     coloured: false
-  "one-light-ui": {}
+  "one-light-ui":
+    layoutMode: "Compact"


updating with new atom packages

updating with new atom packages

--- .atom/config.cson
+++ .atom/config.cson
@@ -10,5 +10,11 @@
     showOnStartup: false
   metrics:
     userId: "2b8d964846053db77b68f8495fd5ad006a0a481b"
-  core: {}
+  core:
+    disabledPackages: [
+      "symbols-view"
+    ]
   "tree-view": {}
+  minimap:
+    plugins:
+      "minimap-autohide": true


Formats code on save.

Formats code on save.

--- .atom/config.cson
+++ .atom/config.cson
@@ -3,6 +3,9 @@
     autoBuildTagsWhenActive: true
     cmdArgs: "--exclude=core --exclude=deploy --exclude=dist --exclude=gulp --exclude=node_modules --exclude=sql --exclude=tests --exclude=vendor"
   "atom-ide-ui":
+    "atom-ide-code-format":
+      formatOnSave: true
+      formatOnType: false
     "atom-ide-diagnostics-ui":
       autoVisibility: true
     "atom-ide-terminal":


uncommented the liscov test

uncommented the liscov test

--- test/unit.coffee
+++ test/unit.coffee
@@ -545,10 +545,14 @@

     emitter = regex: new EventEmitter(), nodejs: new events.EventEmitter()

-    it.skip 'should have the same prototype methods', ->
+    it 'should have the same prototype methods', ->

       for method in Object.keys( events.EventEmitter.prototype )
         EventEmitter.prototype.should.have.property(method).and.be.instanceof Function
+
+    it.skip 'should expect the same amount of arguments for prototype methods', ->
+
+      for method in Object.keys( events.EventEmitter.prototype )
         EventEmitter.prototype[method].length.should.eql events.EventEmitter.prototype[method].length

     it.skip 'should have the same properties & default values', ->


fix path locations for word templates in production

--- server/methods/word.coffee
+++ server/methods/word.coffee
@@ -4,6 +4,12 @@
 angularParser = (tag) ->
     expr = angular_expressions.compile(tag)
     return get: expr
+
+getPath = (fn) ->
+    if process.env.NODE_ENV is "development"
+        return "#{process.env.PWD}/private/docx-templates/#{fn}"
+    else
+        return "#{process.env.PWD}/bundle/programs/server/assets/app/docx-templates/#{fn}"

 Meteor.methods
     epiWordReport: (tbl_id) ->
@@ -16,7 +22,7 @@
                 for riskEst in res.riskEstimates
                     riskEst.riskFormatted = share.riskFormatter(riskEst)

-        path = "#{process.env.PWD}/private/docx-templates/epi-v1.docx"
+        path = getPath("epi-v1.docx")
         docx = new DocxGen().loadFromFile(path, {async: false, parser: angularParser})
         docx.setTags({descriptions: vals})
         docx.applyTags()
@@ -46,7 +52,8 @@
                 getReferences(child)
                 getChildren(child)

-        path = "#{process.env.PWD}/private/docx-templates/mechanistic-v1.docx"
+        path = getPath("mechanistic-v1.docx")
+        console.log(path)
         docx = new DocxGen().loadFromFile(path, {async: false, parser: angularParser})
         docx.setTags(data)
         docx.applyTags()


Minor cleanup

Minor cleanup

--- src/touchclick.coffee
+++ src/touchclick.coffee
@@ -5,7 +5,7 @@
 $ = if typeof jQuery is 'function' then jQuery else require 'jquery'

 activeClass = 'touchactive'
-preventDefault = true
+preventDefaultClick = true

 # Store a timestamp of when the last touch event occurred
 lastTouched = 0
@@ -18,10 +18,7 @@
   if e.type.match 'touchstart|touchmove|touchend'
     lastTouched = currentTimestamp

-  if secondsSinceTouch < 3 and e.type.match 'mouse'
-    true
-  else
-    false
+  secondsSinceTouch < 3 and e.type.match 'mouse'

 getTouchclickEl = (target) ->
   $targetEl = $ target
@@ -54,7 +51,7 @@
 events = (type) ->
   $el = $ this

-  if preventDefault
+  if preventDefaultClick
     $el[type] 'click', (e) ->
       e.preventDefault()


Ctrl+H to backspace

Ctrl+H to backspace

--- src/trix/input.coffee
+++ src/trix/input.coffee
@@ -4,6 +4,7 @@
     0x08: "backspace"
     0x0D: "return"
     0x44: "d"
+    0x48: "h"

   constructor: (@element, @responder) ->
     for event in @constructor.events
@@ -60,6 +61,9 @@
       @responder?.deleteForward()
       event.preventDefault()

+    h: (event) ->
+      @backspace(event)
+
   logAndCancel: (event) =>
     console.log "trapped event:", event
     event.preventDefault()


.

Former-commit-id: 8d4dc0be560d3d7f1c5e9bbc5ec41a01639d2351 [formerly 833d7cff5b755d44eb9a4151a72eb2201977d422] [formerly 7e06d0058872bb741c18590532cf4ac3c01ac3e5 [formerly c71852315eb77874d5d41271c4463fc53a2d7281]] [formerly 19087cf60ac1464ff8773f6d4eac80942254d2cf [formerly 3ff65492608f589fc591913ae1e3f418936b96f7] [formerly 02019806c091fae62388cfd50f39f0cea56b8ceb [formerly 02019806c091fae62388cfd50f39f0cea56b8ceb [formerly 952045bd27b99fe79f5d1b03657db75fa8ae333e]]]]
Former-commit-id: 41f00dc4b437aa91aa74bb179534851e142e2445 [formerly f1065113b134e1ac7a5f63d77cb20af7d44eff14] [formerly a7e2d3020934febd69c70659fc66ea9ee44396a8 [formerly 9395a9be61b2ddf5bd09ca2372758283ad3be44b]]
Former-commit-id: 79afce8abd8616bf6c51d3f560a6c1d9825c41a7 [formerly 1e8334e7a61b3efd401be11cafe98f46f46597af]
Former-commit-id: dcef335ae5fe8a36fdafbc6043c728da8860876b
Former-commit-id: a27f39bd14b0a0078c595a4c3f397204e4886901

--- @client/shared.coffee
+++ @client/shared.coffee
@@ -7,7 +7,7 @@

 window.TRANSITION_SPEED = 700   # Speed of transition from results to crafting (and vice versa)

-window.LIVE_UPDATE_INTERVAL = 1000  #1000 * 60
+window.LIVE_UPDATE_INTERVAL = 1000 * 60

 # live updating
 setInterval ->


Adding support for PBI creation

Adding support for PBI creation

Now the user can create a new Product Backlog Item. This iteration only creates a new one with a specific title.

--- src/vsts-slack.coffee
+++ src/vsts-slack.coffee
@@ -4,24 +4,33 @@
 # Configuration:
 #   HUBOT_VSTS_API_TOKEN
 #   HUBOT_VSTS_API_URL
+#   HUBOT_VSTS_SERVICEACCOUNT
 #
 # Commands:
 #   hubot vsts projects - Gets the list of projects available in Visual Studio Team Services
 #   hubot vsts <project> pullrequests <showDetails>- Gets the active pull requests for the specified Visual Studio Team Services project. <showDetails> is optional
+#   hubot vsts <project> newPBI <title>- Creates a new PBI in the specified Visual Studio Team Services project with the given title. <title> is required
 #
 # Author:
 #   Nithin Shenoy <nshenoy@mimeo.com>

 vstsToken = 'Basic ' + new Buffer(":#{process.env.HUBOT_VSTS_API_TOKEN}").toString('base64')
 vstsBaseUrl = process.env.HUBOT_VSTS_DEFAULTCOLLECTION_URL
+vstsServiceAccount = process.env.HUBOT_VSTS_SERVICEACCOUNT

 module.exports = (robot) ->
-  waitMessages = [
+  prWaitMessages = [
     "Hold on, checking for active PRs ",
     "Wait a sec, let me see if I can find the active PRs ",
     "Mmmmkay, searching for PRs "
   ]

+  pbiWaitMessages = [
+    "Give me a sec to create your new PBI ",
+    "Creating new PBI ",
+    "On it, homeslice. Creating a new PBI "
+  ]
+
   robot.respond /vsts projects$/i, (res) ->
     unless (ensureEnvironment res)
       res.send "Retrieving list of VSTS projects..."
@@ -32,7 +41,7 @@
         project = res.match[1]
         showDetails = res.match[3]?

-        waitMessage = res.random waitMessages
+        waitMessage = res.random prWaitMessages
         res.send waitMessage + "in `#{project}` ..."

         # get the list of repositories
@@ -43,7 +52,7 @@
             .get() (err, _, body) ->
                 if err
                     res.send ":fire: An error was thrown in Node.js/CoffeeScript"
-                    res.send error
+                    res.send err

                 repos = (JSON.parse body).value

@@ -86,6 +95,54 @@
                     res.send "No active pull requests for #{project}" unless hasPullRequests
                 , 2000

+  robot.respond /vsts (.*) (newPBI|newProductBacklogItem|newStory|newBug) (.*)?$/i, (res) ->
+    unless (ensureEnvironment res)
+        project = res.match[1]
+        pbiTitle = res.match[3]
+
+        unless pbiTitle?
+            res.send "I think you may have forgotten to give me a title for the PBI. Please use the format `vsts <project> newPBI <title>`"
+            return
+
+        waitMessage = res.random pbiWaitMessages
+        res.send waitMessage + "in `#{project}` with title `#{pbiTitle}` ..."
+
+        newPbi = [
+            {
+                op : "add",
+                path : "/fields/System.Title",
+                value : "#{pbiTitle}"
+            },
+            {
+                op : "add",
+                path : "/fields/System.CreatedBy",
+                value : "#{vstsServiceAccount}"
+            },
+            {
+                op : "add",
+                path : "/fields/System.ChangedBy",
+                value : "#{vstsServiceAccount}"
+            }
+        ]
+
+        # create a new PBI
+        pbiUrl = "#{vstsBaseUrl}/#{project}/_apis/wit/workitems/$Product%20Backlog%20Item?bypassRules=true&api-version=1.0"
+        res.http(pbiUrl)
+            .header('Authorization', vstsToken)
+            .header('Content-type', 'application/json-patch+json')
+            .patch(JSON.stringify(newPbi)) (err, _, body) ->
+                if err
+                    res.send ":fire: An error was thrown in Node.js/CoffeeScript"
+                    res.send err
+
+                pbiDetails = JSON.parse(body)
+                attachment = createPBIAttachment pbiDetails
+
+                res.robot.adapter.customMessage
+                    channel: res.envelope.room
+                    username: res.robot.name
+                    attachments: [attachment]
+
 #################################################
 ## Checks the presence of required  env variables
 #################################################
@@ -98,6 +155,10 @@

   unless process.env.HUBOT_VSTS_DEFAULTCOLLECTION_URL?
     msg.send "VSTS DefaultCollection URL is missing: Make sure the HUBOT_VSTS_DEFAULTCOLLECTION_URL is set"
+    missingConfig |= true
+
+  unless process.env.HUBOT_VSTS_SERVICEACCOUNT?
+    msg.send "VSTS Service Account is missing: Make sure the HUBOT_VSTS_SERVICEACCOUNT is set to a user for which new PBIs to be opened with (e.g. 'Hubot <hubot@organization.com>')"
     missingConfig |= true

   missingConfig
@@ -150,6 +211,56 @@

     return attachment

+#################################################################
+## Creates attachment message object for new Product Backlog Item
+#################################################################
+createPBIAttachment = (pbiDetails, showDetails) ->
+    pbiId = pbiDetails.id
+    pbiTitle = pbiDetails.fields['System.Title']
+    project = pbiDetails.fields['System.AreaPath']
+    projectUrl = "#{vstsBaseUrl}/#{project}/_backlogs"
+    pbiUrl = "#{vstsBaseUrl}/#{project}/_workitems?id=#{pbiId}"
+    attachment =
+        fallback: "Product Backlog Item #{pbiId} in #{project} \"#{pbiTitle}\" #{pbiUrl}"
+        text: "<#{pbiUrl}|Product Backlog Item #{pbiId}> - \"#{pbiTitle}\" in <#{projectUrl}|#{project}>"
+        color: "#68217A"
+        mrkdwn_in: ["text","fields"]
+        author_name: "Visual Studio Team Services"
+        author_icon: "https://zapier.cachefly.net/storage/services/59152a3a91bfe0ddd2fc9b978448593a.128x128.png"
+
+    if showDetails
+        details =
+            fields: [
+                {
+                        title: "State"
+                        value: "`#{pbiDetails.fields['System.State']}`"
+                        short: true
+                },
+                {
+                        title: "Backlog Status"
+                        value: "`#{pbiDetails.fields['System.BoardColumn']}`"
+                        short: true
+                },
+                {
+                        title: "Created By"
+                        value: "`#{pbiDetails.fields['SystemCreatedBy']}`"
+                        short: true
+                },
+                {
+                        title: "Created Dated"
+                        value: "`#{pbiDetails.fields['System.CreatedDate']}`"
+                        short: true
+                },
+                {
+                        title: "Description"
+                        value: "#{pbiDetails.fields['System.Description']}"
+                        short: false
+                }
+            ]
+        attachment.fields = details.fields
+
+    return attachment
+
 #################################################
 ## Retrieves list of projects
 #################################################


Accurate arrows

Accurate arrows

--- bootstrap-tour.coffee
+++ bootstrap-tour.coffee
@@ -224,20 +224,39 @@
       }).popover("show")

       tip = $(step.element).data("popover").tip()
-      @_reposition(tip)
-      @_scrollIntoView(tip)
+      @_reposition(tip, step)
+      @_scrollIntoView(tip) if options.scrollToStep

     # Prevent popups from crossing over the edge of the window
-    _reposition: (tip) ->
+    _reposition: (tip, step) ->
+      original_offsetWidth = tip[0].offsetWidth
+      original_offsetHeight = tip[0].offsetHeight
+
       tipOffset = tip.offset()
+      original_left = tipOffset.left
+      original_top = tipOffset.top
       offsetBottom = $(document).outerHeight() - tipOffset.top - $(tip).outerHeight()
       tipOffset.top = tipOffset.top + offsetBottom if offsetBottom < 0
-      offsetRight = $(document).outerWidth() - tipOffset.left - $(tip).outerWidth()
+      offsetRight = $("html").outerWidth() - tipOffset.left - $(tip).outerWidth()
       tipOffset.left = tipOffset.left + offsetRight if offsetRight < 0

       tipOffset.top = 0 if tipOffset.top < 0
       tipOffset.left = 0 if tipOffset.left < 0
+
       tip.offset(tipOffset)
+
+      # reposition the arrow
+      if step.placement == 'bottom' or step.placement == 'top'
+        @_bootstrap_tooltip_replaceArrow(tip, (tipOffset.left-original_left)*2, original_offsetWidth, 'left') if original_left != tipOffset.left
+      else
+        @_bootstrap_tooltip_replaceArrow(tip, (tipOffset.top-original_top)*2, original_offsetHeight, 'top') if original_top != tipOffset.top
+
+    # copy pasted from bootstrap-tooltip.js
+    # with some alterations
+    _bootstrap_tooltip_replaceArrow: (tip, delta, dimension, position)->
+      tip
+        .find(".arrow")
+        .css(position, if delta then (50 * (1 - delta / dimension) + "%") else '')

     # Scroll to the popup if it is not in the viewport
     _scrollIntoView: (tip) ->


Add solution key for #192

Add solution key for #192

--- games/tutorial.coffee
+++ games/tutorial.coffee
@@ -173,6 +173,10 @@
           actions: {
             run: ['forward']
             bump: []
+          }
+          solution: {
+            run: ['forward']
+            bump: ['right']
           }
         }
       ]


Render: Sort opaque meshes front to back.

--- engine/render.coffee
+++ engine/render.coffee
@@ -483,6 +483,7 @@
                 if vec4.dot(plane, pos4) < r
                     mesh.culled_in_last_frame = true
                     return
+            # TODO: Also check with @clipping_plane!
             mesh.culled_in_last_frame = false

         # Select alternative mesh / LoD
@@ -789,6 +790,15 @@
         if passes.indexOf(0) >= 0
             # TODO: profile with timsort, etc
             # scene.mesh_passes[0].sort sort_by_mat_id
+            # Sort by distence to camera
+            z = @camera_z
+            v = vec3.create()
+            for ob in scene.mesh_passes[0]
+                wm = ob.world_matrix
+                vec3.set v, wm.m12, wm.m13, wm.m14
+                ob._sqdist = -vec3.dot(v,z) - (ob.zindex * \
+                    (ob.dimensions.x+ob.dimensions.y+ob.dimensions.z)*0.166666)
+            timsort.sort scene.mesh_passes[0], (a,b)-> b._sqdist - a._sqdist
             for ob in scene.mesh_passes[0]
                 if ob.visible == true and not ob.bg and not ob.fg
                     @draw_mesh(ob, ob.world_matrix, 0)


require electron

require electron

--- types/url.coffee
+++ types/url.coffee
@@ -1,4 +1,4 @@
-shell = require 'shell'
+{shell} = require 'electron'
 UrlReplace = require '../lib/url-replace'

 module.exports = (toolBar, button) ->


CLI optimizing the test server source building/binding procedures

--- cli/test/build.coffee
+++ cli/test/build.coffee
@@ -6,42 +6,69 @@
 path    = require('path')
 server  = require('./server')
 source  = require('../source')
-packg   = require('../package')
+packge  = require('../package')
+
+dirs    = {}
+builds  = {}
+binds   = {}
+packs   = {}
+
+#
+# reads package.json definitions from a module reference
+#
+packg = (module)->
+  packs[module.filename] or= packge.read(moddir(module))
+

 #
 # Resolves the module main working directory
 #
 moddir  = (module)->
-  dirname = path.dirname(module.filename)
+  unless module.filename of dirs
+    dirname = path.dirname(module.filename)

-  while dirname != '/'
-    return dirname if fs.existsSync("#{dirname}/package.json")
-    dirname = path.join(dirname, '..')
+    while dirname != '/'
+      if fs.existsSync("#{dirname}/package.json")
+        dirs[module.filename] = dirname
+        break
+
+      dirname = path.join(dirname, '..')
+
+  dirs[module.filename]
+

 #
 # Simply bilds the module source and returns it as a string
 #
 exports.build = build = (module)->
-  source.compile(moddir(module))
+  builds[module.filename] or= source.compile(moddir(module))

+#
+# Bind the built module to the server along with a dummy page for it's test
+#
 exports.bind = bind = (module)->
-  pack = packg.read(moddir(module))
-  src  = build(module)
+  unless module.filename of binds
+    pack = packg(module)
+    src  = build(module)

-  server.set "/#{pack.name}.js",   src
-  server.set "/#{pack.name}.html", """
-    <html>
-      <head>
-        <script src="/core.js"></script>
-        <script type="text/javascript">
-          Lovely(['#{pack.name}'], function() {});
-        </script>
-      </head>
-    </html>
-  """
+    server.set "/#{pack.name}.js",   binds[module.filename] = src
+    server.set "/#{pack.name}.html", """
+      <html>
+        <head>
+          <script src="/core.js"></script>
+          <script type="text/javascript">
+            Lovely(['#{pack.name}'], function() {});
+          </script>
+        </head>
+        <body>
+          Dummy page for: '#{pack.name}'
+        </body>
+      </html>
+    """

-  return src
+  binds[module.filename]
+

 #
 # Creates a callback for the mocha `before` calls
@@ -54,8 +81,8 @@
     callback = options
     options  = {}

-  pack = packg.read(moddir(module))
-  bind(module)
+  pack = packg(module)
+  src  = bind(module)

   if typeof(options) is 'string'
     url = options


Remove duplicate route

Remove duplicate route

--- app/router.cjsx
+++ app/router.cjsx
@@ -127,8 +127,6 @@
       <Route path="project_status/:owner/:name" component={require './pages/admin/project-status'} />
     </Route>

-    <Route path="dev/classifier" component={require './classifier'} />
-
     <Route path="todo" component={->
 TODO
} />
     <Route path="dev/workflow-tasks-editor" component={require './components/workflow-tasks-editor'} />
     <Route path="dev/classifier" component={


First draft of automatic minimization port to React

--- client/message.coffee
+++ client/message.coffee
@@ -342,92 +342,6 @@
 threadAuthors = {}
 threadMentions = {}

-Template.submessage.onCreated ->
-  ## Fold naturally folded (minimized and deleted) messages by default on
-  ## initial load.  But only if not previously manually overridden.
-  oldDefault = defaultFolded.get @data._id
-  oldFolded = messageFolded.get @data._id
-  defaultFolded.set @data._id, naturallyFolded @data
-
-  ## Fold referenced attached files by default on initial load.
-  #@$.children('.panel').children('.panel-body').find('a[href|="/file/"]')
-  #console.log @$ 'a[href|="/file/"]'
-  #images = Session.get 'images'
-  @images = {}
-  @imagesInternal = {}
-  @autorun =>
-    data = Template.currentData()
-    return unless data._id
-    initImage data._id
-    ## initImage calls updateFileQuery which will do this:
-    #images[data._id].file = data.file
-    #initImageInternal data.file if data.file?
-    ## If message is naturally folded, don't count images it references.
-    images[data._id].naturallyFolded = naturallyFolded data
-    images[data._id].children = data.children?.length
-    if images[data._id].naturallyFolded
-      for id of @images
-        images[id].count -= 1
-        checkImage id
-      @images = {}
-      for id of @imagesInternal
-        imagesInternal[id].count -= 1
-        checkImageInternal id
-      @imagesInternal = {}
-    else
-      newImages = {}
-      newImagesInternal = {}
-      $($.parseHTML("<div>#{formatBody data.format, data.body}</div>"))
-      .find """
-        img[src^="#{fileUrlPrefix}"],
-        img[src^="#{fileAbsoluteUrlPrefix}"],
-        img[src^="#{internalFileUrlPrefix}"],
-        img[src^="#{internalFileAbsoluteUrlPrefix}"],
-        video source[src^="#{fileUrlPrefix}"],
-        video source[src^="#{fileAbsoluteUrlPrefix}"],
-        video source[src^="#{internalFileUrlPrefix}"],
-        video source[src^="#{internalFileAbsoluteUrlPrefix}"]
-      """
-      .each ->
-        src = @getAttribute('src')
-        if 0 <= src.indexOf 'gridfs'
-          newImagesInternal[url2internalFile src] = true
-        else
-          newImages[url2file src] = true
-      for id of @images
-        unless id of newImages
-          images[id].count -= 1
-          checkImage id
-      for id of newImages
-        unless id of @images
-          #console.log 'source', id
-          initImage id
-          images[id].count += 1
-          checkImage id
-      @images = newImages
-      for id of @imagesInternal
-        unless id of newImagesInternal
-          imagesInternal[id].count -= 1
-          checkImageInternal id
-      for id of newImagesInternal
-        unless id of @imagesInternal
-          #console.log 'source', id
-          initImageInternal id
-          imagesInternal[id].count += 1
-          checkImageInternal id
-      @imagesInternal = newImagesInternal
-
-  ## Switch messageFolded to defaultFolded if:
-  ## * default has changed
-  ## * default has initialized but messageFolded not already set
-  ##   (e.g. because we just clicked the message)
-  if oldFolded? and (not oldDefault? or oldDefault == defaultFolded.get @data._id)
-    unless oldFolded == messageFolded.get @data._id
-      messageFolded.set @data._id, oldFolded
-  else
-    messageFolded.set @data._id, defaultFolded.get @data._id
-
-#Session.setDefault 'images', {}
 images = {}
 imagesInternal = {}
 id2dom = {}
@@ -518,7 +432,7 @@
 ## A message is "naturally" folded if it is flagged as minimized or deleted.
 ## It still will be default-folded if it's an image referenced in another
 ## message that is not naturally folded.
-export naturallyFolded = (data) -> data.minimized or data.deleted
+export naturallyFolded = (message) -> message.minimized or message.deleted

 ## Cache EXIF orientations, as files should be static
 image2orientation = {}
@@ -552,13 +466,6 @@
       messageFolded.set ancestor._id, false
   ## Also unfold message itself, because you probably want to see it.
   messageFolded.set id, false
-
-Template.submessage.onDestroyed ->
-  checkImage @data._id
-  for id of @images ? {}
-    if id of images
-      images[id].count -= 1
-      checkImage id

 historify = (x, post) -> () ->
   history = messageHistory.get @_id
@@ -1911,7 +1818,9 @@
   ## Maintain id2dom mapping
   useEffect ->
     id2dom[message._id] = ref.current
-    -> delete id2dom[message._id]
+    ->
+      delete id2dom[message._id]
+      checkImage message._id
   , [message._id]
   ## Scroll to this message if it's been requested.
   useEffect ->
@@ -1921,12 +1830,79 @@
     undefined
   , [message._id]

+  ## Fold naturally folded (minimized and deleted) messages by default on
+  ## initial load.  But only if not previously manually overridden.
+  useEffect ->
+    oldDefault = defaultFolded.get message._id
+    oldFolded = messageFolded.get message._id
+    defaultFolded.set message._id, naturallyFolded message
+    ## Initially switch messageFolded to defaultFolded if:
+    ## * default has changed
+    ## * default has initialized but messageFolded not already set
+    ##   (e.g. because we just clicked the message)
+    setTimeout ->
+      if oldFolded? and (not oldDefault? or oldDefault == defaultFolded.get message._id)
+        unless oldFolded == messageFolded.get message._id
+          messageFolded.set message._id, oldFolded
+      else
+        messageFolded.set message._id, defaultFolded.get message._id
+    , 0
+  , [message._id]
+
+  ## Fold referenced attached files by default on initial load.
+  messageBodyRef = useRef()
+  useEffect ->
+    initImage message._id
+    ## initImage calls updateFileQuery which will do this:
+    #images[message._id].file = message.file
+    #initImageInternal message.file if message.file?
+    ## If message is naturally folded, don't count images it references.
+    images[message._id].naturallyFolded = naturallyFolded message
+    images[message._id].children = message.children?.length
+    return if images[message._id].naturallyFolded
+    messageImages = {}
+    messageImagesInternal = {}
+    return unless messageBodyRef.current?
+    for elt in messageBodyRef.current.querySelectorAll """
+      img[src^="#{fileUrlPrefix}"],
+      img[src^="#{fileAbsoluteUrlPrefix}"],
+      img[src^="#{internalFileUrlPrefix}"],
+      img[src^="#{internalFileAbsoluteUrlPrefix}"],
+      video source[src^="#{fileUrlPrefix}"],
+      video source[src^="#{fileAbsoluteUrlPrefix}"],
+      video source[src^="#{internalFileUrlPrefix}"],
+      video source[src^="#{internalFileAbsoluteUrlPrefix}"]
+    """
+      src = elt.getAttribute 'src'
+      if 0 <= src.indexOf 'gridfs'
+        messageImagesInternal[url2internalFile src] = true
+      else
+        messageImages[url2file src] = true
+    for id of messageImages
+      #console.log 'source', id
+      initImage id
+      images[id].count += 1
+      checkImage id
+    for id of messageImagesInternal
+      #console.log 'source', id
+      initImageInternal id
+      imagesInternal[id].count += 1
+      checkImageInternal id
+    ->
+      for id of messageImages
+        images[id].count -= 1
+        checkImage id
+      for id of messageImagesInternal
+        imagesInternal[id].count -= 1
+        checkImageInternal id
+  # too many dependencies to list
+
   ## Transform images
-  messageBodyRef = useRef()
   ## Retransform when window width changes
   [windowWidth, setWindowWidth] = useState window.innerWidth
   useEventListener 'resize', (e) -> setWindowWidth window.innerWidth
   useEffect ->
+    return unless messageBodyRef.current?
     ## Transform any images embedded within message body
     trackers =
       for img in messageBodyRef.current.querySelectorAll """


Make Home and End keys stick to visual line

--- client/message.coffee
+++ client/message.coffee
@@ -314,6 +314,8 @@
                 theme()
           editor.setOption 'extraKeys',
             Enter: 'newlineAndIndentContinueMarkdownList'
+            End: 'goLineRight'
+            Home: 'goLineLeft'
         when 'ace'
           editor.textInput.getElement().setAttribute 'tabindex', 1 + 20 * ti.count + 19
           #editor.meteorData = @  ## currently not needed, also dunno if works


Rendering test

Rendering test

--- client/message.coffee
+++ client/message.coffee
@@ -703,6 +703,7 @@
     else
       formatTitleOrFilename history, false  ## don't write (untitled)
   formatBody: ->
+    #console.log 'rendering', @_id
     history = messageHistory.get(@_id) ? @
     body = history.body
     return body unless body


Moved UI setup to rendered event

Moved UI setup to rendered event

--- client/results.coffee
+++ client/results.coffee
@@ -4,6 +4,7 @@
       ids = SearchResults.find().map (result) -> result._id
       Meteor.subscribe 'publications-by-ids', ids

+  Template.results.rendered = ->
     $('.chzn').chosen()

     $('#score-range').slider


Fix grey screen after configuring

Fix grey screen after configuring

--- client/routing.coffee
+++ client/routing.coffee
@@ -8,7 +8,7 @@
 Router.onBeforeAction((pause) ->
   if not Meteor.user()
     config = share.Configs.findOne()
-    if not config.isSetupComplete
+    if config and not config.isSetupComplete
       @render("setupAdminAccount")
     else
       @render("welcome")
@@ -17,7 +17,7 @@

 Router.onBeforeAction((pause) ->
   config = share.Configs.findOne()
-  if not config.isSetupComplete
+  if config and not config.isSetupComplete
     @render("setupConfig")
     pause()
 , except: ["setup"])


css class to indicate that the window is focused

--- client/init.coffee
+++ client/init.coffee
@@ -32,4 +32,7 @@
     $('body').addClass "theme-#{localStorage.theme}"
     return

+  # CSS class for focused window
+  $(window).on 'focus blur', (e) -> $('body').toggleClass 'window-focused', e.type == 'focus'
+
   return


Use isProduction value in lib:js task

Use isProduction value in lib:js task

--- gulp/tasks/lib.coffee
+++ gulp/tasks/lib.coffee
@@ -1,13 +1,14 @@
 gulp   = require("gulp")
 config = require("../config")
 $      = config.plugins
+isProduction = config.isProduction

 gulp.task("lib", ["lib:js"])

 gulp.task "lib:js", ->
   gulp.src(config.src.lib.js)
-  .pipe($.sourcemaps.init())
-  .pipe($.uglify())
-  .pipe($.concat(config.lib.js))
-  .pipe($.sourcemaps.write(config.map))
-  .pipe(gulp.dest(config.dist.js))+    .pipe($.if(!isProduction, $.sourcemaps.init()))
+    .pipe($.if(isProduction, $.uglify()))
+    .pipe($.concat(config.lib.js))
+    .pipe($.if(!isProduction, $.sourcemaps.write(config.map)))
+    .pipe(gulp.dest(config.dist.js))


Use proxy_pass'd api

Use proxy_pass'd api

--- app/index.coffee
+++ app/index.coffee
@@ -47,14 +47,14 @@
     ExperimentServer.resetExperimentalFlags()
     Geordi.logEvent 'logout'

-Api.init
-  host: if location.origin is 'http://preview.zooniverse.org'
-    'https://dev.zooniverse.org'
-  else if +location.port < 1024
-    'https://api.zooniverse.org'
-  else
-    'https://dev.zooniverse.org'
-    #"#{location.protocol}//#{location.hostname}:3000"
+[host, proxyPath] = if location.origin is 'http://preview.zooniverse.org'
+  ['https://dev.zooniverse.org', '/proxy']
+else if +location.port < 1024
+  [window.location.origin, '/_ouroboros_api/proxy']
+else
+  ['https://dev.zooniverse.org', '/proxy']
+
+Api.init {host, proxyPath}

 # TODO: Don't count on the proxy frame to have no loaded yet.


Set default language in submit form

Set default language in submit form

--- client/components/SubmitForm.coffee
+++ client/components/SubmitForm.coffee
@@ -27,7 +27,7 @@
     constructor: (props) ->
         super(props)
         @state =
-            lang_id: ""
+            lang_id: LANGUAGES[0][0]
             draft: false
         @setField = @setField.bind(this)
         @toggleDraft = @toggleDraft.bind(this)


bumped up the version

bumped up the version

--- client/version.coffee
+++ client/version.coffee
@@ -1 +1 @@
-@version = '2.3.1'
+@version = '2.4.1'


fix: Js error on empty value

fix: Js error on empty value

--- vendor/assets/javascripts/form.authy.coffee
+++ vendor/assets/javascripts/form.authy.coffee
@@ -406,7 +406,7 @@
   setCountryField = ->
     defaultListId = 0
     field = document.getElementById("authy-countries")
-    countryCode = field.value
+    countryCode = field?.value

     if countryCode isnt ''
       for country in countriesList


get rid of spurious signed_in call

get rid of spurious signed_in call

--- salvus/page/account.coffee
+++ salvus/page/account.coffee
@@ -254,7 +254,8 @@
             when "signed_in"
                 ga('send', 'event', 'account', 'create_account')    # custom google analytic event -- user created an account
                 alert_message(type:"success", message: "Account created!  You are now signed in as #{mesg.first_name} #{mesg.last_name}.")
-                signed_in(mesg)
+                ## THIS is taken care of by an event handler elsewhere
+                # signed_in(mesg)
             else
                 # should never ever happen
                 alert_message(type:"error", message: "The server responded with invalid message to account creation request: #{JSON.stringify(mesg)}")
@@ -322,8 +323,10 @@
             switch mesg.event
                 when 'sign_in_failed'
                     alert_message(type:"error", message: mesg.reason)
+
                 when 'signed_in'
-                    signed_in(mesg)
+                    # Signed_in gets handled by the signed_in event listener below -- do not do it here also.
+                    pass=0
                 when 'error'
                     alert_message(type:"error", message: mesg.reason)
                 else
@@ -481,10 +484,12 @@
         salvus_client.get_account_settings
             account_id : account_id
             cb         : (error, settings_mesg) =>
+                #console.log("load got back ", error, settings_mesg)
                 if error or settings_mesg.event == 'error'
                     $("#account-settings-error").show()
                     # try to get settings again in a bit to fix that the settings aren't known
                     f = () =>
+                        #console.log("calling again")
                         @load_from_server()
                     setTimeout(f, 10000)
                     cb?(error)


spinner when loading project log

spinner when loading project log

--- salvus/page/project.coffee
+++ salvus/page/project.coffee
@@ -1545,6 +1545,7 @@

     init_project_activity: () =>
         page = @container.find(".project-activity")
+        page.find("h1").icon_spin(start:true, delay:500)
         @_project_activity_log = page.find(".project-activity-log")
         LOG_FILE = '.sagemathcloud.log'
         async.series([
@@ -1603,6 +1604,7 @@
                 cb()

         ], (err) =>
+            page.find("h1").icon_spin(false)
             if err
                 # Just try again with exponential backoff  This can and does fail if say the project is first being initailized.
                 if not @_init_project_activity?


Fix error

Fix error

--- views/components/main/parts/task-panel.cjsx
+++ views/components/main/parts/task-panel.cjsx
@@ -56,6 +56,7 @@
   delete questRecord[id] if questRecord[id]?
   syncQuestRecord()
 activateQuestRecord = (id) ->
+  console.log questRecord
   if questRecord[id]?
     questRecord[id].active = true
     return
@@ -64,6 +65,7 @@
     required: 0
     active: true
   for k, v of questGoals[id]
+    continue if k == 'type'
     questRecord[id][k] =
       count: v.init
       required: v.required


Add Malichan link info

Add Malichan link info

--- views/components/settings/parts/others.cjsx
+++ views/components/settings/parts/others.cjsx
@@ -23,7 +23,7 @@

 Yunze

 Chibaheit

 KochiyaOcean

-
malichan

+
 马里酱

         </Col>
       </Grid>


we dont use it right now

we dont use it right now

--- vue/src/shared/loaders/thread_loader.coffee
+++ vue/src/shared/loaders/thread_loader.coffee
@@ -194,19 +194,19 @@
         discussion_id: @discussion.id
         comment_id: commentId

-  addLoadPinnedRule: ->
-    @titleKey = 'strand_nav.all_pinned'
-    @addRule
-      name: "all pinned events"
-      local:
-        find:
-          discussionId: @discussion.id
-          pinned: true
-        # position: {$gte: position}
-      remote:
-        discussion_id: @discussion.id
-        pinned: true
-        per: 200
+  # addLoadPinnedRule: ->
+  #   @titleKey = 'strand_nav.all_pinned'
+  #   @addRule
+  #     name: "all pinned events"
+  #     local:
+  #       find:
+  #         discussionId: @discussion.id
+  #         pinned: true
+  #       # position: {$gte: position}
+  #     remote:
+  #       discussion_id: @discussion.id
+  #       pinned: true
+  #       per: 200

   addLoadPositionRule: (position) ->
     @addRule


Ignore vim-mode within mini editors

Ignore vim-mode within mini editors

--- keymaps/vim-mode.cson
+++ keymaps/vim-mode.cson
@@ -1,10 +1,10 @@
 'body':
   'meta-alt-ctrl-o': 'vim-mode:toggle'

-'.vim-mode.insert-mode':
+'.vim-mode.insert-mode:not(.mini)':
   'escape': 'vim-mode:activate-command-mode'

-'.vim-mode.command-mode':
+'.vim-mode.command-mode:not(.mini)':
   '" a': 'vim-mode:register-prefix'
   '" b': 'vim-mode:register-prefix'
   '" c': 'vim-mode:register-prefix'


JApp: Create slug manually since the current workflow of slugifable is not fit with JApp

--- workers/social/lib/social/models/app.coffee
+++ workers/social/lib/social/models/app.coffee
@@ -26,8 +26,6 @@
   @set

     softDelete          : yes
-    # slugifyFrom         : 'name'
-    # slugTemplate        : -> "#{@authorNick}/\#{slug}"

     indexes             :
       slug              : 'unique'
@@ -167,7 +165,15 @@

       app.save (err)->
         return callback err  if err
-        app.addCreator delegate, (err)->
+        slug = "Apps/#{app.manifest.authorNick}/#{app.name}"
+        app.useSlug slug, (err, slugobj)->
+          if err then return app.remove -> callback err
+          slug = slugobj.slug
+          app.update {$set: {slug, slug_: slug}}, (err)->
+            console.warn "Slug update failed for #{slug}", err  if err
+          app.addCreator delegate, (err)->
+            return callback err  if err
+            callback null, app
           return callback err  if err
           slug = "#{app.authorNick}/#{app.name}"
           app.useSlug slug, (err, slugobj)->


add cursor position in js snippets

add cursor position in js snippets

--- snippets/js-templates.cson
+++ snippets/js-templates.cson
@@ -1,7 +1,7 @@
 ".source.js":
   "module":
     "prefix": "ngm"
-    "body": "angular.module('', []);"
+    "body": "angular.module('$1', [$2]);"
   "module as variable":
     "prefix": "ngma"
     "body": "var $1 = angular.module('$2', [$3]);"


Hack ResetTool to deactivate itself immediately

--- bokehjs/src/coffee/tool/reset_tool.coffee
+++ bokehjs/src/coffee/tool/reset_tool.coffee
@@ -21,7 +21,7 @@

     _activated: (e) ->
       @plot_view.update_range()
-      @plot_view.trigger("clear_active_tool")
+      _.delay((() => @plot_view.eventSink.trigger("clear_active_tool")), 100)

   class ResetTool extends Tool.Model
      default_view: ResetToolView


Avoid testing-Turker login anywhere on the admin interface, not just the front page.

--- client/helpers.coffee
+++ client/helpers.coffee
@@ -62,7 +62,7 @@
   # Don't try logging in if we are logged in or already have parameters
   return if Meteor.userId() or Session.get("_loginParams")
   # Don't show this if we are trying to get at the admin interface
-  return if Router.current()?.path is "/turkserver"
+  return if Router.current()?.path?.indexOf("/turkserver") is 0

   str = Random.id()
   hitId = str + "_HIT"


Prevent tag additions from propagating to parents

--- client/message.coffee
+++ client/message.coffee
@@ -453,6 +453,8 @@

 Template.submessage.events
   'click .tagRemove': (e, t) ->
+    e.preventDefault()
+    e.stopPropagation()
     message = t.data._id
     tags = t.data.tags
     tag = e.target.getAttribute 'data-tag'
@@ -465,6 +467,8 @@
       console.warn "Attempt to delete nonexistant tag '#{tag}' from message #{message}"

   'click .tagAdd': (e, t) ->
+    e.preventDefault()
+    e.stopPropagation()
     message = t.data._id
     tags = t.data.tags
     tag = e.target.getAttribute 'data-tag'
@@ -474,6 +478,7 @@
       tags[escapeTag tag] = true
       Meteor.call 'messageUpdate', message,
         tags: tags
+    $(e.target).parents('.dropdown-menu').first().parent().find('.dropdown-toggle').dropdown 'toggle'

   'click .tagAddNew': (e, t) ->
     e.preventDefault()


Small fixes

Small fixes

--- client/message.coffee
+++ client/message.coffee
@@ -2196,14 +2196,15 @@
               {if historified.file and editing

-
+
+                    {' '}
+

                     {###if image
                       +messageImage
-                    +messageReplace _id=_id group=group tabIndex=tabindex0+9
                     ###}
+                    <MessageReplace _id={message._id} group={message.group} tabindex={tabindex0+9}/>

               }


Enabled console pane text selection

Enabled console pane text selection

Solution from https://github.com/tcarlsen/atom-message-panel/issues/24

----
Adding attribute tabindex set to -1 allows me to select text. However, I
cannot copy that selected text.

Adding native-key-bindings class allows me to copy the selected text.

@messagePanelView = new MessagePanelView(title: "Gitter")
@messagePanelView.addClass('native-key-bindings')
@messagePanelView.attr('tabindex', -1)
@messagePanelView.attach()
----

--- lib/Components/ConsolePane.coffee
+++ lib/Components/ConsolePane.coffee
@@ -101,9 +101,14 @@
       }
     }, [
       h('div.debugger-panel-heading', {}, ['stdout/stderr'])
-      h('div.panel-body.padded', style: {
-        flex: '1'
-        overflow: 'auto'
+      h('div.panel-body.padded.native-key-bindings', {
+        attributes: {
+          tabindex: '-1'
+        }
+        style: {
+          flex: '1'
+          overflow: 'auto'
+        }
       }, state.lines.map(tokenizeLine))
       h('div.debugger-editor', style: {
         height: '33px'


Force attribute nodes for undefined values.

--- client/xml-gen.coffee
+++ client/xml-gen.coffee
@@ -1,6 +1,6 @@
 element_or_attribute = (parent, {name, attr ,type, value, data }) ->
   if attr
-    parent.attr name, value
+    parent.attr name, value or ""
     null
   else
     parent.append n = ($ "<#{name}>").text value


Added response status

Added response status

--- spec/e2e.stubs.spec.coffee
+++ spec/e2e.stubs.spec.coffee
@@ -24,7 +24,7 @@

       finish = ->
          sut = new Stubby()
-         sut.start data:endpointData, done
+         sut.start data: endpointData, done

       stopStubby finish

@@ -295,6 +295,5 @@
           createRequest context

           waitsFor ( -> context.done ), 'body match', 1000, ->
-            console.log context.response.data
             assert context.response.data.length isnt 0
             done()


Add spec for selection behavior in command-mode

This should have been done as part of #84 but I forgot to mention it.

--- spec/vim-state-spec.coffee
+++ spec/vim-state-spec.coffee
@@ -162,3 +162,12 @@

       it "operate on the current selection", ->
         expect(editor.getText()).toEqual "\nabcdef"
+
+    describe "returning to command-mode", ->
+      beforeEach ->
+        editor.setText("012345\n\nabcdef")
+        editor.selectLine()
+        keydown('escape')
+
+      it "operate on the current selection", ->
+        expect(editor.getSelection().getText()).toEqual ''


Remove .coffee extension from require path

Refs atom/atom#4884

--- spec/body-parser-spec.coffee
+++ spec/body-parser-spec.coffee
@@ -1,4 +1,4 @@
-BodyParser = require '../lib/snippet-body-parser.coffee'
+BodyParser = require '../lib/snippet-body-parser'

 describe "Snippet Body Parser", ->
   it "breaks a snippet body into lines, with each line containing tab stops at the appropriate position", ->


Support Menu

Support Menu

--- templates/main.coffee
+++ templates/main.coffee
@@ -1,5 +1,6 @@
 app = require('app')
 BrowserWindow = require('browser-window')
+Menu = require("menu")
 require('crash-reporter').start()

 mainWindow = null
@@ -16,3 +17,60 @@
   # mainWindow.openDevTools()
   mainWindow.on 'closed', ->
     mainWindow = null
+
+  template = [
+    {
+      label: 'Application'
+      submenu: [
+        {
+          label: 'About Application'
+          selector: 'orderFrontStandardAboutPanel:'
+        }
+        { type: 'separator' }
+        {
+          label: 'Quit'
+          accelerator: 'Command+Q'
+          click: ->
+            app.quit()
+            return
+        }
+      ]
+    }
+    {
+      label: 'Edit'
+      submenu: [
+        {
+          label: 'Undo'
+          accelerator: 'CmdOrCtrl+Z'
+          selector: 'undo:'
+        }
+        {
+          label: 'Redo'
+          accelerator: 'Shift+CmdOrCtrl+Z'
+          selector: 'redo:'
+        }
+        { type: 'separator' }
+        {
+          label: 'Cut'
+          accelerator: 'CmdOrCtrl+X'
+          selector: 'cut:'
+        }
+        {
+          label: 'Copy'
+          accelerator: 'CmdOrCtrl+C'
+          selector: 'copy:'
+        }
+        {
+          label: 'Paste'
+          accelerator: 'CmdOrCtrl+V'
+          selector: 'paste:'
+        }
+        {
+          label: 'Select All'
+          accelerator: 'CmdOrCtrl+A'
+          selector: 'selectAll:'
+        }
+      ]
+    }
+  ]
+  Menu.setApplicationMenu Menu.buildFromTemplate(template)


Don't directly modify localStorage for integration tests

The underlying store may eventually change.

--- spec/integration_spec.coffee
+++ spec/integration_spec.coffee
@@ -4,7 +4,7 @@

 beforeEach ->
   window.onlineSync.calls = []
-  window.localStorage.clear()
+  window.localsync 'clear', {}, ignoreCallbacks: true, storeName: 'eyes/'
   collection = new Backbone.Collection
     id: 123
     vision: 'crystal'


Handle machine re-initialization process

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -433,6 +433,17 @@
         if event.status in actionRequiredStates
           @showStateMachineModal machineItem, event

+      .on "reinit-#{machineItem._id}", @bound 'handleMachineReinit'
+
+
+  handleMachineReinit: ({status}) ->
+
+    switch status
+      when 'Building'
+        environmentDataProvider.ensureDefaultWorkspace kd.noop
+      when 'Running'
+        @quit()
+

   showStateMachineModal: (machineItem, event) ->


show only "Cursor colour" control for cursor highlight group

--- client/prefs-colours.coffee
+++ client/prefs-colours.coffee
@@ -35,7 +35,7 @@
   {t:'⍝',  c:'.cm-apl-comment',             s:'comment'         }
   {t:'!',  c:'.cm-apl-err',                 s:'error'           }
   {t:'L',  c:'.CodeMirror-linenumber',      s:'line number'     }
-  {t:'cu', c:'div.CodeMirror-cursor',       s:'cursor',           controls:{lb:1}}
+  {t:'cu', c:'div.CodeMirror-cursor',       s:'cursor',           controls:{lb:1,fg:0,bg:0,BIU:0}}
   {t:'mb', c:'.CodeMirror-matchingbracket', s:'matching bracket'}
   {t:'sc', c:'.CodeMirror-search-match',    s:'search match'    }
   {t:'mo', c:'.modified',                   s:'modified (session)'}
@@ -104,7 +104,7 @@
         <input type=checkbox id=col-B>B
         <input type=checkbox id=col-I>I
         <input type=checkbox id=col-U>U
-
<input type=checkbox id=col-lb-cb>Left border <input type=color id=col-lb>
+
<input type=checkbox id=col-lb-cb>Cursor colour <input type=color id=col-lb>

   """
   $cm = $ '#col-cm'


Use setGrammar instead of messing with overrides

--- spec/spell-check-spec.coffee
+++ spec/spell-check-spec.coffee
@@ -84,8 +84,7 @@

       runs ->
         expect(editor.getHighlightDecorations(class: 'spell-check-misspelling').length).toBe 1
-        atom.syntax.setGrammarOverrideForPath(editor.getPath(), 'text.plain')
-        editor.reloadGrammar()
+        editor.setGrammar(atom.grammars.selectGrammar('.txt'))
         expect(editor.getHighlightDecorations(class: 'spell-check-misspelling').length).toBe 0

   describe "when 'spell-check:correct-misspelling' is triggered on the editor", ->


Activate packages asynchronously

Activate packages asynchronously

--- spec/spell-check-spec.coffee
+++ spec/spell-check-spec.coffee
@@ -4,14 +4,23 @@
   [editorView] = []

   beforeEach ->
-    atom.packages.activatePackage('language-text', sync: true)
-    atom.packages.activatePackage('language-javascript', sync: true)
-    atom.workspaceView = new WorkspaceView
-    atom.workspaceView.openSync('sample.js')
-    atom.config.set('spell-check.grammars', [])
-    atom.packages.activatePackage('spell-check', immediate: true)
-    atom.workspaceView.attachToDom()
-    editorView = atom.workspaceView.getActiveView()
+    waitsForPromise ->
+      atom.packages.activatePackage('language-text')
+
+    waitsForPromise ->
+      atom.packages.activatePackage('language-javascript')
+
+    runs ->
+      atom.workspaceView = new WorkspaceView
+      atom.workspaceView.openSync('sample.js')
+      atom.config.set('spell-check.grammars', [])
+
+    waitsForPromise ->
+      atom.packages.activatePackage('spell-check')
+
+    runs ->
+      atom.workspaceView.attachToDom()
+      editorView = atom.workspaceView.getActiveView()

   it "decorates all misspelled words", ->
     editorView.setText("This middle of thiss sentencts has issues.")


:white_check_mark: Write specs for soft-wrapped lines selection

--- spec/text-editor-spec.coffee
+++ spec/text-editor-spec.coffee
@@ -1623,7 +1623,31 @@
             [[6, 22], [6, 28]]
           ]

+        it "selects also soft-wrapped lines", ->
+          editor.setSoftWrapped(true)
+          editor.setDefaultCharWidth(10)
+          editor.setEditorWidthInChars(40)
+
+          editor.setSelectedScreenRange([[6, 20], [6, 25]])
+          editor.addSelectionBelow()
+          expect(editor.getSelectedScreenRanges()).toEqual [
+            [[6, 20], [6, 25]]
+            [[7, 20], [7, 25]]
+          ]
+
       describe "when the selection is empty", ->
+        it "does not skip soft-wrapped lines shorter than the current column", ->
+          editor.setSoftWrapped(true)
+          editor.setDefaultCharWidth(10)
+          editor.setEditorWidthInChars(40)
+
+          editor.setCursorScreenPosition([6, 44])
+          editor.addSelectionBelow()
+          expect(editor.getSelectedScreenRanges()).toEqual [
+            [[6, 44], [6, 44]]
+            [[7, 26], [7, 26]]
+          ]
+
         it "does not skip lines that are shorter than the current column", ->
           editor.setCursorBufferPosition([3, 36])
           editor.addSelectionBelow()
@@ -1687,7 +1711,31 @@
             [[3, 22], [3, 38]]
           ]

+        it "selects also soft-wrapped lines", ->
+          editor.setSoftWrapped(true)
+          editor.setDefaultCharWidth(10)
+          editor.setEditorWidthInChars(40)
+
+          editor.setSelectedScreenRange([[7, 20], [7, 25]])
+          editor.addSelectionAbove()
+          expect(editor.getSelectedScreenRanges()).toEqual [
+            [[7, 20], [7, 25]]
+            [[6, 20], [6, 25]]
+          ]
+
       describe "when the selection is empty", ->
+        it "does not skip soft-wrapped lines shorter than the current column", ->
+          editor.setSoftWrapped(true)
+          editor.setDefaultCharWidth(10)
+          editor.setEditorWidthInChars(40)
+
+          editor.setCursorScreenPosition([6, 44])
+          editor.addSelectionAbove()
+          expect(editor.getSelectedScreenRanges()).toEqual [
+            [[6, 44], [6, 44]]
+            [[5, 30], [5, 30]]
+          ]
+
         it "does not skip lines that are shorter than the current column", ->
           editor.setCursorBufferPosition([6, 36])
           editor.addSelectionAbove()


Fix TextEditor cutToEndOfLine() test

Fix TextEditor cutToEndOfLine() test

…which was failing due to the different soft-wrapping rules.

--- spec/text-editor-spec.coffee
+++ spec/text-editor-spec.coffee
@@ -3803,10 +3803,10 @@
           it "cuts up to the end of the line", ->
             editor.setSoftWrapped(true)
             editor.setDefaultCharWidth(1)
-            editor.setEditorWidthInChars(10)
-            editor.setCursorScreenPosition([2, 2])
+            editor.setEditorWidthInChars(25)
+            editor.setCursorScreenPosition([2, 6])
             editor.cutToEndOfLine()
-            expect(editor.tokenizedLineForScreenRow(2).text).toBe '=  () {'
+            expect(editor.lineTextForScreenRow(2)).toBe '  var  function(items) {'

         describe "when soft wrap is off", ->
           describe "when nothing is selected", ->


Update tests to reflect that #githubRepoUrl is an instance method

--- spec/to-the-hubs-spec.coffee
+++ spec/to-the-hubs-spec.coffee
@@ -1,15 +1,26 @@
-{githubOpen, githubRepoUrl} = require '../lib/to-the-hubs'
+GitHubFile = require '../lib/github-file'

-describe "ToTheHubs", ->
-  describe "githubRepoUrl(gitUrl)", ->
+describe "GitHubFile", ->
+  describe "githubRepoUrl", ->
+    githubFile = null
+
+    beforeEach ->
+      githubFile = new GitHubFile()
+
     it "returns the GitHub.com URL for an HTTP remote URL", ->
-      url = githubRepoUrl("https://github.com/foo/bar.git")
-      expect(url).toBe "https://github.com/foo/bar"
+      githubFile.gitUrl = ->
+        "https://github.com/foo/bar.git"
+
+      expect(githubFile.githubRepoUrl()).toBe "https://github.com/foo/bar"

     it "returns the GitHub.com URL for an SSH remote URL", ->
-      url = githubRepoUrl("git@github.com:foo/bar.git")
-      expect(url).toBe "https://github.com/foo/bar"
+      githubFile.gitUrl = ->
+        "git@github.com:foo/bar.git"
+
+      expect(githubFile.githubRepoUrl()).toBe "https://github.com/foo/bar"

     it "retuns undefined for a non-GitHub remote URL", ->
-      url = githubRepoUrl("https://example.com/foo/bar.git")
-      expect(url).toBeUndefined()
+      githubFile.gitUrl = ->
+        "https://example.com/foo/bar.git"
+
+      expect(githubFile.githubRepoUrl()).toBeUndefined()


collision with regular bounds

collision with regular bounds

--- test/collision.coffee
+++ test/collision.coffee
@@ -61,5 +61,5 @@

   equals p1.y, 0

-  equals
+  equals p2.x
 module()


initial BoardGame class

initial BoardGame class

--- coffee/script2.coffee
+++ coffee/script2.coffee
@@ -63,6 +63,19 @@
   $(document).foundation()
   ko.applyBindings new ViewModel()
   return
+
+class BoardGame
+  constructor: (data) ->
+    for key,value of data
+      @[key] = value
+    @thumbnail ?= ""
+    @goodComments ?= (comment for comment in @comments.comment when comment.value.length > 119 and parseInt(comment.rating) > 0 and comment.value.length < 600)
+    @featuredComment = ko.observable()
+    @pickFeaturedComment()
+
+  pickFeaturedComment: ->
+      @featuredComment(@goodComments[Math.floor(Math.random() * @goodComments.length)])
+      return

 class ViewModel
   constructor: ->
@@ -132,7 +145,7 @@
   # Updates sort direction icons
   # @param type [String] (name,brating)
   # @param event [jQueryEvent]
-  updateTableHeaders: (type,event) ->
+  updateTableHeaders: (type,event) =>
     $("#results-table thead tr th").removeClass "headerSortUp"
     $("#results-table thead tr th").removeClass "headerSortDown"
     if sortDirection is -1
@@ -378,6 +391,9 @@
       i++
     return

+  saveToCache: (type,key,data) ->
+    return
+
   goToSearch: ->
     str = encodeURIComponent($("#search").val())
     location.hash = "search/" + str


more self-contained output

more self-contained output

--- bin/eden.coffee
+++ bin/eden.coffee
@@ -16,14 +16,14 @@
 phase     = require "../lib/phase"

-output = (context) ->
-  if opts.verbose
+output = (opts, settings) ->
+  if opts.verbose and settings?.out isnt "json"
     console.log "\noptions:"
     inspect opts.argv
     console.log "command: #{opts.command}"
-    if context?
+    if settings?
       console.log "context:"
-      inspect ephemeris.settings
+      inspect settings
     console.log ""
     console.log "RESULTS"
     console.log "======="
@@ -37,7 +37,7 @@

   when "pre"
     ephemeris = new Ephemeris opts.merge
-    output ephemeris.settings
+    output opts, ephemeris.settings
     ephemeris.run process.stdout
     # Massaged (Array) ephemeris.settings.out somehow get their "\n" trailing...
     # Extra newlines are not added outside of command-line context, otherwise.
@@ -47,7 +47,7 @@

   when "know"
     ephemeris = new Ephemeris opts.merge, ->
-      output ephemeris.settings
+      output opts, ephemeris.settings
       ephemeris.run process.stdout

   when "eat"
@@ -56,9 +56,11 @@
       # Take them from precious.0 (implement the precious-json "extra").
       process.stdin.resume()
       process.stdin.setEncoding("utf8")
+      output opts, ephemeris.settings
       phase (points process.stdin, ephemeris.settings), process.stdout

   else
+    output opts
     console.log "Unknown command '#{opts.command}' has bypassed the options validator..."
     console.log "Nothing to do."
     process.exit(0)


tar and gzip aw yeah

tar and gzip aw yeah

--- lib/deployers/heroku/index.coffee
+++ lib/deployers/heroku/index.coffee
@@ -5,6 +5,7 @@
 Heroku  = require 'heroku-client'
 tar     = require 'tar'
 fstream = require 'fstream'
+zlib    = require 'zlib'
 request = require 'request'

 module.exports = (root, config) ->
@@ -36,7 +37,7 @@
       return d2.promise
     .then (res) -> W(heroku.apps(config.name).builds(res.id).result().info())
     .finally ->
-      fs.unlinkSync(path.join(root, "#{config.name}.tar"))
+      fs.unlinkSync(path.join(root, "#{config.name}.tar.gz"))
     .done ->
       d.resolve
         deployer: 'heroku'
@@ -83,7 +84,7 @@

 check_app_status = (heroku, id, name, d) ->
   heroku.apps(name).builds(id).info().then (res) ->
-    console.log res.status
+    switch res.status
       when 'pending'
         check_app_status(heroku, id, name, d)
       else
@@ -100,10 +101,11 @@

 create_tar = (root, name) ->
   d = W.defer()
-  tar_path = path.join(root, "#{name}.tar")
+  tar_path = path.join(root, "#{name}.tar.gz")

   stream = fstream.Reader(path: root, type: 'Directory')
     .pipe(tar.Pack(noProprietary: true))
+    .pipe(zlib.createGzip())
     .pipe(fs.createWriteStream(tar_path))

   stream.on('close', d.resolve.bind(d, tar_path))


Correct data type

Correct data type

--- components/DetectAnimatedGif.coffee
+++ components/DetectAnimatedGif.coffee
@@ -11,7 +11,7 @@
   c.description = 'Detect if a given GIF is animated'

   c.inPorts.add 'buffer',
-    datatype: 'all'
+    datatype: 'buffer'
     description: 'An image buffer'
   c.outPorts.add 'animated',
     datatype: 'boolean'
@@ -37,9 +37,7 @@
           out.send isAnimated
           do callback
           return
-    else
-      out.send false
-      do callback
       return
-
-  c
+    out.send false
+    do callback
+    return


testing retrieve

testing retrieve

--- test/quad_tree.coffee
+++ test/quad_tree.coffee
@@ -2,10 +2,21 @@

 test "should have bounds", ->
   quadTree = QuadTree()
-
-  log quadTree
-
+
   equals quadTree.I.bounds.x, 0
   equals quadTree.I.bounds.y, 0
   equals quadTree.I.bounds.width, 480
   equals quadTree.I.bounds.height, 320
+
+test "can insert and retrieve point", ->
+  quadTree = QuadTree()
+
+  quadTree.insert
+    x: 50
+    y: 100
+
+  items = quadTree.retrieve
+    x: 50
+    y: 100
+
+  ok items.length


Update vansteki.gy.coffee

Update vansteki.gy.coffee

--- scripts/vansteki.gy.coffee
+++ scripts/vansteki.gy.coffee
@@ -4,5 +4,7 @@
 	robot.hear /gy (https?:\/\/www.youtube.com\/watch\?\w=.*)/i, (msg) ->
   		msg.send u.inspect msg.match[1]
   		youtube.get msg.match[1], ((data, title) ->
-  			msg.send title data
+  			msg.send title
+  			msg.send data[0].title
+  			msg.send data[0].link
   		).bind(msg)


added automated testing for binarySearch and linearSearch

--- test/searching.coffee
+++ test/searching.coffee
@@ -40,6 +40,14 @@
       results = fn [], 10
       expect(results).to.equal -1

+    it 'automated testing', ->
+
+      for i in [0...100]
+        list = ((Math.floor Math.random()*10000) for i in [0...1000])
+        index = list.indexOf list[(Math.floor Math.random()*999)]
+        expect(fn list, list[index]).to.equal index
+
+
   describe 'binarySearch', ->

     fn = binarySearch
@@ -70,3 +78,12 @@

       results = fn [], 10
       expect(results).to.equal -1
+
+    it 'automated testing', ->
+
+      for i in [0...100]
+        list = ((Math.floor Math.random()*10000) for i in [0...1000])
+        list.sort (a, b)=> a - b
+        index = list.indexOf list[(Math.floor Math.random()*999)]
+        expect(fn list, list[index]).to.equal index
+


video: model add meaningful names for state props

--- client/app/lib/videocollaboration/model.coffee
+++ client/app/lib/videocollaboration/model.coffee
@@ -10,10 +10,11 @@
 module.exports = class VideoCollaborationModel extends kd.Object

   defaultState:
-    video              : on
-    audio              : on
+    publishVideo       : on
+    publishAudio       : on
     publishing         : off
-    connectedToSession : no
+    active             : no
+    connected          : no
     maxConnectionCount : 999
     activeParticipant  : null
     connectionCount    : 0
@@ -82,7 +83,7 @@

     @session = session
     @bindSessionEvents session
-    @setState { connectedToSession: yes }
+    @setState { connected: yes }
     @emit 'SessionConnected', session

@@ -104,10 +105,8 @@
   bindSessionEvents: (session) ->

     session.on 'streamCreated', (event) =>
-      return  if @state.connectedToSession is no
-
-      options = { height: '100%', width: '100%', insertMode: 'append' }
-
+
+      options    = { height: '100%', width: '100%', insertMode: 'append' }
       { stream } = event
       nick       = stream.name
       element    = @getView().getElement()
@@ -136,7 +135,7 @@

     session.on 'sessionDisconnected', (event) =>
-      @setState { connectedToSession: no }
+      @setState { connected: no }
       eventName = switch event.reason
         when 'forceDisconnected'   then 'HostKickedLoggedInUser'
         when 'clientDisconnected'  then 'VideoCollaborationEnded'
@@ -248,8 +247,8 @@
   startPublishing: (options, callbacks) ->

     defaults =
-      publishAudio: @state.audio
-      publishVideo: @state.video
+      publishAudio: @state.publishAudio
+      publishVideo: @state.publishVideo

     options = _.assign {}, defaults, options


update requires

update requires

--- client/apps/articles_list/client/client.coffee
+++ client/apps/articles_list/client/client.coffee
@@ -1,8 +1,6 @@
-Backbone = require 'backbone'
 _ = require 'underscore'
-Q = require 'bluebird-q'
 React = require 'react'
-jqueryOnInfitireScroll = require('jquery-on-infinite-scroll')
+require 'jquery-on-infinite-scroll'
 { div, nav, a, h1 } = React.DOM
 Article = require '../../../models/article.coffee'
 FilterSearch = require '../../../components/filter_search/index.coffee'
@@ -22,7 +20,7 @@
     $.onInfiniteScroll canLoadMore

   canLoadMore: ->
-    return unless !$('.filter-search__input').val()
+    return if $('.filter-search__input').val()
     $('.loading-spinner').fadeIn()
     @fetchFeed @state.published, @state.offset + 10, @appendMore


Check existence of gulpfile in landing site directory

--- client/landing/gulptasks/task.build.all.coffee
+++ client/landing/gulptasks/task.build.all.coffee
@@ -5,7 +5,13 @@
 module.exports = (version) ->

   folders  = (folder for folder in fs.readdirSync('./') when fs.statSync(folder).isDirectory())
-  sites    = folders.filter (folder) -> folder.search(/^site\./) is 0 and folder isnt 'site.boilerplate'
+  sites    = folders.filter (folder) ->
+    siteDir = folder.search(/^site\./) is 0 and folder isnt 'site.boilerplate'
+    return no  unless siteDir
+    try
+      fs.statSync "#{folder}/gulpfile.coffee"
+      return yes
+    return no
   commands = ("gulp --gulpfile ./#{siteDir}/gulpfile.coffee build --uglify" for siteDir in sites)

   gulp.src ''


Reword invalid event name error

Reword invalid event name error

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -956,7 +956,7 @@
 		@_addListener(eventName, listener)

 	addListener: (eventName, listener) =>
-		throw Error("Layer.on needs an event name") unless eventName
+		throw Error("Layer.on needs a valid event name") unless eventName
 		throw Error("Layer.on needs an event listener") unless listener
 		super(eventName, listener)
 		@_addListener(eventName, listener)


Decode entities in old submits

Decode entities in old submits

--- server/api/setupApi.coffee
+++ server/api/setupApi.coffee
@@ -376,7 +376,7 @@
             if not result or result.solved <= 0
                 res.status(403).send('No permissions')
                 return
-        source = submit.sourceRaw || submit.source
+        source = submit.sourceRaw || entities.decode(submit.source)
         mimeType = FileType.fromBuffer(Buffer.from(source))?.mime || "text/plain"
         res.contentType(mimeType)
         res.send(source)


Minor tweaks for Markdown editing

Minor tweaks for Markdown editing

--- atom/config.cson
+++ atom/config.cson
@@ -22,4 +22,9 @@
     rspecFileCommand: "bundle exec rspec --tty {relative_path}"
     rspecSingleCommand: "bundle exec rspec --tty {relative_path}:{line_number}"
   "git-blame": {}
+  "markdown-deluxe":
+    fontFamily: "Hack"
   "expand-region": {}
+"source.gfm":
+  editor:
+    softWrap: true


fix docblockr setting

fix docblockr setting

--- atom/config.cson
+++ atom/config.cson
@@ -4,6 +4,8 @@
       "seti-ui"
       "seti-syntax"
     ]
+  docblockr:
+    extend_double_slash: false
   editor:
     fontFamily: "Andale Mono"
     fontSize: 13


tweak column ruler

tweak column ruler

--- atom/config.cson
+++ atom/config.cson
@@ -208,10 +208,10 @@
 ".js.source":
   "multi-wrap-guide":
     columns: [
-      79
-    ]
-  editor:
-    preferredLineLength: 79
+      80
+    ]
+  editor:
+    preferredLineLength: 80
 ".coffee.source":
   "multi-wrap-guide":
     columns: [


Adjust rubocop path

Adjust rubocop path

--- atom/config.cson
+++ atom/config.cson
@@ -31,7 +31,7 @@
       "rubocop"
     ]
   "linter-rubocop":
-    command: "/Users/jan.habermann/.rbenv/shims/rubocop"
+    command: "/Users/jan/.rbenv/shims/rubocop"
   metrics:
     userId: "9e4d32b140be1cb4e975a34a65c17fc15d35c6d7"
   minimap: {}


update atom config for new version of term3

--- atom/config.cson
+++ atom/config.cson
@@ -190,16 +190,8 @@
     usePreviewTabs: true
   term3:
     colors:
-      brightWhite:
-        red: 214
-        green: 214
-        blue: 214
-        alpha: 1
-      foreground:
-        red: 219
-        green: 218
-        blue: 220
-        alpha: 1
+      brightWhite: "#d6d6d6"
+      foreground: "#dbdadc"
   "theme-switcher":
     syntaxThemeOne: "tomorrow-ya"
     syntaxThemeTwo: "base16-syntax"


Add command for opening todo list

Add command for opening todo list

--- atom/init.coffee
+++ atom/init.coffee
@@ -8,7 +8,6 @@
 path = require 'path'

 oldWindowDimensions = {}
-oldThemes = []

 atom.commands.add 'atom-workspace',
   'custom:open-todo-list': ->
@@ -17,11 +16,7 @@

   'custom:screenshot-prep': ->
     oldWindowDimensions = atom.getWindowDimensions()
-    oldThemes = atom.config.get('core.themes')
-
-    atom.setWindowDimensions('width': 1024, 'height': 768)
-    atom.config.restoreDefault('core.themes')
+    atom.setWindowDimensions('width': 1366, 'height': 768)

   'custom:screenshot-restore': ->
-    atom.config.set('core.themes', oldThemes)
     atom.setWindowDimensions(oldWindowDimensions)


Drop mongodb database between tests

Drop mongodb database between tests

--- server/test/helpers.coffee
+++ server/test/helpers.coffee
@@ -12,8 +12,22 @@
   test_server.close () -> done()
 )

+dropDatabase = (connection, done) ->
+  connection.db.dropDatabase (err) ->
+    if err?
+      console.log 'ERROR'
+      console.log err
+    else
+      done()
+
 beforeEach( (done) ->
-  global.sequelize.sync({force: true}).success(() -> done())
+  connection = mongoose.connection
+  state = connection.readyState
+
+  if state == 2
+    connection.on 'open', -> dropDatabase(connection, done)
+  else if state == 1
+    dropDatabase(connection, done)
 )

 exports.appurl = (path) ->


Delete console log type

Delete console log type

--- components/clock/index.cjsx
+++ components/clock/index.cjsx
@@ -54,7 +54,6 @@

   # -- Render
   render: ->
-    console.log @props

         {


Added toObject methods

Added toObject methods

--- lib/gwmessages.coffee
+++ lib/gwmessages.coffee
@@ -105,9 +105,25 @@
     #TODO: Validation of datetime
     if datetime
       @validityPeriod = datetime
-
+
+  getContent: () ->
+    if @binary
+      return @binary
+    return @text
+
   getPostDataParts: () ->
     return {}
+
+  toObject: () ->
+    return {
+      id: @id
+      recipients: @recipients
+      sender: @sender
+      operator: @operator
+      keywords: @keywords
+      parameters: @parameters
+      content: @getContent()
+    }

   _clearValues: () ->
     @id = null
@@ -259,6 +275,16 @@
       when 10 then "Operator"
       when 11 then "Protocol"
       when 12 then "Canceled"
+
+  toObject: () ->
+    return {
+      id: @message_id
+      recipient: @recipient
+      original_recipient: @original_recipient
+      state: @state
+      error: @error
+      description: @description
+    }

 class MessageUtils


Added rendering helpers

Added rendering helpers

--- lib/json_utils.coffee
+++ lib/json_utils.coffee
@@ -1,5 +1,7 @@
-_ = @_ or require 'underscore'
+util = require 'util'
+_ = require 'underscore'
 moment = @moment or require 'moment'
+Queue = require 'queue-async'

 module.exports = class JSONUtils

@@ -30,4 +32,76 @@
       value[index] = @JSONToValue(item) for index, item of value
     else if _.isObject(value)
       value[key] = @valueToJSON(item) for key, item of value
-    return value+    return value
+
+  @renderModelJSON = (related_model, template, options, callback) ->
+    (callback = options; options = {}) if arguments.length is 3
+    return callback(null, null) unless related_model
+
+    # pick keys
+    if _.isArray(template)
+      queue = new Queue()
+
+      result = {}
+      for key in template
+        do (key) -> queue.defer (callback) ->
+          related_model.get key, (err, value) ->
+            return callback(err) if err
+            result[key] = value
+            callback()
+
+      queue.await (err) ->
+        return callback(err) if err
+        callback(null, result)
+
+    # render template
+    else
+      template related_model, options, (err, related_json) ->
+        return callback(err) if err
+        callback(null, related_json)
+
+  @renderModelsJSON = (related_models, template, options, callback) ->
+    (callback = options; options = {}) if arguments.length is 3
+
+    # many
+    queue = new Queue()
+
+    result = []
+    for related_model in related_models
+      do (related_model) ->
+        queue.defer (callback) ->
+          JSONUtils.renderModelJSON related_model, template, options, (err, related_json) ->
+            return callback(err) if err
+            result.push(related_json)
+            callback()
+
+    queue.await (err) ->
+      return callback(err) if err
+      callback(null, result)
+
+  @appendModelJSON = (json, related_model, attribute_name, template, options, callback) ->
+    (callback = options; options = {}) if arguments.length is 5
+
+    return callback() unless related_model
+    JSONUtils.renderModelJSON related_model, template, options, (err, model_json) ->
+      return callback(err) if err
+
+      # embed keys
+      if _.isArray(template)
+        (json["#{attribute_name}_#{key}"] = model_json[key] for key in template)
+      else
+        json[attribute_name] = model_json
+      callback()
+
+  @appendRelatedJSON = (json, model, attribute_name, template, options, callback) ->
+    (callback = options; options = {}) if arguments.length is 5
+
+    model.get attribute_name, (err, related_models) ->
+      callback(err) if err
+
+      return JSONUtils.appendModelJSON(json, related_models, attribute_name, template, options, callback) unless _.isArray(related_models) # one
+
+      # many
+      JSONUtils.renderModelsJSON related_models, template, options, (err, related_json) ->
+        return callback(err) if err
+        json[attribute_name] = related_json


Fix malformed tooltips on shared query

Fix malformed tooltips on shared query

This overcomes a bug in bootstrap versions < 3.0 where tooltips are not
placed correctly within nested elements. This can be removed later after
we upgrade to bootstrap >= 3.0 but this is a documented and bootstrap
recommended workaround until that point.

--- src/coffee/cilantro/ui/query/item.coffee
+++ src/coffee/cilantro/ui/query/item.coffee
@@ -70,7 +70,10 @@

                 emailHTML = _.pluck(@model.get('shared_users'), 'email').join('<br />')
                 @ui.shareCount.attr('title', emailHTML)
-                @ui.shareCount.tooltip({animation: false, html: true, placement: 'right'})
+                # NOTE: The container needs to be set to overcome and issue
+                # with tooltip placement in bootstrap < 3.0. This container
+                # setting can be removed after we upgrade to bootstrap >= 3.0.
+                @ui.shareCount.tooltip({animation: false, html: true, placement: 'right', container: 'body'})
             else
                 @ui.owner.hide()


Fix getMaxHeight logic to take in account fixed parents

The offsetTop of the element is not accurate when contained within a
fixed element. This traverses up the element's parents and uses the
top position if a fixed parent is found.

--- src/coffee/plugins/jquery-stacked.coffee
+++ src/coffee/plugins/jquery-stacked.coffee
@@ -81,8 +81,19 @@

             windowHeight = $(window).outerHeight()

-            # Offset relative to the document (including the top margin)
-            offsetTop = $elem.offset().top
+            # Determine offset relative to the document. For an accurate
+            # calculation, all parents must be traverse to determine if any
+            # have an absolute or fixed position.
+            offsetTop = null
+
+            $elem.parents().each (i, parent) ->
+                parent = $(parent)
+                if parent.css('position') is 'fixed'
+                    offsetTop = parent.position().top
+                    return false
+
+            if not offsetTop?
+                offsetTop = $elem.offset().top

             # Substract the bottom margin from the height to respect the
             # spacing


Scoring keys finally work

Scoring keys finally work

--- src/components/keyboard_shortcuts.coffee
+++ src/components/keyboard_shortcuts.coffee
@@ -7,22 +7,16 @@

 KeyboardShortcuts = React.createClass
   displayName: 'KeyboardShortcuts'
-  pointKeys: {5: 15, 6: 16, 7: 17, 8: 18, 9: 19, 0: 20, 1: 25}
+  pointKeys: {'5': '15', '6': '16', '7': '17', '8': '18', '9': '19', '0': '20', '1': '25'}

   componentDidMount: ->
     key 'space', => @props.nextRound() and off
     key 'command+z', => @props.undoScore() and off
     key 'command+x', => @props.undoRound() and off
     key 'command+enter', => FinishGame.submitResults(@props.finishGameState) and off
-
-    for num, points of @pointKeys
-      key "#{num}", @scoreKey
-      key "command+#{num}", @scoreKey
-      key("command+shift+#{num}", @scoreKey) unless points is 25
+    $(document).on 'keydown', @scoreKey

   scoreKey: (e) ->
-    console.log e
-    console.log @props.scoreboard
     if @pointKeys[e.key]
       points = @pointKeys[e.key]
       if @isTriple e
@@ -34,11 +28,11 @@
       return off
     on

-  isTriple: (e) -> e.metaKey and e.shiftKey
+  isTriple: (e) -> e.metaKey and e.ctrlKey
   isDouble: (e) -> e.metaKey

   render: ->
-    div {id: 'shortcuts'},
+    div {id: 'shortcuts', onKeyPress: @scoreKey},
       hr {}
       dl {className: 'dl-horizontal'},
         dt {}, '5 - 0'
@@ -50,7 +44,7 @@
         dt {}, '⌘+point'
         dd {}, 'Score double'

-        dt {}, '⌘+⇧+point'
+        dt {}, '⌘+⌃+point'
         dd {}, 'Score triple'

         dt {}, 'Space'


made clear what parameters we need

made clear what parameters we need

and removed outdated comments

--- js-library/app/js/classes/speed_meter.coffee
+++ js-library/app/js/classes/speed_meter.coffee
@@ -1,25 +1,24 @@
 # This class determines whether a certain value is moving too fast
-#
-# Too fast is defined as larger than speeding.
-#
-# We assume that averaging over the last few values gives the best
-# value for speed (to allow for slow-changing discrete values, which
-# would otherwise spike a lot)
 class Factlink.Speedmeter
   constructor: (@options)->
+    unless @options.on_change
+      throw "Method for handling speed changes missing"
+    unless @options.is_fast_treshold
+      throw "Treshold for when to consider speed fast is missing"
+    unless @options.get_measure
+      throw "Function to measure with mising"

   measure:  =>
     @value = @options.get_measure()
     @time = new Date().getTime()

-  evaluate_current_state: =>
+  determine_speed: =>
     last_time = @time
     last_value = @value
     @measure()
     current_speed = Math.abs((@value - last_value)/(@time-last_time))
     @speed = @smooth_with_last_speed(current_speed)
-    @is_fast = @speed > @options.speeding
-    @options.on_change(@is_fast)
+    @is_fast = @speed > @options.is_fast_treshold

   # This smoothes the new measurement with the previous measurement
   # this way we prevent that a small period of slow scrolling breaks


Fix login problems on firefox

Fix login problems on firefox

--- js/libs/github.coffee
+++ js/libs/github.coffee
@@ -18,6 +18,6 @@
     scope = scope.join ","
     scope = encodeURIComponent(scope)

-    redirect = "#{window.location.origin}/auth/github?return-to=#{destination}"
+    redirect = "#{window.location.href}/auth/github?return-to=#{destination}"
     redirect = encodeURIComponent(redirect)
     "#{path}?client_id=#{client_id}&redirect_uri=#{redirect}&scope=#{scope}"


Added more comments

Added more comments

--- lib/pawn-build.coffee
+++ lib/pawn-build.coffee
@@ -7,11 +7,15 @@
   messages: null
   subscriptions: null

+  # Configuration options
   config:
+    # Path to pawncc executable
     pawnExecutablePath:
       title: 'Path to pawncc executable'
       type: 'string'
       default: 'C:\\pawn\\pawncc.exe'
+
+    # List of pawncc options (passed before filename)
     pawnOptions:
       title: 'Pawn options'
       type: 'array'
@@ -20,13 +24,12 @@
         type: 'string'

   activate: (state) ->
-    # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+    # Activate package
     @subscriptions = new CompositeDisposable
-
-    # Register command that toggles this view
     @subscriptions.add atom.commands.add 'atom-workspace', 'pawn-build:build': => @build()

   deactivate: ->
+    # Deactivate package
     @messages?.detatch()
     @subscriptions.dispose()
     @pawnBuildView.destroy()
@@ -34,6 +37,7 @@
   serialize: -> undefined

   createOutputPanel: ->
+    # Create output panel (or return if already exists)
     unless @messages?
       @messages = new MessagePanelView
         title: 'Pawn output'
@@ -42,32 +46,32 @@
     return @messages

   build: ->
+    # get path of currently open file
     filepath = atom.workspace.getActiveTextEditor()?.getPath()

-    # ignore command if current file is not saved, or is not a .pwn file
+    # only continue if file is a .pwn file
     return unless filepath? and path.extname(filepath) == '.pwn'

-    # prepare command
+    # prepare command and arguments from package settigns
     cmd = atom.config.get('pawn-build.pawnExecutablePath')
-
-    # prepare arguments
     args = atom.config.get('pawn-build.pawnOptions')
     args.push path.basename filepath

-    # run pawn
+    # run pawncc command
     process = child_process.spawn cmd, args,
-      cwd: path.dirname filepath
+      cwd: path.dirname filepath # run in the current file's directory

-    procout = []
-    hasError = false
+    procout = []       # store output from pawncc
+    hasError = false   # store error status

+    # read output from pawncc
     process.stdout.on 'data', (data) ->
       procout.push data.toString()
-
     process.stderr.on 'data', (data) ->
       procout.push data.toString()

     process.on 'error', (error) =>
+      # show message if pawncc failed to run (for instance due to invalid path)
       hasError = true
       output = @createOutputPanel()
       output.add new PlainMessageView
@@ -80,10 +84,12 @@
       output = @createOutputPanel()

       if data
+        # show output of pawncc
         lines = data.split('\n')
         for line in lines
           output.add new PlainMessageView
             message: line
       else
+        # show error message if pawncc returned no output (unknown error)
         output.add new PlainMessageView
           message: 'Could not run pawncc: Unknown error (' + exitCode + ')'


Pointless example is pointless

Pointless example is pointless

--- lib/presenters.coffee
+++ lib/presenters.coffee
@@ -9,8 +9,7 @@
     @formatPrediction(prediction) for prediction in @predictions

   formatPrediction: (prediction) ->
-    # Example implimentation
-    "#{prediction.stop.stopName}: #{prediction.prediction.minutes}"
+    throw "Method not implemented: #formatPrediction"

 class WebPresenter extends Presenter
   formatPrediction: (prediction) ->


refactored datatable so that we can use load() to inialize everything

--- src/coffee/data_model.coffee
+++ src/coffee/data_model.coffee
@@ -19,7 +19,7 @@
   default_view : DataTableView
   convert_raw_data : (arraydata) ->
     #converts raw data from blaze into object array data source data,
-    # raw : {'data' : [[1,2,3],[2,3,4]] #2d array, 'cols' : ['a', 'b', 'c']}
+    # raw : {'data' : [[1,2,3],[2,3,4]] #2d array, 'colnames' : ['a', 'b', 'c']}
     # converted : [{'a': 1, 'b' : 2, 'c' :3}, {'a' : 2, 'b' : 3, 'c': 4}]
     transformed = []
     for row in arraydata['data']
@@ -32,16 +32,17 @@

   load : (offset) ->
     slice = [offset, offset + @get('chunksize')]
-    $.get("/data" + @get('url'),
-        data_slice : JSON.stringify(slice)
-      ,
-        (data) =>
-          @set(
-            data_slice : slice
-          )
-          transformed = @convert_raw_data(JSON.parse(data))
-          @get_ref('data_source').set('data', transformed)
-          return null
+    $.get("/data" + @get('url'), {data_slice : JSON.stringify(slice)},
+      (data) =>
+        arraydata = JSON.parse(data)
+        transformed = @convert_raw_data(arraydata)
+        @set(
+          data_slice : slice
+          columns : arraydata['colnames']
+          total_rows: arraydata['shape'][0]
+        )
+        @get_ref('data_source').set('data', transformed)
+        return null
     )
 class DataTables extends Backbone.Collection
   model : DataTable


new rules for bonus moves and extra points

--- src/coffeescript/game.coffee
+++ src/coffeescript/game.coffee
@@ -536,7 +536,7 @@

             onFinish: =>
                 if lastMove
-                    @lowerFields()
+                    @checkEmptyLines()
                 else
                     @moveToNextField(nextField)

@@ -544,7 +544,8 @@

         tween.play()

-    lowerFields: () ->
+    checkEmptyLines: () ->
+        # count empty rows and columns and give extra points
         for x in [0..@board.size-1]
             isEmptyColumn = true
             for y in [0..@board.size-1]
@@ -552,7 +553,7 @@
                     isEmptyColumn = false

             if isEmptyColumn
-                @movePoints += 10
+                @movePoints += @board.size * 10

         for y in [0..@board.size-1]
             isEmptyRow = true
@@ -561,8 +562,11 @@
                     isEmptyRow = false

             if isEmptyRow
-                @movePoints += 10
-
+                @movePoints += @board.size * 10
+
+        @lowerFields()
+
+    lowerFields: () ->
         # lower fields (gravity)
         tweens = []
         for y in [@board.size-2..0]
@@ -636,7 +640,7 @@

         # threshold depends on current score, so more points you have -> longer path
         # you have to create to get moves bonus
-        threshold = Math.floor(@level.score / (@board.size*20)) + @board.size - 1
+        threshold = Math.floor(@level.score / (5*@board.size*@board.size)) + @board.size - 1
         if @moveLength >= threshold
             @level.movesDiff = @moveLength - threshold
             @level.moves += @level.movesDiff


Add ImageChooser and wrap <form> tags

Add ImageChooser and wrap <form> tags

--- src/components/exercise.cjsx
+++ src/components/exercise.cjsx
@@ -5,7 +5,7 @@
 Question = require './question'
 Preview = require './preview'
 {ExerciseActions, ExerciseStore} = require '../stores/exercise'
-
+ImageChooser = require './image-chooser'
 {ArbitraryHtmlAndMath} = require 'openstax-react-components'
 AsyncButton = require 'openstax-react-components/src/components/buttons/async-button.cjsx'

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

   saveExercise: ->
     if confirm('Are you sure you want to save?')
+
       ExerciseActions.save(@props.id)

   publishExercise: ->
@@ -78,6 +79,7 @@
         Exercise Stimulus
         <textarea onChange={@updateStimulus} defaultValue={ExerciseStore.getStimulus(id)}>
         </textarea>
+        <ImageChooser exerciseId={id} />

       {questions}

@@ -128,7 +130,7 @@

         {publishedLabel}
         {editLink}
-        {form}
+        <form>{form}</form>
       </BS.Col><BS.Col xs={6} className="pull-right">
         {preview}
       </BS.Col></BS.Row>