projects actions: make it so none of them return; also use create_project action for creating a project

--- salvus/page/projects.cjsx
+++ salvus/page/projects.cjsx
@@ -39,6 +39,8 @@

 MAX_DEFAULT_PROJECTS = 50

+_create_project_tokens = {}
+
 # Define projects actions
 class ProjectsActions extends Actions
     # Local state events
@@ -80,7 +82,11 @@
         opts = defaults opts,
             title       : 'No Title'
             description : 'No Description'
-            cb          : undefined
+            token       : undefined  # if given, can use wait_until_project_is_created
+        if opts.token?
+            token = opts.token; delete opts.token
+            opts.cb = (err, project_id) =>
+                _create_project_tokens[token] = {err:err, project_id:project_id}
         salvus_client.create_project(opts)

     # Open the given project
@@ -199,6 +205,37 @@

     get_project_open_state: (project_id) =>
         return @get_project_state(project_id, 'open')
+
+    wait_until_project_is_open: (project_id, timeout, cb) =>  # timeout in seconds
+        @wait
+            until   : => @get_project_open_state(project_id)
+            timeout : timeout
+            cb      : (err, x) =>
+                cb(err or x?.err)
+
+    wait_until_project_exists: (project_id, timeout, cb) =>
+        @wait
+            until   : => @state.project_map.get(project_id)?
+            timeout : timeout
+            cb      : cb
+
+    wait_until_project_created: (token, timeout, cb) =>
+        @wait
+            until   : =>
+                x = _create_project_tokens[token]
+                return if not x?
+                {project_id, err} = x
+                if err
+                    return {err:err}
+                else
+                    if @state.project_map.has(project_id)
+                        return {project_id:project_id}
+            timeout : timeout
+            cb      : (err, x) =>
+                if err
+                    cb(err)
+                else
+                    cb(x.err, x.project_id)

 # Register projects store
 store = flux.createStore('projects', ProjectsStore, flux)
@@ -369,24 +406,23 @@
             error            : ''

     create_project: ->
+        token = misc.uuid()
         @setState(state:'saving')
-        salvus_client.create_project
+        actions.create_project
             title       : @state.title_text
             description : @state.description_text
-            public      : false
-            cb          : (err, resp) =>
-                if not err and resp.error
-                    err = misc.to_json(resp.error)
-                if err?
-                    @setState
-                        state : 'edit'
-                        error : "Error creating project -- #{err}"
-                else
-                    @setState
-                        state            : 'view'
-                        title_text       : ''
-                        description_text : ''
-                        error            : ''
+            token       : token
+        store.wait_until_project_created token, 30, (err) =>
+            if err?
+                @setState
+                    state : 'edit'
+                    error : "Error creating project -- #{err}"
+            else
+                @setState
+                    state            : 'view'
+                    title_text       : ''
+                    description_text : ''
+                    error            : ''

     render_create_project_button: ->
         if @state.title_text == '' or @state.state == 'saving'


Restore the original opinion if it is not the opinion that was being set by the popover

--- core/app/backbone/views/fact_relations/vote_view.coffee
+++ core/app/backbone/views/fact_relations/vote_view.coffee
@@ -6,13 +6,21 @@
     'click .js-done': 'save'
     'click .js-cancel': 'close'

+  initialize: ->
+    @_originalFactOpinion = @model.getFact().getFactWheel().userOpinion()
+    @_originalFactRelationOpinion = @model.current_opinion()
+
   set_fact_relation_opinion: (opinion) ->
     is_current_opinion = @model.current_opinion() == opinion
     @model.setOpinion opinion unless is_current_opinion

   unset_fact_relation_opinion: (opinion) ->
     is_current_opinion = @model.current_opinion() == opinion
-    @model.removeOpinion() if is_current_opinion
+    if is_current_opinion
+      if @_originalFactRelationOpinion? && @model.current_opinion() != opinion
+        @model.setOpinion @_originalFactRelationOpinion
+      else
+        @model.removeOpinion()

   set_fact_opinion: (opinion) ->
     is_current_opinion = @model.getFact().getFactWheel().isUserOpinion opinion
@@ -20,7 +28,11 @@

   unset_fact_opinion: (opinion) ->
     is_current_opinion = @model.getFact().getFactWheel().isUserOpinion opinion
-    @_unset_fact_opinion opinion if is_current_opinion
+    if is_current_opinion
+      if @_originalFactOpinion? && !@model.getFact().getFactWheel().isUserOpinion @_originalFactOpinion
+        @_set_fact_opinion @_originalFactOpinion
+      else
+        @_unset_fact_opinion opinion

   _set_fact_opinion: (opinion) ->
     @model.getFact().getFactWheel().setActiveOpinionType opinion


Firefox: Polyfill for selection.type for visual mode.

@mrmr1993... This puts the logic of how we choose the selection type in
one place; so, if you have a better idea of how to determine the
selection type, then we just change it here.  Once.

--- lib/dom_utils.coffee
+++ lib/dom_utils.coffee
@@ -326,9 +326,15 @@
       @suppressEvent event
       handlerStack.suppressEvent

-
+  # Polyfill for selection.type (which is not available in Firefox).
   getSelectionType: (selection) ->
-    selection.type
+    selection.type or do ->
+      if selection.rangeCount == 0
+        "None"
+      else if selection.isCollapsed
+        "Caret"
+      else
+        "Range"

   # Adapted from: http://roysharon.com/blog/37.
   # This finds the element containing the selection focus.


added utf-8 char which fails the archive option

--- test/acceptance/coffee/ArchiveDocsTests.coffee
+++ test/acceptance/coffee/ArchiveDocsTests.coffee
@@ -22,7 +22,7 @@
 			rev: 4
 		}, {
 			_id: ObjectId()
-			lines: ["111", "222", "333"]
+			lines: ["111", "222", "Antônio Sérgio"]
 			rev: 6
 		}]
 		jobs = for doc in @docs


Make sure there is an on_success() method defined before executing.

--- lib/assets/javascripts/websocket_rails/channel.js.coffee
+++ lib/assets/javascripts/websocket_rails/channel.js.coffee
@@ -34,8 +34,8 @@

   # using this method because @on_success will not be defined when the constructor is executed
   on_success_launcher: (data) =>
-    @on_success(data)
+    @on_success(data) if @on_success?

   # using this method because @on_failure will not be defined when the constructor is executed
   on_failure_launcher: (data) =>
-    @on_failure(data)
+    @on_failure(data) if @on_failure?


Hound linter comment

Hound linter comment

--- app/assets/javascripts/frontend/form-validation.js.coffee
+++ app/assets/javascripts/frontend/form-validation.js.coffee
@@ -81,7 +81,7 @@
       if question.find(".js-conditional-question.show-question .js-by-trade-goods-and-services-amount .js-conditional-question.show-question").size() == 0
         conditional = false
     else
-      question.find(".js-conditional-question").each () ->
+      question.find(".js-conditional-question").each ->
         if !$(this).hasClass("show-question")
           conditional = false


Run the entire file if no text is selected

--- lib/script.coffee
+++ lib/script.coffee
@@ -24,8 +24,8 @@
       editor = atom.workspace.getActiveEditor()
       code = editor.getSelectedText()

-      if ! code?
-        return
+      if not code? or not code
+        code = editor.getText()

       if ! editor?
         console.log("Where's my bloody editor?")


Destroy no longer exists, now it's close

--- lib/script.coffee
+++ lib/script.coffee
@@ -7,7 +7,7 @@
     @scriptView = new ScriptView(state.scriptViewState)

   deactivate: ->
-    @scriptView.destroy()
+    @scriptView.close()

   serialize: ->
     scriptViewState: @scriptView.serialize()


Save reference to spawn

Save reference to spawn

--- test/acceptance.coffee
+++ test/acceptance.coffee
@@ -1,6 +1,6 @@
 fs = require 'fs'
 WordGenerator = require 'wordgenerator'
-cp = require 'child_process'
+spawn = require('child_process').spawn
 async = require 'async'
 _ = require 'underscore'
 EventEmitter = require('events').EventEmitter
@@ -10,7 +10,7 @@
     @repo = words
     done()
   afterEach (done) ->
-    rm = cp.spawn "rm", ["-rf", @repo], { cwd: "#{__dirname}/..", stdio: 'inherit' }
+    rm = spawn "rm", ["-rf", @repo], { cwd: "#{__dirname}/..", stdio: 'inherit' }
     rm.on 'close', ->
       done()
   afterEach -> process.chdir.restore()
@@ -57,6 +57,7 @@
     html_url: "https://github.com/tandrewnichols/#{@repo}"
     clone_url: "git@github.com:tandrewnichols/#{@repo}.git"
   Given -> @cp.spawn = sinon.stub()
