Add first draft of price and inventory updater

--- src/coffee/import/productupdateimport.coffee
+++ src/coffee/import/productupdateimport.coffee
@@ -2,22 +2,318 @@
 Q = require 'q'
 _ = require('underscore')._
 _s = require 'underscore.string'
+utils = require '../../lib/utils'
+{InventorySync} = require('sphere-node-sync')
+{Rest} = require('sphere-node-connect')
+ProductImport = require("../import/productimport")

 ###
-TODO: Add class description/comment.
+Imports Brickfox product stock and price updates into Sphere.
 ###
 class ProductUpdateImport

   constructor: (@_options = {}) ->
-    this
-
-  ###
-  TODO: Add method description/comment.
-  @param {function} callback The callback function to be invoked when the method finished its work.
-  @return Result of the given callback
+    throw new Error 'XML source path is required' unless @_options.products
+    throw new Error 'Product import attributes mapping (Brickfox -> SPHERE) file path is required' unless @_options.mapping
+    @inventorySync = new InventorySync @_options
+    @rest = new Rest @_options
+    @productImport = new ProductImport @_options
+    @logger = @_options.appLogger
+    @successCounter = 0
+    @failCounter = 0
+    @success = false
+
+  ###
+  # Reads given product import XML file and creates/updates/deletes product prices and stock / inventories
+  # Detailed steps:
+  # 1) Process import data
+  #
+  # 2) Fetch products to update
+  #
+  # 3) Build product update actions for prices
+  # 3.1) Send product update actions for prices
+  #
+  # 4) Fetch inventories for variant to be updated
+  #
+  # 5) Build inventory creates
+  # 5.1) Build inventory updates
+  # 5.2) Send new inventory creates and updates
+  #
+  # @param {function} callback The callback function to be invoked when the method finished its work.
+  # @return Result of the given callback
   ###
   execute: (callback) ->
-    console.log "ProductUpdateImport executed..."
-    callback true
-
-module.exports = ProductUpdateImport.execute
+    @startTime = new Date().getTime()
+    @logger.info '[ProductsUpdate] ProductUpdateImport execution started.'
+
+    Q.spread [
+      @_loadMappings @_options.mapping
+      @_loadProductsXML @_options.products
+      ],
+      (mappings, productsXML) =>
+        utils.assertProductIdMappingIsDefined mappings
+        utils.assertSkuMappingIsDefined mappings
+        @toBeImported = _.size(productsXML.Products?.ProductUpdate)
+        newProducts = @_processProductUpdatesData(productsXML, mappings)
+        @productExternalIdMapping = mappings.ProductId.to
+        productIds = @_getProductExternalIDs(newProducts, @productExternalIdMapping) if newProducts
+        @newVariants = @_transformToVariantsBySku(newProducts)
+        @logger.info "[ProductsUpdate] Product updates count: #{_.size productIds}"
+        utils.batch(_.map(productIds, (id) => @_queryProductsByExternProductId(id, @productExternalIdMapping)), 100) if productIds
+    .then (fetchedProducts) =>
+      @logger.info "[ProductsUpdate] Fetched products to update count: #{_.size fetchedProducts}"
+      priceUpdates = @_buildPriceUpdates(fetchedProducts, @newVariants, @productExternalIdMapping) if fetchedProducts
+      @logger.info "[ProductsUpdate] Product price updates count: #{_.size priceUpdates}"
+      utils.batch(_.map(priceUpdates, (p) => @productImport.updateProduct p), 100) if priceUpdates
+    .then (priceUpdatesResult) =>
+      @priceUpdatedCount = _.size priceUpdatesResult
+      skus = _.keys(@newVariants)
+      @logger.info "[ProductsUpdate] Inventories to fetch: #{_.size skus}"
+      utils.batch(_.map(skus, (sku) => @_queryInventoriesBySku sku), 100) if skus
+    .then (fetchedInventories) =>
+      @logger.info "[ProductsUpdate] Fetched inventories count: #{_.size fetchedInventories}"
+      createInventoryForSkus = []
+      updateInventoryItems = []
+      _.each fetchedInventories, (i) -> if _.isObject(i) then updateInventoryItems.push(i) else createInventoryForSkus.push(i)
+      inventoryCreates = @_buildInventoryCreates(createInventoryForSkus, @newVariants) if _.size(createInventoryForSkus) > 0
+      inventoryUpdates = @_buildInventoryUpdates(updateInventoryItems, @newVariants) if _.size(updateInventoryItems) > 0
+      promises = []
+      promises.push utils.batch(_.map(inventoryUpdates, (inventory) => @_updateInventory(inventory.newObj, inventory.oldObj)), 100) if inventoryUpdates
+      promises.push utils.batch(_.map(inventoryCreates, (inventory) => @_createInventory inventory), 100) if inventoryCreates
+      promises
+    .spread (updateInventoryResult, createInventoryResult) =>
+      @inventoriesCreated = _.size(createInventoryResult)
+      @inventoriesUpdated = _.size(_.filter updateInventoryResult, (r) -> r is 200)
+    .fail (error) =>
+      @logger.error "Error on execute method; #{error}"
+      @logger.error "Error stack: #{error.stack}" if error.stack
+      @_processResult(callback)
+    .done (result) =>
+      @success = true
+      @_processResult(callback)
+
+  _processResult: (callback) ->
+    endTime = new Date().getTime()
+    result = if @success then 'SUCCESS' else 'ERROR'
+    @logger.info """[ProductsUpdate] ProductUpdateImport finished with result: #{result}.
+                    [ProductsUpdate] Price updated for #{@priceUpdatedCount} out of #{@toBeImported} products.
+                    [ProductsUpdate] Inventories created: '#{@inventoriesCreated}'
+                    [ProductsUpdate] Inventories updated: '#{@inventoriesUpdated}'
+                    [ProductsUpdate] Processing time: #{(endTime - @startTime) / 1000} seconds."""
+    callback @success
+
+  _loadMappings: (path) ->
+    utils.readFile(path)
+
+  _loadProductsXML: (path) ->
+    utils.xmlToJson(path)
+
+  _processProductUpdatesData: (data, mappings) =>
+    extendedMappings = _.extend _.clone(mappings),
+      # extend mapping to ensure that stock information will be processed and included into variant information too
+      Stock:
+        target: "variant"
+        type: "text"
+        to: "tempstock"
+    products = @productImport.buildProducts(data.Products?.ProductUpdate, null, null, extendedMappings)
+
+    if(_.size products) > 0
+      products
+    else
+      null
+
+  _transformToVariantsBySku: (products) ->
+    map = {}
+    _.each products, (p) =>
+      @_addUniqueMapEntry(map, p.masterVariant.sku, p.masterVariant)
+      _.each p.variants, (v) =>
+        @_addUniqueMapEntry(map, v.sku, v)
+    map
+
+  _addUniqueMapEntry: (map, id, object) ->
+    if _.has(map, id)
+      throw new Error "Error on map creation. Map already contains an entry with unique id: '#{id}'. Existing value: \n #{map[id]} \n\n New value: #{object}"
+    else
+      map[id] = object
+
+  _getProductExternalIDs: (products, productExternalIdMapping) ->
+    productIDs = []
+    _.each products, (p) ->
+      _.each p.masterVariant.attributes, (att) ->
+        if att.name is productExternalIdMapping
+          productIDs.push att.value
+    productIDs
+
+  ###
+  # Queries asynchronously for products with given Brickfox product reference id.
+  #
+  # @param {String} id External product id attribute value
+  # @param {String} productExternalIdMapping External product id attribute name
+  # @return {Object} If success returns promise with response body otherwise rejects with error message
+  ###
+  _queryProductsByExternProductId: (id, productExternalIdMapping) ->
+    deferred = Q.defer()
+    predicate = "masterVariant(attributes(name=\"#{productExternalIdMapping}\" and value=#{id}))"
+    query = "/product-projections?where=#{encodeURIComponent(predicate)}&staged=true"
+    @rest.GET query, (error, response, body) ->
+      if error
+        deferred.reject error
+      else
+        if response.statusCode isnt 200
+          message = "Error on query products by extern ProductId; \n Predicate: #{predicate} \n\n GET query: \n #{query} \n\n Response body: '#{utils.pretty body}'"
+          deferred.reject message
+        else
+          customResponse = {}
+          customResponse[productExternalIdMapping] = id
+          customResponse.body = body
+          deferred.resolve customResponse
+    deferred.promise
+
+  ###
+  # Queries inventories by SKU.
+  #
+  # @param {String} sku SKU value
+  # @return {Object} If success returns promise with response body otherwise rejects with error message
+  ###
+  _queryInventoriesBySku: (sku) ->
+    deferred = Q.defer()
+    predicate = "sku=#{sku}"
+    query = "/inventory?where=#{encodeURIComponent(predicate)}"
+    @rest.GET query, (error, response, body) ->
+      if error
+        deferred.reject error
+      else
+        if response.statusCode isnt 200
+          message = "Error on query inventories by sku; \n Predicate: #{predicate} \n\n GET query: \n #{query} \n\n Response body: '#{utils.pretty body}'"
+          deferred.reject message
+        else
+          if body.results[0]
+            deferred.resolve body.results[0]
+          else
+            deferred.resolve sku
+    deferred.promise
+
+  ###
+  # Creates asynchronously inventory in Sphere.
+  #
+  # @param {Object} payload Create inventory request as JSON
+  # @return {Object} If success returns promise with success message otherwise rejects with error message
+  ###
+  _createInventory: (payload) ->
+    deferred = Q.defer()
+    @rest.POST '/inventory', payload, (error, response, body) ->
+      if error
+        deferred.reject "HTTP error on new inventory creation; Error: #{error}; Request body: \n #{utils.pretty payload} \n\n Response body: '#{utils.pretty body}'"
+      else
+        if response.statusCode isnt 201
+          message = "Error on new inventory creation; Request body: \n #{utils.pretty payload} \n\n Response body: '#{utils.pretty body}'"
+          deferred.reject message
+        else
+          message = 'New inventory created.'
+          deferred.resolve message
+    deferred.promise
+
+  ###
+  # Updates asynchronously inventory in Sphere.
+  #
+  # @param {Object} payload
+  # @return {Object} If success returns promise with success message otherwise rejects with error message
+  ###
+  _updateInventory: (new_obj, old_obj) ->
+    deferred = Q.defer()
+    @inventorySync.buildActions(new_obj, old_obj).update (error, response, body) ->
+      if error
+        deferred.reject "HTTP error on inventory update; Error: #{error}; \nNew object: \n#{utils.pretty new_obj} \nOld object: \n#{utils.pretty old_obj} \n\nResponse body: '#{utils.pretty body}'"
+      else
+        if response.statusCode is 200
+          deferred.resolve 200
+        else if response.statusCode is 304
+          # Inventory entry update not neccessary
+          deferred.resolve 304
+        else
+          message = "Error on inventory update; New object: \n#{utils.pretty new_obj} \nOld object: \n#{utils.pretty old_obj} \n\nResponse body: '#{utils.pretty body}'"
+          deferred.reject message
+    deferred.promise
+
+
+  _buildPriceUpdates: (oldProducts, newVariants, productExternalIdMapping) ->
+    updates = []
+    _.each oldProducts, (p) ->
+      actions = []
+      results = _.size(p.body.results)
+      externId = p[productExternalIdMapping]
+
+      if results > 1
+        throw new Error "Make sure '#{productExternalIdMapping}' product type attribute is unique accross all products in SPHERE. #{results} products with same value: '#{externId}' found."
+
+      product = p.body.results[0]
+
+      variantId = product.masterVariant.id
+      _.each product.masterVariant.prices, (price) ->
+        # remove old price for master variant
+        action = utils.buildRemovePriceAction(price, variantId)
+        actions.push action
+        # create new price for master variant
+        newVariant = newVariants[product.masterVariant.sku]
+        _.each newVariant.prices, (newPrice) ->
+          action = utils.buildAddPriceAction(newPrice, variantId)
+          actions.push action
+
+      _.each product.variants, (v) ->
+        variantId = v.id
+        _.each v.prices, (price) ->
+          # remove old price for variant
+          action = utils.buildRemovePriceAction(price, variantId)
+          actions.push action
+          # create new price for variant
+          newVariant = newVariants[v.sku]
+          _.each newVariant.prices, (newPrice) ->
+            action = utils.buildAddPriceAction(newPrice, variantId)
+            actions.push action
+
+      # this will sort the actions ranked in asc order (first 'remove' then 'add')
+      actions = _.sortBy actions, (a) -> a.action is 'addPrice'
+
+      wrapper =
+        id: product.id
+        payload:
+          version: product.version
+          actions: actions
+      updates.push wrapper
+    updates
+
+  _buildInventoryCreates: (skus, newVariants) ->
+    creates = []
+    _.each skus, (sku) ->
+      variant = newVariants[sku]
+      stock = variant.tempstock
+      inventory =
+        sku: sku
+        quantityOnStock: _s.toNumber(stock)
+      creates.push inventory
+
+    count = _.size(creates)
+    if count > 0
+      creates
+    else
+      null
+
+  _buildInventoryUpdates: (oldInventories, newVariants) ->
+    updates = []
+    _.each oldInventories, (old) ->
+      variant = newVariants[old.sku]
+      stock = variant.tempstock
+      pair =
+        newObj:
+          quantityOnStock: _s.toNumber(stock)
+        oldObj: old
+
+      updates.push pair
+
+    count = _.size(updates)
+    if count > 0
+      updates
+    else
+      null
+
+module.exports = ProductUpdateImport


