Effect targets accept multiple identifiers.

--- app/assets/javascripts/pwn-fx.js.coffee
+++ app/assets/javascripts/pwn-fx.js.coffee
@@ -143,8 +143,9 @@
 #
 # Attributes:
 #   data-pwnfx-move: an identifier connecting the move's target element
-#   data-pwnfx-move-target: set to the same value as data-pwnfx-move on the
-#       element that will receive the moved element as its last child
+#   data-pwnfx-move-target: a space-separated list of identifiers, one of which
+#       should match the value of data-pwnfx-move; set on the element that will
+#       receive the moved element as its last child
 #   data-pwnfx-move-method: 'append' adds the moved as the element as the
 #       target's last child, 'replace' clears the target element, then adds the
 #       moved element as the target's only child
@@ -152,7 +153,7 @@
   constructor: (element, identifier, scopeId) ->
     scope = PwnFx.resolveScope scopeId, element
     method = element.getAttribute('data-pwnfx-move-method') || 'append'
-    target = document.querySelector "[data-pwnfx-move-target=\"#{identifier}\"]"
+    target = document.querySelector "[data-pwnfx-move-target~=\"#{identifier}\"]"

     switch method
       when 'append'
@@ -175,14 +176,15 @@
 #       beforebegin, afterbegin, beforeend, afterend; defaults to beforeend
 #   data-pwnfx-render-randomize: regexp pattern whose matches will be replaced
 #       with a random string; useful for generating unique IDs
-#   data-pwnfx-render-target: set on the element(s) receiving the rendered HTML;
-#       set to the identifier in data-pwnfx-render
+#   data-pwnfx-render-target: a space-separated list of identifiers, one of
+#       which should match the value of data-pwnfx-render; set on the element(s)
+#       receiving the rendered HTML
 #   data-pwnfx-render-source: set to the identifier in data-pwnfx-render, on the
 #       <script> tag containing the source HTML
 class PwnFxRender
   constructor: (element, identifier, scopeId) ->
     sourceSelector = "script[data-pwnfx-render-source=\"#{identifier}\"]"
-    targetSelector = "[data-pwnfx-render-target=\"#{identifier}\"]"
+    targetSelector = "[data-pwnfx-render-target~=\"#{identifier}\"]"
     insertionPoint = element.getAttribute('data-pwnfx-render-where') ||
                      'beforeend'
     randomizedPatten = element.getAttribute('data-pwnfx-render-randomize')
@@ -221,11 +223,12 @@
 #   data-pwnfx-delayed-method: the HTTP method of AJAX request (default: POST)
 #   data-pwnfx-delayed-ms: the delay between the page load and the issuing of
 #                          the AJAX request (default: 1000ms)
-#   data-pwnfx-delayed-target: set to the value of data-pwnfx-delayed on the
-#                              element populated with the AJAX response
+#   data-pwnfx-delayed-target: a space-separated list of identifiers, one of which
+#       should match the value of data-pwnfx-delayed; set on the element
+#       populated with the AJAX response
 class PwnFxDelayed
   constructor: (element, identifier, scopeId) ->
-    targetSelector = "[data-pwnfx-delayed-target=\"#{identifier}\"]"
+    targetSelector = "[data-pwnfx-delayed-target~=\"#{identifier}\"]"
     xhrUrl = element.getAttribute('data-pwnfx-delayed-url')
     xhrMethod = element.getAttribute('data-pwnfx-delayed-method') || 'POST'
     xhrForm = PwnFx.parentForm element
@@ -254,11 +257,12 @@
 #   data-pwnfx-refresh-method: the HTTP method of AJAX request (default: POST)
 #   data-pwnfx-refresh-ms: delay between a change on the source element and
 #                          AJAX refresh requests (default: 200ms)
-#   data-pwnfx-refresh-target: set to the value of data-pwnfx-refresh on the
-#                              element populated with the AJAX response
+#   data-pwnfx-refresh-target: a space-separated list of identifiers, one of which
+#       should match the value of data-pwnfx-refresh; set on the element
+#       populated with the AJAX response
 class PwnFxRefresh
   constructor: (element, identifier, scopeId) ->
-    targetSelector = "[data-pwnfx-refresh-target=\"#{identifier}\"]"
+    targetSelector = "[data-pwnfx-refresh-target~=\"#{identifier}\"]"
     xhrUrl = element.getAttribute('data-pwnfx-refresh-url')
     xhrMethod = element.getAttribute('data-pwnfx-refresh-method') || 'POST'
     xhrForm = PwnFx.parentForm element
@@ -444,11 +448,12 @@
 #
 # Attributes:
 #   data-pwnfx-remove: an identifier connecting the elements to be removed
-#   data-pwnfx-remove-target: set to the same value as data-pwnfx-remove on
-#       elements that will be removed when the element is clicked
+#   data-pwnfx-remove-target: a space-separated list of identifiers, one of which
+#       should match the value of data-pwnfx-remove; set on elements that will
+#       be removed when the element is clicked
 class PwnFxRemove
   constructor: (element, identifier, scopeId) ->
-    targetSelector = "[data-pwnfx-remove-target=\"#{identifier}\"]"
+    targetSelector = "[data-pwnfx-remove-target~=\"#{identifier}\"]"

     onClick = (event) ->
       scope = PwnFx.resolveScope scopeId, element


check if prop tooltip

check if prop tooltip

--- src/components/name.cjsx
+++ src/components/name.cjsx
@@ -19,7 +19,7 @@

     span = {name}

-    if @props.tooltip.enable
+    if @props.tooltip?.enable
       tooltip = <BS.Tooltip>{name}</BS.Tooltip>
       <BS.OverlayTrigger
         placement={@props.tooltip.placement}


Extract method (refactor only)

Extract method (refactor only)

--- app/assets/javascripts/static.js.coffee
+++ app/assets/javascripts/static.js.coffee
@@ -5,18 +5,21 @@
 roundUpBy = (value, round_to) ->
   return round_to * Math.ceil(value / round_to)

+formatNumber = (number) ->
+  Math.ceil(number).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
+
 animateNumber = (element, target_count, duration) ->
   jQuery(counter: 0).animate { counter: target_count },
     duration: duration
     easing: "easeInOutQuint"
     step: ->
-      element.text Math.ceil(@counter).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
+      element.text formatNumber(@counter)
     complete: ->
       jQuery(counter: target_count).animate { counter: (40 + target_count) },
         duration: 400
         easing: "easeOutQuint"
         step: ->
-          element.text Math.ceil(@counter).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
+          element.text formatNumber(@counter)
           return
       return
   return


Split link dest from link reference, more explicit

Still fails, though. cc @aroben

--- app/assets/javascripts/task_list.coffee
+++ app/assets/javascripts/task_list.coffee
@@ -125,10 +125,20 @@
     #{escapePattern(complete)}|
     #{escapePattern(incomplete)}
   )