+  #Given -> @cp.spawn.withArgs('grep', ['-rlP', sinon.match.string, "./#{@repo}"])
   Given -> @clone = new EventEmitter()
   Given -> @cp.spawn.withArgs('git', ['clone', 'git@github.com:tandrewnichols/tinder-template.git', @repo],
     stdio: 'inherit'


also include in browserification

also include in browserification

--- test/browser/main.iced
+++ test/browser/main.iced
@@ -1,5 +1,6 @@

 mods =
+  rfc3394 : require '../files/rfc3394.iced'
   google_end_to_end : require '../files/google_end_to_end.iced'
   detached_sigs : require '../files/verify_detached_sigs.iced'
   hide : require '../files/hide.iced'


LEV-5 # debug background

LEV-5 # debug background

--- modules/levelcreator/app/assets/javascripts/level_creator/plugins/draw_art_cells.coffee
+++ modules/levelcreator/app/assets/javascripts/level_creator/plugins/draw_art_cells.coffee
@@ -32,12 +32,13 @@
     shadowColor : "\"rgba(0, 0, 255, 0.3)\""
     mergeSegments : "true, false (default)"
     minSize : "Number"
+    maxRewardedEndSegments : "Number"

   constructor : () ->

-  execute : ({ input : { rgba, segments, relativeTime, dimensions, mission }, minSize, startShape, fillColor, strokeColor, hitMode, lineWidth, colorRandom, customTime, reverse, endPosition, size, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, mergeSegments}) ->
+  execute : ({ input : { rgba, segments, relativeTime, dimensions, mission }, maxRewardedEndSegments, minSize, startShape, fillColor, strokeColor, hitMode, lineWidth, colorRandom, customTime, reverse, endPosition, size, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, mergeSegments}) ->

     width = dimensions[0]
     height = dimensions[1]
@@ -53,6 +54,9 @@

     if customTime?
       relativeTime = customTime
+
+    unless maxRewardedEndSegments?
+      maxRewardedEndSegments = 3

     shadowOffsetX = 0 unless shadowOffsetX
     shadowOffsetY = 0 unless shadowOffsetY
@@ -78,7 +82,7 @@
     activeSegments = _.sortBy(activeSegments, (s) -> -s.size)
     threeEndValues = []
     sortedEnds = _.sortBy(mission.possibleEnds, (s) -> -s.probability)
-    for f in [0...Math.min(3, sortedEnds.length)]
+    for f in [0...Math.min(maxRewardedEndSegments, sortedEnds.length)]
       threeEndValues.push sortedEnds[f].id

     for segment in activeSegments


Directly fetch subject group

Directly fetch subject group

--- app/annotation/summary.cjsx
+++ app/annotation/summary.cjsx
@@ -3,6 +3,7 @@

 Subject = require 'zooniverse/models/subject'
 ProjectGroup = require 'zooniverse/models/project-group'
+Api = require 'zooniverse/lib/api'
 Share = require '../share'
 steps = require '../lib/steps'

@@ -37,11 +38,12 @@

   onProjectGroupFetch: ->
     groupId = Subject.current?.group_id
-    group = ProjectGroup.find groupId
-    @setState({
-      group: group
-      newChimpsLink: group.metadata.new_chimps_link
-    }, => @getSiteLocation())
+    Api.current.get("/projects/#{Api.current.project}/groups/#{groupId}")
+      .then (subjectGroup) =>
+        @setState({
+          group: subjectGroup
+          newChimpsLink: subjectGroup.metadata.new_chimps_link
+        }, => @getSiteLocation())

   onProjectGroupFail: ->
     console.error 'Project Group fetch failed'


Fix "Exiting the image viewer modal jumps you to the top of page"

Will close #915

--- _coffee/app/lightbox.coffee
+++ _coffee/app/lightbox.coffee
@@ -13,6 +13,7 @@
     i = @getLinkIndex ev.srcElement.parentElement

     @lightbox = new Lightbox(@, i)
+    window.currentLightbox = @lightbox

   getLinkIndex: (elem) ->
     # Given a link return it's index in the galleryLinks array
@@ -48,7 +49,7 @@
     window.disable_keyboard_nav = true

     # Freeze scrolling
-    @scrollTop = document.body.scrollTop
+    @scrollTop = $(document).scrollTop()
     document.body.style.position = "fixed"
     document.body.style.top = "-#{@scrollTop}px"

@@ -154,7 +155,7 @@
   close: =>
     # Unfreeze scrolling
     document.body.style.position = "static"
-    document.body.scrollTop = @scrollTop
+    $(document).scrollTop(@scrollTop)

     # Remove blur
     @blur.classList.remove('lightbox-blur--show')


add ability to reload single file. #24

add ability to reload single file. #24

--- lib/server.coffee
+++ lib/server.coffee
@@ -127,7 +127,7 @@
             name = path.basename src, '.coffee'

             # new products
-            if config.type == 'product'
+            if config.type in ['product', 'plugin']
               asset = new wrap.Coffee {
                 src: src
                 preprocess: (source) ->
@@ -226,7 +226,7 @@
           tmpl = "#{pkg_id_version}.Templates"
           page = "#{pkg_id_version}.Pages" # TODO: deprecate

-          if asset.pkg_config.type in ['product', 'app', 'theme', 'site']
+          if asset.pkg_config.type in ['product', 'app', 'theme', 'site', 'plugin']
             for s in [lt3, pkg, pkg_id, pkg_id_version, pres, tmpl, page]
               header += check(s)
             header += check("#{pkg_id_version}.config", asset.pkg_config)
@@ -274,14 +274,6 @@
               next()

         loadVariables ->
-          getContainer = (name) ->
-            if name in config.pages
-              return '#content .pages'
-            for c, o of config.collections
-              return '#content' if name is c
-              return '#content .pages' if name is o
-            return ''
-
           add = (src) ->
             return unless src
             return unless fs.existsSync src
@@ -296,17 +288,24 @@
               asset = new wrap.Stylus {
                 src: src
                 preprocess: (source) ->
-                  id = ".#{config.id}"
-                  version = ".v#{config.version.replace /\./g, '-'}"
+                  id = config.id
+                  version = config.version.replace /\./g, '-'
                   name = path.basename src, '.styl'
-                  container = getContainer name
-
-                  # ex: .exports -> .artist-hq.v3-0-0 #page > .soundcloud
-                  if config.type == 'product'
-                    source = source.replace /^.exports$/gm, "#{id}#{version} #{container} > .#{name}"
+
+                  if config.type in ['product', 'plugin']
+                    p = ".#{id}.v#{version} #{name}"
+                    subs = [
+                      ['.exports.collection',  "#{p}.collection"]
+                      ['.exports.object',      "#{p}.object"]
+                      ['.exports.page',        "#{p}.page"]
+                      ['.exports.presenter',   "#{p}.presenter"]
+                      ['.exports.region',      "#{p}.region"]
+                    ]
+                    for sub in subs
+                      source = source.replace sub[0], sub[1]

                   # ex: html.exports -> html.artist-hq.v3-0-0
-                  source = source.replace /.exports/g, "#{id}#{version}"
+                  source = source.replace /.exports/g, ".#{id}.v#{version}"

                   # add variables code
                   return variables_code + source
@@ -365,7 +364,12 @@
           ref = firebase.child "users/#{user}/developer/listener"
           config.modified =
             time: Date.now()
-            file: path.basename file
+            base: path.basename file
+            ext: path.extname(file).replace '.', ''
+            file: file
+            name: path.basename file, path.extname(file)
+
+            # TODO: deprecate
             file_ext: path.extname(file).replace '.', ''
             file_name: path.basename file, path.extname(file)
           ref.set config
@@ -480,6 +484,111 @@
           return error 400, err if err
           res.send data

+    # Package Single File Reload
+    unless prod
+      router.route 'GET', '/pkg/:id/:version/reload/*', (req, res, next) ->
+
+        # grab parameters
+        id = req.params.id
+        version = req.params.version
+        file = req.params[0]
+        name = path.basename file
+        filepath = path.join "#{pkgDir id, version}", file
+
+        # make sure file exists
+        fs.exists filepath, (exists) ->
+          return error 404, "File #{file} does not exists" unless exists
+
+          ext = path.extname filepath
+          switch ext
+
+            # schema files
+            when '.cson'
+              readCSON filepath, (err, data) ->
+                return error 400, err if err
+                contentType 'text/javascript'
+                pkg = "lt3.pkg[\"#{id}\"][\"#{version}\"]"
+                res.send "#{pkg}.schema[\"#{name}\"] = #{JSON.stringify(data)}"
+
+            # presenters and templates
+            when '.coffee'
+              asset = new wrap.Coffee {
+                src: filepath
+                preprocess: (source) ->
+                  pkg = "lt3.pkg['#{id}']['#{version}']"
+                  p = "#{pkg}.Presenters['#{name}'] extends lt3.presenters"
+                  t = "#{pkg}.Templates['#{name}']"
+                  subs = [
+                    ['exports.Collection',  "#{p}.Collection"]
+                    ['exports.Object',      "#{p}.Object"]
+                    ['exports.Page',        "#{p}.Page"]
+                    ['exports.Presenter',   "#{p}.Presenter"]
+                    ['exports.Region',      "#{p}.Region"]
+                    ['exports.Template',    "#{t}"]
+                  ]
+                  for sub in subs
+                    source = source.replace sub[0], sub[1]
+                  return source
+              }, (err) ->
+                return error 400, err if err
+                contentType 'text/javascript'
+                res.send asset.data
+
+
+            # style files
+            when '.styl'
+
+              # check for style/variables.styl
+              variables_code = ''
+              root = pkgDir id, version
+              loadVariables = (next) ->
+                vars_path = path.join(root, 'style', 'variables.styl')
+                fs.exists vars_path, (exists) ->
+                  if exists
+                    fs.readFile vars_path, 'utf8', (err, data) ->
+                      variables_code = data
+                      next()
+                  else
+                    next()
+
+              loadVariables ->
+                getContainer = (name) ->
+                  if name in config.pages
+                    return '#content .pages'
+                  for c, o of config.collections
+                    return '#content' if name is c
+                    return '#content .pages' if name is o
+                  return ''
+
+                console.log req.query
+                asset = new wrap.Stylus {
+                  src: filepath
+                  vars: req.query
+                  vars_prefix: '$'
+                  preprocess: (source) ->
+                    id = id
+                    version = version.replace /\./g, '-'
+
+                    p = ".#{id}.v#{version} #{name}"
+                    subs = [
+                      ['.exports.collection',  "#{p}.collection"]
+                      ['.exports.object',      "#{p}.object"]
+                      ['.exports.page',        "#{p}.page"]
+                      ['.exports.presenter',   "#{p}.presenter"]
+                      ['.exports.region',      "#{p}.region"]
+                    ]
+                    for sub in subs
+                      source = source.replace sub[0], sub[1]
+
+                    # add variables code
+                    return variables_code + source
+                }, (err) ->
+                  return error 400, err if err
+                  contentType 'text/css'
+                  res.send asset.data
+            else
+              error 400, "invalid file type"
+
     # Package Javascript
     router.route 'GET', '/pkg/:id/:version/main.js', (req, res, next) ->
       contentType 'text/javascript'


Use toggle class for removing selected class

--- lib/file-view.coffee
+++ lib/file-view.coffee
@@ -53,6 +53,6 @@
                 rootView.open(tempFilePath)

   select: ->
-    @closest('.archive-view').find('.selected').view()?.name.removeClass('selected')
+    @closest('.archive-view').find('.selected').toggleClass('selected')
     @name.addClass('selected')
     @focus()


Refactor FindView.anySelectionsMatchFindPattern to rely on markers instead of buffer scans.

--- lib/find-view.coffee
+++ lib/find-view.coffee
@@ -447,30 +447,27 @@
     else
       optionButton.removeClass 'selected'

-  anySelectionsMatchFindPattern: =>
-    editor = @model.getEditor()
-    selections = editor?.getSelections() or []
+  anyMarkersAreSelected: =>
+    selections = @model.getEditor()?.getSelections() or []
     _.any selections, (selection) =>
-      selectionMatchesFindPattern = false
-      editor.scanInBufferRange @model.getFindPatternRegex(), selection.getBufferRange(), ({range}) ->
-        selectionMatchesFindPattern = selectionMatchesFindPattern or _.isEqual(range, selection.getBufferRange())
-      selectionMatchesFindPattern
+      _.any @model.markers or [], (marker) =>
+        _.isEqual(marker.getBufferRange(), selection.getBufferRange())

   toggleRegexOption: =>
     @search(useRegex: not @model.getFindOptions().useRegex)
-    @selectFirstMarkerAfterCursor() unless @anySelectionsMatchFindPattern()
+    @selectFirstMarkerAfterCursor() unless @anyMarkersAreSelected()

   toggleCaseOption: =>
     @search(caseSensitive: not @model.getFindOptions().caseSensitive)
-    @selectFirstMarkerAfterCursor() unless @anySelectionsMatchFindPattern()
+    @selectFirstMarkerAfterCursor() unless @anyMarkersAreSelected()

   toggleSelectionOption: =>
     @search(inCurrentSelection: not @model.getFindOptions().inCurrentSelection)
-    @selectFirstMarkerAfterCursor() unless @anySelectionsMatchFindPattern()
+    @selectFirstMarkerAfterCursor() unless @anyMarkersAreSelected()

   toggleWholeWordOption: =>
     @search(@findEditor.getText(), wholeWord: not @model.getFindOptions().wholeWord)
-    @selectFirstMarkerAfterCursor() unless @anySelectionsMatchFindPattern()
+    @selectFirstMarkerAfterCursor() unless @anyMarkersAreSelected()

   updateReplaceEnablement: ->
     canReplace = @markers?.length > 0


[master] redis

[master] redis

--- database/redis.coffee
+++ database/redis.coffee
@@ -1,7 +1,7 @@
 redis = require 'redis'
 winston = require 'winston'
-connection = null
-clientCount = 0
+Q = require 'q'
+connectionPool = {}

 logger = new (winston.Logger)(
   transports: [
@@ -13,22 +13,31 @@
   ]
 )

-closeConnection = ->
-  clientCount -= 1
-  if clientCount == 0
-    connection.quit()
-    logger.debug 'redis connection closed'
-    connection = null
+closeConnection = (db) ->
+  connectionPool[db].count -= 1
+  if connectionPool[db].count == 0
+    connectionPool[db].connection.quit()
+    logger.debug "redis connection to #{db} closed"
+    delete connectionPool[db]
     return

-getConnection = ->
-  clientCount += 1
-  if not connection
+getConnection = (db) ->
+  db ?= 0
+  if not connectionPool[db]
     connection = redis.createClient
       parser: 'hiredis'
       auth_pass: 'whatever'
-    connection.closeConnection = closeConnection
-    logger.debug 'redis connection established'
-  connection
+    Q.ninvoke connection, 'select', db
+    .then ->
+      connection.closeConnection = ->
+        closeConnection db
+      connectionPool[db] =
+        connection: connection
+        count: 1
+      logger.debug "redis connection to #{db} established"
+      connection
+  else
+    connectionPool[db].count += 1
+    Q connectionPool[db].connection

 exports.getConnection = getConnection


Comments and cleanup.

Comments and cleanup.

--- src/runtime.coffee
+++ src/runtime.coffee
@@ -51,7 +51,7 @@
     @loaded_classes = Object.create null

     @high_oref = 1
-    @string_pool = {}
+    @string_pool = Object.create null
     @lock_refs = {}  # map from monitor -> thread object
     @lock_counts = {}  # map from monitor -> count
     @waiting_threads = {}  # map from monitor -> list of waiting thread objects
@@ -194,9 +194,7 @@
     @class_lookup type
     @set_obj type, obj
   init_string: (str,intern=false) ->
-    # this is a bit of a kludge: if the string we want to intern is __proto__ or a function name,
-    # we fail to intern it.
-    return @string_pool[str] if intern and @string_pool[str]?.type?.toClassString?() is 'java/lang/String'
+    return @string_pool[str] if intern and @string_pool[str]?
     carr = @init_carr str
     jvm_str = new JavaObject c2t('java/lang/String'), @, {'value':carr, 'count':str.length}
     @string_pool[str] = jvm_str if intern
@@ -214,7 +212,9 @@
     @jclass_obj_pool[type]

   # Returns a ClassFile object. Loads the underlying class, but does not
-  # initialize it.
+  # initialize it. :dyn should be set if the class may not have been present at
+  # compile time, e.g. if we are loading a class as a result of a
+  # Class.forName() call.
   load_class: (type, dyn) ->
     unless @loaded_classes[type]?
       if type instanceof types.ArrayType
@@ -225,6 +225,8 @@
       else if type instanceof types.PrimitiveType
         @loaded_classes[type] = '<primitive>'
       else
+        # a class gets loaded with the loader of the class that is triggering
+        # this class resolution
         defining_class_state = @class_states[@curr_frame().method.class_type]
         if loader = defining_class_state.loader?
           rs.push loader
@@ -250,11 +252,10 @@
   # 5.5 of the SE7 spec.
   class_lookup: (type, dyn) ->
     UNSAFE? || throw new Error "class_lookup needs a type object, got #{typeof type}: #{type}" unless type instanceof types.Type
-    return '<primitive>' if type instanceof types.PrimitiveType
+    UNSAFE? || throw new Error "class_lookup was passed a PrimitiveType" if type instanceof types.PrimitiveType
     class_file = @load_class type, dyn
-    cls = type.toClassString?() ? type.toString()
     unless @class_states[type].fields?
-      trace "initializing class: #{cls}"
+      trace "initializing class: #{type.toClassString()}"
       @class_states[type].fields = Object.create null
       if type instanceof types.ArrayType
         component = type.component_type
@@ -282,6 +283,8 @@
         delete c.$in_progress
         class_file.methods['<clinit>()V']?.run(this)
     class_file
+
+  # called by user-defined classloaders
   define_class: (cls, data, loader) ->
     # replicates some logic from class_lookup
     class_file = new ClassFile(data)


Added Ice Level to terrain generator

Added Ice Level to terrain generator

--- app/views/editor/level/modals/GenerateTerrainModal.coffee
+++ app/views/editor/level/modals/GenerateTerrainModal.coffee
@@ -105,6 +105,14 @@
   }
   'mountain_walls': {
     'thangs': ['Mountain 1','Mountain 3']
+    'margin': 6
+  }
+  'ice_floor': {
+    'thangs': ['Firn 1', 'Firn 2', 'Firn 3', 'Firn 4', 'Firn 5', 'Firn 6']
+    'margin': -1
+  }
+  'ice_walls': {
+    'thangs': ['Ice Wall']
     'margin': 6
   }
 }
@@ -261,6 +269,25 @@
     'type': 'mountain'
     'floors': 'mountain_floor'
     'borders': 'mountain_walls'
+    'borderNoise': 1
+    'borderSize': 1
+    'borderThickness': 1
+    'decorations': {
+      'hero': {
+        'num': [1, 1]
+        'width': 2
+        'height': 2
+        'clusters': {
+          'hero': [1, 1]
+        }
+      }
+    }
+  },
+  'ice': {
+    'terrainName': 'Ice'
+    'type': 'ice'
+    'floors': 'ice_floor'
+    'borders': 'ice_walls'
     'borderNoise': 1
     'borderSize': 1
     'borderThickness': 1


Changes for midi_particles in the Un Sospiro video

- Remove MIDI inputs that only make sense on the Q49 keyboard
- Expire particles with zero intensity, and cap the max particles
- Lower particle generation rate

--- examples/node/midi_particles.coffee
+++ examples/node/midi_particles.coffee
@@ -48,6 +48,7 @@

 # Live particles
 particles = []
+maxParticles = 500

 # Notes for low frequency oscillators
 lfoNotes = {}
@@ -64,7 +65,7 @@
 origin = [0, 0, 0]

 # Physics
-frameDelay = 5
+frameDelay = 10
 timestepSize = 0.010
 gain = 0.1

@@ -82,7 +83,7 @@

 # Musical Constants
 # Boundary between the left-hand and right-hand patterns.
-LIMINAL_KEY = 40
+LIMINAL_KEY = 46
 MAX_VELOCITY = 100
 SHARP_NAMES = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#']
 FLAT_NAMES = ['A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab']
@@ -116,18 +117,18 @@
             else
                 lfoNotes[key] = info

-        when 0xb  # Voice 0, Control Change
-            switch message[1]
-                when 7, 33   # "Data entry" / "C1" slider, brightness
-                    brightness = message[2] * 2.0 / 127
-
-                when 1, 34   # "Modulation" / "C2" slider, particle speed
-                    console.log message[2]
-                    particleLifetime = 0.1 + message[2] * 2.0 / 127
-
-        when 0xe  # Voice 0, Pitch Bend
-            # Default spin 1.0, but allow forward/backward
-            spinRate = (message[2] - 64) * 10.0 / 64
+        # when 0xb  # Voice 0, Control Change
+        #     switch message[1]
+        #         when 7, 33   # "Data entry" / "C1" slider, brightness
+        #             brightness = message[2] * 2.0 / 127
+
+        #         when 1, 34   # "Modulation" / "C2" slider, particle speed
+        #             console.log message[2]
+        #             particleLifetime = 0.1 + message[2] * 2.0 / 127
+
+        # when 0xe  # Voice 0, Pitch Bend
+        #     # Default spin 1.0, but allow forward/backward
+        #     spinRate = (message[2] - 64) * 10.0 / 64
     console.log logMsg

 draw = () ->
@@ -152,6 +153,10 @@
             velocity: [0, 0, 0]
             timestamp: note.timestamp

+    # Discard particles if necessary
+    if particles.length > maxParticles
+        particles = particles.slice particles.length - maxParticles
+
     # Update appearance of all particles
     for p in particles

@@ -180,7 +185,7 @@
         p.intensity *= Math.max(0, 1 - (noteAge / noteSustain))

         # Falloff gets sharper as the note gets higher
-        p.falloff = 15 * Math.pow(2, (p.note.key - LIMINAL_KEY) / 6)
+        p.falloff = 12 * Math.pow(1.18, (p.note.key - LIMINAL_KEY) / 6)

         # Add influence of LFOs
         for key, note of lfoNotes
@@ -215,7 +220,7 @@
         p.life -= timestepSize / particleLifetime

     # Filter out dead particles
-    particles = particles.filter (p) -> p.life > 0
+    particles = particles.filter (p) -> p.life > 0 && p.intensity > 0

     # Render particles to the LEDs
     client.mapParticles particles, model


(- new debug) Clean TRACE_REACTOR output up

Conflicts:
	Source/reactor.coffee

--- Paws/Source/reactor.coffee
+++ Paws/Source/reactor.coffee
@@ -215,18 +215,23 @@
    realize: ->
       return no unless staging = @next()
       {stagee, result, requestedMask} = staging
-      prior_position = stagee.position
+
+      if process.env['TRACE_REACTOR']
+         exec_printout = if not stagee.position? then '' else
+            ("\n" + stagee.position.with(context: yes, tag: no).toString())
+            .replace /\n/g, "\n   │  "
+         Paws.warning ">> #{stagee} ← #{result}" + exec_printout

       # Remove complete stagees from the queue, with no further action.
-      return yes if stagee.complete()
+      if stagee.complete()
+         Paws.warning "   ╰┄ complete!" if process.env['TRACE_REACTOR']
+         return yes

       combo = reactor.advance stagee, result
       @current = stagee

-      if process.env['TRACE_REACTOR']
-         Paws.alert ">> #{stagee} ← #{result}"
-         Paws.alert "   #{prior_position.with(context: yes, tag: no).toString()}" if prior_position?
-         Paws.alert "   ┗> #{combo.subject} × #{combo.message}" if combo.subject?
+      if process.env['TRACE_REACTOR'] and combo.subject?
+         Paws.warning "   ╰┄ combo: #{combo.subject} × #{combo.message}"

       # If the staging has passed #next, then it's safe to grant it the ownership it's requesting
       @table.give stagee, requestedMask if requestedMask


Workspace: Trimmed session key on join modal.

--- client/app/CommonViews/workspace/collaborativeworkspace.coffee
+++ client/app/CommonViews/workspace/collaborativeworkspace.coffee
@@ -158,7 +158,7 @@
   joinSession: (sessionKey) ->
     {parent}           = @
     options            = @getOptions()
-    options.sessionKey = sessionKey
+    options.sessionKey = sessionKey.trim()
     @destroy()

     @forceDisconnect()


Added modulo operator and more haskell functions


Stream.fromArray no longer constructs the whole Stream explicitly.

--- lib/stream.coffee
+++ lib/stream.coffee
@@ -134,9 +134,9 @@
 Stream.range = (start, end) -> Stream.from(start).take(end - start + 1)

 Stream.fromArray = (a) ->
-  step = (s, i) ->
-    if i > 0 then recur -> step(new Stream(a[i-1], -> s), i-1) else s
-  resolve step(null, a.length)
+  n = a.length
+  partial = (i) -> if i < n then new Stream(a[i], -> partial(i+1)) else null
+  partial(0)

 # --------------------------------------------------------------------


ok - moving on

ok - moving on

--- spec/notice/notifier_factory_spec.coffee
+++ spec/notice/notifier_factory_spec.coffee
@@ -167,18 +167,23 @@
                 that 'survives a serously heavy callbackblat', (done) ->

                     class SpannerInTheWorks
-
+                    class Xpert
                     class AntiqueConduit

                         constructor: (@mechanism) ->
                         send: (  -> => ->
-                            @mechanism.bigLever.pull.apply (
-                                new SpannerInTheWorks()
-                            ), arguments
+                            @mechanism.bigLever.pull.evenHarder.apply (
+                                new SpannerInTheWorks(
+                                    new Xpert(
+                                        "no dude! ...you'll snap it"
+                                    )
+                                )
+                            ), arguments
+
                         )()()

-                    email = new AntiqueConduit bigLever: pull: -> notify.apply {spam:{spam:{spam:{}}}}, arguments
+                    email = new AntiqueConduit bigLever: pull: evenHarder: -> notify.apply {spam:{spam:{spam:{}}}}, arguments

                     require('when/sequence')( [


enable toggling outlier removal on/off

enable toggling outlier removal on/off

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -84,13 +84,15 @@
   processLightcurve: (removeOutliers=false) ->

     # this step is necessary or (top) x-axis breaks
-    @smallestX = Math.min @data.x...
+    @smallestX = Math.min @data_raw.x...
     @originalMin = @smallestX
-    for xValue, idx in [@data.x...]
+    for xValue, idx in [@data_raw.x...]
       @data.x[idx] = xValue - @smallestX

     if removeOutliers
-      @data.y = @removeOutliers(@data.y, nsigma=3) # NOTE: nsigma < 8 removes tutorial subject transits
+      @data = @removeOutliers(@data_raw, nsigma=3) # NOTE: nsigma < 8 removes tutorial subject transits
+    else
+      @data.y = @data_raw.y # restore to original values

     @data.y = @normalize(@data.y)

@@ -114,18 +116,23 @@
     return y_new

   removeOutliers: (data, nsigma) ->
-    y_new = []
-    mean = @mean(data)
-    std = @std(data)
+    console.log 'data.x.length: ', data.x.length
+    console.log 'data.y.length: ', data.y.length
+    data_new = {}
+    data_new.x = []
+    data_new.y = []
+    mean = @mean(data.y)
+    std = @std(data.y)

-    for y, i in [data...]
+    for y, i in [data.y...]
       if Math.sqrt( Math.pow( y - mean, 2 ) ) > nsigma * std
         console.log 'removed outlier' if DEBUG
         continue # skip (outlier)
       else
-        y_new.push data[i]
-
-    return y_new
+        data_new.x.push data.x[i]
+        data_new.y.push data.y[i]
+
+    return data_new

   std: (data) ->
     mean = @mean(data)


Add POST /db/level/:handle test

Add POST /db/level/:handle test

It's erroring in my dev console, complaining of duplicate keys, and not sure why.
Adding a test to make sure creating a new level version doesn't break.

--- spec/server/functional/level.spec.coffee
+++ spec/server/functional/level.spec.coffee
@@ -39,6 +39,21 @@
       body = JSON.parse(body)
       expect(body.type).toBeDefined()
       done()
+
+
+describe 'POST /db/level/:handle', ->
+  it 'creates a new version', utils.wrap (done) ->
+    yield utils.clearModels([Campaign, Course, CourseInstance, Level, User])
+    admin = yield utils.initAdmin()
+    yield utils.loginUser(admin)
+    @level = yield utils.makeLevel()
+    levelJSON = @level.toObject()
+    levelJSON.name = 'New name'
+
+    url = getURL("/db/level/#{@level.id}")
+    [res, body] = yield request.postAsync({url: url, json: levelJSON})
+    expect(res.statusCode).toBe(200)
+    done()

 describe 'GET /db/level/:handle/session', ->


Fix accidental shadowing of the chosen variable

--- src/chosen.coffee
+++ src/chosen.coffee
@@ -1,8 +1,8 @@
 angular.module('localytics.directives', [])

-chosen = angular.module('localytics.directives')
+chosenModule = angular.module('localytics.directives')

-chosen.provider 'chosen', ->
+chosenModule.provider 'chosen', ->
   options = {}
   {
     setOption: (newOpts) ->
@@ -12,7 +12,7 @@
       options
   }

-chosen.directive 'chosen', ['chosen', '$timeout', (config, $timeout) ->
+chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) ->
   # coffeelint: disable=max_line_length
   # This is stolen from Angular...
   NG_OPTIONS_REGEXP =  /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/


add getActive method

add getActive method

--- js/umobi.page.coffee
+++ js/umobi.page.coffee
@@ -1,17 +1,21 @@
 define ["cs!umobi.core"], ->
+
+  # static methods of page object.
   umobi.page =
+
+    getActive: () -> $('.ui-page-active')

     # $p: page element object.
     reveal: ($p) ->
       # hide current active page
-      $('.ui-page-active').removeClass('ui-page-active')
+      this.getActive().removeClass('ui-page-active')
       $p.addClass('ui-page-active').trigger('pagereveal')
       $(document).trigger('pagereveal',[$p])

     revealByHash: (hash) ->
       # show first page if page not found.
       $page = $(hash)
-      $page = $($('[data-role="page"]').get(0)) if not $page.get(0)
+      $page = $('[data-role="page"]').first() if not $page.get(0)
       umobi.page.reveal($page)

     init: (el) ->


Implement https://api.slack.com/events/star_added and https://api.slack.com/events/star_removed (for everything, not just messages). It's up to the app to react to what's being starred/unstarred.

--- src/client.coffee
+++ src/client.coffee
@@ -131,6 +131,12 @@

       @ws.on 'ping', (data, flags) =>
         @ws.pong
+
+      @ws.on 'star_added', (data, flags) =>
+        @onStarAdded JSON.parse(data)
+
+      @ws.on 'star_added', (data, flags) =>
+        @onStarRemoved JSON.parse(data)

       return true

@@ -307,6 +313,16 @@
       if group.unread_count > 0 then unreads.push group

     unreads
+
+   #
+   # Stars handler callbacks and dispatches
+   #
+
+   onStarAdded: (data) ->
+     @emit 'star_added', data
+
+   onStarRemoved: (data) ->
+     @emit 'star_removed', data

   #
   # Message handler callback and dispatch


Remove debugging code

Remove debugging code

--- src/renderer/scripts/app/SpeechHost.coffee
+++ src/renderer/scripts/app/SpeechHost.coffee
@@ -26,7 +26,6 @@
         return

     speech : (text) ->
-        console.log text
         return if app.config.get("nco.speech", false) is false

         s = new SpeechSynthesisUtterance(text)


incrementing test

incrementing test

--- test/flow-interruption.coffee
+++ test/flow-interruption.coffee
@@ -5,6 +5,9 @@

   out = null
   router = null
+
+  render = null
+  destroy = null

   before ->
     out = {}
@@ -19,14 +22,14 @@
       out?.log 'destroy', req
       done()

+  it 'should interrupt a running flow and starts another (render+destroy)', (done)->
+
+
     router = new Router 'destroy+render'
     router.get '/', render, destroy
     router.get '/pages', render, destroy, '/'
     router.get '/pages/:id', render, destroy, '/pages'
     router.get '/login', render, destroy
-
-
-  it 'should render route with param from scratch', (done)->

     requests =  [
       {url: '/', pattern: '/', params: {}}
@@ -45,4 +48,34 @@
           done()
         , 500

+    router.push '/pages/33'
+
+
+  it 'should interrupt a running flow and starts another (destroy+render)', (done)->
+
+    router = new Router 'render+destroy'
+    router.get '/', render, destroy
+    router.get '/pages', render, destroy, '/'
+    router.get '/pages/:id', render, destroy, '/pages'
+    router.get '/auth', render, destroy
+    router.get '/login', render, destroy, '/auth'
+
+    requests =  [
+      {url: '/', pattern: '/', params: {}}
+      {url: '/pages', pattern: '/pages', params: {}}
+      {url: '/auth', pattern: '/auth', params: {}}
+      {url: '/login', pattern: '/login', params: {}}
+    ]
+
+    out.log = (type, req)->
+      type.should.equal 'render'
+      req.should.deep.equal requests.shift()
+      if requests.length is 0
+        out.log = null
+
+        # waits some time so if something goes wrong, it will throw
+        timeout = new setTimeout ->
+          done()
+        , 500
+
     router.push '/pages/33'


Fix config bug.

Fix config bug.

--- src/config.coffee
+++ src/config.coffee
@@ -124,13 +124,18 @@
     config = _.defaults conf, exports.defaults
     callback err

-recurseConf = (keys, conf, cb) ->
+recurseConf = (keys, conf, create, cb) ->
+  [create, cb] = [false, create] if _.isFunction(create)
   [key, rest...] = keys
   val = conf[key]
   if _.isEmpty(rest) || _.isUndefined(val)
-    cb(key, conf)
+    if create && !_.isEmpty(rest)
+      conf[key] = {}
+      recurseConf(rest, conf[key], create, cb)
+    else
+      cb(key, conf)
   else
-    recurseConf(rest, val, cb)
+    recurseConf(rest, val, create, cb)

 exports.get = (key, type) ->
   keys = key.split('.')
@@ -141,7 +146,7 @@
   keys = key.split('.')
   conf = configs[type]
   return unless conf?
-  recurseConf keys, conf, (k, c) -> c[k] = val
+  recurseConf keys, conf, true, (k, c) -> c[k] = val

 exports.unset = (key, type='project') ->
   keys = key.split('.')


Allow single-line JS comments without a newline in the end.

--- lib/js2coffee.coffee
+++ lib/js2coffee.coffee
@@ -1,6 +1,6 @@
-if typeof module == 'undefined'
-  narcissus = this.Narcissus
-  _         = this._
+if window?
+  narcissus = window.Narcissus
+  _         = window._
 else
   narcissus = require('./narcissus_packed')
   _         = require('underscore')
@@ -424,10 +424,11 @@
 exports =
   version: '0.0.3'
   build: (str) ->
-    trim(build(parser.parse(str)))
+    trim(build(parser.parse("#{str}\n")))
   UnsupportedError: UnsupportedError

-if typeof module == 'undefined'
-  this.Js2coffee = exports
-else
+if window?
+  window.Js2coffee = exports
+
+if module?
   module.exports = exports


refactoring a bit to evenutally support different kinds of embeddables, including those that need to happen as a callaback on post rendering

--- src/scripts/modules/media/body/body.coffee
+++ src/scripts/modules/media/body/body.coffee
@@ -138,33 +138,48 @@
             $el = $(el)
             $el.css('counter-reset', 'list-item ' + $el.attr('start'))

-        @fakeExercises($temp);
-
+        @fakeExercises($temp)
+        @embeddablesPromise = @getExercises($temp)

       catch error
         # FIX: Log the error
         console.log error

       @$el?.html($temp.html())
-      @renderExercises(@$el)
-
-
-    renderExercises: ($parent)->
+      @embeddablesPromise?.then(@postRenderEmbeddables)
+
+    getExercises: ($parent)->
+
+      defer = $.Deferred()
+      @postRenderQueue = []

       exercises = @findExerciseDOMS($parent)
-      _.each(exercises, @renderExercise, @)
-
-
-    renderExercise: (exercise, iter)->
+      exercisePromises = _.map(exercises, @getExercise, @)
+
+      $.when.apply(@, exercisePromises).then(()=>
+        @postRenderQueue
+      )
+
+
+    getExercise: (exercise, iter)->
       $exercise = $(exercise)
       exerciseLink = $exercise.attr('href')

-      _render = (questions)->
-        _.each(questions, (question)->
-          $exercise.replaceWith(question.stem_html)
-        )
-
-      @setExerciseHTML(exerciseLink).then(_render)
+      _addToRenderQueue = (questions)=>
+        _.each(questions, (question)=>
+          @postRenderQueue.push({
+            selector : '.' + $exercise.attr('class') + '[href="' + $exercise.attr('href') + '"]',
+            html : question.stem_html
+          })
+
+        , @)
+
+      @setExerciseHTML(exerciseLink).then(_addToRenderQueue)
+
+    postRenderEmbeddables: (postRenderQueue)=>
+      _.each(postRenderQueue, (toRender)=>
+        @$el.find(toRender.selector).replaceWith(toRender.html)
+      , @)

     setExerciseHTML: (exerciseLink)->
       exerciseUID = _.last(exerciseLink.split('/'))
@@ -172,10 +187,9 @@
       parseForExerciseHTML = (data)->
         exerciseData = _(data.items).findWhere({uid: exerciseUID})
         exerciseQuestions = exerciseData.questions
-        return exerciseQuestions

       request = $.get(settings.exercisesAPIBase)
-      request.pipe(parseForExerciseHTML)
+      request.then(parseForExerciseHTML)

     findExerciseDOMS: ($parent)->
       exercisesToRender = $parent.find('[data-type="exercise"]').find('.os-embed')


more tests

more tests

--- test/model.test.coffee
+++ test/model.test.coffee
@@ -44,6 +44,18 @@
         user.isNewRecord.should.be.false
         done()

+    it 'should get a record from db', (done) ->
+      user = new User getFakeUserData()
+      user.save (err) ->
+        should.not.exist err
+        User.get user.id, (err, record) ->
+          should.not.exist err
+          should.exist record
+          record.should.have.property 'id', user.id
+
+          done()
+
+
     it 'should delete a model from db', (done) ->
       user2 = new User getFakeUserData()
       user2.save (err) ->
@@ -85,12 +97,14 @@
           done()

     # describe 'Validation', ->
-    #   it 'should validate by schema type', ->
+    #   it 'should validate by schema type', (done) ->
     #     user = new User
     #     user.firstName = 123
     #     user.save (err) ->
-    #       should.exist err, user
-    #       console.log err
+    #       # should.exist err, user
+    #       console.log err, user
+    #       done()
+

   describe 'Model Class', ->


Stub out more project edit pages

Stub out more project edit pages

--- app/pages/edit-project.cjsx
+++ app/pages/edit-project.cjsx
@@ -21,25 +21,31 @@
           <Link href="/build/#{TODO_PROJECT_NAME}/science-case" className="tabbed-content-tab">Science case</Link>
           <Link href="/build/#{TODO_PROJECT_NAME}/subjects" className="tabbed-content-tab">Subjects</Link>
           <Link href="/build/#{TODO_PROJECT_NAME}/workflows" className="tabbed-content-tab">Workflows</Link>
-          <Link href="/build/#{TODO_PROJECT_NAME}/user-roles" className="tabbed-content-tab">User roles</Link>
-          <Link href="/build/#{TODO_PROJECT_NAME}/user-roles" className="tabbed-content-tab">Admin</Link>
+          <Link href="/build/#{TODO_PROJECT_NAME}/roles" className="tabbed-content-tab">User roles</Link>
+          <Link href="/build/#{TODO_PROJECT_NAME}/admin" className="tabbed-content-tab">Admin</Link>

         <Route path="/build/#{TODO_PROJECT_NAME}" className="content-container">
-
-
Title

-
<input type="text" defaultValue="Galaxy Zoo" className="full" />

-
+

+
+
Title

+
<input type="text" defaultValue="Galaxy Zoo" className="full" />

+
+

-
-
Tag line

-            <input type="text" defaultValue="Help further our understanding of galaxy formation." className="full" />
-
+

+
+
Tag line

+              <input type="text" defaultValue="Help further our understanding of galaxy formation." className="full" />
+
+

-
-
Introductory text

-            <MarkdownEditor rows="10" className="full" />
-
+

+
+
Introductory text

+              <MarkdownEditor rows="10" className="full" />
+
+

         </Route>

         <Route path="/build/#{TODO_PROJECT_NAME}/science-case" className="content-container">
@@ -69,5 +75,15 @@

}

         </Route>
+
+        <Route path="/build/#{TODO_PROJECT_NAME}/workflows">
+
workflows

+        </Route>
+        <Route path="/build/#{TODO_PROJECT_NAME}/roles">
+
roles

+        </Route>
+        <Route path="/build/#{TODO_PROJECT_NAME}/admin">
+
admin

+        </Route>


Remove the useless script.coffee

Remove the useless script.coffee

Removed the not used script.coffee


enable scheduling job by mistake

enable scheduling job by mistake

--- src/service.coffee
+++ src/service.coffee
@@ -334,16 +334,16 @@
 progressRule.minute = [10, 40]

 schedule.scheduleJob progressRule, ->
-  #service.progressTimeline ->
-    #logger.info "progressTimeline is DONE!!!"
+  service.progressTimeline ->
+    logger.info "progressTimeline is DONE!!!"

 summarizeRule = new schedule.RecurrenceRule()
 summarizeRule.hour = [new schedule.Range(0, 23)]
 summarizeRule.minute = [0, 5, 30, 35]

 schedule.scheduleJob summarizeRule, ->
-  #service.summarizeScore ->
-    #logger.info "summarizeScore is DONE!!!"
+  service.summarizeScore ->
+    logger.info "summarizeScore is DONE!!!"

 hasLang = (sum, elem) ->
   sum.some (el) ->


Fix for pixel lookups outside of bounds.

--- packages/landsofillusions-assets/documents/mesh/object/layer/picture/picture.coffee
+++ packages/landsofillusions-assets/documents/mesh/object/layer/picture/picture.coffee
@@ -102,6 +102,7 @@
     @maps[type]

   getMapValuesForPixel: (x, y) ->
+    return unless @_bounds
     @getMapValuesForPixelRelative x - @_bounds.x, y - @_bounds.y

   getMapValuesForPixelRelative: (x, y) ->
@@ -117,9 +118,11 @@
     mapValues

   _relativeCoordinateInBounds: (x, y) ->
+    return unless @_bounds
     0 <= x < @_bounds.width and 0 <= y < @_bounds.height

   getClusterIdForPixel: (x, y) ->
+    return unless @_bounds
     @getClusterIdForPixelRelative x - @_bounds.x, y - @_bounds.y

   getClusterIdForPixelRelative: (x, y) ->
@@ -128,6 +131,7 @@
     clusterIdMap.getPixel x, y

   pixelExists: (x, y) ->
+    return unless @_bounds
     @pixelExistsRelative x - @_bounds.x, y - @_bounds.y

   pixelExistsRelative: (x, y) ->


Adding block and labelWidth examples

Adding block and labelWidth examples

--- examples/horizontal-login/coffeescript/horizontal-login.coffee
+++ examples/horizontal-login/coffeescript/horizontal-login.coffee
@@ -16,12 +16,15 @@
       h2 {className: "col-xs-12"}, "Please sign in"
       f.errors()
       f.input "account",
+        block: true
         autoFocus: true
         inputHtml: {autoComplete: "off"}
       f.input "password",
-        type: "password"
+        block: true
         inputHtml: {autoComplete: "off"}
-      f.input "remember_me", type: "boolean"
+      f.input "remember_me",
+        labelWidth: {sm: 4}
+        type: "boolean"
       f.submit "Sign in",
         bsStyle: "primary"
         bsSize: "lg"


finish full coverages on phabs module

finish full coverages on phabs module

--- test/phabs_test.coffee
+++ test/phabs_test.coffee
@@ -106,6 +106,29 @@
         it 'gives information about the task Txxx', ->
           expect(hubotResponse()).to.eql 'T42 has status open, priority Low, owner toto'

+    context 'task id is provided but doesn not exist', ->
+      beforeEach ->
+        do nock.disableNetConnect
+        nock(process.env.PHABRICATOR_URL)
+          .get('/api/maniphest.info')
+          .reply(200, { result: { error_info: 'No such Maniphest task exists.' } })
+
+      afterEach ->
+        nock.cleanAll()
+
+      context 'phab T42', ->
+        hubot 'phab T42'
+        it 'gives information about the task Txxx', ->
+          expect(hubotResponse()).to.eql 'oops T42 No such Maniphest task exists.'
+
+
+    context 'failed implicit re-use of the object id', ->
+      context 'when user is known and his phid is in the brain', ->
+        hubot 'ph', 'user_with_phid'
+        it 'complains that there is no active object id in memory', ->
+          expect(hubotResponse()).to.eql "Sorry, you don't have any task active right now."
+
+
   # ---------------------------------------------------------------------------------
   context 'user asks about a user', ->

@@ -258,7 +281,6 @@
           expect(hubotResponse(1)).to.eql 'Task T24 created = http://example.com/T24'
           expect(hubotResponse(3)).to.eql 'T24 has status open, priority Low, owner user_with_phid'

-
   # ---------------------------------------------------------------------------------
   context 'user changes status for a task', ->
     context 'when the task is unknown', ->


playing with avatar popups

playing with avatar popups

--- client/landing/app/Applications/Members.kdapplication/ContentDisplays/profileview.coffee
+++ client/landing/app/Applications/Members.kdapplication/ContentDisplays/profileview.coffee
@@ -1,11 +1,30 @@
 class ProfileView extends KDView
   constructor:->
+
     super
+
     memberData = @getData()
+
     @avatar = new AvatarStaticView
-      size      :
+      size     :
         width  : 90
         height : 90
+      click    : =>
+        pos =
+          top  : @avatar.getBounds().y - 8
+          left : @avatar.getBounds().x - 8
+        modal = new KDModalView
+          # title   : "#{memberData.profile.firstName} #{memberData.profile.lastName}"
+          width    : 400
+          fx       : yes
+          overlay  : yes
+          draggable: yes
+          position : pos
+        modal.addSubView new AvatarStaticView
+          size     :
+            width  : 400
+            height : 400
+        , memberData
     , memberData

     defaultState  = if memberData.followee then "Unfollow" else "Follow"


removed extra whitespace from qunit_spec.coffee

--- test/qunit_spec.coffee
+++ test/qunit_spec.coffee
@@ -31,7 +31,7 @@
 unless CompilerContext?
   # Note that this doesn't have the same context separation as the rspec test.
   # Both should be run for full acceptance of the two libary modes.
-  CompilerContext =
+  CompilerContext =
     compile: (template, options) ->
       Emblem.compile(Handlebars, template, options)

@@ -199,14 +199,14 @@
     and {{sally: span Hello}}!
   '''

-  shouldCompileTo emblem,
+  shouldCompileTo emblem,
                   '
Hello, <sally class="none">Hello</sally>, and <sally class="none">Hello</sally>!
'

   emblem =
   '''
   p Hello, #{ sally: span: a Hello}!
   '''
-  shouldCompileTo emblem,
+  shouldCompileTo emblem,
                   '
Hello, <sally class="none">Hello</sally>!
'

 test "with followup", ->
@@ -225,7 +225,7 @@
   '''
   span Yo #{foo}, I herd.
   '''
-  shouldCompileTo emblem,
+  shouldCompileTo emblem,
     { foo: '123' },
     "Yo <span>123</span>, I herd."


remove dead code

remove dead code

--- src/editor.coffee
+++ src/editor.coffee
@@ -27,23 +27,14 @@
     @root.setAttribute('contenteditable', enabled)

   applyDelta: (delta, source) ->
-    localDelta = this._update()
-    if localDelta
-      tempDelta = localDelta
-      localDelta = localDelta.transform(delta, true)
-      delta = delta.transform(tempDelta, false)
-      @delta = @doc.toDelta()   # Only for our error check below, otherwise dont need to update yet
-    unless delta.isIdentity()   # Follows may have turned delta into the identity
-      throw new Error("Trying to apply delta to incorrect doc length") if delta.startLength != @delta.endLength
-      delta = this._trackDelta( =>
-        delta.apply(this._insertAt, this._deleteAt, this._formatAt, this)
-        @selection.shiftAfter(0, 0, _.bind(@doc.optimizeLines, @doc))
-      )
-      @delta = @doc.toDelta()
-      @innerHTML = @root.innerHTML
-      @quill.emit(@quill.constructor.events.TEXT_CHANGE, delta, source) if delta and source != 'silent'
-    if localDelta and !localDelta.isIdentity() and source != 'silent'
-      @quill.emit(@quill.constructor.events.TEXT_CHANGE, localDelta, 'user')
+    this._update()
+    delta = this._trackDelta( =>
+      delta.apply(this._insertAt, this._deleteAt, this._formatAt, this)
+      @selection.shiftAfter(0, 0, _.bind(@doc.optimizeLines, @doc))
+    )
+    @delta = @doc.toDelta()
+    @innerHTML = @root.innerHTML
+    @quill.emit(@quill.constructor.events.TEXT_CHANGE, delta, source) if delta and source != 'silent'

   checkUpdate: (source = 'user') ->
     return clearInterval(@timer) if !@renderer.iframe.parentNode? or !@root.parentNode?


No need these

No need these

--- client/app/Applications/Apps.kdapplication/AppView.coffee
+++ client/app/Applications/Apps.kdapplication/AppView.coffee
@@ -11,14 +11,3 @@
     @addSubView header = new HeaderViewSection
       type  : "big"
       title : "App Catalog"
-
-  showContentDisplay:(content,contentType)->
-    contentDisplayController = @getSingleton "contentDisplayController"
-    controller = new ContentDisplayControllerApps null, content
-    contentDisplay = controller.getView()
-    contentDisplayController.emit "ContentDisplayWantsToBeShown",contentDisplay
-
-  _windowDidResize:()->
-    # @appsSplitView.setRightColumnClass()
-    # @appsSplitView.panels[1].$(".listview-wrapper").height @appsSplitView.getHeight() - 28
-


global settings menu default hidden

global settings menu default hidden

--- client/landing/app/MainApp/maincontroller/mainview.coffee
+++ client/landing/app/MainApp/maincontroller/mainview.coffee
@@ -105,18 +105,20 @@
       cssClass : "kdtabhandlecontainer"
       delegate : @

+    getFrontAppManifest = ->
+      appManager = KD.getSingleton "appManager"
+      appController = KD.getSingleton "kodingAppsController"
+      frontApp = appManager.getFrontApp()
+      frontAppName = name for name, instances of appManager.appControllers when frontApp in instances
+      appController.constructor.manifests?[frontAppName]
+
     @mainSettingsMenuButton = new KDButtonView
       domId    : "main-settings-menu"
       cssClass : "kdsettingsmenucontainer clean-gray"
       iconOnly : yes
       iconClass: "cog"
       callback : ->
-        appManager = KD.getSingleton "appManager"
-        appController = KD.getSingleton "kodingAppsController"
-        frontApp = appManager.getFrontApp()
-        frontAppName = name for name, instances of appManager.appControllers when frontApp in instances
-        appManifest = appController.constructor.manifests?[frontAppName]
-
+        appManifest = getFrontAppManifest()
         if appManifest?.menu
           appManifest.menu.forEach (item, index)->
             item.callback = (contextmenu)->
@@ -135,9 +137,7 @@
                 placement : "top"
                 margin    : -10
             , appManifest.menu
-        else
-          new KDNotificationView
-            title: "There is no option."
+    @mainSettingsMenuButton.hide()

     @mainTabView = new MainTabView
       domId              : "main-tab-view"
@@ -148,9 +148,8 @@
     ,null

     @mainTabView.on "PaneDidShow", =>
-      mainView = KD.getSingleton "mainView"
-      view = mainView.mainTabView.activePane?.mainView
-      @getSingleton('appManager').getFrontApp()
+      appManifest = getFrontAppManifest()
+      @mainSettingsMenuButton.$()[if appManifest?.menu then "fadeIn" else "fadeOut"]()

     mainController = @getSingleton('mainController')
     mainController.popupController = new VideoPopupController


Add span tags to match old behaviour of React

This is what happens when you rely on non-specified behaviour ;)

--- client/skr/screens/time-tracking/PopoverMiniControls.cjsx
+++ client/skr/screens/time-tracking/PopoverMiniControls.cjsx
@@ -12,8 +12,8 @@
     render: ->

-                {@props.date.format('MMM')}
-                {@props.date.format('Do')}
+                {@props.date.format('MMM')}
+                {@props.date.format('Do')}

             <BS.Button onClick={@props.onCancel} title="Hide Controls">
                 <LC.Icon type='ban' 2x flush />


Explicitly track whether or not engine is running

--- src/engine.coffee
+++ src/engine.coffee
@@ -6,6 +6,8 @@
     fps:    24
     width:  660
     height: 480
+
+  running: false

   # Internal queue of callbacks to invoke once the engine starts
   _queue: []
@@ -16,14 +18,15 @@
   #     Kona.Engine.queue =>
   #       console.log "Engine is running now!"
   #
-  queue: (fn) -> @_queue.push(fn)
+  queue: (fn) -> if @running then fn() else @_queue.push(fn)

   # Set the initial scene (specified as active), and kick off
   # the animation loop. To be invoked after all other necessary game setup.
   # Ex: `Kona.Engine.start()`
   start: (opts={}) ->
-    @fps = opts.fps || @defaults.fps
+    @fps     = opts.fps || @defaults.fps
     Kona.Scenes.currentScene = Kona.Scenes.scenes[0]
+    @running = true
     fn() for fn in @_queue
     @run()


Remove several tests.

Remove several tests.

- default parameters of functions and default values of destructuring
assignment are not supported yet.  I'll add those later again.

--- test/references.coffee
+++ test/references.coffee
@@ -70,53 +70,6 @@
             expect(reference.isWrite()).to.be.false
             expect(reference.isRead()).to.be.true

-        it 'the reference in default parameters should be resolved.', ->
-            ast = harmony.parse """
-            let a = 0;
-            function foo(b = a) {
-            }
-            """
-
-            console.log(JSON.stringify(ast, null, 2))
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 2  # [global, foo]
-
-            scope = scopeManager.scopes[1]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.equal scopeManager.scopes[0].variables[0]
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
-        it 'the reference in default values of destructuring should be resolved.', ->
-            ast = harmony.parse """
-            let a = 0;
-            function foo() {
-                let {b: a} = {};
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 2  # [global, foo]
-
-            scope = scopeManager.scopes[1]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.equal scopeManager.scopes[0].variables[0]
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
     describe 'When there is a `const` declaration on global,', ->
         it 'the reference on global should be resolved.', ->
             ast = harmony.parse """
@@ -161,51 +114,6 @@
             expect(reference.isWrite()).to.be.false
             expect(reference.isRead()).to.be.true

-        it 'the reference in default parameters should be resolved.', ->
-            ast = harmony.parse """
-            const a = 0;
-            function foo(b = a) {
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 2  # [global, foo]
-
-            scope = scopeManager.scopes[1]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.equal scopeManager.scopes[0].variables[0]
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
-        it 'the reference in default values of destructuring should be resolved.', ->
-            ast = harmony.parse """
-            const a = 0;
-            function foo() {
-                const {b: a} = {};
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 2  # [global, foo]
-
-            scope = scopeManager.scopes[1]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.equal scopeManager.scopes[0].variables[0]
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
     describe 'When there is a `var` declaration on global,', ->
         it 'the reference on global should NOT be resolved.', ->
             ast = harmony.parse """
@@ -250,51 +158,6 @@
             expect(reference.isWrite()).to.be.false
             expect(reference.isRead()).to.be.true

-        it 'the reference in default parameters should NOT be resolved.', ->
-            ast = harmony.parse """
-            var a = 0;
-            function foo(b = a) {
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 2  # [global, foo]
-
-            scope = scopeManager.scopes[1]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.be.null
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
-        it 'the reference in default values of destructuring should NOT be resolved.', ->
-            ast = harmony.parse """
-            var a = 0;
-            function foo() {
-                var {b: a} = {};
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 2  # [global, foo]
-
-            scope = scopeManager.scopes[1]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.be.null
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
     describe 'When there is a `function` declaration on global,', ->
         it 'the reference on global should NOT be resolved.', ->
             ast = harmony.parse """
@@ -340,51 +203,6 @@
             expect(reference.isWrite()).to.be.false
             expect(reference.isRead()).to.be.true

-        it 'the reference in default parameters should NOT be resolved.', ->
-            ast = harmony.parse """
-            function a() {}
-            function foo(b = a()) {
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 3  # [global, a, foo]
-
-            scope = scopeManager.scopes[2]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.be.null
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
-        it 'the reference in default values of destructuring should NOT be resolved.', ->
-            ast = harmony.parse """
-            function a() {}
-            function foo() {
-                let {b: a()} = {};
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 3  # [global, a, foo]
-
-            scope = scopeManager.scopes[2]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.be.null
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
     describe 'When there is a `class` declaration on global,', ->
         it 'the reference on global should be resolved.', ->
             ast = harmony.parse """
@@ -421,51 +239,6 @@
             scope = scopeManager.scopes[2]
             expect(scope.variables).to.have.length 2  # [arguments, b]
             expect(scope.references).to.have.length 2  # [b, A]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'A'
-            expect(reference.resolved).to.equal scopeManager.scopes[0].variables[0]
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
-        it 'the reference in default parameters should be resolved.', ->
-            ast = harmony.parse """
-            class A {}
-            function foo(b = new A()) {
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 3  # [global, A, foo]
-
-            scope = scopeManager.scopes[2]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, A]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'A'
-            expect(reference.resolved).to.equal scopeManager.scopes[0].variables[0]
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
-        it 'the reference in default values of destructuring should be resolved.', ->
-            ast = harmony.parse """
-            class A {}
-            function foo() {
-                let {b: new A()} = {};
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 3  # [global, A, foo]
-
-            scope = scopeManager.scopes[2]
-            expect(scope.variables).to.have.length 2  # [arguments, A]
-            expect(scope.references).to.have.length 2  # [b, a]

             reference = scope.references[1]
             expect(reference.from).to.equal scope
@@ -523,36 +296,35 @@
             expect(reference.isWrite()).to.be.false
             expect(reference.isRead()).to.be.true

-        it 'the reference in default parameters body should be resolved.', ->
-            ast = harmony.parse """
-            function foo() {
-                let a = 0;
-                function bar(b = a) {
-                }
-            }
-            """
-
-            scopeManager = escope.analyze ast, ecmaVersion: 6
-            expect(scopeManager.scopes).to.have.length 3  # [global, foo, bar]
-
-            scope = scopeManager.scopes[2]
-            expect(scope.variables).to.have.length 2  # [arguments, b]
-            expect(scope.references).to.have.length 2  # [b, a]
-
-            reference = scope.references[1]
-            expect(reference.from).to.equal scope
-            expect(reference.identifier.name).to.equal 'a'
-            expect(reference.resolved).to.equal scopeManager.scopes[1].variables[1]
-            expect(reference.writeExpr).to.be.undefined
-            expect(reference.isWrite()).to.be.false
-            expect(reference.isRead()).to.be.true
-
-        it 'the reference in default values of destructuring should be resolved.', ->
-            ast = harmony.parse """
-            function foo() {
-                let a = 0;
+    describe 'When there is a `var` declaration in functions,', ->
+        it 'the reference on the function should be resolved.', ->
+            ast = harmony.parse """
+            function foo() {
+                var a = 0;
+            }
+            """
+
+            scopeManager = escope.analyze ast, ecmaVersion: 6
+            expect(scopeManager.scopes).to.have.length 2  # [global, foo]
+
+            scope = scopeManager.scopes[1]
+            expect(scope.variables).to.have.length 2  # [arguments, a]
+            expect(scope.references).to.have.length 1
+
+            reference = scope.references[0]
+            expect(reference.from).to.equal scope
+            expect(reference.identifier.name).to.equal 'a'
+            expect(reference.resolved).to.equal scope.variables[1]
+            expect(reference.writeExpr).to.not.be.undefined
+            expect(reference.isWrite()).to.be.true
+            expect(reference.isRead()).to.be.false
+
+        it 'the reference in nested functions should be resolved.', ->
+            ast = harmony.parse """
+            function foo() {
+                var a = 0;
                 function bar() {
-                    let {b: a} = {};
+                    var b = a;
                 }
             }
             """


stash

stash

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -719,7 +719,9 @@
             # Let's bring back the data explorer to its old state (if there was)
             if @state?.query_result?
                 # ATN: set_query_result?
-                @results_view.set_query_result @state.query_result
+                @results_view.set_query_result
+                    query_result: @state.query_result
+                    current_index: @state.current_position # ATN: TODO current_index

             # ATN: results_view.render?
             @$('.results_container').html @results_view.render(query_has_changed: @query_has_changed).$el
@@ -2634,7 +2636,9 @@
                                 error: (err) =>
                                     @results_view.render_error(@query, err)

-                        # ATN: pass the query_result object to the view
+                        # ATN ?
+                        @results_view.set_query_result
+                            query_result: @state.query_result

                     @driver_handler.run_with_new_connection rdq_query,
                         optargs:
@@ -3494,33 +3498,11 @@
                 js_error: js_error is true
             return @

-        # ATN: get rid of this, set the query_result in the initializer
+        # ATN: conitnue ripping this into pieces
         render_result: (args) =>

-            # ATN: now only two arguments: query_result and query_has_changed
-
-            if args? and args.results isnt undefined
-                @results = args.results
-                @ATN_results_array = null # if @results is not an array (possible starting from 1.4), we will transform @results_array to [@results] for the table view
-            if args? and args.profile isnt undefined
-                @ATN_profile = args.ATN_profile
-            if args?.ATN_metadata?
-                @ATN_metadata = args.ATN_metadata
-            if args?.ATN_metadata?.skip_value?
-                # @container.state.start_record is the old value of @container.state.skip_value
-                # Here we just deal with start_record
-                # TODO May have to remove this line as we have metadata.start now
-                @container.state.start_record = args.ATN_metadata.skip_value
-
-                if args.ATN_metadata.execution_time?
-                    @ATN_metadata.execution_time_pretty = Utils.prettify_duration args.ATN_metadata.execution_time
-
-            num_results = @ATN_metadata.skip_value
-            if @ATN_metadata.has_more_data isnt true
-                if Object::toString.call(@results) is '[object Array]'
-                    num_results += @results.length
-                else # @results can be a single value or null
-                    num_results += 1
+            # ATN TODO
+            @ATN_metadata.execution_time_pretty = Utils.prettify_duration args.ATN_metadata.execution_time

             @$el.html @template
                 limit_value: ATN_from_old_metadata


Explicit return

Explicit return

--- lib/assets/javascripts/unpoly/proxy.js.coffee
+++ lib/assets/javascripts/unpoly/proxy.js.coffee
@@ -394,6 +394,7 @@
       promise = load(entry.request)
       promise.done (args...) -> entry.deferred.resolve(args...)
       promise.fail (args...) -> entry.deferred.reject(args...)
+      return

   ###*
   Makes the proxy assume that `newRequest` has the same response as the


Fix for review

Fix for review

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -3401,18 +3401,17 @@

         binary_to_string: (bin) =>
             # We print the size of the binary, not the size of the base 64 string
-            size = bin.data.length*3/4
             blocks_of_76 = Math.floor(bin.data.length/78) # 78 to count \r\n
             leftover = bin.data.length-blocks_of_76*78

             base64_digits = 76*blocks_of_76+leftover

             blocks_of_4 = Math.floor(base64_digits/4)
-            leftover2 = base64_digits-Math.floor(base64_digits/4)
-
-            if bin.data.slice(-2) is '=='
+
+            end = bin.data.slice(-2)
+            if end is '=='
                 number_of_equals = 2
-            else if bin.data.slice(-1) is '='
+            else if end.slice(-1) is '='
                 number_of_equals = 1
             else
                 number_of_equals = 0
@@ -3421,15 +3420,13 @@

             if size >= 1073741824
                 sizeStr = (size/1073741824).toFixed(1)+'GB'
-            else if size>=1048576
+            else if size >= 1048576
                 sizeStr = (size/1048576).toFixed(1)+'MB'
-            else if size>=1024
+            else if size >= 1024
                 sizeStr = (size/1024).toFixed(1)+'KB'
-            else if size > 1
-                sizeStr = size+' bytes'
             else if size is 1
                 sizeStr = size+' byte'
-            else if size is 0
+            else
                 sizeStr = size+' bytes'

@@ -3437,7 +3434,7 @@
             if size is 0
                 return "<binary, #{sizeStr}>"
             else
-                str = atob bin.data.replace(/[ \r\n]+$/, "")
+                str = atob bin.data.slice(0, 8)
                 snippet = ''
                 for char, i  in str
                     next = str.charCodeAt(i).toString(16)
@@ -3449,11 +3446,7 @@
                         break
                     else
                         snippet += " "
-
-            return "<binary, #{sizeStr}, \"#{snippet}\">"
-
-
-
+                return "<binary, #{sizeStr}, \"#{snippet}\">"

     class @ResultView extends DataExplorerView.SharedResultView
         className: 'result_view'


Remove unused function up.viewport.finishScrolling()

--- lib/assets/javascripts/unpoly/viewport.coffee
+++ lib/assets/javascripts/unpoly/viewport.coffee
@@ -144,21 +144,6 @@
     viewport = f.get(viewport, options)
     motion = new up.ScrollMotion(viewport, scrollTop, options)
     scrollingController.startMotion(viewport, motion, options)
-
-  ###**
-  Finishes scrolling animations in the given element, its ancestors or its descendants.
-
-  @function up.viewport.finishScrolling
-  @param {string|Element|jQuery}
-  @return {Promise}
-  @internal
-  ###
-  finishScrolling = (element) ->
-    # Don't emit expensive events if no animation can be running anyway
-    return Promise.resolve() unless up.motion.isEnabled()
-
-    viewport = closest(element)
-    scrollingController.finish(viewport)

   ###**
   @function up.viewport.anchoredRight


Added missing return false statement

Added missing return false statement

--- src/MunKirjat/MainBundle/Resources/coffee/view/AuthorView.coffee
+++ src/MunKirjat/MainBundle/Resources/coffee/view/AuthorView.coffee
@@ -22,6 +22,7 @@
             $.ajax(
                 url: Routing.getBaseUrl() + '/new-token/new-author',
                 dataType: 'json'
+
                 success: (data) ->
                     self.csrf = data.csrf_token

@@ -34,7 +35,7 @@
                             csrf_token: data.csrf_token
                             firstName:  self.model.get("firstName")
                             lastName:   self.model.get("lastName")
-                        )
+                    )
                     self.loaded = true
             )

@@ -63,13 +64,13 @@
                         self.setTitle 'author.edit'
                         # update url
                         self.options.dispatcher.trigger "url:change", "#author/" + self.model.id
-                        console.log self.model.isNew()
+
                         if(isNew)
-                            console.log "RAI RSI: " + JSON.stringify(response)
                             self.options.collection.add self.model
                             self.options.dispatcher.trigger "author:add", self.model

                     App.Notifier.success "Author saved".t()
+            return false

         show: (id) ->
             @$el.show()


Do not create a new instance of Driver

Do not create a new instance of Driver

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -3898,13 +3898,9 @@
         # This class does a little more than window.Driver
         query_error_template: Handlebars.templates['dataexplorer-query_error-template']

-        # I don't want that thing in window
-        constructor: (args) ->
-            @driver = new Driver()
-
         close_connection: =>
             if @connection?.open is true
-                @driver.close @connection
+                driver.close @connection
                 @connection = null

         create_connection: (cb, id_execution, connection_cb) =>
@@ -3914,14 +3910,14 @@
             @id_execution = id_execution

             ((_id_execution) =>
-                @driver.connect (error, connection) =>
+                driver.connect (error, connection) =>
                     if _id_execution is @id_execution
                         connection.removeAllListeners 'error' # See issue 1347
                         connection.on 'error', connection_cb
                         @connection = connection
                         cb(error, connection)
                     else
-                        @driver.close connection
+                        driver.close connection
             )(id_execution)

         destroy: =>


Fix doc with mock stuff

Fix doc with mock stuff

--- lib/atom-perl6-editor-tools-hyperclick.coffee
+++ lib/atom-perl6-editor-tools-hyperclick.coffee
@@ -15,6 +15,9 @@
     return {
       range: range,
       callback: () ->
-        atom.notifications.addWarning("TODO perl6doc " + text)
+        options =
+          detail: "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
+          dismissable: true
+        atom.notifications.addInfo("p6doc #{text}", options)
         return
     }


New event touch >>> touchend

New event touch >>> touchend

--- src/quo.gestures.coffee
+++ src/quo.gestures.coffee
@@ -6,7 +6,8 @@
     CURRENT_TOUCH = []
     TOUCH_TIMEOUT = undefined
     HOLD_DELAY = 650
-    GESTURES = ["tap", "singleTap", "doubleTap", "hold",
+    GESTURES = ["touch",
+                "tap", "singleTap", "doubleTap", "hold",
                 "swipe", "swiping", "swipeLeft", "swipeRight", "swipeUp", "swipeDown",
                 "rotate", "rotating", "rotateLeft", "rotateRight",
                 "pinch", "pinching", "pinchIn", "pinchOut",
@@ -15,7 +16,8 @@
     GESTURES.forEach (event) ->
         $$.fn[event] = (callback) ->
             # @on event, callback
-            $$(document.body).delegate @selector, event, callback
+            event_name = if event is "touch" then "touchend" else event
+            $$(document.body).delegate @selector, event_name, callback
         @

     $$(document).ready -> _listenTouches()
@@ -79,6 +81,8 @@
         it_is

     _onTouchEnd = (event) ->
+        _trigger "touch"
+
         if GESTURE.fingers is 1
             if GESTURE.taps is 2 and GESTURE.gap
                 _trigger "doubleTap"


rewritten validateGroup function

rewritten validateGroup function

--- servers/lib/server/handlers/createteam.coffee
+++ servers/lib/server/handlers/createteam.coffee
@@ -125,8 +125,9 @@
     client.sessionToken        = result.newToken
     client.connection.delegate = result.account

-    if (validationResult = validateGroupData body, res) isnt yes
-      return res.status(400).send(validationResult)
+    if validationError = validateGroupDataAndReturnError body
+      { statusCode, errorMessage } = validationError
+      return res.status(statusCode).send errorMessage

     JGroup.create client,
       slug            : slug
@@ -167,15 +168,15 @@
         res.redirect 301, redirect

-validateGroupData = (body, res) ->
+validateGroupDataAndReturnError = (body) ->

   if not body.slug
-    return 'Group slug can not be empty.'
+    return { statusCode : 400, errorMessage : 'Group slug can not be empty.' }

   else if not body.companyName
-    return 'Company name can not be empty.'
-
-  else true
+    return { statusCode : 400, errorMessage : 'Company name can not be empty.' }
+
+  else null

 # convertToArray converts given comma separated string value into cleaned,
 # trimmed, lowercased, unified array of string


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -15,6 +15,8 @@
   y: 50
   width: 10
   height: 10
+
+en

 player.update()
 </pre></code>


remove debugger and console.log

remove debugger and console.log

--- src/components/widgets/sales-number-of-leads/sales-number-of-leads.directive.coffee
+++ src/components/widgets/sales-number-of-leads/sales-number-of-leads.directive.coffee
@@ -27,8 +27,6 @@
         'impac.widget.settings.time_period.period.week',
         'impac.widget.settings.time_period.period.day']).then(
           (translations) ->
-            debugger;
-            console.log('debugger');
             $scope.periodOptions = [
               {label: translations['impac.widget.settings.time_period.period.year'], value: 'YEARLY'},
               {label: translations['impac.widget.settings.time_period.period.quarter'], value: 'QUARTERLY'},


`Element` attrs now are clones deeply on clone


Detect @dashed-nicks in pull request body

--- src/scripts/github-pull-request-notifier.coffee
+++ src/scripts/github-pull-request-notifier.coffee
@@ -46,7 +46,7 @@

 announcePullRequest = (data, cb) ->
   if data.action == 'opened'
-    mentioned = data.pull_request.body.match(/(^|\s)(@\w+)/g)
+    mentioned = data.pull_request.body.match(/(^|\s)(@[\w\-]+)/g)

     if mentioned
       unique = (array) ->


Correct wrong logic on coffeescript mirror code because of identation

--- app/assets/javascripts/artifacts.coffee
+++ app/assets/javascripts/artifacts.coffee
@@ -131,18 +131,18 @@

 initialize_mirror_links = ->
   $('.mirror-link').on 'click', (e) ->
-  # Get the first mirror (which is our own)
-  url = $('ul#mirrors a:first').attr('href')
-  clicked_url = $('a:first', $(this)).attr('href')
-
-  reg = new RegExp(location.host)
-
-  # Tests if the first mirror is present (we have the file hosted in musical artifacts)
-  if reg.test(url) and clicked_url != url
-    # Do a head request to increase download count
-    http = new XMLHttpRequest()
-    http.open('HEAD', url)
-    http.send()
+    # Get the first mirror (which is our own)
+    url = $('ul#mirrors a:first').attr('href')
+    clicked_url = $('a:first', $(this)).attr('href')
+
+    reg = new RegExp(location.host)
+
+    # Tests if the first mirror is present (we have the file hosted in musical artifacts)
+    if reg.test(url) and clicked_url != url
+      # Do a head request to increase download count
+      http = new XMLHttpRequest()
+      http.open('HEAD', url)
+      http.send()

 updateFormParameters = ->
   params = parseQueryString($('#artifact_search').val())


fixing typo

fixing typo

--- src/gitlab.coffee
+++ src/gitlab.coffee
@@ -31,7 +31,7 @@
   gitlab.users.all (users) ->
     message = ""
     for user in users
-      message = messasge + "#{user.id}: #{user.name}, #{user.email}, State: #{user.state}\n"
+      message = message + "#{user.id}: #{user.name}, #{user.email}, State: #{user.state}\n"
     msg.send (message)

 gitlabShowProject = (msg) ->


fix accounts issue

fix accounts issue

--- _source/lib/apollos.coffee
+++ _source/lib/apollos.coffee
@@ -88,6 +88,10 @@
       user.personGuid = Apollos.utilities.makeNewGuid()

       person = Apollos.person user
+
+      if typeof(person) isnt 'object'
+        person = {}
+
       # # no existing user so create one
       if not Object.keys(person).length
         Apollos.people.upsert({guid: user.personGuid}, {


Turned Factlink.Prepare into a class

Turned Factlink.Prepare into a class

--- app/js/views/prepare.coffee
+++ app/js/views/prepare.coffee
@@ -1,38 +1,34 @@
-Factlink.Prepare = ->
+class Factlink.Prepare

-  el = null
-  loading = false
+  constructor: ->
+    @$el = $(Factlink.templates.create)
+    @$el.appendTo Factlink.el
+    @$el.hide()

-  el = $(Factlink.templates.create)
-  el.appendTo Factlink.el
-  el.hide()
-  el.bind "mouseup", (e) -> e.stopPropagation()
-  el.bind "mousedown", (e) -> e.preventDefault()
-  el.bind "click", (e) =>
-    e.preventDefault()
-    e.stopPropagation()
-    @startLoading()
-    Factlink.createFactFromSelection()
+    @$el.bind "mouseup", (e) -> e.stopPropagation()
+    @$el.bind "mousedown", (e) -> e.preventDefault()
+    @$el.bind "click", (e) =>
+      e.preventDefault()
+      e.stopPropagation()
+      @startLoading()
+      Factlink.createFactFromSelection()

-  @show = (top, left) =>
-    Factlink.set_position_of_element top, left, window, el
-    el.fadeIn "fast"
+  show: (top, left) =>
+    Factlink.set_position_of_element top, left, window, @$el
+    @$el.fadeIn "fast"

-  @hide = (callback) =>
-    el.fadeOut "fast", =>
-      @stopLoading() if loading
+  hide: (callback) =>
+    @$el.fadeOut "fast", =>
+      @stopLoading() if @_loading
       callback?()

-  @startLoading = ->
-    loading = true
-    el.addClass "fl-loading"
+  startLoading: ->
+    @_loading = true
+    @$el.addClass "fl-loading"

-  @stopLoading = ->
-    loading = false
-    @hide ->
-      el.removeClass "fl-loading"
+  stopLoading: ->
+    @_loading = false
+    @hide => @$el.removeClass "fl-loading"

-  @isVisible = ->
-    el.is ":visible"
-
-  return this
+  isVisible: ->
+    @$el.is ":visible"


Restricted crude-glasses in more later levels so it'd be less likely to get them equipped after the wooden ones should be on your face.

--- app/lib/LevelOptions.coffee
+++ app/lib/LevelOptions.coffee
@@ -243,7 +243,7 @@
     restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
   'munchkin-swarm':
     requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
-    restrictedGear: {'programming-book': 'programmaticon-i'}
+    restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}

   # Ranger branch
   'munchkin-harvest':
@@ -266,32 +266,32 @@
     allowedHeroes: ['captain', 'knight', 'samurai']
   'touch-of-death':
     requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'}
-    restrictedGear: {'programming-book': 'programmaticon-i'}
+    restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
     allowedHeroes: ['librarian', 'potion-master', 'sorcerer']
   'bonemender':
     requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'book-of-life-i', wrists: 'sundial-wristwatch'}
-    restrictedGear: {'left-hand': 'unholy-tome-i', 'programming-book': 'programmaticon-i'}
+    restrictedGear: {'left-hand': 'unholy-tome-i', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
     requiredCode: ['canCast']
     allowedHeroes: ['librarian', 'potion-master', 'sorcerer']

   'coinucopia':
     requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags'}
-    restrictedGear: {'programming-book': 'programmaticon-i'}
+    restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
   'copper-meadows':
     requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses'}
-    restrictedGear: {'programming-book': 'programmaticon-i'}
+    restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
   'drop-the-flag':
     requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
-    restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i'}
+    restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
   'deadly-pursuit':
     requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
-    restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i'}
+    restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
   'rich-forager':
     requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'}
-    restrictedGear: {'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
+    restrictedGear: {'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
   'multiplayer-treasure-grove':
     requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'}
-    restrictedGear: {'programming-book': 'programmaticon-i'}
+    restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
   'siege-of-stonehold':
     requiredGear: {}
     restrictedGear: {}


remove #pointerXInElement

remove #pointerXInElement

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -91,7 +91,7 @@
       </div>
     """

-    @element.style.left = @pointerXInElement(e) + "px"
+    @element.style.left = @toCanvasXPoint(e) + "px"
     @element.style.position = 'absolute'
     @element.style.top = e.target.offsetTop + "px"
     @element.style.height = @canvas.height + 'px'
@@ -169,8 +169,6 @@
     @canvasGraph.marks.add(@) unless (@ in @canvasGraph.marks.all)
     @canvasGraph.marks.remove(@) if e.target.className is "top-bar"

-  toCanvasXPoint: (e) -> e.pageX - @canvas.getBoundingClientRect().left - window.scrollX
-
-  pointerXInElement: (e) -> e.offsetX || e.clientX - e.target.offsetLeft + window.pageXOffset - @canvas.getBoundingClientRect().left
+  toCanvasXPoint: (e) -> e.pageX - @canvas.getBoundingClientRect().left - window.scrollXg

 module?.exports = CanvasGraph: CanvasGraph, Marks: Marks, Mark: Mark


leftover

leftover

--- app/controllers/Main.coffee
+++ app/controllers/Main.coffee
@@ -8,8 +8,7 @@
 extended_tools = {}
 for tool in _.functions Ubret
   try
-    extended_tool = require 'controllers/tools/' + tool
-    Ubret[tool] = extended_tool
+    Ubret[tool] = require 'controllers/tools/' + tool
   catch err

 State = require 'controllers/state'
@@ -27,7 +26,6 @@
     if typeof params.state is 'string' and params.state.length > 0
       # Load passed state
       @loadState params.state
-

   setup: =>
     @html require('views/main')()


props need canOnlyContinue

props need canOnlyContinue

--- test/components/exercise/index.spec.coffee
+++ test/components/exercise/index.spec.coffee
@@ -6,6 +6,9 @@

 step = null
 props = null
+
+canOnlyContinue = (partId) ->
+  false

 FREE_RESPONSE_PROPS =
   taskId: '1'
@@ -30,6 +33,7 @@
   getReadingForStep: sinon.spy()
   refreshStep: sinon.spy()
   recoverFor: sinon.spy()
+  canOnlyContinue: sinon.spy(canOnlyContinue)

   review: ''
   focus: false