form workflow: WIP a few fair tests

form workflow: WIP a few fair tests

--- client/app/CommonViews/formworkflow.coffee
+++ client/app/CommonViews/formworkflow.coffee
@@ -1,4 +1,111 @@
 class FormWorkflow extends KDView
+
+  make = ->
+    fw = new FormWorkflow
+    fw.on 'DataCollected', -> log arguments
+    fw
+
+  assert = (truth) ->
+    unless !!truth
+
+
+      { stack } = new Error
+
+      debugger
+
+      throw {
+        message: "Assertion error!", stack
+      }
+
+  tests = [
+    ->
+      @requireData [
+        'foo'
+        'bar'
+      ]
+
+      @collectData { foo: 42 }
+
+      assert not @isSatisfied()
+
+      @collectData { bar: 0 }
+
+      assert @isSatisfied()
+
+    ->
+      @requireData @all(
+        'foo'
+        'bar'
+      )
+
+      @collectData { foo: 42 }
+
+      assert not @isSatisfied()
+
+      @collectData { bar: 0 }
+
+      assert @isSatisfied()
+
+    ->
+      @requireData @any(
+        'foo'
+        'bar'
+      )
+
+      @collectData { foo: 42 }
+
+      assert @isSatisfied()
+
+      @collectData { bar: 0 }
+
+      assert @isSatisfied()
+
+      @clearData 'foo'
+
+      assert @isSatisfied()
+
+      @clearData 'bar'
+
+      assert not @isSatisfied()
+    ->
+      @requireData @all(
+        'foo'
+        'bar'
+        @any('baz', 'qux')
+      )
+
+      @collectData { foo: 42 }
+
+      assert not @isSatisfied()
+
+      @collectData { bar: 0 }
+
+      assert not @isSatisfied()
+
+      @collectData { baz: 16 }
+
+      assert @isSatisfied()
+
+      @clearData 'baz'
+
+      assert not @isSatisfied()
+
+      @collectData { qux: -88 }
+
+      assert @isSatisfied()
+
+      @clearData 'bar'
+
+      assert not @isSatisfied()
+
+
+  ]
+
+  @runTests = ->
+    test.call make()  for test in tests
+
+    return this
+

   constructor: (options = {}, data) ->
     super options, data
@@ -17,6 +124,11 @@
     @collector.addRequirement gate

     return this
+
+  getData: -> @data
+
+  isSatisfied: ->
+    @collector.gate.isSatisfied()

   collectData: (data) ->
     @collector.collectData data
@@ -78,7 +190,7 @@
   all: (fields...) -> new All fields
   any: (fields...) -> new Any fields

-  class Collector extends KDEventEmitter
+  @Collector = class Collector extends KDEventEmitter

     constructor: (@gate = new Gate) ->
       super
@@ -86,12 +198,12 @@
       @gate.on 'report', (state) => switch state
         when 'Satisfied'
           @emit 'DataCollected', @data
-        when 'Dissatisfied' then #ignore
+        when 'Dissatisfied' then # ignore

     addRequirement: (requirement) ->
       @gate.addField requirement

-    getData: -> @gate.getData()
+    getData: -> @data

     collectData: (data) ->
       @defineKey key, val  for own key, val of data
@@ -104,7 +216,7 @@
       @data[key] = value
       @gate.satisfy key

-  class Satisfier extends KDEventEmitter
+  @Satisfier = class Satisfier extends KDEventEmitter

     isSatisfied: -> @satisfied ?= no

@@ -153,8 +265,8 @@
           when 'Satisfied'
             @emit 'RequirementSatisfied', field
             satisfier.satisfy()
-          when 'Canceled'
-            satisfier.cancel()
+          when 'Canceled' then # ignore
+            # satisfier.cancel()

       return this

@@ -187,18 +299,18 @@

       return this

-    signal: -> @compliment no
+    kill: -> @compliment no

     compliment: (value) -> value

     isSatisfied: ->
       for own _, field of @fields
-        return @signal()  unless @compliment field.isSatisfied()
+        return @kill()  unless @compliment field.isSatisfied()

       for child in @children
-        return @signal()  unless @compliment child.isSatisfied()
-
-      return !@signal()
+        return @kill()  unless @compliment child.isSatisfied()
+
+      return !@kill()

     toString: -> "gate-#{@id}"

@@ -212,4 +324,4 @@
     compliment: (value) -> !value

     # Any#createSatisfier returns a singleton satisfier :)
-    createSatisfier: -> @safisfier ?= super()
+    createSatisfier: -> @satisfier ?= super()


Tests: Environments: Domain: Added domain check to not create another domain.

--- client/test/lib/environments/domain.coffee
+++ client/test/lib/environments/domain.coffee
@@ -40,6 +40,18 @@
     user = helpers.beginTest(browser)
     helpers.waitForVMRunning(browser)

+    environmentHelpers.openDomainSettings(browser)
+
+    browser.elements 'css selector', domainSelector, (result) =>
+      if result.value.length > 1
+        console.log result.value
+
+        console.log '✔  Another domain is already added. Ending test...'
+        browser.end()
+      else
+        console.log '✔  Creating a new domain...'
+        environmentHelpers.addDomain(browser, user)
+        browser.end()

   deleteDomain: (browser) ->


Quick-fix some image paths

Quick-fix some image paths