-  (?!                     # is not a link
-    \s*                   # with optional whitespace
-    (?:\(.*?\)|\[.*?\])   # because of destination or reference
-  )
+  (?!                     # but not a link to this destination
+    \s*
+    \(.*?\)
+  )
+  (?=                     # and not part of a link reference
+    \s*
+    (?:(?:\[.*?\]\s*){2})*
+    (?:[^\[]|$)
+  )
+  # (?!                     # is not a link
+  #   \s*                   # with optional whitespace
+  #   (?:\(.*?\)|\[.*?\])   # because of destination or reference
+  #   (?:\s*[^\[\(])        # but itself not first part of a link
+  # )
   (?=\s)                  # is followed by whitespace
 ///


アビリティ検索条件を修正

アビリティ検索条件を修正

--- app/assets/javascripts/viewer.js.coffee
+++ app/assets/javascripts/viewer.js.coffee
@@ -537,7 +537,6 @@
     'atkup_random'
     'defup_all'
     'speedup_all'
-    'defdown_all'
     'buff_jobs'
     'boost_skill'
     'mana_charge'
@@ -581,7 +580,7 @@
     'guard_seal'
     'atkup_all'
     'defup_all'
-    'buff_all'
+    'defdown_all'
     'mana_charge'
     'mana_boost'
     'treasure'


permit any whole number for tabLength

permit any whole number for tabLength

from #8261

--- src/config-schema.coffee
+++ src/config-schema.coffee
@@ -171,7 +171,7 @@
       tabLength:
         type: 'integer'
         default: 2
-        enum: [1, 2, 3, 4, 6, 8]
+        minimum: 1
         description: 'Number of spaces used to represent a tab.'
       softWrap:
         type: 'boolean'


Make the wording more clear and bold the Note

--- src/config-schema.coffee
+++ src/config-schema.coffee
@@ -197,7 +197,7 @@
       softWrapAtPreferredLineLength:
         type: 'boolean'
         default: false
-        description: 'Instead of wrapping lines to the window\'s width, wrap lines to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when the soft wrap config setting is enabled globally or for the current language. Note: If you like to hide the wrap guide (the vertical line) you can disable the `wrap-guide` package.'
+        description: 'Instead of wrapping lines to the window\'s width, wrap lines to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when the soft wrap config setting is enabled globally or for the current language. **Note:** If you want to hide the wrap guide (the vertical line) you can disable the `wrap-guide` package.'
       softWrapHangingIndent:
         type: 'integer'
         default: 0


SmallFix: Fixed problem working with tasks created on taskboard

--- app/coffee/controllers/taskboard.coffee
+++ app/coffee/controllers/taskboard.coffee
@@ -220,6 +220,8 @@
             $scope.$emit("spinner:stop")
             closeModal()
             $scope.overlay.close()
+            $scope.form.id = data.id
+            $scope.form.ref = data.ref
             $scope.defered.resolve($scope.form)
             $gmFlash.info($i18next.t('taskboard.user-story-saved'))


half-baked, going to reverse it back out

--- src/kvstore.iced
+++ src/kvstore.iced
@@ -89,6 +89,9 @@

 exports.Flat = class Flat extends Base

+  constructor : () ->
+    @flat_lock = new Lock
+
   make_kvstore_key : ({type,key}) ->
     type or= key[-2...]
     "kv:" + super { type, key }
@@ -96,8 +99,27 @@
   make_lookup_name : ({type,name}) ->
     "lo:" + super { type, name }

+  _get_reverse_lookup : ({name}, cb) ->
+    await @_get { key : ("r" + name) }, defer err, rlookup
+    if err? and (err instanceof E.NotFoundError)
+      err = null
+      rlookup = []
+    cb err, rlookup
+
+  _put_reverse_lookup : ({name, list}, cb) ->
+    await @_put { key : ("r" + name), value : list }, defer err
+    cb err
+
   _link : ({key, name}, cb) ->
-    await @_put { key : name, value : key }, defer err
+    await @flat_lock.acquire defer()
+    esc = make_esc cb, "Flat::_link"
+    await @_get_reverse_lookup { name }, defer err, list
+    unless err?
+      list.push name
+      await @_put_reverse_lookup { name, list }, defer err
+    unless err?
+      await @_put { key : name, value : key }, defer err
+    @flat_lock.release()
     cb err

   _unlink : ({name}, cb) ->
@@ -114,3 +136,8 @@
       err = new E.LookupNotFoundError "No lookup available for #{name}"

 ##=======================================================================
+
+# A memory-backed store, mainly for testing...
+exports.Memory = class Memory extends Base
+
+##=======================================================================


No more need to override setImmediate

No more need to override setImmediate

It is now done in Node.js.

--- atom/common/lib/init.coffee
+++ atom/common/lib/init.coffee
@@ -37,8 +37,6 @@
     process.activateUvLoop()
     func.apply this, arguments
 process.nextTick = wrapWithActivateUvLoop process.nextTick
-global.setImmediate = wrapWithActivateUvLoop timers.setImmediate
-global.clearImmediate = timers.clearImmediate

 # setTimeout needs to update the polling timeout of the event loop, when called
 # under Chromium's event loop the node's event loop won't get a chance to update


Ignore commands within comments when parsing document

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee
@@ -39,7 +39,7 @@
 			return commands

 		# Ignore single letter commands since auto complete is moot then.
-		commandRegex: /\\([a-zA-Z][a-zA-Z]+)/
+		commandRegex: /^[^%]*?\\([a-zA-Z][a-zA-Z]+).*$/m

 		nextCommand: () ->
 			i = @doc.search(@commandRegex)


Cleaner style.

Cleaner style.

--- src/lexer.coffee
+++ src/lexer.coffee
@@ -134,8 +134,8 @@
     return 0 unless match = NUMBER.exec @chunk
     number = match[0]
     lexedLength = number.length
-    if is_binary = /0b([01]+)/.exec number
-      number = (parseInt is_binary[1], 2).toString()
+    if binaryLiteral = /0b([01]+)/.exec number
+      number = (parseInt binaryLiteral[1], 2).toString()
     @token 'NUMBER', number
     lexedLength


don't mind me

don't mind me

--- src/atom/modal-selector.coffee
+++ src/atom/modal-selector.coffee
@@ -48,7 +48,6 @@
       @filter()

   show: ->
-    console.log 'cool'
     super
     @filter()


accept 'youtu.be' videos

accept 'youtu.be' videos

--- src/scripts/controllers/subreddit.coffee
+++ src/scripts/controllers/subreddit.coffee
@@ -10,11 +10,12 @@
     $scope.noMorePosts = null
     $scope.loadingPosts = false

+    validDomains = ['youtube.com', 'youtu.be']
     validPost = (post) ->
         # when switching subreddits,
         # the post might be leftover from a different subreddit.
         # we only know how to youtube, currently.
-        (post.domain is 'youtube.com') and (post.subreddit.toLowerCase() is $scope.currentSubreddit)
+        (post.domain in validDomains) and (post.subreddit.toLowerCase() is $scope.currentSubreddit)

     ensureEnoughPosts = ->
         if $scope.posts.length < 8 and not $scope.noMorePosts


fixed animating not working sometimes

fixed animating not working sometimes

--- lib/autohide-tree-view.coffee
+++ lib/autohide-tree-view.coffee
@@ -1,17 +1,6 @@
 'use strict'
 path = require 'path'
 SubAtom = require 'sub-atom'
-
-# debug = do ->
-#   log = ''
-#   length = 0
-#   timeout = null
-#
-#   (message) ->
-#     clearTimeout timeout
-#     timeout = setTimeout ->
-#       fs.writeFileSync "#{__dirname}/../log", log
-#     , 500

 class AutohideTreeView
   treeView = null
@@ -108,18 +97,15 @@
     @initialized = true

   enable: (treeViewPkg) ->
-    try
-      treeViewPkg ?= atom.packages.getActivePackage 'tree-view'
-      return unless treeViewPkg?
-      @enabled = true
-      treeView = treeViewPkg.mainModule.createView()
-      treeViewEl = atom.views.getView treeView
-      treeViewEl.classList.add 'autohide', 'autohide-hover-events'
-      @applyHiddenWidth()
-      @applyAnimate()
-      @hide true
-    catch e
-      console.error e
+    treeViewPkg ?= atom.packages.getActivePackage 'tree-view'
+    return unless treeViewPkg?
+    @enabled = true
+    treeView = treeViewPkg.mainModule.createView()
+    treeViewEl = atom.views.getView treeView
+    treeViewEl.classList.add 'autohide', 'autohide-hover-events'
+    @applyHiddenWidth()
+    @applyAnimate()
+    @hide true

   disable: ->
     @enabled = false
@@ -140,6 +126,7 @@
   show: (noDelay, disableHoverEvents) ->
     return unless @enabled
     @applyHiddenWidth()
+    @applyAnimate()
     width = treeViewEl.querySelector('.tree-view').clientWidth
     if width > treeViewEl.clientWidth > @getHiddenWidth()
       noDelay = true


Streamlining manual RTTI

Streamlining manual RTTI

--- src/lists.coffee
+++ src/lists.coffee
@@ -82,6 +82,7 @@
       inner l, seq

   {
+    List: ConsList
     cons: cons
     nil: nil
     car: car


Scroll a bit less to stop at the top of Violations (re #19)

--- app/scripts/directives/headerbar.coffee
+++ app/scripts/directives/headerbar.coffee
@@ -57,7 +57,7 @@
           scope.toggleAddPage()

       _scrollToElement = (el) ->
-        srcollY = el[0].offsetTop + parseInt(el.css('margin-top'), 10) * 2
+        srcollY = el[0].offsetTop + parseInt(el.css('margin-top'), 10) / 2
         dy = Math.abs($window.pageYOffset - srcollY)
         $window.scrollTo(0, srcollY)
         return dy


hide date from logs final fix

hide date from logs final fix

--- lib/actions/views/logs.coffee
+++ lib/actions/views/logs.coffee
@@ -47,7 +47,7 @@
     showMore()

 this.renderLog = (data, level, ignore_date) ->
-  if ignore_date == new Date(data.start * 1000)
+  if ignore_date == new Date(data.start * 1000).toLocaleFormat("%Y-%m-%d")
     data.date = new Date(data.start * 1000).toLocaleFormat("%H:%M:%S")
   else
     data.date = new Date(data.start * 1000).toLocaleFormat("%Y-%m-%d %H:%M:%S")


override tree-view show|hide|toggle commands

--- lib/autohide-tree-view.coffee
+++ lib/autohide-tree-view.coffee
@@ -154,7 +154,15 @@

     # respond to tree view commands
     @disposables.add atom.commands.add 'atom-workspace',
-      'tree-view:toggle': => @update()
+      'tree-view:show': (event) =>
+        event.stopImmediatePropagation()
+        @show 0, true
+      'tree-view:hide': (event) =>
+        event.stopImmediatePropagation()
+        @hide 0
+      'tree-view:toggle': (event) =>
+        event.stopImmediatePropagation()
+        @toggle()
       'tree-view:show': => @update()
       'tree-view:toggle-side': => @update()
       'tree-view:reveal-active-file': => @show 0, true


check if current project folder is directory and use path otherwise. This may very well be a bug in Atom, since it allows files to be set as project folder.

--- lib/connection/process.coffee
+++ lib/connection/process.coffee
@@ -32,7 +32,7 @@

   workingDir: ->
     paths = atom.workspace.project.getDirectories()
-    if paths.length == 1
+    if paths.length == 1 and fs.statSync(paths[0].path).isDirectory()
       paths[0].path
     else
       process.env.HOME || process.env.USERPROFILE


add gzip output

add gzip output

--- lib/carte/client/tasks.coffee
+++ lib/carte/client/tasks.coffee
@@ -3,6 +3,7 @@
 gulp = require 'gulp'
 gulpUtil = require 'gulp-util'
 gulpIf = require 'gulp-if'
+gulpGzip = require 'gulp-gzip'
 sourceStream = require 'vinyl-source-stream'
 browserify = require 'browserify'
 watchify = require 'watchify'
@@ -75,4 +76,5 @@
       .bundle()
       .pipe sourceStream file
       .pipe gulpIf(!@watching, streamify(uglify(compress: {drop_console: !@watching})))
+      .pipe gulpGzip()
       .pipe gulp.dest dir


configure: exemple

configure: exemple

--- lib/commands/configure.coffee
+++ lib/commands/configure.coffee
@@ -11,9 +11,19 @@
     CSON = require 'cson'
     string = require 'nikita/lib/misc/string'
     Module = require 'module'
+    run = require '../run'

     # ./bin/ryba configure -o output_file -p JSON
     module.exports = ->
+      # EXAMPLE START
+      params = params.parse()
+      config params.config, (err, config) ->
+        # console.log config
+        {contexts} = run params, config
+        console.log contexts[0].config
+        process.exit()
+      return
+      # EXAMPLE END
       # behave like the first pass of run command
       # creates a context perserver by executing all configure functions
       valid_extensions = ['json','cson', 'js', 'coffee']


Throw FormErrors in validation methods

Throw FormErrors in validation methods

--- lib/documents/accounts.coffee
+++ lib/documents/accounts.coffee
@@ -22,21 +22,21 @@
       updatedAt: UpdatedAtTrigger ['username', 'emails', 'person._id']
       lastActivity: LastActivityTrigger ['services']

-  @validateUsername = (username) ->
-    throw new Meteor.Error 400, "Username must be at least 3 characters long." unless username and username.length >= 3
+  @validateUsername = (username, field) ->
+    throw new FormError 400, "Username must be at least 3 characters long.", field unless username and username.length >= 3

-    throw new Meteor.Error 400, "Username must contain only a-zA-Z0-9_- characters." unless USERNAME_REGEX.test username
+    throw new FormError 400, "Username must contain only a-zA-Z0-9_- characters.", field unless USERNAME_REGEX.test username

-    throw new Meteor.Error 400, "Username already exists." if FORBIDDEN_USERNAME_REGEX.test username
+    throw new FormError 400, "Username already exists.", field if FORBIDDEN_USERNAME_REGEX.test username

     # Check for unique username in a case insensitive manner.
     # We do not have to escape username because we have already
     # checked that it contains only a-zA-Z0-9_- characters.
-    throw new Meteor.Error 400, "Username already exists." if User.documents.findOne username: new RegExp "^#{ username }$", 'i'
+    throw new FormError 400, "Username already exists.", field if User.documents.findOne username: new RegExp "^#{ username }$", 'i'

     # Username must not match any existing Person _id otherwise our queries for
     # Person documents querying both _id and slug would return multiple documents
-    throw new Meteor.Error 400, "Username already exists." if Person.documents.findOne _id: username
+    throw new FormError 400, "Username already exists.", field if Person.documents.findOne _id: username

-  @validatePassword = (password) ->
-    throw new Meteor.Error 400, "Password must be at least 6 characters long." unless password and password.length >= 6
+  @validatePassword = (password, field) ->
+    throw new FormError 400, "Password must be at least 6 characters long.", field unless password and password.length >= 6


use different env variable for fs path

use different env variable for fs path

--- lib/nsync/nsync-helper.coffee
+++ lib/nsync/nsync-helper.coffee
@@ -21,7 +21,7 @@
   config = _.defaults
     host: process.env['IDE_WS_HOST']
     port: process.env['IDE_WS_PORT']
-    path: process.env['IDE_WS_PATH']
+    path: process.env['IDE_WS_FS_PATH']
   ,
     host: '159.203.101.28',
     port: 8080,


use share code, relativize path/ show project name  for bookmarks provider

--- lib/provider/bookmarks.coffee
+++ lib/provider/bookmarks.coffee
@@ -35,7 +35,8 @@
   getItems: ->
     items = []
     for {editor, markerLayer} in getBookmarks() when markerLayer.getMarkerCount() > 0
-      filePath = editor.getPath()
-      items.push(header: "# #{filePath}", filePath: filePath, skip: true)
       items.push(@getItemsForEditor(editor, markerLayer)...)
-    items
+    @getItemsWithHeaders(items)
+
+  filterItems: (items, filterSpec) ->
+    @getItemsWithoutUnusedHeader(super)


app/lazyrouter: lowercase name since it starts with upcase now

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

 dispatch = (m, type, info, state, path) ->
-  emitter.emit m.name, type, info, state, path, this
+  emitter.emit m.name.toLowerCase(), type, info, state, path, this

 exports.bind = (name, fn) ->


remove constructor.

remove constructor.

--- lib/simple-flake8-view.coffee
+++ lib/simple-flake8-view.coffee
@@ -60,16 +60,6 @@
     mcCabeComplexityThreshold: ""
     validateOnSave: true
   keyBindings: null
-
-  constructor: (serializedState) ->
-    atom.workspaceView.command 'simple-flake8:toggle', => @toggle()
-    atom.workspaceView.command 'core:save', =>
-      on_save = atom.config.get "simple-flake8.validateOnSave"
-      if on_save == true
-        @toggle()
-
-  # Returns an object that can be retrieved when package is activated
-  serialize: ->

   @activate: ->
     view = new SimpleFlake8View


about to rename

about to rename

--- ref/gen_combine_text_vectors.iced
+++ ref/gen_combine_text_vectors.iced
@@ -1,10 +1,9 @@

 {prng} = require 'crypto'
-{sign} = require '../src/hmax'
 {WordArray} = require '../src/wordarray'

 out = []
-for kl in [16...128] by 4
+for kl in [16...256] by 8  # make them even multiple of 32-bit-guys
   for ml in [0...2048] by 256
     k = prng kl
     m = prng ml


tweak, "Visual" is a good label for characterwise visual

--- lib/status-bar-manager.coffee
+++ lib/status-bar-manager.coffee
@@ -6,7 +6,6 @@
   insert:  ["status-bar-vim-mode-insert", "Insert"]
   command: ["status-bar-vim-mode-command", "Command"]
   visual:  ["status-bar-vim-mode-visual", "Visual"]
-  visual$characterwise: ["status-bar-vim-mode-visual", "Visual Char"]
   visual$linewise:      ["status-bar-vim-mode-visual", "Visual Line"]
   visual$blockwise:     ["status-bar-vim-mode-visual", "Visual Block"]


Fix Bitbucket to use url

Fix Bitbucket to use url

--- plugins/bitbucket-issues/index.coffee
+++ plugins/bitbucket-issues/index.coffee
@@ -2,7 +2,8 @@
 url = require "url"
 qs = require 'qs'

-class BitbucketIssue extends NotificationPlugin
+class BitbucketIssue extends NotificationPlugin
+  BASE_URL = "https://bitbucket.org"
   @receiveEvent: (config, event, callback) ->

     query_object =
@@ -13,7 +14,7 @@

     # Send the request
     @request
-      .post(url.resolve(config.url, "/api/1.0/repositories/#{config.repo}/issues"))
+      .post(url.resolve(BASE_URL, "/api/1.0/repositories/#{config.repo}/issues"))
       .timeout(4000)
       .auth(config.username, config.password)
       .set('Accept', 'application/json')
@@ -25,6 +26,6 @@

         callback null,
           id: res.body.local_id
-          url: url.resolve(config.url, "#{res.body.resource_uri}")
-
+          url: url.resolve(BASE_URL, "#{res.body.resource_uri}")
+
 module.exports = BitbucketIssue


Add Extension class.

Add Extension class.

The Extension class is only a placeholder at this point, which may be
extended in the future to provide additional functionality for
extensions.


Defer migration 36

Defer migration 36

--- server/startup/migrations/v36.coffee
+++ server/startup/migrations/v36.coffee
@@ -15,9 +15,13 @@

 			try
 				Meteor.startup ->
-					result = HTTP.get requestUrl, npmRequestOptions: {encoding: 'binary'}
-					if result.statusCode is 200
-						RocketChat.Assets.setAsset(result.content, result.headers['content-type'], 'logo')
+					Meteor.setTimeout ->
+						result = HTTP.get requestUrl, npmRequestOptions: {encoding: 'binary'}
+						if result.statusCode is 200
+							RocketChat.Assets.setAsset(result.content, result.headers['content-type'], 'logo')
+					, 5000
+			catch e
+				console.log e

 		RocketChat.models.Settings.remove _id: 'Layout_Login_Header'


fixes random failures of event array comparisons

--- tests/infrastructure/commit_store.unit.coffee
+++ tests/infrastructure/commit_store.unit.coffee
@@ -75,40 +75,35 @@

     it 'returns all events versioned by batch for given aggregate', ->

-      fakeTimers = sinon.useFakeTimers('Date')
       sourceId = '123'
-      firstChanges = events: [new CreatedEvent sourceId: sourceId]
+      firstEvent = new CreatedEvent sourceId: sourceId
+      secondEvent = new QuantityChangedEvent sourceId: sourceId, quantity: 1
+      thirdEvent = new TotalChangedEvent sourceId: sourceId, total: 10

-      secondChanges = events: [
-        new QuantityChangedEvent sourceId: sourceId, quantity: 1
-        new TotalChangedEvent sourceId: sourceId, total: 10
-      ]
-
-      @commitStore.add firstChanges, sourceId, 0
-      @commitStore.add secondChanges, sourceId, 1
+      @commitStore.add { events: [firstEvent] }, sourceId, 0
+      @commitStore.add { events: [secondEvent, thirdEvent] }, sourceId, 1

       events = @commitStore.getEvents sourceId

-      for event in events
-        expect(event).to.be.instanceof Event
+      expect(event).to.be.instanceof(Event) for event in events

-      expect(events).to.deep.equal [
-        new CreatedEvent sourceId: sourceId, version: 1
-        new QuantityChangedEvent sourceId: sourceId, quantity: 1, version: 2
-        new TotalChangedEvent sourceId: sourceId, total: 10, version: 2
-      ]
-      fakeTimers.restore()
+      # Set expected versions of the events
+      firstEvent.version = 1
+      secondEvent.version = thirdEvent.version = 2
+      expect(events).toMatch [firstEvent, secondEvent, thirdEvent]

     it 'skips events if a version offset is given', ->

       sourceId = '123'
       versionOffset = 2
-      @commitStore.add {events: [new CreatedEvent sourceId: sourceId]}, sourceId, 0
-      @commitStore.add {events: [new QuantityChangedEvent sourceId: sourceId, quantity: 1]}, sourceId, 1
-      @commitStore.add {events: [new TotalChangedEvent sourceId: sourceId, total: 10]}, sourceId, 2
+      firstEvent = new CreatedEvent sourceId: sourceId
+      secondEvent = new QuantityChangedEvent sourceId: sourceId, quantity: 1
+      thirdEvent = new TotalChangedEvent sourceId: sourceId, total: 10
+      @commitStore.add {events: [firstEvent]}, sourceId, 0
+      @commitStore.add {events: [secondEvent]}, sourceId, 1
+      @commitStore.add {events: [thirdEvent]}, sourceId, 2

       events = @commitStore.getEvents sourceId, versionOffset
-      expect(events).toMatch [
-        new QuantityChangedEvent sourceId: sourceId, quantity: 1, version: 2
-        new TotalChangedEvent sourceId: sourceId, total: 10, version: 3
-      ]
+      secondEvent.version = 2
+      thirdEvent.version = 3
+      expect(events).toMatch [secondEvent, thirdEvent]


switch to deep equal as it seems to be more stable

I used our custom toMatch expectation which matches
the JSONified versions. It seems like for complex
objects and arrays it sometimes fails for unknown
reasons (should look into that).

--- tests/infrastructure/commit_store.unit.coffee
+++ tests/infrastructure/commit_store.unit.coffee
@@ -92,7 +92,7 @@
       for event in events
         expect(event).to.be.instanceof Event

-      expect(events).toMatch [
+      expect(events).to.deep.equal [
         new CreatedEvent sourceId: sourceId, version: 1
         new QuantityChangedEvent sourceId: sourceId, quantity: 1, version: 2
         new TotalChangedEvent sourceId: sourceId, total: 10, version: 2


Fix copy dashboard action to redirect to new url

--- app/app_view.coffee
+++ app/app_view.coffee
@@ -66,7 +66,7 @@
   forkDashboard: =>
     @dashboardModel.fork().done (response) =>
       @dashboardModel = new DashboardModel response
-      @loadUbretTools()
+      @navigateToDashboard()

   createDashboardFromDialog: =>
     dashboardDialog = new DashboardDialog { parent: @ }


fix find promise to return data

fix find promise to return data

--- app/graphite.coffee
+++ app/graphite.coffee
@@ -183,6 +183,7 @@
     promise = graphite.find(query)
     .then (r) =>
       results.set r.result
+      r
     .fail (reason) =>
       Q.reject 'Find request failed'


State at user movie list

State at user movie list

--- api/user.coffee
+++ api/user.coffee
@@ -21,7 +21,12 @@
       if error
         response.json message: error.message, error.code
       else
-        response.json movies: (item.movie.parse() for item in list)
+        movies = []
+        for item in list
+          movie = item.movie.parse()
+          movie.state = item.state
+          movies.push movie
+        response.json movies: movies

   server.post "/api/user/movie", (request, response) ->
     if request.required ["imdb"]


Adding links

Adding links

--- app/pages/home.cjsx
+++ app/pages/home.cjsx
@@ -21,18 +21,17 @@
     about:
       contribute:
         title: 'Contribute to new research'
-        content: '''The Zooniverse lets everyone take part in real, cutting-edge research in many fields across the sciences, humanities, and more. There’s no previous experience required; just pick a project and get started right away.'''
+        content: '''The Zooniverse lets everyone take part in real, cutting-edge research in many fields across the sciences, humanities, and more. There’s no previous experience required; just pick a project and [get started right away](./#/projects).'''
       explore:
         title: 'Explore incredible worlds'
         content: '''Astronomical marvels, exotic wildlife in their natural habitats, original historical documents: just a few of the fascinating things you’ll get to experience in the Zooniverse. In many cases, you’ll literally be seeing things no one has seen before.'''
       collaborate:
         title: 'Collaborate with researchers'
-        content:'''Professional researchers and volunteers share our discussion boards, using them to explore and analyse project data. Much of the most exciting research produced by the Zooniverse originates from these partnerships.'''
+        content:'''Professional researchers and volunteers share our [discussion boards](./#/talk), using them to explore and analyse project data. Much of the most exciting research produced by the Zooniverse originates from these partnerships.'''
       discover:
         title: 'Discover, teach, and learn'
         content: '''Our platform offers many opportunities for education, from using projects
-        in classrooms to sharing information between volunteers. You can even use the Project
-        Builder to create your very own Zooniverse project!'''
+        in classrooms to sharing information between volunteers. You can even use the [Zooniverse Project Builder](./#/lab) to create your very own project!'''
     featuredProjects:
       title: 'Get started on a project right now!'
       button: 'See all projects'


Make the rounding symmetric when multiplying

--- src/money.coffee
+++ src/money.coffee
@@ -72,7 +72,8 @@
       new Money(@cents - other.cents)

     multiply: (num) ->
-      new Money(Math.round(@cents * num))
+      absoluteResult = Math.abs(@cents) * num
+      new Money Math.round(absoluteResult) * @sign()

     render: ->
       "#{@currencyFormat()}"


:art move packageName to the top

:art move packageName to the top

--- lib/notification-issue.coffee
+++ lib/notification-issue.coffee
@@ -175,7 +175,9 @@

   getPackageName: ->
     options = @notification.getOptions()
-    return unless options.stack? or options.detail? or options.packageName?
+
+    return options.packageName if options.packageName?
+    return unless options.stack? or options.detail?

     packagePaths = @getPackagePathsByPackageName()
     for packageName, packagePath of packagePaths
@@ -190,8 +192,6 @@
           return packName unless /^\.\./.test(relativePath)
       @getPackageNameFromFilePath(filePath)

-    return options.packageName if options.packageName?
-
     if options.detail? and packageName = getPackageName(options.detail)
       return packageName


Stack: check revision fix

Stack: check revision fix

--- workers/social/lib/social/models/stack.coffee
+++ workers/social/lib/social/models/stack.coffee
@@ -544,10 +544,7 @@
   checkRevisionByTemplate: (template) ->

     if not @baseStackId
-      return callback null, stackRevisionErrors.NOTFROMTEMPLATE
-
-    if @baseStackId isnt template._id
-      return stackRevisionErrors.TEMPLATEDIFFERENT
+      return stackRevisionErrors.NOTFROMTEMPLATE

     if not template?.template?.sum or not @stackRevision
       return stackRevisionErrors.INVALIDTEMPLATE


JStack: fix issues related with ::some

JStack: fix issues related with ::some

--- workers/social/lib/social/models/stack.coffee
+++ workers/social/lib/social/models/stack.coffee
@@ -149,17 +149,15 @@
       JStack.some selector, options, (err, _stacks)->

         stacks = []
-        queue  = []
-
-        for stack in _stacks
-          queue.push -> stack.revive (err, revivedStack)->
+
+        daisy queue = _stacks.map (stack) -> ->
+          stack.revive (err, revivedStack)->
             stacks.push revivedStack
             queue.next()

         queue.push ->
           callback null, stacks

-        daisy queue

   revive: (callback)->


Remove any references to the (now unneeded) pixelPerDay (and similar) variables

--- src/d3.timeslider.coffee
+++ src/d3.timeslider.coffee
@@ -3,7 +3,6 @@
     # TODO
     #  * Allow for the registration of datasets to be shown beneath the brush
     #    * WCS  DescribeEOCoverageSet (using libcoveragejs (schindlerf))
-    #  * Rename pixelPerDay to something more generic (could also be hours, ... depending on the time frame)
     #  * Cleanup the mess that is the axis labels right now
     #  * Compute the padding at the left & right of the timeslider
     #  * Convert all dates to UTC
@@ -18,18 +17,11 @@
         @root = svg.append('g').attr('class', 'root')

         # default options and other variables for later
-        @options.minPixelPerDay ||= 50
         @options.width  = svg[0][0].clientWidth
         @options.height = svg[0][0].clientHeight
         @options.brush ||= {}
         @options.brush.start || = @options.start
         @options.brush.end ||= new Date(new Date(@options.brush.start).setDate(@options.brush.start.getDate() + 3))
-        # compute the number of days, (end - start / milliseconds per day)
-        @options.numberOfDays = Math.ceil( (@options.end.getTime() - @options.start.getTime()) / (1000 * 60 * 60 * 24) )
-        @element.zoomLevel = 0
-
-        @options.pixelPerDay = @options.width / @options.numberOfDays
-        @options.pixelPerDay = @options.minPixelPerDay if @options.pixelPerDay < @options.minPixelPerDay

         # array to hold individual data points / data ranges
         @data = {}


Stick with setting DOM ranges synchronously

We're setting them way less frequently now since we no longer render with each key press, and we're also typically setting them outside of the key event call stack so the slowness in WebKit isn't an issue.

--- src/trix/models/selection_manager.coffee
+++ src/trix/models/selection_manager.coffee
@@ -22,11 +22,7 @@
       new Trix.LocationRange start, end

     if domRange = @createDOMRangeFromLocationRange(locationRange)
-      # Selection#addRange is unreasonably slow in WebKit when performed in the
-      # same call stack as a mouse or key event so defer calling it.
-      # https://code.google.com/p/chromium/issues/detail?id=423170
-      # https://code.google.com/p/chromium/issues/detail?id=138439
-      setDOMRange(domRange) if document.activeElement is @element
+      setDOMRange(domRange)
       @updateCurrentLocationRange(locationRange)

   setLocationRangeFromPoint: (point) ->


Removed Flash via USB menu item

Removed Flash via USB menu item

--- lib/utils/menu-manager.coffee
+++ lib/utils/menu-manager.coffee
@@ -54,9 +54,9 @@
         label: 'Setup device\'s WiFi...',
         command: 'spark-dev:setup-wifi'
       },{
-        label: 'Flash device via USB',
-        command: 'spark-dev:try-flash-usb'
-      },{
+      #   label: 'Flash device via USB',
+      #   command: 'spark-dev:try-flash-usb'
+      # },{
         type: 'separator'
       },{
         label: 'Compile in the cloud',


Removed unused functions from utils.coffee

--- src/scripts/utils.coffee
+++ src/scripts/utils.coffee
@@ -32,33 +32,3 @@
         return "#{Math.floor(diff / 60)}m"
     if (!day_diff && diff < 86400)
         return "#{Math.floor(diff / 3600)}h"
-
-
-vendors = ['webkit', 'moz', 'ms']
-
-
-@Fontana.utils.requestFullScreen = (el) ->
-    request = el.requestFullscreen || el.requestFullScreen
-    vendors.some (vendor) ->
-        request ?= el[vendor + 'RequestFullScreen']
-        return !!request
-    if request
-        request.call(el, Element.ALLOW_KEYBOARD_INPUT)
-
-
-@Fontana.utils.cancelFullScreen = ->
-    request = document.exitFullscreen || document.cancelFullScreen
-    vendors.some (vendor) ->
-        request ?= document[vendor + 'CancelFullScreen']
-        return !!request
-    if request
-        request.call(document)
-
-
-@Fontana.utils.isFullScreen = ->
-    request = document.fullScreen || document.isFullScreen
-    vendors.some (vendor) ->
-        request ?= document[vendor + 'FullScreen'] || document[vendor + 'IsFullScreen']
-        return !!request
-    return !!request
-


a couple of fixes

a couple of fixes

--- library/exposure/fonts.coffee
+++ library/exposure/fonts.coffee
@@ -67,10 +67,10 @@
         assert googlefonts = try googlefonts.reverse()
         j = (strc) -> return strc.typographs.join ","
         t = "http://fonts.googleapis.com/css?family=%s"
+        foldtyp = (x) -> family: x.family, joined: j(x)
         infusor = (fx) -> "#{fx.family}:#{fx.joined}"
-        compare = (xa, xb) -> xa.family is xb.family
-        foldtyp = (x) -> family: x.family, joined: j(x)
-        googlefonts = _.unique googlefonts, compare
+        extract = (record) -> try return record.family
+        googlefonts = _.unique googlefonts, extract
         assert prepped = _.map googlefonts, foldtyp
         assert infused = try _.map prepped, infusor
         assert not _.isEmpty blob = infused.join "|"
@@ -91,8 +91,8 @@
         assert typographs = _.map typographs, string
         assert _.all(typographs, isntEmpty), empty
         @googlefonts = previous.concat new Object
+            family: family.replace /\s/g, "+"
             typographs: _.toArray typographs
-            family: family.replace /\s/, "+"

     # This is the composition hook that gets invoked when compound
     # is being composed into other services and components. Merges


fix misnomer

fix misnomer

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -183,7 +183,7 @@
     interceptExceptions: ->
         assert crash = "kernel:crashOnException"
         assert @domain = require("domain").create()
-        fatal = => @shutdownKernel "kernel panic error"
+        fatal = => @shutdownKernel "fatal kernel panic"
         str = (err) -> err.stack or err.message or err
         assert bark = "kernel domain panic:\r\n%s".red
         @on "panic", (e) -> logger.error bark, str(e)


Apply setAccessors to SetSort before we override them.

--- src/set/set_proxy.coffee
+++ src/set/set_proxy.coffee
@@ -25,11 +25,11 @@
     length.expose()
     result

+  Batman.Set._applySetAccessors(@)
+
   for k in ['add', 'remove', 'find', 'clear', 'has', 'merge', 'toArray', 'isEmpty', 'indexedBy', 'indexedByUnique', 'sortedBy']
     do (k) =>
       @::[k] = -> @base[k](arguments...)
-
-  Batman.Set._applySetAccessors(@)

   @accessor 'length',
     get: ->


proxy server -- also support websocket

proxy server -- also support websocket

--- src/smc-hub/proxy.coffee
+++ src/smc-hub/proxy.coffee
@@ -393,10 +393,7 @@
                     #    dbg("(mark: #{misc.walltime(tm)}) got response from the target")

                 if internal_url?
-                    dbg("changing req.url '#{internal_url}'")
                     req.url = internal_url
-                else
-                    dbg("leaving req.url unchanged")
                 proxy.web(req, res)

     winston.debug("starting proxy server listening on #{opts.host}:#{opts.port}")
@@ -411,7 +408,7 @@

         req_url = req.url.slice(base_url.length)  # strip base_url for purposes of determining project location/permissions
         dbg = (m) -> winston.debug("http_proxy_server websocket(#{req_url}): #{m}")
-        target undefined, req_url, (err, location) ->
+        target undefined, req_url, (err, location, internal_url) ->
             if err
                 dbg("websocket upgrade error -- #{err}")
             else
@@ -428,6 +425,8 @@
                     _ws_proxy_servers[t] = proxy
                 else
                     dbg("websocket upgrade -- using cache")
+                if internal_url?
+                    req.url = internal_url
                 proxy.ws(req, socket, head)

     public_raw_paths_cache = {}


:racehorse: Cache screen lines

:racehorse: Cache screen lines

We use the spatial line id as our cache key: we don’t care about the
invalidation count because we will always have only one screen line per
spatial line.

:f5725c40a856541d86da91b2d9a933819d8610a2@nathansobo :sparkles:

--- src/display-layer.coffee
+++ src/display-layer.coffee
@@ -24,6 +24,7 @@
     @textDecorationLayer = new EmptyDecorationLayer
     @emitter = new Emitter
     @invalidationCountsByScreenLineId = new Map
+    @screenLinesBySpatialLineId = new Map
     @reset({
       invisibles: settings.invisibles ? {}
       tabLength: settings.tabLength ? 4
@@ -161,12 +162,14 @@
     deletedScreenLineIds = @screenLineIndex.splice(startScreenRow, oldRowExtent, newScreenLines)
     for screenLineId in deletedScreenLineIds
       @invalidationCountsByScreenLineId.delete(screenLineId)
+      @screenLinesBySpatialLineId.delete(screenLineId)
     return

   invalidateScreenLines: (screenRange) ->
     for screenLineId in @screenLineIdsForScreenRange(screenRange)
       count = @invalidationCountsByScreenLineId.get(screenLineId) ? 0
       @invalidationCountsByScreenLineId.set(screenLineId, count + 1)
+      @screenLinesBySpatialLineId.delete(screenLineId)
     return

   decorationLayerDidInvalidateRange: (bufferRange) ->
@@ -663,90 +666,101 @@
     screenLines = []
     @screenLineIterator.seekToScreenRow(startRow)
     containingTags = decorationIterator.seek(@screenLineIterator.getBufferStart())
+    previousLineWasCached = false

     while @screenLineIterator.getScreenRow() < endRow
-      bufferStart = @screenLineIterator.getBufferStart()
-      tokens = []
-      spatialDecoration = null
-      closeTags = []
-      openTags = containingTags.slice()
-      atLineStart = true
-
-      if comparePoints(decorationIterator.getPosition(), bufferStart) < 0
-        bufferRow = decorationIterator.getPosition().row
-        iteratorPosition = decorationIterator.getPosition()
-        throw new Error("""
-          Invalid text decoration iterator position: (#{iteratorPosition.row}, #{iteratorPosition.column}).
-          Buffer row #{bufferRow} has length #{@buffer.lineLengthForRow(bufferRow)}.
-        """)
-
-      for {screenExtent, bufferExtent, metadata} in @screenLineIterator.getTokens()
-        spatialTokenBufferEnd = traverse(bufferStart, bufferExtent)
-        tagsToClose = []
-        tagsToOpen = []
-
-        if metadata?.fold
-          @updateTags(closeTags, openTags, containingTags, containingTags.slice().reverse(), [], atLineStart)
-          tagsToReopenAfterFold = decorationIterator.seek(spatialTokenBufferEnd)
-          while comparePoints(decorationIterator.getPosition(), spatialTokenBufferEnd) is 0
-            for closeTag in decorationIterator.getCloseTags()
-              tagsToReopenAfterFold.splice(tagsToReopenAfterFold.lastIndexOf(closeTag), 1)
-            tagsToReopenAfterFold.push(decorationIterator.getOpenTags()...)
+      screenLineId = @screenLineIterator.getId()
+      if @screenLinesBySpatialLineId.has(screenLineId)
+        screenLines.push(@screenLinesBySpatialLineId.get(screenLineId))
+        previousLineWasCached = true
+      else
+        bufferStart = @screenLineIterator.getBufferStart()
+        if previousLineWasCached
+          containingTags = decorationIterator.seek(bufferStart)
+          previousLineWasCached = false
+        tokens = []
+        spatialDecoration = null
+        closeTags = []
+        openTags = containingTags.slice()
+        atLineStart = true
+
+        if comparePoints(decorationIterator.getPosition(), bufferStart) < 0
+          bufferRow = decorationIterator.getPosition().row
+          iteratorPosition = decorationIterator.getPosition()
+          throw new Error("""
+            Invalid text decoration iterator position: (#{iteratorPosition.row}, #{iteratorPosition.column}).
+            Buffer row #{bufferRow} has length #{@buffer.lineLengthForRow(bufferRow)}.
+          """)
+
+        for {screenExtent, bufferExtent, metadata} in @screenLineIterator.getTokens()
+          spatialTokenBufferEnd = traverse(bufferStart, bufferExtent)
+          tagsToClose = []
+          tagsToOpen = []
+
+          if metadata?.fold
+            @updateTags(closeTags, openTags, containingTags, containingTags.slice().reverse(), [], atLineStart)
+            tagsToReopenAfterFold = decorationIterator.seek(spatialTokenBufferEnd)
+            while comparePoints(decorationIterator.getPosition(), spatialTokenBufferEnd) is 0
+              for closeTag in decorationIterator.getCloseTags()
+                tagsToReopenAfterFold.splice(tagsToReopenAfterFold.lastIndexOf(closeTag), 1)
+              tagsToReopenAfterFold.push(decorationIterator.getOpenTags()...)
+              decorationIterator.moveToSuccessor()
+          else
+            if spatialDecoration?
+              tagsToClose.push(spatialDecoration)
+
+            if not metadata?.softLineBreak
+              if tagsToReopenAfterFold?
+                tagsToOpen.push(tagsToReopenAfterFold...)
+                tagsToReopenAfterFold = null
+
+              if comparePoints(decorationIterator.getPosition(), bufferStart) is 0
+                tagsToClose.push(decorationIterator.getCloseTags()...)
+                tagsToOpen.push(decorationIterator.getOpenTags()...)
+                decorationIterator.moveToSuccessor()
+
+          if spatialDecoration = @getSpatialTokenTextDecoration(metadata)
+            tagsToOpen.push(spatialDecoration)
+
+          @updateTags(closeTags, openTags, containingTags, tagsToClose, tagsToOpen, atLineStart)
+
+          text = @buildTokenText(metadata, screenExtent, bufferStart, spatialTokenBufferEnd)
+          startIndex = 0
+          while comparePoints(decorationIterator.getPosition(), spatialTokenBufferEnd) < 0
+            endIndex = startIndex + decorationIterator.getPosition().column - bufferStart.column
+            tokens.push({closeTags, openTags, text: text.substring(startIndex, endIndex)})
+            bufferStart = decorationIterator.getPosition()
+            startIndex = endIndex
+            closeTags = []
+            openTags = []
+            @updateTags(closeTags, openTags, containingTags, decorationIterator.getCloseTags(), decorationIterator.getOpenTags())
             decorationIterator.moveToSuccessor()
-        else
-          if spatialDecoration?
-            tagsToClose.push(spatialDecoration)
-
-          if not metadata?.softLineBreak
-            if tagsToReopenAfterFold?
-              tagsToOpen.push(tagsToReopenAfterFold...)
-              tagsToReopenAfterFold = null
-
-            if comparePoints(decorationIterator.getPosition(), bufferStart) is 0
-              tagsToClose.push(decorationIterator.getCloseTags()...)
-              tagsToOpen.push(decorationIterator.getOpenTags()...)
-              decorationIterator.moveToSuccessor()
-
-        if spatialDecoration = @getSpatialTokenTextDecoration(metadata)
-          tagsToOpen.push(spatialDecoration)
-
-        @updateTags(closeTags, openTags, containingTags, tagsToClose, tagsToOpen, atLineStart)
-
-        text = @buildTokenText(metadata, screenExtent, bufferStart, spatialTokenBufferEnd)
-        startIndex = 0
-        while comparePoints(decorationIterator.getPosition(), spatialTokenBufferEnd) < 0
-          endIndex = startIndex + decorationIterator.getPosition().column - bufferStart.column
-          tokens.push({closeTags, openTags, text: text.substring(startIndex, endIndex)})
-          bufferStart = decorationIterator.getPosition()
-          startIndex = endIndex
+
+          tokens.push({closeTags, openTags, text: text.substring(startIndex)})
+
           closeTags = []
           openTags = []
+          bufferStart = spatialTokenBufferEnd
+          atLineStart = false
+
+        if containingTags.length > 0
+          tokens.push({closeTags: containingTags.slice().reverse(), openTags: [], text: ''})
+
+        if tagsToReopenAfterFold?
+          containingTags = tagsToReopenAfterFold
+          tagsToReopenAfterFold = null
+        else if spatialDecoration?
+          containingTags.splice(containingTags.indexOf(spatialDecoration), 1)
+
+        while not @screenLineIterator.isSoftWrappedAtEnd() and comparePoints(decorationIterator.getPosition(), spatialTokenBufferEnd) is 0
           @updateTags(closeTags, openTags, containingTags, decorationIterator.getCloseTags(), decorationIterator.getOpenTags())
           decorationIterator.moveToSuccessor()

-        tokens.push({closeTags, openTags, text: text.substring(startIndex)})
-
-        closeTags = []
-        openTags = []
-        bufferStart = spatialTokenBufferEnd
-        atLineStart = false
-
-      if containingTags.length > 0
-        tokens.push({closeTags: containingTags.slice().reverse(), openTags: [], text: ''})
-
-      if tagsToReopenAfterFold?
-        containingTags = tagsToReopenAfterFold
-        tagsToReopenAfterFold = null
-      else if spatialDecoration?
-        containingTags.splice(containingTags.indexOf(spatialDecoration), 1)
-
-      while not @screenLineIterator.isSoftWrappedAtEnd() and comparePoints(decorationIterator.getPosition(), spatialTokenBufferEnd) is 0
-        @updateTags(closeTags, openTags, containingTags, decorationIterator.getCloseTags(), decorationIterator.getOpenTags())
-        decorationIterator.moveToSuccessor()
-
-      screenLineId = @screenLineIterator.getId()
-      invalidationCount = @invalidationCountsByScreenLineId.get(screenLineId) ? 0
-      screenLines.push({id: "#{screenLineId}-#{invalidationCount}", tokens})
+        invalidationCount = @invalidationCountsByScreenLineId.get(screenLineId) ? 0
+        screenLine = {id: "#{screenLineId}-#{invalidationCount}", tokens}
+        @screenLinesBySpatialLineId.set(screenLineId, screenLine)
+        screenLines.push(screenLine)
+
       break unless @screenLineIterator.moveToSuccessor()
     screenLines


check that html exists before trimming

check that html exists before trimming

--- src/quill.coffee
+++ src/quill.coffee
@@ -207,7 +207,7 @@
     this.updateContents(delta, source)

   setHTML: (html, source = Quill.sources.API) ->
-    html = "<#{dom.DEFAULT_BLOCK_TAG}><#{dom.DEFAULT_BREAK_TAG}></#{dom.DEFAULT_BLOCK_TAG}>" unless html.trim()
+    html = "<#{dom.DEFAULT_BLOCK_TAG}><#{dom.DEFAULT_BREAK_TAG}></#{dom.DEFAULT_BLOCK_TAG}>" unless html or html.trim()
     @editor.doc.setHTML(html)
     @editor.checkUpdate(source)


Remove left over debug code

Remove left over debug code

--- src/robot.coffee
+++ src/robot.coffee
@@ -150,13 +150,6 @@
         result = @brain.data.users[k]
     result

-  # Public: Update a User objects name property.
-  updateUserName: (id, name) ->
-    user = @userForId id
-    unless user.name is name
-      user.name = name
-    user
-
   # Public: Run Hubot using the loaded adapter.
   #
   # Returns nothing.


Remove route function on Robot

Remove route function on Robot

--- src/robot.coffee
+++ src/robot.coffee
@@ -50,15 +50,6 @@

     @documentation = {}

-  # Public: Specify a router and callback to register as Connect middleware.
-  #
-  # route    - A String of the route to match.
-  # callback - A Function that is called when the route is requested.
-  #
-  # Returns nothing.
-  route: (route, callback) ->
-    @router.get route, callback
-
   # Public: Adds a Listener that attempts to match incoming messages based on
   # a Regex.
   #


add Response.finish() to indicate a message is "handled"

* Message now has a @done attribute you can check
* Message and Response both have a finish() method to stop execution
* Addresses #200

Pull Request: master

--- src/robot.coffee
+++ src/robot.coffee
@@ -84,15 +84,18 @@

   # Public: Passes the given message to any interested Listeners.
   #
-  # message - A Robot.Message instance.
+  # message - A Robot.Message instance. Listeners can flag this message as
+  #  'done' to prevent further execution
   #
   # Returns nothing.
   receive: (message) ->
-    for lst in @listeners
-      try
-        lst.call message
-      catch ex
-        @logger.error "Unable to call the listener: #{ex}"
+    for listener in @listeners
+      unless message.done
+        try
+          listener.call message
+        catch ex
+          @logger.error "Unable to call the listener: #{ex}"
+

   # Public: Loads every script in the given path.
   #
@@ -248,7 +251,11 @@
   # Represents an incoming message from the chat.
   #
   # user - A User instance that sent the message.
-  constructor: (@user) ->
+  constructor: (@user, @done = false) ->
+
+  # Indicates that no other Listener should be called on this object
+  finish: () ->
+    @done = true

 class Robot.TextMessage extends Robot.Message
   # Represents an incoming message from the chat.
@@ -351,9 +358,15 @@
   #
   # items - An Array of items (usually Strings).
   #
-  # Returns a random item.
+  # Returns an random item.
   random: (items) ->
     items[ Math.floor(Math.random() * items.length) ]
+
+  # Public: Tell the message to stop dispatching to listeners
+  #
+  # Returns nothing.
+  finish: () ->
+    @message.finish()

   # Public: Creates a scoped http client with chainable methods for
   # modifying the request.  This doesn't actually make a request though.


Persist more user data from Slack in the brain

--- src/slack.coffee
+++ src/slack.coffee
@@ -73,7 +73,17 @@
       if id is user.name then delete @robot.brain.data.users[user.id]

   userChange: (user) =>
-    newUser = {name: user.name, real_name: user.real_name, email_address: user.profile.email}
+    newUser =
+      name: user.name
+      real_name: user.real_name
+      email_address: user.profile.email
+      slack: {}
+    for key, value of user
+      # _client is the slack-client instance and contains references to the all the data types (users, channels) plus things like the token, s
+      # so, don't bother storing it
+      continue if key is '_client'
+      newUser.slack[key] = value
+
     if user.id of @robot.brain.data.users
       for key, value of @robot.brain.data.users[user.id]
         unless key of newUser


Update sound.coffee

Update sound.coffee

Remove all HTML5 audio tag code in favor of AudioContext API. This greatly reduces the complexity of the Sound class and is potentially far more powerful. Testing is required as soon as possible, and if all goes well support of different audio nodes is the next major goal as far as the Sound class is concerned.

--- src/sound.coffee
+++ src/sound.coffee
@@ -4,94 +4,41 @@
 Greenhorn Gaming Engine Sound class
 ###

-#audio context for webkit browsers
-_webkitAudioContext = new webkitAudioContext?()
+#determine what kind of AudioContext is avaliable
+AudioContext = @AudioContext ? @webkitAudioContext
+
+#instance of AudioContext
+_audioContext = new AudioContext()

 #simple sound class
 class @Sound
     #constructor
     constructor: (url = env.SOUND_DEFAULT_URL) ->
-        #instance variable
+        #instance variables
         @_url = env.SOUND_PATH.concat url
+        @_source = _audioContext.createBufferSource()

-        if webkitAudioContext?
-            #webkit instance variables
-            @_source = _webkitAudioContext.createBufferSource()
-            @_gainNode = _webkitAudioContext.createGainNode()
-
-            #request setup
-            request = new XMLHttpRequest()
-            request.responseType = 'arraybuffer'
-            request.open 'GET', @_url, true
-
-            #request event handlers
-            request.successCallback = (buffer) =>
-                @_source.buffer = buffer
-                @_source.connect @_gainNode
-                @_gainNode.connect _webkitAudioContext.destination
-                return
-            request.failureCallback = ->
-                throw new Error "Webkit Sound Error"
-            request.onload = ->
-                _webkitAudioContext.decodeAudioData @response, @successCallback, @failureCallback
-
-            #send request
-            request.send()
+        #request setup
+        request = new XMLHttpRequest()
+        request.responseType = 'arraybuffer'
+        request.open 'GET', @_url, true

-        else
-            #non-webkit instance variable
-            @_audio = document.createElement 'audio'
-            @_audio.setAttribute 'controls', 'none'
-            @_audio.style.display = 'none'
-
-            #_audio sources
-            mp3_src = document.createElement 'source'
-            ogg_src = document.createElement 'source'
-            wav_src = document.createElement 'source'
-
-            mp3_src.type = 'audio/mpeg'
-            ogg_src.type = 'audio/ogg'
-            wav_src.type = 'audio/wav'
-
-            #determine source extension
-            if @_url.indexOf('.mp3') isnt -1
-                mp3_src.src = @_url
-                ogg_src.src = @_url.replace '.mp3', '.ogg'
-                wav_src.src = @_url.replace '.mp3', '.wav'
-            else if @_url.indexOf('.ogg') isnt -1
-                ogg_src.src = @_url
-                mp3_src.src = @_url.replace '.ogg', '.mp3'
-                wav_src.src = @_url.replace '.ogg', '.wav'
-            else if @_url.indexOf('.wav') isnt -1
-                wav_src.src = @_url
-                mp3_src.src = @_url.replace '.wav', '.mp3'
-                ogg_src.src = @_url.replace '.wav', '.ogg'
-            else throw new Error "Sound url must be .mp3, .ogg, or .wav extension"
-
-            #append sources to audio tag
-            @_audio.appendChild mp3_src
-            @_audio.appendChild ogg_src
-            @_audio.appendChild wav_src
-
-            #append audio tag to document body
-            Greenhorn._elmnts.canvas.appendChild @_audio
-        return
+        #request event handlers
+        request.successCallback = (buffer) =>
+            @_source.buffer = buffer
+            @_source.connect _audioContext.destination
+        request.errorCallback = ->
+            throw new Error "AudioContext Error"
+        request.onload = ->
+            _audioContext.decodeAudioData @response, @successCallback, @errorCallback
+
+        #send request
+        request.send()

     #sound control
     play: (opt = {}) ->
-        if webkitAudioContext?
-            @_gainNode.gain.value = opt.gainValue if opt.gainValue?
-            @_source.loop = opt.loop if opt.loop?
-            @_source.start 0
-        else
-            @_audio.loop = opt.loop if opt.loop?
-            @_audio.play()
-        return
+        @_source.loop = opt.loop ? false
+        @_source.start 0
     stop: ->
-        if webkitAudioContext?
-            @_source.stop 0
-        else
-            @_audio.pause()
-            @_audio.currentTime = 0
-        return
-#end class Sound+        @_source.stop 0
+#end class Sound


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/state.coffee
+++ src/state.coffee
@@ -4,11 +4,11 @@

   self = GameObject().extend

-  defaultState: () ->
-    return I.initialState
-
-  transition: (toState) ->
-    return currentState = toState
+    defaultState: () ->
+      return I.initialState
+
+    transition: (toState) ->
+      return currentState = toState

   self.attrAccessor('currentState')


Set rectable selectable=false

Set rectable selectable=false

--- src/tanks.coffee
+++ src/tanks.coffee
@@ -7,20 +7,25 @@
   canvas.setHeight 480
   canvas.setWidth  640

+  tank_width  = 20
+  tank_height = 20
+
   tank1 = new fabric.Rect
-    width:  20
-    height: 20
-    left:   20
-    top:    230
-    angle:  0
-    fill:   'rgba(255,0,0,0.5)'
+    width:      tank_width
+    height:     tank_height
+    left:       20
+    top:        230
+    angle:      0
+    fill:       'rgba(255,0,0,0.5)'
+    selectable: false

   tank2 = new fabric.Rect
-    width:  20
-    height: 20
-    left:   600
-    top:    230
-    angle:  0
-    fill:   'rgba(0,255,0,0.5)'
+    width:      tank_width
+    height:     tank_height
+    left:       600
+    top:        230
+    angle:      0
+    fill:       'rgba(0,255,0,0.5)'
+    selectable: false

   canvas.add tank1, tank2


Remove unused ivar

Remove unused ivar

--- src/ten-k.coffee
+++ src/ten-k.coffee
@@ -12,8 +12,8 @@
       else
         callback(null, new TenK(data))

-  constructor: (@contents) ->
-    @document = new DOMParser().parseFromString(@contents)
+  constructor: (contents) ->
+    @document = new DOMParser().parseFromString(contents)

   select: (element) ->
     nodes = []
@@ -27,7 +27,7 @@
       nodes = @select('NetIncomeLossAvailableToCommonStockholdersBasic')
     netIncomeLoss = 0
     for node in nodes
-      nodeYear = dates.getYear(xpath.select("@contextRef", node)[0]?.value)
+      nodeYear = dates.getYear(xpath.select('@contextRef', node)[0]?.value)
       nodeNetIncomeLoss = parseFloat(node.firstChild.data)
       continue if isNaN(nodeNetIncomeLoss)
       netIncomeLoss += nodeNetIncomeLoss if year is nodeYear


Added rudimentary tweening, wut!

Added rudimentary tweening, wut!

--- src/tween.coffee
+++ src/tween.coffee
@@ -28,9 +28,9 @@

   player = GameObject()

-  player.tween
+  player.tween 5
     x:
-
+    y:
   # => This causes the sprite to flicker between full opacity
   # => and 50% opacity every 3 frames for 30 frames


refactor child methods

refactor child methods

--- src/world.coffee
+++ src/world.coffee
@@ -75,7 +75,7 @@
         assert _.isObject(value), "authority isn't dictionary"
         authority = @_from_dict(value)
         @put(AUTHORITY, authority)
-      else if key == CHILDREN
+      else if key == CHILDREN and !_.isFunction(value)
         for child in value
           assert child, 'import child'
           @add_child child
@@ -94,7 +94,7 @@
     world

   _from_dict: (dict) ->
-    label = dict[LABEL] or "#{@get(LABEL)}:#{@get(CHILDREN).length()}"
+    label = dict[LABEL] or "#{@get(LABEL)}:#{@_child_count()}"
     world = @_spawn_world label
     world.import_dict dict

@@ -103,6 +103,11 @@
     return @_from_dict(value) if isObject(value)
     @_from_value value

+  _children: -> @get(CHILDREN)
+  _child_array: -> @_children().all()
+  _child_count: -> @_children().length()
+  has_children: -> @_child_count() > 0
+
   add_child: (value) ->
     child = @make_world value
     assert child, "add_child"
@@ -110,21 +115,18 @@
     child

   find_children: (label) ->
-    return @get(CHILDREN).all() unless label?
+    return @_child_array() unless label?
     result = []
-    for child in @get(CHILDREN).all()
+    for child in @_child_array()
       result.push child if child.label() == label
     result

   find_child: (label) ->
     @find_children(label)[0]

-  has_children: ->
-    @get(CHILDREN).length() > 0
-
   map_children: (callback) ->
     result = @rx().array()
-    for child in @get(CHILDREN).all()
+    for child in @_child_array()
       result.push callback(child)
     result


Lock down regexes more tightly

Lock down regexes more tightly

--- spec/archive-editor-view-spec.coffee
+++ spec/archive-editor-view-spec.coffee
@@ -8,11 +8,11 @@

   beforeEach ->
     spyOn(File::, 'onDidDelete').andCallFake (callback) ->
-      onDidDeleteCallback = callback if @getPath().match /.tar/
+      onDidDeleteCallback = callback if @getPath().match /\.tar$/
       new Disposable

     spyOn(File::, 'onDidChange').andCallFake (callback) ->
-      onDidChangeCallback = callback if @getPath().match /.tar/
+      onDidChangeCallback = callback if @getPath().match /\.tar$/
       new Disposable

     waitsForPromise ->


Add analytics, closes #73

Add analytics, closes #73

--- app/main.coffee
+++ app/main.coffee
@@ -44,9 +44,10 @@
   t7e.load strings
   t7e.refresh()

-# googleAnalytics.init
-#   account: 'Y?'
-#   domain: 'X?'
+GoogleAnalytics = require 'zooniverse/lib/google-analytics'
+new GoogleAnalytics
+  account: 'UA-53428944-3'
+  domain: 'chicagowildlifewatch.org'

 app = {}
 api = new Api project: 'chicago'


Properly reset drag styles on drag end

Properly reset drag styles on drag end

--- src/scripts/modules/media/tabs/contents/contents.coffee
+++ src/scripts/modules/media/tabs/contents/contents.coffee
@@ -13,8 +13,8 @@
       button: '.add.btn'

     events:
-      'dragstart .toc .draggable': 'onDragStart'
-      'dragend .toc .draggable': 'onDragEnd'
+      'dragstart .toc [draggable]': 'onDragStart'
+      'dragend .toc [draggable]': 'onDragEnd'

     initialize: () ->
       super()
@@ -30,12 +30,11 @@

     onDragStart: (e) ->
       # Prevent children from interfering with drag events
-      @$el.find('.draggable').children().css('pointer-events', 'none')
+      @$el.find('[draggable]').children().css('pointer-events', 'none')

     onDragEnd: (e) ->
-      $draggable = @$el.find('.draggable')
       # Restore pointer events
-      $draggable.children().css('pointer-events', 'auto')
+      @$el.find('[draggable]').children().css('pointer-events', 'auto')

       # Reset styling for all draggable elements
-      $draggable.removeClass('before after insert')
+      e.currentTarget.className = ''


Fix case

Fix case

--- server/materials/data/news.coffee
+++ server/materials/data/news.coffee
@@ -4,7 +4,7 @@
 export default allNews = () ->
     return news([
         newsItem("Занятия для старших уровней", String.raw"""
-            С 21 ноября возобновляются занятия для старших уровней — по воскресеньям, параллельно с основным занятием во ВШЭ. подробнее.
+            С 21 ноября возобновляются занятия для старших уровней — по воскресеньям, параллельно с основным занятием во ВШЭ. Подробнее.
         """),
         newsItem("Районная олимпиада", String.raw"""
             Районная олимпиада в Нижегородской области в этом году будет, предварительно, 16 декабря. На сайт добавлена подробная информация, также в архив районных олимпиад добавлены задачи прошлого года.


Convert Chat to a Coffee class

Convert Chat to a Coffee class

--- app/assets/javascripts/chat.coffee
+++ app/assets/javascripts/chat.coffee
@@ -1,188 +1,177 @@
 (($) ->

-  $.Chat = (element) ->
-    base = this
-    base.init = ->
-      board = $(element)
-      base.input = board.find("input[type=text]")
-      base.inbox = board.find(".inbox")
-      base.chan = board.data("chat")
-      base.cursor = base.findCursor()
-      base.sleepTime = 500
-      board.find(".board-left .norloge").click base.norloge
-      board.find("form").submit base.postMessage
-      base.totoz_type = $.cookie("totoz-type")
-      base.totoz_url = $.cookie("totoz-url")
-      base.totoz_url = "http://sfw.totoz.eu/gif/"  unless base.totoz_url?
-      board.find(".board-right").each base.norlogize
-      board.find("time").live("mouseenter", base.highlitizer).live "mouseleave", base.deshighlitizer
-      if base.totoz_type == "popup"
-        base.totoz = board.append($("
")).find("#les-totoz")
-        board.find(".totoz").live("mouseenter", base.createTotoz).live "mouseleave", base.destroyTotoz
-      base.poll()
+  class Chat
+    constructor: (@board) ->
+      @input = @board.find("input[type=text]")
+      @inbox = @board.find(".inbox")
+      @chan = @board.data("chat")
+      @cursor = @findCursor()
+      @sleepTime = 500
+      @board.find(".board-left .norloge").click @norloge
+      @board.find("form").submit @postMessage
+      @totoz_type = $.cookie("totoz-type")
+      @totoz_url = $.cookie("totoz-url") or "http://sfw.totoz.eu/gif/"
+      @norloge right  for right in @board.find(".board-right")
+      @board.find("time").live("mouseenter", @highlitizer).live("mouseleave", @deshighlitizer)
+      if @totoz_type == "popup"
+        @totoz = @board.append($("
")).find("#les-totoz")
+        @board.find(".totoz").live("mouseenter", @createTotoz).live("mouseleave", @destroyTotoz)
+      @poll()

-    base.findCursor = ->
-      first = base.inbox.children().first()
+    findCursor: ->
+      first = @inbox.children().first()
       if first.length > 0
         id = first[0].id
         return id.slice 6  if id.slice(0, 6) == "board-"
       null

-    base.postMessage = ->
-      form = $(this)
+    postMessage: (event) =>
+      form = $(event.target)
       data = form.serialize()
-      base.input.val("").select()
+      @input.val("").select()
       $.ajax url: form.attr("action"), data: data, type: "POST", dataType: "script"
       false

-    base.norloge = ->
-      time = $(this).text()
-      value = base.input.val()
-      range = base.input.caret()
+    norloge: (event) =>
+      time = $(event.target).text()
+      value = @input.val()
+      range = @input.caret()
       unless range.start?
         range.start = 0
         range.end = 0
-      base.input.val value.substr(0, range.start) + time + " " + value.substr(range.end, value.length)
-      base.input.caret range.start + time.length + 1
-      base.input.focus()
+      @input.val value.substr(0, range.start) + time + " " + value.substr(range.end, value.length)
+      @input.caret range.start + time.length + 1
+      @input.focus()

-    base.norlogize = ->
+    norlogize: ->
       @innerHTML = @innerHTML.replace(/[0-2][0-9]:[0-6][0-9](:[0-6][0-9])?([⁰¹²³⁴⁵⁶⁷⁸⁹]+|[:\^][0-9]+)?/g, "<time>$&</time>")
-      if base.totoz_type == "popup"
+      if @totoz_type == "popup"
         @innerHTML = @innerHTML.replace(/\[:([^\]]+)\]/g, "$&")
-      else if base.totoz_type == "inline"
-        @innerHTML = @innerHTML.replace(/\[:([^\]]+)\]/g, "")
+      else if @totoz_type == "inline"
+        @innerHTML = @innerHTML.replace(/\[:([^\]]+)\]/g, "")

-    base.highlitizer = ->
-      time = $(this).text()
-      base.inbox.find("time").filter(->
-        $(this).text() == time
-      ).addClass "highlighted"
+    highlitizer: (event) =>
+      time = $(event.target).text()
+      @inbox.find("time").filter(-> $(this).text() == time).addClass "highlighted"

-    base.deshighlitizer = ->
-      base.inbox.find("time.highlighted").removeClass "highlighted"
+    deshighlitizer: =>
+      @inbox.find("time.highlighted").removeClass "highlighted"

-    base.createTotoz = (e) ->
+    createTotoz: (event) =>
       totozName = @getAttribute("data-totoz-name")
       totozId = encodeURIComponent(totozName).replace(/%/g, "")
-      totoz = base.totoz.find("#totoz-" + totozId).first()
+      totoz = @totoz.find("#totoz-" + totozId).first()
       if totoz.size() == 0
         totoz = $("

")
                 .css(display: "none", position: "absolute")
-                .append("")
-        base.totoz.append totoz
-      x = $(this).offset().left
-      y = $(this).offset().top
+                .append("")
+        @totoz.append totoz
+      offset = $(event.target).offset()
+      [x, y] = [offset.left, offset.top]
       totoz.css "z-index": "15", display: "block", top: y + 20, left: x + 20

-    base.destroyTotoz = ->
+    destroyTotoz: =>
       totozId = encodeURIComponent(@getAttribute("data-totoz-name")).replace(/%/g, "")
-      totoz = base.totoz.find("#totoz-" + totozId).first()
-      totoz.css "display", "none"
+      totoz = @totoz.find("#totoz-" + totozId).first()
+      totoz.css display: "none"

-    base.poll = ->
+    poll: =>
       args = {}
-      args.cursor = base.cursor  if base.cursor
+      args.cursor = @cursor  if @cursor
       $.ajax
-        url: "/b/" + base.chan
+        url: "/b/" + @chan
         type: "GET"
         dataType: "json"
         data: $.param(args)
-        success: base.onSuccess
-        error: base.onError
+        success: @onSuccess
+        error: @onError

-    base.onSuccess = (response) ->
+    onSuccess: (response) =>
       try
-        base.newMessages response
-        base.sleepTime = 500
-        setTimeout base.poll, 0
+        @newMessages response
+        @sleepTime = 500
+        setTimeout @poll, 0
       catch e
-        base.onError()
+        @onError()

-    base.onError = ->
-      base.sleepTime *= 2
-      setTimeout base.poll, base.sleepTime
+    onError: =>
+      @sleepTime *= 2
+      setTimeout @poll, @sleepTime

-    base.newMessages = (messages) ->
+    newMessages: (messages) ->
       if messages
-        base.cursor = messages[messages.length - 1].id
-        i = 0
-        while i < messages.length
-          message = messages[i]
+        @cursor = messages[messages.length - 1].id
+        for message in messages
           existing = $("#board_" + message.id)
           continue  if existing.length > 0
           method = "on_" + message.kind
-          if base[method]
-            base.inbox.prepend(message.msg).find(".board-left:first .norloge").click base.norloge
-            base[method] message.msg
-          base.inbox.find(".board-right:first").each base.norlogize
-          i += 1
+          if this[method]
+            @inbox.prepend(message.msg).find(".board-left:first .norloge").click @norloge
+            this[method] message.msg
+          @inbox.find(".board-right:first").each @norlogize

-    base.on_chat = ->
+    on_chat: ->

-    base.on_indication = ->
+    on_indication: ->

-    base.on_vote = ->
+    on_vote: ->

-    base.on_submission = (message) ->
+    on_submission: (message) ->
       $.noticeAdd text: message, stay: true

-    base.on_moderation = (message) ->
+    on_moderation: (message) ->
       $.noticeAdd text: message, stay: true

-    base.on_locking = (message) ->
-      el = base.inbox.children().first()
-      el.find(".clear").each ->
+    on_locking: (message) ->
+      el = @inbox.children().first()
+      for clear in el.find(".clear")
         $(".locked").removeClass "locked"
         $.noticeAdd text: message

-      el.find(".link").each ->
-        id = $(this).data("id")
+      for link in el.find(".link")
+        id = $(link).data("id")
         $("#link_" + id).addClass "locked"

-      el.find(".paragraph").each ->
-        id = $(this).data("id")
+      for p in el.find(".paragraph")
+        id = $(p).data("id")
         $("#paragraph_" + id).addClass "locked"

-    base.on_creation = (message) ->
-      el = base.inbox.children().first()
-      el.find(".link").each ->
-        id = $(this).data("id")
+    on_creation: (message) ->
+      el = @inbox.children().first()
+      for link in el.find(".link")
+        id = $(link).data("id")
         html = "
#{$(this).html()}
"
+               $(link).find("a").attr("hreflang") +
+               "\" data-url=\"/redaction/links/#{id}/modifier\">#{$(link).html()}
"
         $("#links").append html
         $("#link_" + id).editionInPlace()

-      el.find(".paragraph").each ->
-        id = $(this).data("id")
-        after = $(this).data("after")
+      for p in el.find(".paragraph")
+        id = $(p).data("id")
+        after = $(p).data("after")
         html = "
" +
-               $(this).html() + "
"
+               $(p).html() + "
"
         $("#paragraph_" + after).after html
         $("#paragraph_" + id).editionInPlace()

-    base.on_edition = (message) ->
-      el = base.inbox.children().first()
-      el.find(".news").each ->
-        $("#news_header").html $(this).clone()
+    on_edition: (message) ->
+      el = @inbox.children().first()
+      for news in el.find(".news")
+        $("#news_header").html $(news).clone()

-      el.find(".link").each ->
-        $("#link_" + $(this).data("id")).html $(this).html()
+      for link in el.find(".link")
+        $("#link_" + $(link).data("id")).html $(link).html()

-      el.find(".paragraph").each ->
-        $("#paragraph_" + $(this).data("id")).html $(this).html()
+      for p in el.find(".paragraph")
+        $("#paragraph_" + $(p).data("id")).html $(p).html()

-    base.on_deletion = (message) ->
-      el = base.inbox.children().first()
-      el.find(".link").each ->
-        $("#link_" + $(this).data("id")).remove()
-      el.find(".paragraph").each ->
-        $("#paragraph_" + $(this).data("id")).remove()
-
-    base.init()
+    on_deletion: (message) ->
+      el = @inbox.children().first()
+      for link in el.find(".link")
+        $("#link_" + $(link).data("id")).remove()
+      for p in el.find(".paragraph")
+        $("#paragraph_" + $(p).data("id")).remove()

   $.fn.chat = ->
-    @each -> new $.Chat(this)
-
+    @each -> new Chat($(this))
 ) window.jQuery


initialize correct payments js

initialize correct payments js

--- app/assets/javascripts/init.coffee
+++ app/assets/javascripts/init.coffee
@@ -31,7 +31,7 @@
     pageClasses =
       welcome_index: BikeIndex.WelcomeIndex
       info_where: BikeIndex.InfoWhere
-      info_support_the_index: BikeIndex.Payments
+      info_support_bike_index: BikeIndex.Payments
       payments_new: BikeIndex.Payments
       bikes_new: BikeIndex.BikesNew
       bikes_create: BikeIndex.BikesNew


return an empty string if no chapter section

--- src/flux/exercise.coffee
+++ src/flux/exercise.coffee
@@ -18,6 +18,7 @@
   for tag in exercise.tags when tag.type is 'cnxmod'
     section = TocStore.findWhere(ecosystemId, uuid: tag.data)
     return section.chapter_section.join('.') if section?
+  '' # return empty string if section wasn't found

 getTagName = (tag) ->
   name = _.compact([tag.name, tag.description]).join(' ')


minor fix

minor fix

--- app/js/controllers/trollbox.coffee
+++ app/js/controllers/trollbox.coffee
@@ -122,7 +122,7 @@
     new_messages = []
     # get latest chat messages (limit n messages)
     BlockchainAPI.get_account_ads(chatAdPositionAcct, limit).then (response) ->
-      if response.length > 0
+      if response?.length > 0
         my_account_ids = $scope.accounts.map (acct) -> acct.account_id
         # min amount is 200000
         for message in (response.filter (r) -> checkMessageFee(r)).reverse()


Production groups

Production groups

--- app/models/subject_selector.coffee
+++ app/models/subject_selector.coffee
@@ -2,11 +2,16 @@
 Notify = require "../lib/notifier"
 translate = require "t7e"

-SubjectSelector =
-  groups:
+groups = if !!~location.hostname.indexOf('demo') || +location.port >= 1024
     gold: '53f61f71edf877ca7f000003'
     winter2013: '53f61f8cedf877ca7f000004'
     spring2014: '53f61f8cedf877ca7f000005'
+  else
+    gold: '5410f7213ae740c000000001'
+    spring2012: '5410f5723ae740bff2000001'
+
+SubjectSelector =
+  groups: groups

   goldStandardRate: 1 / 3


Adds Equals expression component, misc updates to expression components

--- javascripts/lib/math/expression_components.coffee
+++ javascripts/lib/math/expression_components.coffee
@@ -24,6 +24,13 @@
       isNumber: -> false
       preceedingSubexpression: -> false
       clone: -> @ # by default, cloning does nothing
+
+
+    class Equals extends ExpressionComponent
+      toString: -> "="
+      isOperator: -> true
+    class_mixer(Equals)
+

     class Expression extends ExpressionComponent
       @buildWithContent: (content)->
@@ -71,6 +78,9 @@
       size: ->
         _(@expression).size();

+      isBlank: ->
+        @size() == 0
+
       set: (expression)->
         @expression = expression

@@ -97,8 +107,7 @@
         tf = (it)-> it ? "t" : "f"
         subexpressions = _(@expression).chain().map((it)->
           it.toString()).join(", ").value()
-        "Expr(o: #{tf @is_open}, e: #{tf @is_error}, exp: [#{subexpressions}])"
-
+        "Expression(o: #{tf @is_open}, e: #{tf @is_error}, exp: [#{subexpressions}])"

     class_mixer(Expression)

@@ -109,7 +118,7 @@
         @future_as_decimal = opts.future_as_decimal

       toString: ->
-        "N(#{@val})"
+        "##{@val}"

       isNumber: -> true

@@ -153,7 +162,6 @@
         @baseval = opts.base
         @powerval = opts.power
       isOperator: -> true
-
       base: -> @baseval
       power: -> @powerval

@@ -163,38 +171,38 @@
         @klass.build base: @base(), power: power

       toString: ->
-        "Exp(b: #{@base().toString()}, p: #{@power().toString()})"
+        "^(b: #{@base().toString()}, p: #{@power().toString()})"

       isPowerOpen: -> @power().isOpen()

     class_mixer(Exponentiation)

     class Pi extends ExpressionComponent
-      toString: -> "PI()"
+      toString: -> "PI"
     class_mixer(Pi)

     class Addition extends ExpressionComponent
-      toString: -> "Add()"
+      toString: -> "Add"
       isOperator: -> true
     class_mixer(Addition)

     class Subtraction extends ExpressionComponent
       isOperator: -> true
-      toString: -> "Sub()"
+      toString: -> "Sub"
     class_mixer(Subtraction)

     class Multiplication extends ExpressionComponent
       isOperator: -> true
-      toString: -> "Mult()"
+      toString: -> "Mult"
     class_mixer(Multiplication)

     class Division extends ExpressionComponent
       isOperator: -> true
-      toString: -> "Div()"
+      toString: -> "Div"
     class_mixer(Division)

     class Blank extends ExpressionComponent
-      toString: -> "Blnk()"
+      toString: -> "Blnk"
     class_mixer(Blank)

     components =
@@ -207,6 +215,7 @@
       subtraction: Subtraction
       exponentiation: Exponentiation
       pi: Pi
+      equals: Equals
       blank: Blank

     return components


Removes superfluous variable

Removes superfluous variable

--- autolink.coffee
+++ autolink.coffee
@@ -10,7 +10,7 @@

   return @replace(pattern, "$1$2") unless options.length > 0

-  option, callback = options[0]
+  option = options[0]
   callback = option["callback"]

   linkAttributes = (


Remove commented-out code

Remove commented-out code

--- services/track-changes/app.coffee
+++ services/track-changes/app.coffee
@@ -25,7 +25,6 @@
 Path = require "path"
 Metrics = require "metrics-sharelatex"
 Metrics.initialize("track-changes")
-# Metrics.mongodb.monitor(Path.resolve(__dirname + "/node_modules/mongojs/node_modules/mongodb"), logger)
 Metrics.memory.monitor(logger)

 child_process = require "child_process"


Handle undefined @options

Handle undefined @options

--- framer/Animation.coffee
+++ framer/Animation.coffee
@@ -72,7 +72,7 @@
 	@define "looping",
 		get: -> @options.looping
 		set: (value) ->
-			@options.looping = value
+			@options?.looping = value
 			if @options?.looping and @options?.layer? and !@isAnimating
 				@restart()


Prevent double logging when switching runtimes

--- components/ConnectRuntime.coffee
+++ components/ConnectRuntime.coffee
@@ -17,7 +17,12 @@
       editor: new noflo.Port 'object'
       packet: new noflo.Port 'object'

-    @inPorts.editor.on 'data', (@editor) =>
+    @inPorts.editor.on 'connect', =>
+      if @editor and @runtime
+        do @disconnect
+      @editor = null
+    @inPorts.editor.on 'data', (editor) =>
+      @editor = editor
       @connect @editor, @runtime
       if @outPorts.editor.isAttached()
         @outPorts.editor.send @editor
@@ -29,9 +34,10 @@
     @inPorts.example.on 'data', (@example) =>
       @sendGraph @runtime, @example
     @inPorts.runtime.on 'connect', =>
+      if @runtime
+        do @disconnect
       @runtime = null
     @inPorts.runtime.on 'data', (runtime) =>
-      @runtime.stop() if @runtime
       @runtime = runtime
       @connect @editor, @runtime

@@ -145,52 +151,68 @@
     runtime.once 'connected', =>
       for name, def of editor.$.graph.library
         delete editor.$.graph.library[name]
-    runtime.on 'connected', =>
-      @connected = true
-      runtime.sendComponent 'list', ''
-      @sendProject @runtime, @project if @project
-      @sendGraph @runtime, @example if @example
-    runtime.on 'disconnected', =>
-      @connected = false
-
-    runtime.on 'component', (message) ->
-      if message.payload.name is 'Graph' or message.payload.name is 'ReadDocument'
-        return
-      definition =
-        name: message.payload.name
-        description: message.payload.description
-        icon: message.payload.icon
-        subgraph: message.payload.subgraph or false
-        inports: []
-        outports: []
-      for port in message.payload.inPorts
-        definition.inports.push
-          name: port.id
-          type: port.type
-          required: port.required
-          description: port.description
-          addressable: port.addressable
-      for port in message.payload.outPorts
-        definition.outports.push
-          name: port.id
-          type: port.type
-          required: port.required
-          description: port.description
-          addressable: port.addressable
-      editor.registerComponent definition
-    edges = {}
-    runtime.on 'network', ({command, payload}) =>
-      return if command is 'error'
-      return unless payload.id
-      @outPorts.packet.send
-        edge: payload.id
-        type: command
-        group: if payload.group? then payload.group else ''
-        data: if payload.data? then payload.data else ''
-        subgraph: if payload.subgraph? then payload.subgraph else ''
-        runtime: runtime.definition.id
-    runtime.on 'icon', ({id, icon}) ->
-      return unless editor.updateIcon
-      editor.updateIcon id, icon
+    runtime.on 'connected', @onRuntimeConnected
+    runtime.on 'disconnected', @onRuntimeDisconnected
+    runtime.on 'component', @onRuntimeComponent
+    runtime.on 'network', @onRuntimeNetwork
+    runtime.on 'icon', @onRuntimeIcon
+
+  disconnect: ->
+    @connected = false
+    @runtime.removeListener 'connected', @onRuntimeConnected
+    @runtime.removeListener 'disconnected', @onRuntimeDisconnected
+    @runtime.removeListener 'component', @onRuntimeComponent
+    @runtime.removeListener 'network', @onRuntimeNetwork
+    @runtime.removeListener 'icon', @onRuntimeIcon
+
+  onRuntimeConnected: =>
+    @connected = true
+    @runtime.sendComponent 'list', ''
+    @sendProject @runtime, @project if @project
+    @sendGraph @runtime, @example if @example
+
+  onRuntimeDisconnected: =>
+    @connected = false
+
+  onRuntimeComponent: (message) =>
+    if message.payload.name is 'Graph' or message.payload.name is 'ReadDocument'
+      return
+    definition =
+      name: message.payload.name
+      description: message.payload.description
+      icon: message.payload.icon
+      subgraph: message.payload.subgraph or false
+      inports: []
+      outports: []
+    for port in message.payload.inPorts
+      definition.inports.push
+        name: port.id
+        type: port.type
+        required: port.required
+        description: port.description
+        addressable: port.addressable
+    for port in message.payload.outPorts
+      definition.outports.push
+        name: port.id
+        type: port.type
+        required: port.required
+        description: port.description
+        addressable: port.addressable
+    @editor.registerComponent definition
+
+  onRuntimeNetwork: ({command, payload}) =>
+    return if command is 'error'
+    return unless payload.id
+    @outPorts.packet.send
+      edge: payload.id
+      type: command
+      group: if payload.group? then payload.group else ''
+      data: if payload.data? then payload.data else ''
+      subgraph: if payload.subgraph? then payload.subgraph else ''
+      runtime: @runtime.definition.id
+
+  onRuntimeIcon: ({id, icon}) =>
+    return unless @editor.updateIcon
+    @editor.updateIcon id, icon

 exports.getComponent = -> new ConnectRuntime


Fix references

Fix references

--- components/ContextToDebug.coffee
+++ components/ContextToDebug.coffee
@@ -18,27 +18,27 @@
     out.send context

   c.setRuntimeDebug = (enable) ->
-    @runtime.sendNetwork 'debug',
-      graph: @graph.name
+    c.runtime.sendNetwork 'debug',
+      graph: c.graph.name
       enable: enable

   c.addListener = (runtime, graph) ->
     return unless runtime? and graph?
-    @runtime = runtime
-    @graph = graph
-    @listener = (status) =>
+    c.runtime = runtime
+    c.graph = graph
+    c.listener = (status) =>
       return unless status.online
-      @setRuntimeDebug true
-    @runtime.on 'status', @listener
-    @setRuntimeDebug true if @runtime.isConnected()
+      c.setRuntimeDebug true
+    c.runtime.on 'status', c.listener
+    c.setRuntimeDebug true if c.runtime.isConnected()

-  c.removeListener = () ->
-    return unless @listener
+  c.removeListener = ->
+    return unless c.listener
     # Disable debug on old runtime
-    @setRuntimeDebug false if @runtime.isConnected()
+    c.setRuntimeDebug false if c.runtime.isConnected()
     # Stop listening
-    @runtime.removeListener 'status', @listener
-    delete @runtime
-    delete @graph
+    c.runtime.removeListener 'status', c.listener
+    delete c.runtime
+    delete c.graph

   c


Tabview: tab handle cssClass is now being added by tabpane name

--- components/tabs/KDTabView.coffee
+++ components/tabs/KDTabView.coffee
@@ -58,25 +58,30 @@
       @addPane new @tabConstructor paneOptions, null

   addPane:(paneInstance, shouldShow=yes)->
+
     if paneInstance instanceof KDTabPaneView
-      paneInstance.setOption "detachable", @getOption "detachPanes"
+
+      { tabHandleClass, sortable, detachPanes } = @getOptions()
+
+      paneInstance.setOption "detachable", detachPanes
       @panes.push paneInstance
-      {tabHandleClass} = @getOptions()
-      paneOptions      = paneInstance.getOptions()
+
+      { name, title, hiddenHandle, tabHandleView, closable, lazy } = paneInstance.getOptions()

       @addHandle newTabHandle = new tabHandleClass
         pane      : paneInstance
-        title     : paneOptions.name or paneOptions.title
-        hidden    : paneOptions.hiddenHandle
-        view      : paneOptions.tabHandleView
-        closable  : paneOptions.closable
-        sortable  : @getOptions().sortable
+        title     : name or title
+        hidden    : hiddenHandle
+        cssClass  : KD.utils.slugify name.toLowerCase()
+        view      : tabHandleView
+        closable  : closable
+        sortable  : sortable
         click     : (event)=> @handleMouseDownDefaultAction newTabHandle, event

       paneInstance.tabHandle = newTabHandle
       @appendPane paneInstance
-      if shouldShow and not paneInstance.getOption 'lazy'
-        @showPane paneInstance
+      @showPane paneInstance  if shouldShow and not lazy
+
       @emit "PaneAdded", paneInstance

       {minHandleWidth, maxHandleWidth} = @getOptions()


tests

tests

--- test/generators/query_cache/cache.coffee
+++ test/generators/query_cache/cache.coffee
@@ -55,7 +55,7 @@
       queue = new Queue(1)

       # destroy all
-      queue.defer (callback) -> Utils.resetSchemas [Reverse, Owner], callback
+      queue.defer (callback) -> Utils.resetSchemas [Flat, Reverse, Owner], callback

       # create all
       queue.defer (callback) ->


updating video download links.

updating video download links.

--- assets/coffeescript/product_list.coffee
+++ assets/coffeescript/product_list.coffee
@@ -27,12 +27,8 @@
     img: "../media/images/config_images/pupil-config-world-1080p.jpg"
     videos: [
       {
-        title: "90 deg. lens - 1080p @30fps",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfSEdEQU1GdTNlR00/view?usp=sharing"
-      },
-      {
-        title: "90 deg. lens - 720p @30fps",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfY2FDRFJNYVFLUXc/view?usp=sharing"
+        title: "Download sample videos",
+        link: "https://drive.google.com/file/d/0B4Q4b_aBOfU-QW1ncXpkdS02R1k/view?usp=sharing"
       }
     ]
     order: 2
@@ -52,28 +48,8 @@
     img: "../media/images/config_images/pupil-config-world-120hz.jpg"
     videos: [
       {
-        title: "110 deg. lens - VGA @120fps",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfUkcxQU1GSGk3NDA/view?usp=sharing"
-      },
-      {
-        title: "60 deg. lens - VGA @120fps",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfRDZBaENVNldLR3M/view?usp=sharing"
-      },
-      {
-        title: "110 deg. lens - 720p @60fps",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfYUhNWkxYSW1FLWs/view?usp=sharing"
-      },
-      {
-        title: "60 deg. lens - 720p @60fps",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfSWp6TTZKNTFfSGM/view?usp=sharing"
-      },
-      {
-        title: "110 deg. lens - 1080p @30fps",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfRi11YnpITHk2ZUU/view?usp=sharing"
-      },
-      {
-        title: "60 deg. lens - 1080p @30fps",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfaTNQVmRfUEZhZFE/view?usp=sharing"
+        title: "Download sample videos",
+        link: "https://drive.google.com/file/d/0B4Q4b_aBOfU-TmJvSFhFeWp1NEk/view?usp=sharing"
       }
     ]
     order: 3
@@ -105,8 +81,8 @@
     img: "../media/images/config_images/pupil-config-eye-30hz.jpg"
     videos: [
       {
-        title: "eye cam - VGA @30hz",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfWUVEZWpNTDlXcHM/view?usp=sharing"
+        title: "Download sample video",
+        link: "https://drive.google.com/file/d/0B4Q4b_aBOfU-N1FOOWVVVHUtVUE/view?usp=sharing"
       }
     ]
     order: 2
@@ -126,8 +102,8 @@
     img: "../media/images/config_images/pupil-config-eye-120hz.jpg"
     videos: [
       {
-        title: "pupil eye cam - VGA @120hz",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfbTIxTFhPQ0NOeVU/view?usp=sharing"
+        title: "Download sample video",
+        link: "https://drive.google.com/file/d/0B4Q4b_aBOfU-OEVUcVlURFJzdkU/view?usp=sharing"
       }
     ]
     order: 3
@@ -147,8 +123,8 @@
     img: "../media/images/config_images/pupil-config-eye-120hz-binocular.jpg"
     videos: [
       {
-        title: "pupil eye cam - VGA @120hz",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfbTIxTFhPQ0NOeVU/view?usp=sharing"
+        title: "Download sample video",
+        link: "https://drive.google.com/file/d/0B4Q4b_aBOfU-OEVUcVlURFJzdkU/view?usp=sharing"
       }
     ]
     order: 4
@@ -164,8 +140,8 @@
     img: "../media/images/additional_products/web/hs_cam.png"
     videos: [
       {
-        title: "pupil eye cam - VGA @120hz",
-        link: "https://drive.google.com/file/d/0Byap58sXjMVfbTIxTFhPQ0NOeVU/view?usp=sharing"
+        title: "Download sample video",
+        link: "https://drive.google.com/file/d/0B4Q4b_aBOfU-OEVUcVlURFJzdkU/view?usp=sharing"
       }
     ]
     order: 1


fixed weight calculation for eye_30hz

fixed weight calculation for eye_30hz

--- assets/coffeescript/product_list.coffee
+++ assets/coffeescript/product_list.coffee
@@ -68,8 +68,8 @@
         latency: "35.6ms"
     cartlabel: "Eye camera: 30hz eye camera"
     cartSpecs: "Resolution: 640x480 @30fps. IR illumination."
-    dimensions: 30 # 12 headset frame, 2 harness cable, 12 cable clip weight, 3 eye camera (add 1g for just in case)
-    weight: 300
+    dimensions: 0 # 12 headset frame, 2 harness cable, 12 cable clip weight, 3 eye camera (add 1g for just in case)
+    weight: 30
     img: "../media/images/config_images/pupil-config-eye-30hz.jpg"
     video: "#"
     order: 2


removed a clog

removed a clog

--- test/read.coffee
+++ test/read.coffee
@@ -46,7 +46,6 @@
                    , {title: "A title 2", description: "w00t describing 2"}]
         Thing.create expected[0], ->
           Thing.create expected[1], ->
-            console.log(Thing.modelName)
             things = Thing.read {}, (docs) ->
               process.nextTick ->
                 docs.at(0).get("title").should.equal expected[0].title