--- app/pages/lab/how-to-page.cjsx
+++ app/pages/lab/how-to-page.cjsx
@@ -12,17 +12,17 @@
       This tutorial will help walk you through the process, using Kitteh Zoo as an example.
       You can [explore the actual project](https://www.zooniverse.org/projects/vrooje/kitteh-zoo).

-      [![Kitteh Zoo screenshot](./assets/how-to-lab/how-to-1.jpg)](./assets/how-to-lab/how-to-1.jpg)
+      [![Kitteh Zoo screenshot](/assets/how-to-lab/how-to-1.jpg)](/assets/how-to-lab/how-to-1.jpg)

       ### Getting Started

       To get started building, go to [the Project Builder home page](/lab) and log in to your Zooniverse account, then click the "Build a Project" button in the top right. Here you can see all of the projects you own and collaborate on. Click on "Create a project" to start building.

-      [![Project Builder screenshot](./assets/how-to-lab/how-to-2.jpg)](./assets/how-to-lab/how-to-2.jpg)
+      [![Project Builder screenshot](/assets/how-to-lab/how-to-2.jpg)](/assets/how-to-lab/how-to-2.jpg)

       **Start building:** Now you're in the Project Builder itself. This is where the magic happens. On the left-hand side, you've got your main menus: Project, Workflow, and Subjects. These are terms you'll see a lot, and they have specific meanings in the Zooniverse.  Project is pretty self-explanatory; Galaxy Zoo, Penguin Watch, and of course, Kitteh Zoo, are all examples of Zooniverse projects that you could build using the project builder. A workflow is the sequence of tasks that you ask volunteers to do, and subjects are the things (usually images) that volunteers do those tasks on.

-      [![Project details editor screenshot](./assets/how-to-lab/how-to-3.jpg)](./assets/how-to-lab/how-to-3.jpg)
+      [![Project details editor screenshot](/assets/how-to-lab/how-to-3.jpg)](/assets/how-to-lab/how-to-3.jpg)

       ### Define your project.

@@ -32,15 +32,15 @@

       This is where you build the tasks that volunteers actually do. When you first get to this page, you'll see there is a sample task (specifically a question) already in place.

-      [![Project workflow editor screenshot](./assets/how-to-lab/how-to-4.jpg)](./assets/how-to-lab/how-to-4.jpg)
+      [![Project workflow editor screenshot](/assets/how-to-lab/how-to-4.jpg)](/assets/how-to-lab/how-to-4.jpg)

       We want to create this:

-      [![Project workflow example](./assets/how-to-lab/how-to-5.jpg)](./assets/how-to-lab/how-to-5.jpg)
+      [![Project workflow example](/assets/how-to-lab/how-to-5.jpg)](/assets/how-to-lab/how-to-5.jpg)

       We'll start by replacing the sample text with our question, which asks people how many cats are in the image. We add more answers using the "+" button under the "Yes" answer. Use the screenshot below to fill in the workflow details (you may need to zoom in!)

-      [![Project workflow editing screenshot](./assets/how-to-lab/how-to-6.jpg)](./assets/how-to-lab/how-to-6.jpg)
+      [![Project workflow editing screenshot](/assets/how-to-lab/how-to-6.jpg)](/assets/how-to-lab/how-to-6.jpg)

       We added both text and images into the _Help Text_ box using the markdown language ([learn more about markdown](http://markdownlivepreview.com)).

@@ -67,13 +67,13 @@

       Now we want to draw circles around the cat's faces and mark a point on their tails. Why? Because we can. (For your own project you'd obviously want to think carefully about the reasons for adding tasks to a workflow, and what you want to get from the answers/marks.)

-      [![Project workflow task editing screenshot](./assets/how-to-lab/how-to-7.jpg)](./assets/how-to-lab/how-to-7.jpg)
+      [![Project workflow task editing screenshot](/assets/how-to-lab/how-to-7.jpg)](/assets/how-to-lab/how-to-7.jpg)

       So under the _Task_ list, we'll click on **drawing**. We're asking folks to draw (with ellipses) around the cats' faces, as well as mark their tail tips with a point. We've changed the color on the Cattail points so they stand out more too. As usual, the main text gives people basic instructions on what we want, and the help text provides some more explanation on how to do the task.

       In addition to marking all the cat faces, we want to know just how cute they are. So every time someone marks a cat-face, we've added a pop-up question to ask just that. Add this question by clicking on the _sub-tasks_ button below the _Type_ and _Color_ task specifications.

-      [![Project workflow task details editing screenshot](./assets/how-to-lab/how-to-8.jpg)](./assets/how-to-lab/how-to-8.jpg)
+      [![Project workflow task details editing screenshot](/assets/how-to-lab/how-to-8.jpg)](/assets/how-to-lab/how-to-8.jpg)

       When building your own project, you can combine any number of tasks in any order. You can start with a drawing task instead of a question. You can add sub-tasks for any drawing tool you make.

@@ -85,19 +85,19 @@

       The first question, "How many cats are in this image?" only allows one answer, so you can specify the next task depending on the answer.  If folks say "None" for the number of cats, the classification ends. But if they say there's at least one cat, then they go on to the next question.

-      [![Project workflow task editing screenshot](./assets/how-to-lab/how-to-9.jpg)](./assets/how-to-lab/how-to-9.jpg)
+      [![Project workflow task editing screenshot](/assets/how-to-lab/how-to-9.jpg)](/assets/how-to-lab/how-to-9.jpg)

       ### Upload subjects

       To really get started building a project, you need images to work with. Normally you would add your own images by clicking on the "New Subject Set" button on the left hand side of the screen. This is one of the trickier steps in project creation -- for the purposes of this tutorial you can simply copy the Kitteh Zoo subject set, but check out the next section "Uploading subjects -- the nitty gritty" if you want to practice the full approach. To do this go to the workflow you created and under the _associated subject_ set section click on _add an example subject set_. You should now see the 'kittehs' subject set selected.

-      [![Project workflow task editing screenshot](./assets/how-to-lab/how-to-10.jpg)](./assets/how-to-lab/how-to-10.jpg)
+      [![Project workflow task editing screenshot](/assets/how-to-lab/how-to-10.jpg)](/assets/how-to-lab/how-to-10.jpg)

       **CONGRATULATIONS!**

       You should have successfully created Kitteh Zoo! To view it, got back to the _Build a Project_ page (by clicking the button in the top right of the page) and then click on the view button next to the new project you have just made.

-      [![Project list screenshot](./assets/how-to-lab/how-to-11.jpg)](./assets/how-to-lab/how-to-11.jpg)
+      [![Project list screenshot](/assets/how-to-lab/how-to-11.jpg)](/assets/how-to-lab/how-to-11.jpg)

       ### Uploading subjects - the Nitty Gritty

@@ -159,7 +159,7 @@

       _Research Case, FAQ, Results, and Education_: These pages are where you really get to share all the cool things about your project. All of these pages use Markdown (see link above) to format text and display images.

-      [![Project additional content editor screenshot](./assets/how-to-lab/how-to-12.jpg)](./assets/how-to-lab/how-to-12.jpg)
+      [![Project additional content editor screenshot](/assets/how-to-lab/how-to-12.jpg)](/assets/how-to-lab/how-to-12.jpg)

       _Research case_: Explain your research to your audience here in as much detail as you'd like. This page displays no matter what, since explaining your motivation to volunteers is critical for the success of your project!

@@ -170,22 +170,22 @@
       _Education_: If you are a researcher open to collaborating with educators you can state that here, include educational content, and describe how you'd like to help educators use your project. Also, if your project is primarily for educational purposes you can describe that here. This page will only display if you add content to it.

       ### DETAILS - Media
-
+
       You can upload your own media to your project (such as example images for your help pages) so  you can link to it without an external host. To start uploading, drop an image into the box (or click it to bring up your file browser and select a file).
-
+
       Once the image has uploaded, it will appear above the "Add an image" box. You can then copy the markdown text beneath the image into your project, or add another image.
-
+
       Currently there is a limit of 20 uploaded images per project.
-
+
       ### DETAILS - Visibility
-
+
       This page is where you decide whether your project is public and whether it's ready to go live. For more information on the different project stages, see our [project builder policies](/lab-policies).
-
+
       ### DETAILS - Collaborators

       Add people to your team and specify what their roles are so that they have the right access to the tools they need (including access to the project before it's public).

-      [![Project collaborator editor screenshot](./assets/how-to-lab/how-to-13.jpg)](./assets/how-to-lab/how-to-13.jpg)
+      [![Project collaborator editor screenshot](/assets/how-to-lab/how-to-13.jpg)](/assets/how-to-lab/how-to-13.jpg)

       _Owner_: The owner is the original project creator. There can be only one.

@@ -204,11 +204,11 @@
       ### DETAILS - Subject sets and manifest details, a.k.a. "What is a manifest?"

       *The condensed answer:*
-
+
       A manifest is a file that tells our software how to combine the images you have into units of data (subjects) to be classified. The manifest also allows you to link your classifications back to the rest of your data. A manifest is formatted as a CSV file with 1 line per subject, with a unique identifier and the names of images to be associated with a subject on each row (with additional information often included in other fields as well). There is an example in the ["Kitteh" zip file](https://data.zooniverse.org/tutorial/kitteh_zoo.zip).

       *The full answer:*
-
+
       What we call a "manifest" is really just a plain text file with a specific format to each line.

       To understand the format, let's start with the first few lines from the Kitteh Zoo manifest:
@@ -218,7 +218,7 @@
           2,8300920648_d4a21bba59_z,Flickr,https://www.flickr.com/photos/aigle_dore/8300920648,Moyan Brenn,Creative Commons - share adapt attribute,grandfather kitteh has ear hair. a lot of it
           3,6713782851_82fc8c73e5_z.jpg,Flickr,https://www.flickr.com/photos/hellie55/6713782851,hehaden,Creative Commons - share adapt attribute,juvenile kittehs practice break-in at the catnip factory

-      The first line of the file is a header line that specifies the name of each of the manifest fields. In this case, our manifest has 7 fields (or columns), called "subject\_id", "image\_name", "origin", "link", "attribution", "license" and “#secret\_description”. They are separated by commas: this is what's known as a "comma separated values" file, or CSV file.
+      The first line of the file is a header line that specifies the name of each of the manifest fields. In this case, our manifest has 7 fields (or columns), called "subject\_id", "image\_name", "origin", "link", "attribution", "license" and “#secret\_description”. They are separated by commas: this is what's known as a "comma separated values" file, or CSV file.

       After the first line, each row of the file contains information about 1 subject. The first field, corresponding with the "subject\_id" header, is a unique number that identifies the subject. The second field, which aligns with the "image\_name" header, contains the name of the image that's associated with that subject. These 2 fields are critically important: the image name is obviously important, and a unique identifier is important for matching your classifications to the rest of your data.


Another hotfix for people having preferredLanguage set to null. It's preventing people from subscribing because they can't put a User object that does not validate. Still can't repro, but it's happened to a lot of people and continues to happen to new people signing up.

--- app/schemas/models/user.coffee
+++ app/schemas/models/user.coffee
@@ -103,7 +103,7 @@
   emailHash: {type: 'string'}

   #Internationalization stuff
-  preferredLanguage: {type: 'string', 'enum': c.getLanguageCodeArray()}
+  preferredLanguage: {'enum': [null].concat(c.getLanguageCodeArray())}

   signedCLA: c.date({title: 'Date Signed the CLA'})
   wizard: c.object {},


fix redirect

fix redirect

--- app/scripts/controllers.coffee
+++ app/scripts/controllers.coffee
@@ -43,12 +43,17 @@
   $scope.letter = $state.params.letter || "a"
   $scope.artworks = Artworks.getList({title__istartswith: $scope.letter}).$object
   $scope.alphabet = "abcdefghijklmnopqrstuvwxyz".split("")
-  console.debug("yo")
 )

-.controller('SchoolController', ($scope, Promotions, Students) ->
-  $scope.promotions = Promotions.getList({order_by: "-starting_year"}).$object
+.controller('SchoolController', ($scope, $state, Promotions, Students) ->
+  $scope.promotions = []
+
+  Promotions.getList({order_by: "-starting_year"}).then((promotions) ->
+    $scope.promotions = promotions
+    $state.go('school.promotion', {id: $scope.promotions[0].id})
+  )
+
 )

 .controller('PromotionController', ($scope, $stateParams, Students, Promotions) ->


Fix sort

Fix sort

--- controllers/stats_controller.coffee
+++ controllers/stats_controller.coffee
@@ -23,4 +23,4 @@
         pWeek: limit 10, scores[1]
         pMonth: limit 10, scores[2]
         pAlltime: limit 10, scores[3]
-        games: Game.find().populate('rankings.player').limit(10).sort([['date', 'descending']]).exec()
+        games: Game.find().populate('rankings.player').limit(10).sort('date', -1).exec()


added 'excuse' to pass fail dropdown in srgb

testplan
  - create a pass / fail assignment
  - in srgb verify you can set the grade of the submission
    to 'Excused'

closes CNVS-21867

Change-Id: I0a3b5b60156b191400317ef12b9ab03e7b54d903
Reviewed-on: https://gerrit.instructure.com/58349
Tested-by: Jenkins
Reviewed-by: Josh Simpson <bfb4987b134a00f73d881fff5113b1d5fa1876bc@instructure.com>
QA-Review: Jason Carter <976bfc4bed0555ff96b3266aea18e7b17a9d471c@instructure.com>
Product-Review: Josh Simpson <bfb4987b134a00f73d881fff5113b1d5fa1876bc@instructure.com>

--- app/coffeescripts/ember/screenreader_gradebook/components/grading_cell_component.coffee
+++ app/coffeescripts/ember/screenreader_gradebook/components/grading_cell_component.coffee
@@ -30,6 +30,10 @@
         {
           label: I18n.t "grade_incomplete", "Incomplete"
           value: "incomplete"
+        }
+        {
+          label: I18n.t "Excused"
+          value: 'EX'
         }
     ]


made Schedule.prototype.set_date more robust by preventing class name conflicts between event column and chag tables

--- js/mincha.coffee
+++ js/mincha.coffee
@@ -70,8 +70,9 @@
       else console.warn "This should never happen!"
     "#{name_of_chag} #{rishon_or_sheni}")
   set_date: ->
-    $(".#{@chag().split('.')[0]}").removeClass('hidden')
+    $(".#{@chag().split('.')[0]}").not('.event').removeClass('hidden')
     $(".#{@chag()} .date").html(@sunset.format('D MMM'))
+    $(".#{@chag()} .dow").html(if @hebrew_date.isShabbat() then "שַׁבָּת" else @sunset.format('dddd'))
   hadlakat_nerot_schedule: ->
     @set_date()
     $(".#{@chag()}.hadlakat-nerot .time").html(@hadlakat_nerot_text())


scale screen size

scale screen size

--- app/utils/connector-rfb.coffee
+++ app/utils/connector-rfb.coffee
@@ -25,6 +25,10 @@
       'onUpdateState': updateState
       'onXvpInit': xvpInit
     @rfb.connect ENV.deviceFarmHost, ENV.deviceFarmPort, '1234', "#{ENV.deviceFarmPath}?token=#{@deviceToken}"
+    if @rfb.get_display
+      display = @rfb.get_display
+      scaleRatio = display.autoscale screen.w, screen.h, true
+      @rfb.get_mouse().set_scale scaleRatio

   disconnect: ->
     @rfb.disconnect()


Add Time

Add Time

--- js/script.coffee
+++ js/script.coffee
@@ -4,9 +4,10 @@
 timedRefresh = (timeoutPeriod) ->
 	id = Math.floor Math.random() * screens.length
 	screen = screens[id]
-	document.querySelector('.mdber').innerHTML = "{{ site.time }}"
+	document.querySelector('.mdber').innerHTML = "#{screen.code} - {{ site.time | date_to_long_string | upcase }}"
 	document.querySelector('#upper').innerHTML = screen.name.toUpperCase() + " - #{id}/" + screens.length
-	document.querySelector('.code').innerHTML = screen.code.split('').join('  ')
+	# document.querySelector('.code').innerHTML = screen.code.split('').join('  ')
+	document.querySelector('.code').innerHTML = ('0' + new Date().getHours()).slice(-2) + ":" + ('0' + new Date().getMinutes()).slice(-2)
 	document.querySelector('#lower').innerHTML = "{% include releases.html %}" +
 		"<br>BUILD_REVISION: {{ site.github.build_revision }}"
 	document.body.style.background = screen.color


show correct count on unread discussions

fixes VICE-757
flag=none

The unshown stats were being calculated by recursively
walking the remaining items based on the presence of a
"replies" key. The unread view flattens all replies, so
there was no need to traverse the children here.

test plan:
  - create a discussion post that allows threaded
    replies (this is an option on the discussion)
  - as one user, reply to a thread 10 different
    times (all attached to the root reply)
  - as another user, visit the discussion and click
    the 'unread' button
    - at the top of the page, you should see the correct
      counts for unread/total
    - scroll to the bottom of the page
      - you should see correct counts for what is
        currently not displayed (paginated)
  - as the original user, nest a couple replies up to
    4x deep on the original reply
  - repeat the steps above as the second user, making
    sure to mark any read replies as unread (blue dot)
    - the unshown counts should be correct

qa risk: low

Change-Id: Id311a68c2d4fec1bc9bf8bc90340908c731d7c9e
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/247316
Tested-by: Service Cloud Jenkins <9144042a601061f88f1e1d7a1753ea3e2972119d@instructure.com>
Reviewed-by: Matthew Lemon <7bd6faf60934a850234395b3b97cb1fc9250fa11@instructure.com>
QA-Review: Matthew Lemon <7bd6faf60934a850234395b3b97cb1fc9250fa11@instructure.com>
Product-Review: Matthew Lemon <7bd6faf60934a850234395b3b97cb1fc9250fa11@instructure.com>

--- app/coffeescripts/views/DiscussionTopic/EntryCollectionView.coffee
+++ app/coffeescripts/views/DiscussionTopic/EntryCollectionView.coffee
@@ -142,7 +142,10 @@
     unshown = @collection.fullCollection.toJSON().slice start, end
     total = 0
     unread = 0
-    walk unshown, 'replies', (entry) ->
+    # No need to recursively traverse unshown here as
+    # the collection has already been flattened. Using
+    # undefined as the prop prevents the recursive walk
+    walk unshown, undefined, (entry) ->
       total++
       unread++ if entry.read_state is 'unread'
     {total, unread}


handle django debug exception messages

handle django debug exception messages

--- graphite.coffee
+++ graphite.coffee
@@ -15,7 +15,16 @@
       success: options.success
       error: (response) ->
         html = $.parseHTML(response.responseText).filter (n) -> n.nodeType isnt 3
-        msg = $(html[0].getElementsByTagName 'pre').text()
+        pre = $(html[0].getElementsByTagName 'pre')
+        if pre.length > 0
+          # graphite style error message in a pre
+          msg = pre.text()
+        else
+          for n in html
+            pre = n.querySelectorAll 'pre.exception_value'
+            if pre.length > 0
+              msg = pre[0].innerText
+              break
         options.error msg

   complete: (query, options) ->


deleted cruft

deleted cruft

--- src/javascript/inspector/inspector_ui.coffee
+++ src/javascript/inspector/inspector_ui.coffee
@@ -37,9 +37,7 @@
       @props.eid

     componentViews = if @state.foldOpen
-      # sc = @_sortedComponents()
-      sc = @props.components
-      sc.map((comp,cid) =>
+      @props.components.map((comp,cid) =>
         React.createElement Component, {component: comp, key: cid, inspectorConfig: @props.inspectorConfig}
       ).toList()
     else
@@ -48,28 +46,6 @@
     React.createElement 'div', {className: 'entity'},
       header,
       componentViews
-
-  # _sortedComponents: ->
-  #   console.log "_sortedComponents"
-  #   unsorted = @props.components
-  #
-  #   layout = @_componentLayout()
-  #   sorted = Immutable.OrderedMap()
-  #   layout.forEach (info,type) =>
-  #     @_findCompsByType(unsorted,type).forEach (comp,cid) ->
-  #       unsorted = unsorted.delete(cid)
-  #       sorted = sorted.set(cid,comp)
-  #
-  #   sorted.merge(unsorted)
-  #
-  # _componentLayout: ->
-  #   if @props.inspectorConfig? and @props.inspectorConfig.has('componentLayout')
-  #     @props.inspectorConfig.get('componentLayout')
-  #   else
-  #     Immutable.OrderedMap()
-  #
-  # _findCompsByType: (compsByCid, type) ->
-  #   compsByCid.filter (comp) -> comp.get('type') == type

 ReservedComponentKeys = Set.of('type','eid','cid')


Revert plan cancel code

Revert plan cancel code

--- client/landing/app/Applications/Account.kdapplication/account/accSettingsSubscriptions.coffee
+++ client/landing/app/Applications/Account.kdapplication/account/accSettingsSubscriptions.coffee
@@ -62,58 +62,6 @@

     listView = @getDelegate()

-    if data.status == 'canceled'
-      title     = "Renew Next Month"
-      iconClass = "canceled"
-    else if data.status in ['active', 'modified']
-      title     = "Don't Renew Next Month"
-      iconClass = "active"
-
-    @changePlan = new KDButtonView
-      style       : "clean-gray"
-      cssClass    : "edit-plan"
-      icon        : yes
-      iconOnly    : yes
-      iconClass   : iconClass
-      tooltip     :
-        title     : title
-        placement : "left"
-      loader      :
-        color     : "#666"
-        diameter  : 16
-      callback    : =>
-        if data.status in ['active', 'modified', 'canceled']
-          @editPlan listView, data
-          @changePlan.hideLoader()
-
-  confirmOperation: (message, cb)->
-    modal = new KDModalView
-      title        : "Warning"
-      content      : "
#{message}
"
-      height       : "auto"
-      overlay      : yes
-      buttons      :
-        Continue   :
-          loader   :
-            color  : "#ffffff"
-            diameter : 16
-          style    : "modal-clean-gray"
-          callback : ->
-            modal.destroy()
-            cb?()
-
-  editPlan: (listView, data)->
-    if data.status == 'canceled'
-      @confirmOperation 'Are you sure you want to resume your subscription?', ->
-        data.resume (err, res)->
-          unless err
-            listView.emit "reload"
-    else
-      @confirmOperation 'Are you sure you want to cancel your subscription?', ->
-        data.cancel (err, res)->
-          unless err
-            listView.emit "reload"
-
   viewAppended:->
     @setTemplate @pistachio()
     @template.update()
@@ -140,7 +88,6 @@

{{#(plan.title)}} - $#{amount}

       #{statusNotice}
-      {{> @changePlan}}

#{dateNotice}


Calculate distance to both sides

Calculate distance to both sides

Instead of always picking the right side.

--- resources/assets/coffee/_classes/line-chart.coffee
+++ resources/assets/coffee/_classes/line-chart.coffee
@@ -209,15 +209,17 @@

   positionTooltip: =>
-    d = @lookupX @options.scales.x.invert(d3.mouse(@svgHoverArea.node())[0])
-
-    return unless d
+    x = @options.scales.x.invert(d3.mouse(@svgHoverArea.node())[0])
+    i = @lookupIndexFromX x
+
+    return unless i

     # only work in mobile
     @showTooltip()
     clearTimeout @_autoHideTooltip
     @_autoHideTooltip = setTimeout @hideTooltip, 3000

+    d = if x - @data[i - 1].x <= @data[i].x - x then @data[i - 1] else @data[i]
     coords = ['x', 'y'].map (axis) => @options.scales[axis] d[axis]

     # avoids blurry positioning
@@ -240,8 +242,8 @@
         .style 'width', "#{@tooltipContainer.node().getBoundingClientRect().width * 1.2}px"

-  lookupX: (x) =>
-    @data[d3.bisector((d) => d.x).right @data, x]
+  lookupIndexFromX: (x) =>
+    d3.bisector((d) => d.x).left @data, x

   resize: =>


test properties on document body

test properties on document body

--- hexaflip.coffee
+++ hexaflip.coffee
@@ -6,14 +6,13 @@

 baseName = 'hexaFlip'
 className = baseName[0].toUpperCase() + baseName.slice 1
-testEl = document.createElement 'div'
 prefixList = ['Webkit', 'Moz', 'O', 'ms']

 prefixProp = (prop) ->
-  return prop.toLowerCase() if testEl.style[prop.toLowerCase()]?
+  return prop.toLowerCase() if document.body.style[prop.toLowerCase()]?
   for prefix in prefixList
     prefixed = prefix + prop
-    return prefixed if testEl.style[prefixed]?
+    return prefixed if document.body.style[prefixed]?
   false

 css = {}


use css map for transform property

use css map for transform property

--- hexaflip.coffee
+++ hexaflip.coffee
@@ -145,7 +145,7 @@

   _setSides: (cube) ->
-    cube.el.style[transform] = @_getTransform cube.yDelta
+    cube.el.style[css.transform] = @_getTransform cube.yDelta
     cube.offset = offset = Math.floor cube.yDelta / 90
     return if offset is cube.lastOffset
     cube.lastOffset = faceOffset = setOffset = offset
@@ -221,7 +221,7 @@
       cube.yLast -= cube.yLast % 90

     cube.el.classList.remove 'no-tween'
-    cube.el.style[transform] = @_getTransform cube.yLast
+    cube.el.style[css.transform] = @_getTransform cube.yLast

   _onTouchLeave: (e, cube) ->


Minor: remove unused variable

Minor: remove unused variable

--- src/resources-packaged/ops/javascript/orbeon/util/FiniteStateMachine.coffee
+++ src/resources-packaged/ops/javascript/orbeon/util/FiniteStateMachine.coffee
@@ -21,7 +21,6 @@
             _.each transition.events, (eventName) ->
                 events[eventName] (event) ->
                     if transition.elements?                                                                             # State if per-element
-                        elementsFromEvent = elements[transition.elements]
                         elts = elements[transition.elements] event                                                      # Get elements from event (e.g. editable inside cell for mouseover)
                         if transition.from?                                                                             # Filter elements that are in the 'from' state
                             elts = _.filter elts, (e) -> elementStateMatches transition.from, $ e


Comment.

Comment.

--- server/jobs/tumblr.coffee
+++ server/jobs/tumblr.coffee
@@ -17,6 +17,8 @@
   constructor: (data) ->
     super

+    # We throw a fatal error to stop retrying a job if settings are not
+    # available anymore, but they were in the past and a periodic job was added.
     throw new Job.FatalJobError "Tumblr settings missing" unless Meteor.settings?.tumblr?.baseHostname and Meteor.settings?.tumblr?.apiKey

   enqueueOptions: (options) =>


Remove unnecessary new Error (error handler shows generic message anyway)

--- server/models/user.coffee
+++ server/models/user.coffee
@@ -179,7 +179,7 @@

           search.on('end', (result) ->
             if result.status > 0 || !theUser?
-              deferred.reject(new Error())
+              deferred.reject()
           )
       )
   )


Fix level sort

Fix level sort

--- server/models/user.coffee
+++ server/models/user.coffee
@@ -95,7 +95,7 @@
     if a.active != b.active
         return if a.active then -1 else 1
     if a.level.current != b.level.current
-        return if a.level.current > b.level.current then -1 else 1
+        return compareLevels(a.level.current, b.level.current)
     if a.rating != b.rating
         return if a.rating > b.rating then -1 else 1
     return 0


Code cleanup

Code cleanup

--- server/routes/user.coffee
+++ server/routes/user.coffee
@@ -13,10 +13,15 @@

 YEARS = [1965..1995]

+years = []
+
 Array::getRandomElement = -> this[Math.floor(Math.random() * this.length)]

+# Initialize years
+Year.find {}, 'year', sort: { year: 1 }, (err, y) -> years = y
+
 router.get '/', (req, res, next) ->
-  bootstrapData (err, years, shows, songs) ->
+  bootstrapData (err, shows, songs) ->

     unless years && shows && songs
       return res.send 500, 'Sorry something broke, please try refreshing.'
@@ -96,17 +101,10 @@
 bootstrapData = (cb) ->
   async.waterfall [
     (callback) ->
-      async.parallel [
-        (callback) ->
-          Year.find {}, 'year', sort: { year: 1 }, callback
-        , (callback) ->
-          Year.findOne(year: YEARS.getRandomElement(), '_days year !_shows')
-            .populate('_days', '', null, sort: { date: 1 })
-            .exec callback
-      ], callback
-    , (arr, callback) ->
-      [years, year] = arr
-
+      Year.findOne(year: YEARS.getRandomElement(), '_days year !_shows')
+        .populate('_days', '', null, sort: { date: 1 })
+        .exec callback
+    , (year, callback) ->
       Day.findRandom year: year.year, (err, day) ->
         return callback err if err

@@ -120,7 +118,7 @@
               .populate('_songs')
               .exec (err, show) ->
                 day.show = show
-                callback null, years, year, day
+                callback null, year, day
     ], cb

 module.exports = router


fix(toolbar): state to toolbar visible

fix(toolbar): state to toolbar visible

--- internal_packages/composer/lib/floating-toolbar-container.cjsx
+++ internal_packages/composer/lib/floating-toolbar-container.cjsx
@@ -83,7 +83,7 @@
       left={@state.toolbarLeft}
       pos={@state.toolbarPos}
       mode={@state.toolbarMode}
-      visible={true}
+      visible={@state.toolbarVisible}
       onSaveUrl={@props.onSaveUrl}
       onMouseEnter={@_onEnterToolbar}
       onChangeMode={@_onChangeMode}


Refs #68; Added a slew of other option change methods (currently untested).

--- src/time/heatmap.coffee
+++ src/time/heatmap.coffee
@@ -232,4 +232,22 @@
   # Changes the number of buckets in response to an option:buckets event.
   bucketsChanged: -> @redraw()

+  # Changes the range of the buckets in response to an option:bucketRange event.
+  bucketRangeChanged: ->
+    @_transitionRangeAxes()
+    @redraw()
+
+  # Changes the opacity function in response to an option:opacity event.
+  opacityChanged: -> @redraw()
+
+  # Changes the bucket padding in response to an option:bucketPadding event.
+  bucketPaddingChanged: -> @redraw()
+
+  # Changes whether or not to paint zeros in response to an option:paintZeroValues event.
+  paintZeroValuesChanged: -> @redraw()
+
+  # Changes whether or not to cut outliers when bucketing in response to an
+  # option:cutOutliers event.
+  cutOutliersChanged: -> @redraw()
+
 # "Audio... Audio... Audio... Video Disco..." - Justice


More changes.

More changes.

--- test/javascripts/integration/message_card_test.js.coffee
+++ test/javascripts/integration/message_card_test.js.coffee
@@ -1,4 +1,14 @@
 setupMessagePayload = (messageTaskId) ->
+
+createPaperWithOneTask = (taskType, taskAttrs) ->
+  ef = ETahi.Factory
+  journal = ef.createRecord('journal', id: 1)
+  paper = ef.createRecord('paper', journal_id: journal.id)
+  litePaper = ef.createLitePaper(paper)
+  phase = ef.createPhase(paper)
+  task = ef.createTask(taskType, paper, phase, taskAttrs)
+
+  [paper, task, journal, litePaper, phase]

 module 'Integration: MessageCards',
   teardown: -> ETahi.reset()
@@ -21,23 +31,19 @@

 test 'Showing a message card will work', ->
   expect(1)
-  ef = ETahi.Factory
-  journal = ef.createRecord('journal', id: 1)
-  paper = ef.createRecord('paper', journal_id: journal.id)
-  litePaper = ef.createLitePaper(paper)
-  phase = ef.createPhase(paper)
-  messageTask = ef.createTask('messageTask', paper, phase,
+  [paper, task, records...] = createPaperWithOneTask('messageTask'
     title: "Message Time"
     participant_ids: [fakeUser.id]
   )
+  ef = ETahi.Factory
   paperPayload = ef.createPayload('paper')
-  paperPayload.addRecords([journal, paper, litePaper, phase, messageTask, fakeUser])
+  paperPayload.addRecords(records.concat(paper, task, fakeUser))

   server.respondWith 'GET', "/papers/#{paper.id}", [
     200, {"Content-Type": "application/json"}, JSON.stringify paperPayload.toJSON()
   ]

-  visit '/papers/1/tasks/1'
+  visit "/papers/#{paper.id}/tasks/#{task.id}"
   .then -> equal(find('.overlay-content h1').text(), "Message Time")

 test 'A message card with a comment works', ->
@@ -48,70 +54,18 @@
     message_task_id: 1
     body: "My comment"
   )
-
-  recordDefs =
-    paper:
-      assignee_ids: [fakeUser.id]
-      phases: [
-        tasks: [
-          messageTask:
-            id: 1
-            title: "Message Time"
-            participant_ids: [fakeUser.id]
-            comment_ids: [comment.id]
-        ]
-      ]
-  records = ef.createBasicPaper(recordDefs)
+  [paper, task, records...] = createPaperWithOneTask('messageTask'
+    title: "Message Time"
+    participant_ids: [fakeUser.id]
+    comment_ids: [comment.id]
+  )
+  ef = ETahi.Factory
   paperPayload = ef.createPayload('paper')
-  _.forEach(records, (record) -> paperPayload.addRecord(record))
-
-  paperPayload.addRecord(fakeUser)
-  paperPayload.addRecord(comment)
-
-  {paper} = paperPayload.toJSON()
-
+  paperPayload.addRecords(records.concat(paper, task, fakeUser, comment))
   server.respondWith 'GET', "/papers/#{paper.id}", [
     200, {"Content-Type": "application/json"}, JSON.stringify paperPayload.toJSON()
   ]

   expect(1)
-  visit("/papers/#{paper.id}/tasks/1")
+  visit("/papers/#{paper.id}/tasks/#{task.id}")
   andThen -> equal(find('.message-comments .comment-body').text(), "My comment")
-
-test 'A comment that has a commentLook shows up as unread', ->
-  expect(1)
-  ef = ETahi.Factory
-  comment = ef.createRecord('comment',
-    commenter_id: fakeUser.id
-    message_task_id: 1
-    body: "My comment"
-  )
-
-  recordDefs =
-    paper:
-      assignee_ids: [fakeUser.id]
-      phases: [
-        tasks: [
-          messageTask:
-            id: 1
-            title: "Message Time"
-            participant_ids: [fakeUser.id]
-            comment_ids: [comment.id]
-        ]
-      ]
-  records = ef.createBasicPaper(recordDefs)
-  paperPayload = ef.createPayload('paper')
-  _.forEach(records, (record) -> paperPayload.addRecord(record))
-
-  paperPayload.addRecord(fakeUser)
-  paperPayload.addRecord(comment)
-
-  {paper} = paperPayload.toJSON()
-
-  server.respondWith 'GET', "/papers/#{paper.id}", [
-    200, {"Content-Type": "application/json"}, JSON.stringify paperPayload.toJSON()
-  ]
-
-  expect(1)
-  visit("/papers/#{paper.id}/tasks/1")
-  andThen -> equal(find('.message-comments .comment-body').text(), "My comment")


Only render flot graph if the view is rendered.

--- app/assets/javascripts/dot_ledger/views/accounts/balance_graph.js.coffee
+++ app/assets/javascripts/dot_ledger/views/accounts/balance_graph.js.coffee
@@ -4,7 +4,8 @@

     template: 'accounts/balance_graph'

-    balances: new DotLedger.Collections.Balances()
+    initialize: ->
+      @balances = new DotLedger.Collections.Balances()

     fetchBalances: ->
       @balances.fetch
@@ -48,18 +49,18 @@
         tickColor: 'rgba(238, 238, 238, 1)'

     renderBalanceGraph: ->
-      $.plot(@ui.balanceGraph, @balanceGraphData(), @balanceGraphOptions())
-
-      @ui.balanceGraph.bind "plothover", (event, pos, item) =>
-        if item
-          balance = DotLedger.Helpers.Format.money(item.datapoint[1])
-          @ui.balanceTooltipInner.html(balance)
-          @ui.balanceTooltip.css(
-            top: item.pageY - 35
-            left: item.pageX - 40
-          ).addClass('in')
-        else
-          @ui.balanceTooltip.removeClass('in')
+      if @isRendered
+        @graph = $.plot(@ui.balanceGraph, @balanceGraphData(), @balanceGraphOptions())
+        @ui.balanceGraph.bind "plothover", (event, pos, item) =>
+          if item
+            balance = DotLedger.Helpers.Format.money(item.datapoint[1])
+            @ui.balanceTooltipInner.html(balance)
+            @ui.balanceTooltip.css(
+              top: item.pageY - 35
+              left: item.pageX - 40
+            ).addClass('in')
+          else
+            @ui.balanceTooltip.removeClass('in')

     onRender: ->
       @balances.on 'sync', =>


add check to exclude temporary ops from packs

--- app/coffee/PackManager.coffee
+++ app/coffee/PackManager.coffee
@@ -263,10 +263,14 @@
 		docs = docs.slice(0,-KEEP_OPS)

 		docs.forEach (d,i) ->
+			# skip existing packs
 			if d.pack?
-				# util.log "skipping existing pack of #{d.pack.length}"
-				top = null if top? # flush the current pack
-				return # and try next
+				top = null
+				return
+			# skip temporary ops (we could pack these into temporary packs in future)
+			if d.expiresAt?
+				top = null
+				return
 			sz = BSON.calculateObjectSize(d)
 			if top?	&& top.pack.length < MAX_COUNT && top.sz + sz < MAX_SIZE
 				top.pack = top.pack.concat {v: d.v, meta: d.meta,  op: d.op, _id: d._id}
@@ -308,6 +312,7 @@
 					prev = v
 					v = p.v
 					error('bad version', v, 'in', p) if v <= prev
+					#error('expired op', p, 'in pack') if p.expiresAt?
 			else
 				prev = v
 				v = d.v


consider all packs for processing

consider all packs for processing

to allow finalisation of old head packs

--- app/coffee/PackManager.coffee
+++ app/coffee/PackManager.coffee
@@ -277,10 +277,13 @@
 			packs.push(last) if last.finalised # it's finalised so we push it back to archive it
 			callback(null, packs)

-	# findPacks: (project_id, doc_id, queryFilter, callback) ->
-	# 	query = { doc_id: ObjectId(doc_id.toString()) }
-	# 	query = _.defaults query, queryFilter if queryFilter?
-	# 	db.docHistory.find(query, {pack:false}).sort {v:1}, callback
+	findPacks: (project_id, doc_id, callback) ->
+		query = { doc_id: ObjectId(doc_id.toString()), expiresAt: {$exists:false} }
+		db.docHistory.find(query, {pack:false}).sort {v:1}, (err, packs) ->
+			return callback(err) if err?
+			return callback() if not packs?
+			return callback() if not packs?.length
+			callback(null, packs)

 	findUnindexedPacks: (project_id, doc_id, callback) ->
 		PackManager.getIndexWithKeys doc_id, (err, indexResult) ->
@@ -361,7 +364,7 @@
 	# Extra methods to test archive/unarchive for a doc_id

 	pushOldPacks: (project_id, doc_id, callback) ->
-		PackManager.findCompletedPacks project_id, doc_id, (err, packs) ->
+		PackManager.findPacks project_id, doc_id, (err, packs) ->
 			return callback(err) if err?
 			return callback() if not packs?.length
 			PackManager.processOldPack project_id, doc_id, packs[0]._id, callback


Removed a debug-only feature which accidentally slipped into release

--- app/coffee/controllers.coffee
+++ app/coffee/controllers.coffee
@@ -35,7 +35,7 @@
             @matchDistance = 1000
             @searchDistinct = true
             @searchCaseSensitice = false
-            @render() #TODO: remove this, only for testing
+#            @render() #TODO: remove this, only for testing
           when "page"
             @sourceModeNeedsInput = true
             @sourceURL = null


fix paths for sample images

fix paths for sample images

--- app/controllers/signup.coffee
+++ app/controllers/signup.coffee
@@ -5,8 +5,8 @@
   avatar: null

   avatars: [
-    { imageUrl: '/assets/images/avatar1.jpg' }
-    { imageUrl: '/assets/images/avatar2.jpg' }
+    { imageUrl: 'assets/images/avatar1.jpg' }
+    { imageUrl: 'assets/images/avatar2.jpg' }
   ]

   validations: {


The modeling property returns a {scan, registration, all} associative object.

--- app/javascripts/router.coffee
+++ app/javascripts/router.coffee
@@ -79,15 +79,17 @@
                   sortByKey = _.partialRight(_.sortBy, 'key')

                   # The modelables with modeling.
-                  modeled = modelables.filter((mdlbl) -> mdlbl.modeling)
+                  modeled = modelables.filter (modelable) ->
+                    # Check for both existence and length > 0.
+                    modelable.modeling
                   # The modeled modelables sorted by key.
                   modelablesSorted = sortByKey(modeled)
                   # The sorted array of arrays of modeling objects.
-                  mdlArraysSorted = (
+                  mdlArrays = (
                     modelable.modeling for modelable in modelablesSorted
                   )
                   # Combine the sorted modeling object arrays.
-                  _.reduce(mdlArraysSorted, (a, b) -> a.concat(b))
+                  _.reduce(mdlArrays, (a, b) -> a.concat(b))

                 # Extend the scan sets.
                 for scanType, scanSet of detail.scanSets
@@ -100,14 +102,16 @@
                 # Add the subject modeling property.
                 Object.defineProperty detail, 'modeling',
                   enumerable: true
-                  # Returns the subject modeling objects sorted as follows:
-                  # * The scan modeling objects precede the registration
-                  #   modeling objects.
-                  # * The scan modeling objects are sorted by scan type.
+                  # Returns the subject modeling associative object
+                  # {scan: [modeling, ...] registration: [modeling, ...]
+                  #  all: scan + registration},
+                  # where:
+                  # * The scan set modeling objects are sorted by scan type.
                   # * The registration modeling objects are sorted by
                   #   the registration configuration key.
-                  #
-                  # @returns the sorted modeling objects
+                  # * all is the concatenation of scan and registration
+                  #   modeling objects
+                  # @returns the {scan, registration, all} object
                   get: ->
                     # Sort the scan modeling objects.
                     scanSets = _.values(detail.scanSets)
@@ -117,9 +121,21 @@
                     regCfgs = _.values(detail.registrationConfigurations)
                     regMdlSorted = sortModeling(regCfgs)

-                    # Return all of the modeling objects in sorted order,
-                    # scan before registration.
-                    scanMdlSorted.concat(regMdlSorted)
+                    # Build the {scan, registration, all} associative
+                    # object.
+                    sbjMdl = {}
+                    if scanMdlSorted
+                      sbjMdl.scan = scanMdlSorted
+                    if regMdlSorted
+                      sbjMdl.registration = regMdlSorted
+                    if scanMdlSorted or regMdlSorted
+                      sbjMdl.all = scanMdlSorted.concat(regMdlSorted)
+
+                    # Return the {scan, registration, all} associative
+                    # object.
+                    scan: scanMdlSorted
+                    registration: regMdlSorted
+                    all: scanMdlSorted.concat(regMdlSorted)

               # Fixes the detail date properties.
               fixDates = ->


Show either countryName or name of country

--- client/controllers/incidentReports/incidentSecondaryDetails.coffee
+++ client/controllers/incidentReports/incidentSecondaryDetails.coffee
@@ -11,10 +11,7 @@

   firstLocationName: ->
     firstLocation = Template.instance().data.locations[0]
-    firstLocation?.countryName or
-      firstLocation?.admin1Name or
-      firstLocation?.admin2Name or
-      firstLocation?.name
+    firstLocation?.countryName or firstLocation?.name

   hasAdditionalInfo: ->
     @locations.length > 1 or formatLocation(@locations[0]).split(',').length > 1


Canvas on hover to pointer

Canvas on hover to pointer

--- site/app.coffee
+++ site/app.coffee
@@ -165,6 +165,12 @@
     $('#button_zoom_in').click(() => @zoomIn())
     $('#button_zoom_out').click(() => @zoomOut())
     @canvasMap = $('#canvas_map')
+    @canvasMap.mousemove((e) =>
+      loc = @hitTest(e.offsetX, e.offsetY)
+      if loc?
+        $('#canvas_map').css('cursor','pointer')
+      else
+        $('#canvas_map').css('cursor','auto'))
     @canvasChart = $('#canvas_chart')
     $(window).resize(() => @resizeCanvas())
     @pointerInput = new PointerInput(@canvasMap, @)


activity: make channel title function to be used everywhere in the file

--- servers/lib/crawler/staticpages/feed.coffee
+++ servers/lib/crawler/staticpages/feed.coffee
@@ -52,8 +52,8 @@
         callback null, fullPage

-  getChannelTitleContent = (channelName) ->
-    content = "
##{channelName}
"
+getChannelTitleContent = (channelName) ->
+  content = "
##{channelName}
"

 buildContent = (models, messageList, options, callback) ->


Update session code

Update session code

--- services/real-time/app/coffee/Router.coffee
+++ services/real-time/app/coffee/Router.coffee
@@ -35,69 +35,71 @@
 		app.get "/clients/:client_id", HttpController.getConnectedClient

 		app.post "/project/:project_id/message/:message", httpAuth, bodyParser.json(limit: "5mb"), HttpApiController.sendMessage
-
+
 		session.on 'connection', (error, client, session) ->
 			if error?
 				logger.err err: error, "error when client connected"
 				client?.disconnect()
 				return
-
+
 			metrics.inc('socket-io.connection')
-
+
 			logger.log session: session, client_id: client.id, "client connected"
-
-			if !session or !session.user?
+
+			if session?.passport?.user?
+				user = session.passport.user
+			else if session?.user?
+				user = session.user
+			else
 				user = {_id: "anonymous-user"}
-			else
-				user = session.user
-
+
 			client.on "joinProject", (data = {}, callback) ->
 				WebsocketController.joinProject client, user, data.project_id, (err, args...) ->
 					if err?
 						Router._handleError callback, err, client, "joinProject", {project_id: data.project_id, user_id: user?.id}
 					else
 						callback(null, args...)
-
+
 			client.on "disconnect", () ->
 				metrics.inc('socket-io.disconnect')
 				WebsocketController.leaveProject io, client, (err) ->
 					if err?
 						Router._handleError null, err, client, "leaveProject"
-
-
+
+
 			client.on "joinDoc", (doc_id, fromVersion, callback) ->
 				# fromVersion is optional
 				if typeof fromVersion == "function"
 					callback = fromVersion
 					fromVersion = -1
-
+
 				WebsocketController.joinDoc client, doc_id, fromVersion, (err, args...) ->
 					if err?
 						Router._handleError callback, err, client, "joinDoc", {doc_id, fromVersion}
 					else
 						callback(null, args...)
-
+
 			client.on "leaveDoc", (doc_id, callback) ->
 				WebsocketController.leaveDoc client, doc_id, (err, args...) ->
 					if err?
 						Router._handleError callback, err, client, "leaveDoc"
 					else
 						callback(null, args...)
-
+
 			client.on "clientTracking.getConnectedUsers", (callback = (error, users) ->) ->
 				WebsocketController.getConnectedUsers client, (err, users) ->
 					if err?
 						Router._handleError callback, err, client, "clientTracking.getConnectedUsers"
 					else
 						callback(null, users)
-
+
 			client.on "clientTracking.updatePosition", (cursorData, callback = (error) ->) ->
 				WebsocketController.updateClientPosition client, cursorData, (err) ->
 					if err?
 						Router._handleError callback, err, client, "clientTracking.updatePosition"
 					else
 						callback()
-
+
 			client.on "applyOtUpdate", (doc_id, update, callback = (error) ->) ->
 				WebsocketController.applyOtUpdate client, doc_id, update, (err) ->
 					if err?


Get anonToken from joinProject payload

Get anonToken from joinProject payload

--- services/real-time/app/coffee/Router.coffee
+++ services/real-time/app/coffee/Router.coffee
@@ -67,9 +67,8 @@
 				user = {_id: "anonymous-user"}

 			client.on "joinProject", (data = {}, callback) ->
-				anonToken = session?.anonReadOnlyTokenAccess?[data.project_id]
-				if anonToken
-					user.anonToken = anonToken
+				if data.anonToken
+					user.anonToken = data.anonToken
 				WebsocketController.joinProject client, user, data.project_id, (err, args...) ->
 					if err?
 						Router._handleError callback, err, client, "joinProject", {project_id: data.project_id, user_id: user?.id}


[Router] gracefully set and do not reset the ol_context

--- services/real-time/app/coffee/Router.coffee
+++ services/real-time/app/coffee/Router.coffee
@@ -55,7 +55,7 @@
 		app.post "/client/:client_id/disconnect", httpAuth, HttpApiController.disconnectClient

 		session.on 'connection', (error, client, session) ->
-			client.ol_context = {}
+			client.ol_context = {} unless client.ol_context

 			client?.on "error", (err) ->
 				logger.err { clientErr: err }, "socket.io client error"
@@ -113,13 +113,9 @@
 				metrics.inc('socket-io.disconnect')
 				metrics.gauge('socket-io.clients', io.sockets.clients()?.length - 1)

-				cleanup = () ->
-					delete client.ol_context
 				WebsocketController.leaveProject io, client, (err) ->
 					if err?
-						Router._handleError cleanup, err, client, "leaveProject"
-					else
-						cleanup()
+						Router._handleError (() ->), err, client, "leaveProject"

 			# Variadic. The possible arguments:
 			# doc_id, callback


heroku postgres

heroku postgres

--- lib/modules/blog/models/Blogs.coffee
+++ lib/modules/blog/models/Blogs.coffee
@@ -30,7 +30,7 @@
       _categories       : {writeable: false, value: []}
       _category_names   : {writeable: false, value: {}}

-    #@queue @_load_categories
+    @queue @_load_categories

   #
   # Get all


Change regex member provider

Change regex member provider

--- lib/providers/member-provider.coffee
+++ lib/providers/member-provider.coffee
@@ -18,7 +18,7 @@
     ###
     fetchSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
         # Autocompletion for class members, i.e. after a ::, ->, ...
-        @regex = /([a-zA-Z0-9_]+)(?:\(.*\))?(?:->|::)/g
+        @regex = /(?:[a-zA-Z0-9_]+)(?:\(.*\))?(?:->|::)([a-zA-Z0-9_]*)/g

         prefix = @getPrefix(editor, bufferPosition)
         return unless prefix.length


Remove old error messages when saving model was successfull.

--- app/assets/javascripts/controllers/nodes.controller.js.coffee
+++ app/assets/javascripts/controllers/nodes.controller.js.coffee
@@ -104,6 +104,7 @@

       promise.then ->
         that.get('controllers.flash-messages').pushMessage('notice', I18n.t('nodes.update.flash.successfull'))
+        that.set('errors', []) # Remove old error messages

         # Wrap the model saved event into a promise so we can call async stuff before we actually redirect to the model page
         # Used e.g. for calling the photo upload queue after submitting the new node model


turns out that we already implemented enough to do this better

--- src/smc-hub/kucalc/project-changefeed.coffee
+++ src/smc-hub/kucalc/project-changefeed.coffee
@@ -1,62 +0,0 @@
-###
-Singleton object that listens for changes to any of the following columns in the
-database projects table:
-
- - state
- - status
- - action_request
- (maybe users?)
-
-When any project with any of those properties changes, it fires a change
-event with the project_id.
-###
-
-# maybe we don't need this!
-
-{EventEmitter} = require('events')
-
-misc = require('smc-util/misc')
-{defaults, required} = misc
-
-the_uniq_object = undefined
-exports.project_changefeed = (opts) ->
-    opts = defaults opts,
-        db     : required
-        logger : undefined
-        cb     : required
-    if the_uniq_object?
-        opts.cb(undefined, the_uniq_object)
-    else
-        new ProjectsChangefeed opts.db, opts.logger, (err, obj) ->
-            if err
-                opts.cb(err)
-            else
-                the_uniq_object = obj
-                opts.cb(undefined, the_uniq_object)
-
-class ProjectsChangefeed extends EventEmitter
-    constructor: (@db, @logger, cb) ->
-        @db.changefeed
-            table  : 'projects'
-            select :
-                state :
-                status :
-
-            required   # Map from field names to postgres data types. These must
-                                # determine entries of table (e.g., primary key).
-            watch  : required   # Array of field names we watch for changes
-            where  : required   # Condition involving only the fields in select; or function taking obj with select and returning true or false
-            cb     : required
-
-        cb(undefined, @)
-
-
-
-
-
-
-
-
-
-
-


introducing new raw, command escaping

introducing new raw, command escaping


NEW: setting transition to null at the end of animation and on reset()

--- src/Moon.coffee
+++ src/Moon.coffee
@@ -267,5 +267,8 @@
             this._stack = []
             this._loop = 1
             this._direction = true
+            this.set(
+                "transition": null
+            )

     window.Moon = Moon


Fixed very long query for taxons on /admin/taxons

Fixes #6261

--- backend/app/assets/javascripts/spree/backend/taxons.js.coffee
+++ backend/app/assets/javascripts/spree/backend/taxons.js.coffee
@@ -24,6 +24,7 @@
         data: (term, page) ->
           per_page: 50,
           page: page,
+          without_children: true,
           token: Spree.api_key,
           q:
             name_cont: term


Added implementation of weight calc

Added implementation of weight calc

--- src/calc.coffee
+++ src/calc.coffee
@@ -44,7 +44,15 @@

 	## Intermediate calculations
 	weight: (subject) ->
-		0
+		weightOf = (i) -> i.weight ? 0
+
+		invWeight = subject.inventory
+			.map (item) -> weightOf item
+			.reduce ((p, c) -> p+c), 0
+
+		eqpWeight = (weightOf item for slot,item of subject.equipment)
+
+		(weightOf subject) + invWeight + eqpWeight

 	excessWeight: (subject) ->
 		Math.max 0, (subject.calc 'weight') - (subject.calc 'maxWeight')


remove whitespace

remove whitespace

--- common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee
+++ common/static/coffee/src/discussion/views/discussion_thread_show_view.coffee
@@ -82,7 +82,7 @@
       if window.user.voted(@model)
         @unvote()
       else
-        @vote()
+        @vote()

     toggleFollowing: (event) ->
       $elem = $(event.target)
@@ -96,7 +96,7 @@
       DiscussionUtil.safeAjax
         $elem: $elem
         url: url
-        type: "POST"
+        type: "POST"

     vote: ->
       window.user.vote(@model)


Added new classes for application views and added app view controllers for Ace and Terminal apps.

--- CakefileIncludes.coffee
+++ CakefileIncludes.coffee
@@ -151,7 +151,8 @@
         StartTabAppController      : "./client/app/Applications/StartTab.kdapplication/AppController.coffee"

         # new ace
-        AceAppController           : "./client/app/Applications/Ace.kdapplication/AppController.coffee"
+        AceAppView            : "./client/app/Applications/Ace.kdapplication/aceappview.coffee"
+        AceAppController      : "./client/app/Applications/Ace.kdapplication/AppController.coffee"
         AceView               : "./client/app/Applications/Ace.kdapplication/AppView.coffee"
         Ace                   : "./client/app/Applications/Ace.kdapplication/ace.coffee"
         AceSettingsView       : "./client/app/Applications/Ace.kdapplication/acesettingsview.coffee"
@@ -168,6 +169,7 @@
         Viewer          : './client/app/Applications/Viewer.kdapplication/AppController.coffee'

         # webterm
+        WebTermAppView        : "./client/app/Applications/WebTerm.kdapplication/webtermappview.coffee"
         WebTermController     : "./client/app/Applications/WebTerm.kdapplication/AppController.coffee"
         WebTermView           : "./client/app/Applications/WebTerm.kdapplication/AppView.coffee"
         WebtermSettingsView   : "./client/app/Applications/WebTerm.kdapplication/webtermsettingsview.coffee"
@@ -395,6 +397,9 @@

         # COMMON VIEWS

+        ApplicationTabHandleHolder : "./client/app/CommonViews/applicationview/applicationtabhandleholder.coffee"
+        ApplicationTabView         : "./client/app/CommonViews/applicationview/applicationtabview.coffee"
+
         linkView                    : "./client/app/CommonViews/linkviews/linkview.coffee"
         customLinkView              : "./client/app/CommonViews/linkviews/customlinkview.coffee"
         linkGroup                   : "./client/app/CommonViews/linkviews/linkgroup.coffee"


Add documentation on AbstractLoader#getCacheObject

--- vendor/assets/javascripts/brainstem/loaders/abstract-loader.coffee
+++ vendor/assets/javascripts/brainstem/loaders/abstract-loader.coffee
@@ -53,7 +53,6 @@
    * Sets up both the `internalObject` and `externalObject`.
    * In the case of models the `internalObject` and `externalObject` are the same.
    * In the case of collections the `internalObject` is a proxy object that updates the `externalObject` when all loading is completed.
-   * @return {[type]} [description]
   ###
   _createObjects: ->
     throw "Implement in your subclass"
@@ -72,6 +71,10 @@
     else
       @_loadFromServer()

+  ###*
+   * Returns the cache object from the storage manager.
+   * @return {object} Object containing `count` and `results` that were cached.
+  ###
   getCacheObject: ->
     @storageManager.getCollectionDetails(@_getCollectionName()).cache[@loadOptions.cacheKey]


Corrige bug na barra de busca na home

Corrige bug na barra de busca na home

Não podemos usar o pushState nesse caso. Precisamos forçar o refresh.

--- app/assets/javascripts/angular_app/controllers/app_controller.js.coffee
+++ app/assets/javascripts/angular_app/controllers/app_controller.js.coffee
@@ -16,20 +16,25 @@
     updateRoute = ->
       newUrl = generateUrl($scope.entity, $scope.year)
       if window.location.pathname != newUrl
-        if window.history
+        if window.history && !inRootPage()
           window.history.pushState({}, '', newUrl)
         else
           window.location.pathname = newUrl

     parseUrl = ->
-      urlParts = window.location.pathname.substr(1).split('/')
-      id = if urlParts == 3
-             slugToId(urlParts[1]) # É uma unidade orçamentária
+      return if inRootPage()
+      path = window.location.pathname.substr(1)
+      parts = path.split('/')
+      id = if parts == 3
+             slugToId(parts[1]) # É uma unidade orçamentária
            else
-             slugToId(urlParts[0]) # É um órgão
+             slugToId(parts[0]) # É um órgão

       $scope.entity = entity for entity in $scope.entities when parseInt(entity.id) is id
-      $scope.year = parseInt(urlParts[urlParts.length-1])
+      $scope.year = parseInt(parts[parts.length-1])
+
+    inRootPage = () ->
+      window.location.pathname == '/'

     generateUrl = (entity, year) ->
       entitySlug = slugify(entity)


Small directional changes

Small directional changes

--- app/assets/javascripts/farmbot_app/directives/directionbutton.js.coffee
+++ app/assets/javascripts/farmbot_app/directives/directionbutton.js.coffee
@@ -18,7 +18,10 @@
     $scope.move       = (axis, direction) ->
       console.log "Set #{axis} to #{direction}"
       $scope.zeroCoords()
-      if direction is 'up' then direction = 1 else direction = -1
+      if direction == 'up'
+        direction = -1
+      else
+        direction = 1
       $scope[axis] = 100 * direction
       $scope.goRel()


removed a console.log

removed a console.log

--- app/assets/javascripts/trade/controllers/shipments_controller.js.coffee
+++ app/assets/javascripts/trade/controllers/shipments_controller.js.coffee
@@ -189,7 +189,6 @@
   rawDownloadUrl: (->
     params = @searchParamsForUrl()
     params['report_type'] = 'raw' #TODO this will likely be a property in its own right
-    console.log 'hello download url', $.param({'filters': params})
     '/trade/exports/download?' + $.param({'filters': params})
   ).property(
     'selectedTaxonConcepts.@each', 'selectedAppendices.@each',


Faster response speed

Faster response speed

--- views/components/info/alert.cjsx
+++ views/components/info/alert.cjsx
@@ -109,7 +109,7 @@
           top: #{historyHeight + alertHeight}px;
         }
       """
-    , 350
+    , 10

   componentDidMount: ->
     window.addEventListener 'poi.alert', @handleAlert


Make Auth class global

Make Auth class global

It will allow to extend it in travis-pro-web

--- assets/scripts/app/auth.coffee
+++ assets/scripts/app/auth.coffee
@@ -1,4 +1,4 @@
-Auth = Ember.Object.extend
+window.Auth = Ember.Object.extend
   state:        "signed-out"
   receivingEnd: "#{location.protocol}//#{location.host}"


working mouse support

working mouse support

--- source/assets/javascripts/konsole.js.coffee
+++ source/assets/javascripts/konsole.js.coffee
@@ -49,6 +49,35 @@
   # firefox returns code 0 for ö
   code = 192 if code == 0 and e.key == 'ö'
   return code
+
+processKeyDown = (key, isMouse) ->
+  if key.data('level')
+    curClass = key.data('level')
+    key.addClass('is-active')
+    key.closest('.level').find('.is-active').not(key).removeClass('is-active')
+    $('body').removeClass().addClass(curClass)
+  else
+    $('#js-lyrics').html('').find('span').addClass('fadeOutUp').html(key.data('lyric'))
+    if !isMouse
+      key.addClass('is-active')
+
+  switch $('.level').find('i.is-active').data('level')
+    when 'Normal'
+      level_num = 1
+    when 'Pitch-1'
+      level_num = 2
+    when 'Low'
+      level_num = 3
+    when 'Pitch-2'
+      level_num = 4
+    when 'High'
+      level_num = 5
+
+  code = key.data('code')
+  sound_name = sound_keys[code]
+  if sound_name
+    ion.sound.play sound_name + level_num
+

 init = ->
   level = 'Normal'
@@ -150,49 +179,21 @@

   $(document).keydown (e) ->
     e.preventDefault()
-    code = e.keyCode or e.which
-    # my firefox (linux) returns code 0 for oe
-    if code == 0 and e.key == 'ö'
-      code = 192
+    code = getKeyCode e
     key = $('[data-code=' + code + ']')
-
-    if key.data('level')
-      curClass = key.data('level')
-      key.addClass('is-active')
-      key.closest('.level').find('.is-active').not(key).removeClass('is-active')
-      $('body').removeClass().addClass(curClass)
-
-    else
-      key.addClass('is-active')
-      $('#js-lyrics').html('').find('span').addClass('fadeOutUp').html(key.data('lyric'))
-
-    switch $('.level').find('i.is-active').data('level')
-      when 'Normal'
-        level_num = 1
-      when 'Pitch-1'
-        level_num = 2
-      when 'Low'
-        level_num = 3
-      when 'Pitch-2'
-        level_num = 4
-      when 'High'
-        level_num = 5
-
-    sound_name = sound_keys[code]
-    if sound_name
-      ion.sound.play sound_name + level_num
-
+    if key.length
+      processKeyDown key, false

   $(document).keyup (e) ->
     e.preventDefault()
-    code = e.keyCode or e.which
-    # my firefox (linux) returns code 0 for oe
-    if code == 0 and e.key == 'ö'
-      code = 192
+    code = getKeyCode e
     key = $('[data-code=' + code + ']')
-
     if !key.data('level')
       key.removeClass('is-active')
+
+  $('[data-code]').mousedown (e) ->
+    e.preventDefault()
+    processKeyDown $(this), true

   $('#js-daft_1').jPlayer
     ready: (event) ->


Refactor load handling

Refactor load handling

--- source/javascripts/jquery.windoze.js.coffee
+++ source/javascripts/jquery.windoze.js.coffee
@@ -89,12 +89,7 @@
       @forceReflow()
       @$modal.add(@$overlay).addClass('wdz-active')
       if @modal_duration then setTimeout $.proxy(@showModal, @), @modal_duration else @showModal()
-      if e
-        href = $(e.target).attr('href') || $(e.target).closest('a').attr('href')
-        if href.match(/\.(gif|jpg|jpeg|png)$/)
-          @loadImage href
-        else if href and href != '#'
-          @loadRemote href
+      @loadFromEvent(e)
     hideAll: (e) ->
       if !@$modal.is(':visible') then return
       @fireCallback('beforeClose')
@@ -121,19 +116,23 @@
     hideOverlay: ->
       @$overlay.hide()
       $(document.body).removeClass('wdz-modal-open')
-    loadImage: (href) ->
+    loadFromEvent: (e) ->
+      return if !e
+      href = $(e.target).attr('href') || $(e.target).closest('a').attr('href')
+      return if !href or href == '#'
       @$modal.addClass('wdz-loading')
       @fireCallback('beforeLoad')
-      @$modal.empty()
+      if href.match(/\.(gif|jpg|jpeg|png)$/)
+        @loadImage href
+      else
+        @loadRemote href
+    loadImage: (href) ->
       $img = $('<img />', { src: href }).on("load", $.proxy(->
         @$modal.removeClass('wdz-loading')
         @fireCallback('afterLoad')
       , @))
       @$modal.append($('<article />').append($img))
     loadRemote: (href) ->
-      @$modal.empty()
-      @$modal.addClass('wdz-loading')
-      @fireCallback('beforeLoad')
       @$modal.load href, $.proxy(->
         @$modal.removeClass('wdz-loading')
         @fireCallback('afterLoad')


Allowing classes to be better decorated with implicit calls. Issue #548

--- rewriter.coffee
+++ rewriter.coffee
@@ -162,6 +162,7 @@
       size
     @scanTokens (prev, token, post, i) =>
       tag = token[0]
+      before = @tokens[i - 2] and @tokens[i - 2][0]
       stack[stack.length - 2] += stack.pop() if tag is 'OUTDENT'
       open = stack[stack.length - 1] > 0
       if prev and prev.spaced and include(IMPLICIT_FUNC, prev[0]) and include(IMPLICIT_CALL, tag) and
@@ -171,7 +172,8 @@
         stack.push 0 if include(EXPRESSION_START, tag)
         return 2
       if include(EXPRESSION_START, tag)
-        if tag is 'INDENT' and !token.generated and open and not (prev and include(IMPLICIT_BLOCK, prev[0]))
+        if tag is 'INDENT' and !token.generated and open and not
+            ((prev and include(IMPLICIT_BLOCK, prev[0])) or before and before is 'CLASS')
           size = closeCalls(i)
           stack.push 0
           return size
@@ -329,7 +331,7 @@

 # If preceded by an `IMPLICIT_FUNC`, indicates a function invocation.
 IMPLICIT_CALL    = [
-  'IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START',
+  'IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START', 'CLASS',
   'TRY', 'DELETE', 'TYPEOF', 'SWITCH', 'THIS', 'NULL',
   'TRUE', 'FALSE', 'YES', 'NO', 'ON', 'OFF',
   '!', '!!', '@', '->', '=>', '[', '(', '{'


Add some whitespace so we're Textmate friendly.

--- app/assets/javascripts/ample_assets/classes/ample_assets.js.coffee
+++ app/assets/javascripts/ample_assets/classes/ample_assets.js.coffee
@@ -1,14 +1,14 @@
 class window.AmpleAssets
-
+
   constructor: (opts=undefined) ->
     @set_options(opts)
     @init()
-
+
   init: ->
     @options.onInit()
     @setup()
     @events()
-
+
   set_options: (opts) ->
     @current = 0
     @keys_enabled = true
@@ -65,10 +65,10 @@
     @options = default_options
     for k of opts
       @options[k] = opts[k]
-
+
   log: (msg) ->
     console.log "ample_assets.log: #{msg}" if @options.debug
-
+
   setup: ->
     id = @options.id
     layout = Mustache.to_html(@tpl('layout'),{ id: id, pages: @get_pages(), tabs: @get_pages('tab') })
@@ -81,13 +81,13 @@
     @goto(0) if @options.expanded
     $('body').bind 'ample_uploadify.complete', =>
       @reload(0)
-
+
   style: ->
     @loading = $("##{@options.id}-tabs span.loading")
     $("##{@options.id} .container").css('height',200)
     if @options.expanded
       $("##{@options.id}").css({height:@options.expanded_height});
-
+
   reload: (i) ->
     @log "reload(#{i})"
     @reloading = true
@@ -102,7 +102,7 @@
     selector = "##{@options.id} .pages .page:nth-child(#{(i+1)})"
     selector += " ul" if @options.pages[i]['panels']
     $(selector).empty()
-
+
   goto: (i) ->
     @log "goto(#{i})"
     @current = i
@@ -111,19 +111,19 @@
     @activate(i)
     @load(i) unless @already_loaded(i)
     @enable_panel(i) if @already_loaded(i)
-
+
   show: (i) ->
     $("##{@options.id} .pages .page").hide()
     $("##{@options.id} .pages .page:nth-child(#{i+1})").show()
-
+
   drag_drop: ->
     base_url = @options.base_url
     thumb_url = @options.thumb_url
-
+
     $(".draggable").liveDraggable
       appendTo: "body"
       helper: "clone"
-
+
     $("textarea").droppable
       activeClass: "notice"
       hoverClass: "success"
@@ -134,7 +134,7 @@
         textile = "!#{url}!"
         html = ""
         $(this).insertAtCaret (if $(this).hasClass('textile') then textile else html)
-
+
     $(".droppable").droppable
       activeClass: "notice"
       hoverClass: "success"
@@ -143,31 +143,31 @@
         asset_id = $(ui.draggable).attr("id").split("-")[1]
         $(this).parent().children().first().val asset_id
         $(this).parent().find('a.asset-remove').removeClass('hide').show()
-
+
   activate: (i) ->
     @log "activate(#{i})"
     $("##{@options.id} a.tab").removeClass('on')
     $("##{@options.id} a.tab:nth-child(#{i+2})").addClass('on')
-
+
   next: ->
     if @current < @options.pages.length - 1
       @log "next()"
       @current += 1
       @goto(@current)
-
+
   previous: ->
     unless @current == 0
       @log "previous()"
       @current -= 1
       @goto(@current)
-
+
   get_pages: (tpl = 'page') ->
     html = ''
     $.each @options.pages, (idx,el) =>
       el['classes'] = 'first-child' if idx == 0
       html += Mustache.to_html @tpl(tpl), el
     html
-
+
   toggle: ->
     el = $("##{@options.id}")
     if @options.expanded
@@ -182,7 +182,7 @@
       el.animate {height: @options.expanded_height}, "fast", =>
         @expand()
         @options.onExpand()
-
+
   load: (i) ->
     ref = this
     if !@options.pages[i]['last_request_empty'] && @options.pages[i]['url']
@@ -205,7 +205,7 @@
       , data_type
     else
       @log "ERROR --> Couldn't load page because there was no url" unless @options.pages[i]['last_request_empty']
-
+
   load_empty: (i) ->
     @log "load_empty(#{i})"
     empty = Mustache.to_html(@tpl('empty'))
@@ -214,14 +214,14 @@
     $('li.empty').css('width',$('.ampn').first().width())
     $('li.empty a').click =>
       @goto(@options.pages.length-2)
-
+
   load_html: (i, response) ->
     @log "load(#{i}) html"
     selector = "##{@options.id} .pages .page:nth-child(#{(i+1)})"
     selector += " ul" if @options.pages[i]['panels']
     $(selector).html(response)
     @panels(i)
-
+
   load_json: (i, response) ->
     @log "load(#{i}) json"
     panels_loaded = if @options.pages[i]['panel_selector'] then true else false
@@ -239,7 +239,7 @@
     if @reloading
       @reloading = false
       @controls()
-
+
   load_results: (response) ->
     @log "load_results()"
     $.each response, (j,el) =>
@@ -250,7 +250,7 @@
     @active_panel = $("#asset-results ul")
     @show(@options.pages.length-1)
     @controls()
-
+
   build: (el) ->
     ref = this
     show_url = Mustache.to_html @options.show_url, { id: el.id }
@@ -264,7 +264,7 @@
       ref.modal_open(el)
       false
     link
-
+
   modal_open: (data) ->
     @modal_active = true
     if data.document == 'true'
@@ -282,7 +282,7 @@
       console.log html
       $.facebox("
#{html}
")
     @touch(data)
-
+
   load_img: (el,src) ->
     img = new Image()
     $(img).load(->
@@ -290,17 +290,17 @@
       $(el).html this
       $(this).fadeIn()
     ).attr src: src
-
+
   next_page_url: (i) ->
     @options.pages[i]['pages_loaded'] = 0 unless @options.pages[i]['pages_loaded']
     @options.pages[i]['pages_loaded'] += 1
     "#{@options.pages[i]['url']}?page=#{@options.pages[i]['pages_loaded']}"
-
+
   touch: (el) ->
     @log "touch()"
     touch_url = Mustache.to_html @options.touch_url, { id: el.id }
     $.post "#{@options.base_url}#{touch_url}"
-
+
   panels: (i) ->
     ref = this
     if @options.pages[i]['panels']
@@ -314,21 +314,21 @@
       $(el).amplePanels(@options.pages_options)
         .bind 'slide_horizontal', (e,d,dir) ->
           ref.load(i) if dir == 'next'
-
+
   disable_panels: ->
     @log "disable_panels()"
     ref = this
     @controls(false)
     $.each @options.pages, (i,el) ->
       $(ref.options.pages[i]['panel_selector']).amplePanels('disable') if ref.options.pages[i]['panel_selector']
-
+
   enable_panel: (i) ->
     @log "enable_panel(#{i})"
     if @options.pages[i]['panel_selector']
       @active_panel = @options.pages[i]['panel_selector']
       $(@options.pages[i]['panel_selector']).amplePanels('enable')
       @controls()
-
+
   controls: (display=true) ->
     @log "controls(#{display})"
     display = false if $(@active_panel).find('li').length < @options.pages_options.per_page
@@ -337,22 +337,22 @@
         $('nav.controls').show()
       when false
         $('nav.controls').hide()
-
+
   already_loaded: (i) ->
     typeof @options.pages[i]['loaded'] == 'boolean' && @options.pages[i]['loaded']
-
+
   remove: (el) ->
     parent = $(el).parent()
     parent.find('.droppable').empty().html('Drag Asset Here')
     parent.find('input').val('')
     $(el).hide()
-
+
   collapse: ->
     @disable_panels()
-
+
   expand: ->
     @goto(0)
-
+
   events: ->
     @modal_events()
     @global_events()
@@ -374,18 +374,18 @@
       $(el).click ->
         ref.goto(idx)
         false
-
+
   global_events: ->
     $('a.global.next').click =>
       $(@active_panel).amplePanels('next')

     $('a.global.previous').click =>
       $(@active_panel).amplePanels('previous')
-
+
   drag_events: ->
     @log "drag_events()"
     # TODO: kill key events during drag?
-
+
   drop_events: ->
     ref = this
     $('.asset-drop .droppable a').live 'click', ->
@@ -394,14 +394,14 @@
         ref.modal_open(response)
       , 'json'
       false
-
+
   field_events: ->
     @log "field_events()"
     $('textarea, input').live 'blur', =>
       @keys_enabled = true
     $('textarea, input').live 'focus', =>
       @keys_enabled = false
-
+
   modal_events: ->
     @modal_active = false
     $(document).bind 'afterClose.facebox', =>
@@ -410,7 +410,7 @@
     $(document).bind 'loading.facebox', =>
       @keys_enabled = false
       @modal_active = true
-
+
   search: ->
     @log 'search_events()'
     search_url = "#{@options.base_url}#{@options.search_url}"
@@ -423,7 +423,7 @@
       $.post search_url, $(this).serialize(), (response) ->
         ref.load_results(response)
       , 'json'
-
+
   key_down: ->
     ref = this
     previous = 37
@@ -452,10 +452,10 @@
           when down
             @next()
       e.stopPropagation();
-
+
   tpl: (view) ->
     @tpls()[view]
-
+
   tpls: ->
     layout: '

@@ -499,11 +499,12 @@

{{ filename }}

'
     empty: '
Oops. There\'s nothing here. You should upload something.
'
+

 jQuery.fn.liveDraggable = (opts) ->
   @live "mouseover", ->
     $(this).data("init", true).draggable opts  unless $(this).data("init")
-
+

 jQuery.fn.insertAtCaret = (value) ->
   @each (i) ->
@@ -523,4 +524,5 @@
       @scrollTop = scrollTop
     else
       @value += value
-      @focus()+      @focus()
+


Add breadcrumb methods in router

Add breadcrumb methods in router

--- app/assets/javascripts/backbone/routers/trees_controller.js.coffee
+++ app/assets/javascripts/backbone/routers/trees_controller.js.coffee
@@ -1,14 +1,23 @@
+# Takes care of routing.
+# Relays data to breadcrumbs and tree_fetcher models
+
 class Water.TreesController extends Backbone.Router
+  initialize: (params) =>
+    @fetcher = params.fetcher
+    @breadcrumbs = params.breadcrumbs
   routes:
     "/tree/*path": "fetch_tree"
     "/blob/*path": "fetch_blob"
     "*anything": "root"

   fetch_tree: (path) ->
+    @breadcrumbs.path(path)
     @fetcher.fetch("trees", path)
   fetch_blob: (path) ->
+    @breadcrumbs.push(["/blob"].concat(path.split("/")))
     @fetcher.fetch("blobs", path)
   root: () ->
+    @breadcrumbs.root()
     @fetcher.fetch("trees", "")


Fixed Filterwatcher

Fixed Filterwatcher

--- wzk/ui/grid/FilterWatcher.coffee
+++ wzk/ui/grid/FilterWatcher.coffee
@@ -58,7 +58,7 @@
   handleLoad: (e) =>
     if @initialCheck
       @query.each (k, v) =>
-        if v[0]?
+        if v[0]? and @fields[k]?
           @fields[k].setValue v[0]
     for k, field of @fields
       @filter field