Refactor initializeInstance function

Refactor initializeInstance function

--- src/projection/projection.coffee
+++ src/projection/projection.coffee
@@ -8,47 +8,44 @@

   initializeInstance: (projectionName, Projection, params) ->
-    new Promise (resolve, reject) =>
+    if typeof Projection is 'function'
+      projection = new Projection
+    else
+      projection = Projection

-      if typeof Projection is 'function'
-        projection = new Projection
-      else
-        projection = Projection
+    if @_context._di
+      for diName, diFn of @_context._di
+        projection[diName] = diFn

-      if @_context._di
-        for diName, diFn of @_context._di
-          projection[diName] = diFn
+    projectionId = @_eventric.generateUid()

-      projectionId = @_eventric.generateUid()
+    aggregateId = null
+    projection.$subscribeHandlersWithAggregateId = (_aggregateId) ->
+      aggregateId = _aggregateId

-      aggregateId = null
-      projection.$subscribeHandlersWithAggregateId = (_aggregateId) ->
-        aggregateId = _aggregateId
-
-
-      @log.debug "[#{@_context.name}] Clearing ProjectionStores #{projection.stores} of #{projectionName}"
-      eventNames = null
-      @_clearProjectionStores projection.stores, projectionName
-      .then =>
-        @log.debug "[#{@_context.name}] Finished clearing ProjectionStores of #{projectionName}"
-        @_injectStoresIntoProjection projectionName, projection
-      .then =>
-        @_callInitializeOnProjection projectionName, projection, params
-      .then =>
-        @log.debug "[#{@_context.name}] Replaying DomainEvents against Projection #{projectionName}"
-        @_parseEventNamesFromProjection projection
-      .then (_eventNames) =>
-        eventNames = _eventNames
-        @_applyDomainEventsFromStoreToProjection projectionId, projection, eventNames, aggregateId
-      .then =>
-        @log.debug "[#{@_context.name}] Finished Replaying DomainEvents against Projection #{projectionName}"
-        @_subscribeProjectionToDomainEvents projectionId, projectionName, projection, eventNames, aggregateId
-      .then =>
-        @_projectionInstances[projectionId] = projection
-        resolve projectionId
-      .then ->
-        projection.isInitialized = true
-      .catch reject
+    @log.debug "[#{@_context.name}] Clearing ProjectionStores #{projection.stores} of #{projectionName}"
+    eventNames = null
+    @_clearProjectionStores projection.stores, projectionName
+    .then =>
+      @log.debug "[#{@_context.name}] Finished clearing ProjectionStores of #{projectionName}"
+      @_injectStoresIntoProjection projectionName, projection
+    .then =>
+      @_callInitializeOnProjection projectionName, projection, params
+    .then =>
+      @log.debug "[#{@_context.name}] Replaying DomainEvents against Projection #{projectionName}"
+      @_parseEventNamesFromProjection projection
+    .then (_eventNames) =>
+      eventNames = _eventNames
+      @_applyDomainEventsFromStoreToProjection projectionId, projection, eventNames, aggregateId
+    .then =>
+      @log.debug "[#{@_context.name}] Finished Replaying DomainEvents against Projection #{projectionName}"
+      @_subscribeProjectionToDomainEvents projectionId, projectionName, projection, eventNames, aggregateId
+    .then =>
+      @_projectionInstances[projectionId] = projection
+    .then ->
+      projection.isInitialized = true
+    .then ->
+      projectionId

   _callInitializeOnProjection: (projectionName, projection, params) ->


zendesk: preserve double slash in URLs

zendesk: preserve double slash in URLs

--- src/smc-hub/support.coffee
+++ src/smc-hub/support.coffee
@@ -167,7 +167,7 @@
         # below the body message, add a link to the location
         # TODO fix hardcoded URL
         if opts.location?
-            url  = path.join('https://cloud.sagemath.com/', opts.location)
+            url  = "https://" + path.join('cloud.sagemath.com/', opts.location)
             body = opts.body + "\n\n#{url}"
         else
             body = opts.body + "\n\nNo location provided."


Improved loading of ticket list.

Improved loading of ticket list.

--- app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee
+++ app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee
@@ -46,7 +46,7 @@

     # get data
     App.Com.ajax(
-      id:    'ticket_overview',
+      id:    'ticket_overview_' + @start_page,
       type:  'GET',
       url:   '/ticket_overviews',
       data:  {


Increased size/area of infinite scrolling.

--- app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee
+++ app/assets/javascripts/app/controllers/agent_ticket_view.js.coffee
@@ -168,7 +168,7 @@
         @start_page = @start_page + 1
         @fetch()

-      $('footer').waypoint( a, { offset: '120%', triggerOnce: true } )
+      $('footer').waypoint( a, { offset: '140%', triggerOnce: true } )

   page: (e) =>
     e.preventDefault()


Activity: made private messaging available for the poor. (who doesn't have cinema displays)

--- client/Social/Activity/AppView.coffee
+++ client/Social/Activity/AppView.coffee
@@ -146,9 +146,9 @@

     bounds = @sidebar.sections.messages.options.headerLink.getBounds()

-    top       = bounds.y - 40
+    top       = bounds.y - 310
     left      = bounds.x + bounds.w + 40
-    arrowTop  = 40 + (bounds.h / 2) - 10 #10 = arrow height
+    arrowTop  = 310 + (bounds.h / 2) - 10 #10 = arrow height

     modal = new PrivateMessageModal
       delegate     : this


reticulating sax

reticulating sax

--- coffee/main.coffee
+++ coffee/main.coffee
@@ -253,6 +253,7 @@
       @props = {
         name: "sax0",
         sys: "root",
+        design: dsg,
         sense: "",
         actuate: ""
       }


fix: relative URL to absolute in ext f mode

--- coffee/mode.coffee
+++ coffee/mode.coffee
@@ -358,7 +358,14 @@
       opentab: (target) -> @hitMode.open.call(this, target, true)

       yank: (target) ->
-          url = $(target).attr('href')
+          String.prototype.escapeHTML = ->
+              this.split('&').join('&').split('"').join('"').split('<').join('<')
+          qualifyURL = (url) ->
+              element = document.createElement('span')
+              element.innerHTML = ' '
+              element.firstChild.href
+
+          url = qualifyURL($(target).attr('href'))
           if url?
               (new g.CommandExecuter).set("Copy #{url}").parse().execute()
           g.model.enterNormalMode()


Add GitHub Enterprise support to github-issues

--- src/scripts/github-issues.coffee
+++ src/scripts/github-issues.coffee
@@ -11,6 +11,7 @@
 #   HUBOT_GITHUB_USER
 #   HUBOT_GITHUB_REPO
 #   HUBOT_GITHUB_USER_(.*)
+#   HUBOT_GITHUB_API
 #
 # Commands:
 #   hubot show [me] [<limit> [of]] [<assignee>'s|my] [<label>] issues [for <user/repo>] [about <query>] -- Shows open GitHub issues for repo.
@@ -23,6 +24,8 @@
 #   If, for example, HUBOT_GITHUB_USER_JOHN is set to GitHub user login
 #   'johndoe1', you can ask `show john's issues` instead of `show johndoe1's
 #   issues`. This is useful for mapping chat handles to GitHub logins.
+#
+#   HUBOT_GITHUB_API allows you to set a custom URL path (for Github enterprise users)
 #
 # Author:
 #   davidsiegel
@@ -81,7 +84,8 @@
     query_params.labels = criteria.label if criteria.label?
     query_params.assignee = criteria.assignee if criteria.assignee?

-    github.get "https://api.github.com/repos/#{criteria.repo}/issues", query_params, (issues) ->
+    base_url = process.env.HUBOT_GITHUB_API || 'https://api.github.com'
+    github.get "#{base_url}/repos/#{criteria.repo}/issues", query_params, (issues) ->
       issues = filter_issues issues, criteria

       if _.isEmpty issues


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -106,7 +106,7 @@
             r = -r
         #return "translate(" + (x/h * labelRadius) + "," + (y/h * labelRadius) + ")"
         #return "translate(" + r + "," + y + ")"#(y/h * labelRadius) + ")"
-        return "translate(" + ((x/h * labelRadius)*mx) + "," + y*my + ")"#(y/h * labelRadius) + ")"
+        return "translate(" + ((x/h * labelRadius)+r) + "," + y + ")"#(y/h * labelRadius) + ")"
         )

         .attr('fill', "#fff")


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -54,7 +54,7 @@
     sum=0
     for val in data
       sum += val.value
-#    textRadiusInc = Math.Round(sum / data.count)
+    textRadiusInc = Math.Round(sum / data.length)
     if !sum
       arcs.append("svg:text")
         .attr('fill', "#fff")


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -3,8 +3,8 @@

   onData: (data) ->
     $(@node).fadeOut().fadeIn()
-    @render(data.value)
-    #@update(data.value)
+    #@render(data.value)
+    @update(data.value)

   render: (data) ->
     if !data


Agent pie: start/end angle pie transition work

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -178,9 +178,9 @@
             #    b = i(t)
             #    return arc b
         #)
-        path.transition().duration(750).attrTween('d', (d,i) ->
+        path.transition().duration(750).attrTween('d', (d) ->
             `var i`
-            i = d3.interpolate(this._current, a)
+            i = d3.interpolate(this._current, d)
             this._current = i(0)
             return (t) ->
                 return arc(i(t))


Remove unecessary console.log

Remove unecessary console.log

--- widgets/progress_list/progress_list.coffee
+++ widgets/progress_list/progress_list.coffee
@@ -15,7 +15,6 @@

     $('.pagination a').click (event) ->
       event.preventDefault()
-      console.log(autopaginator)
       clearInterval(autopaginator)

   nextPage: =>


Allow to pass a path to method startServer

--- www/coffee/servers.coffee
+++ www/coffee/servers.coffee
@@ -318,7 +318,11 @@
 		servers = {}

-	startServer: (baseUrl, cb) ->
+	startServer: (baseUrl, path, cb) ->
+		if not cb? and typeof path is 'function'
+			cb = path
+			path = ''
+
 		if not httpd?
 			return console.error 'CorHttpd plugin not available/ready.'

@@ -335,8 +339,7 @@
 			servers.active = baseUrl
 			@save()
 			cb? null, baseUrl
-			location.href = 'http://'+@baseUrlToDir(baseUrl) + '.meteor.local/'
-			# document.getElementById('serverFrame').src = 'http://meteor.local/'
+			location.href = "http://#{options.host}/#{path}"

 		failure = (error) ->
 			cb? error


added ShareLaTeX server pro options with ldap into settings.defaults

--- services/web/config/settings.defaults.coffee
+++ services/web/config/settings.defaults.coffee
@@ -331,3 +331,19 @@
 	proxyUrls: {}

 	reloadModuleViewsOnEachRequest: true
+
+	# ShareLaTeX Server Pro options (https://www.sharelatex.com/university/onsite.html)
+	# ----------
+
+
+
+	# ldap:
+	# 	host: 'ldap://ldap.forumsys.com'
+	# 	dnObj: 'uid'
+	# 	dnSuffix: 'dc=example,dc=com'
+	# 	failMessage: 'LDAP User Fail'
+	# 	fieldName: 'LDAP User'
+	# 	placeholder: 'LDAP User ID'
+	# 	emailAtt: 'mail'
+
+


Show day in tooltip date

Show day in tooltip date

--- static/coffee/v2_polls.coffee
+++ static/coffee/v2_polls.coffee
@@ -195,7 +195,7 @@
         tooltip: {
           enabled: true
           headerFormat: '{series.name}
'
-          pointFormat: '{point.x: %b %Y}: {point.y}'
+          pointFormat: pointFormat
         }
         plotOptions: {
           spline: {


Improved descriptions (English).

Improved descriptions (English).

--- test/cli-test.coffee
+++ test/cli-test.coffee
@@ -36,7 +36,7 @@

 describe "Command line interface", ->

-  describe "When raml file not found", (done) ->
+  describe "When RAML file not found", (done) ->
     before (done) ->
       cmd = "./bin/abao ./test/fixtures/nonexistent_path.raml http://localhost:#{PORT}"

@@ -51,9 +51,9 @@
       assert.include stderr, 'Error: ENOENT'

-  describe "Arguments with existing raml and responding server", () ->
-
-    describe "when executing the command and the server is responding as specified in the raml", () ->
+  describe "Arguments with existing RAML and responding server", () ->
+
+    describe "when executing the command and the server is responding as specified in the RAML", () ->
       before (done) ->
         cmd = "./bin/abao -r json ./test/fixtures/single-get.raml http://localhost:#{PORT}"

@@ -76,14 +76,14 @@
       it 'exit status should be 0', () ->
         assert.equal exitStatus, 0

-      it 'should print count of tests will run', ->
+      it 'should print count of tests run', ->
         assert.equal 1, report.tests.length
         assert.equal 1, report.passes.length

       it 'should print correct title for response', ->
         assert.equal report.tests[0].fullTitle, 'GET /machines -> 200 Validate response code and body'

-    describe "when executing the command and raml includes other ramls", () ->
+    describe "when executing the command and RAML includes other RAML files", () ->
       before (done) ->
         cmd = "./bin/abao ./test/fixtures/include_other_raml.raml http://localhost:#{PORT}"

@@ -106,12 +106,12 @@
       it 'exit status should be 0', () ->
         assert.equal exitStatus, 0

-      it 'should print count of tests will run', ->
+      it 'should print count of tests run', ->
         assert.include stdout, '1 passing'

   describe 'when called with arguments', ->

-    describe "when using additional reporters with -r", ->
+    describe "when using additional reporters with --reporter", ->
       before (done) ->
         cmd = "./bin/abao -r spec ./test/fixtures/single-get.raml http://localhost:#{PORT}"

@@ -134,7 +134,7 @@
       it 'should print using the new reporter', ->
         assert.include stdout, '1 passing'

-    describe "when adding additional headers with -h", ->
+    describe "when adding additional headers with --header", ->

       receivedRequest = {}

@@ -164,11 +164,11 @@
       it 'exit status should be 0', () ->
         assert.equal exitStatus, 0

-      it 'should print count of tests will run', ->
+      it 'should print count of tests run', ->
         assert.include stdout, '1 passing'

-    describe "when printing test cases with -n", ->
+    describe "when printing test cases with --names", ->
       before (done) ->
         cmd = "./bin/abao ./test/fixtures/single-get.raml -n"


Invoke failure handler for not found errors

--- workers/social/lib/social/models/sidebar.coffee
+++ workers/social/lib/social/models/sidebar.coffee
@@ -176,6 +176,11 @@
     SocialChannel.byId client, id: workspace.channelId, (err, channel) ->

       if err
+
+        switch err.error
+          when 'koding.NotFoundError'
+            return failureFn()
+
         data =
           channelId   : workspace.channelId
           workspaceId : workspace.getId()


Upgrade to be compatiable with latest hubot-slack v4

--- src/victorops.coffee
+++ src/victorops.coffee
@@ -28,9 +28,15 @@
 fuzzy = require 'fuzzy'

 module.exports = (robot) ->
+  users = robot.brain.users() or @robot.adapter.client.rtm.dataStore.users
   url = process.env.HUBOT_VICTOROPS_POST_URL
   teams = JSON.parse process.env.HUBOT_VICTOROPS_TEAMS_MAP
   calendars = JSON.parse process.env.HUBOT_VICTOROPS_TEAMS_ICAL
+
+  getRoom = (context) ->
+    room = robot.adapter.client.rtm.dataStore.getChannelOrGroupByName context.message.room
+    room = robot.adapter.client.rtm.dataStore.getChannelGroupOrDMById context.message.room unless room
+    room

   parseJSON = (response) ->
     return response.json()
@@ -52,7 +58,6 @@

   lookupUser = (name) ->
     name = name.replace '.', ' ' if name?
-    users = robot.brain.users()
     users = _(users).keys().map (id) ->
       user = users[id]
       id: id
@@ -87,10 +92,10 @@
         [ __, person ] = oncall.summary.match /(?:OVERRIDE:\s)?(.*)/
       cb person

-  robot.respond /page oncall(?:\s+(.*))?/, (msg) ->
+  robot.respond /page on\s?call(?:\s+(.*))?/, (msg) ->
     [ __, message ] = msg.match
-    room = msg.message.room
-    team = teams[room]
+    room = getRoom msg
+    team = teams[room.name]
     return invalidChannel msg unless team

     whoIsOncall team, (person) ->
@@ -100,7 +105,7 @@
           body: JSON.stringify
            entity_id: "slack-ops"
            message_type: "critical"
-           entity_display_name: "Paged by @#{msg.message.user.name} in ##{room}"
+           entity_display_name: "Paged by @#{msg.message.user.name} in ##{room.name}"
            state_message: message
         .then (res) ->
           checkStatus res
@@ -114,9 +119,9 @@
       else
         msg.reply "I can't determine who is on call, please contact your friendly administrator"

-  robot.respond /whois oncall/, (msg) ->
-    room = msg.message.room
-    team = teams[room]
+  robot.respond /who\s?is on\s?call/, (msg) ->
+    room = getRoom msg
+    team = teams[room.name]
     return invalidChannel msg unless team

     whoIsOncall team, (person) ->


cooldown finilazation

cooldown finilazation

--- test/cooldown.coffee
+++ test/cooldown.coffee
@@ -108,4 +108,6 @@
   obj.cooldown 'health',
     target: 10

+
+
 module()


add Fotolife#_toXml test

add Fotolife#_toXml test

--- test/fotolife.coffee
+++ test/fotolife.coffee
@@ -167,24 +167,51 @@
           done()

   describe '_toXml', ->
-    it 'works', ->
-      Fotolife = require '../src/fotolife'
-      json =
-        entry:
-          $:
-            xmlns: 'http://purl.org/atom/ns#'
-          title:
-            _: '<TITLE'
-          content:
+    describe 'single elements', ->
+      it 'works', ->
+        Fotolife = require '../src/fotolife'
+        json =
+          entry:
             $:
-              mode: 'base64'
-              type: '"TYPE'
-            _: '<ENCODED'
-      xml = Fotolife.prototype._toXml json
-      assert xml is '''
-        <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-        <entry xmlns="http://purl.org/atom/ns#">
-          <title><TITLE</title>
-          <content mode="base64" type=""TYPE"><ENCODED</content>
-        </entry>
-      '''
+              xmlns: 'http://purl.org/atom/ns#'
+            title:
+              _: '<TITLE'
+            content:
+              $:
+                mode: 'base64'
+                type: '"TYPE'
+              _: '<ENCODED'
+        xml = Fotolife.prototype._toXml json
+        assert xml is '''
+          <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+          <entry xmlns="http://purl.org/atom/ns#">
+            <title><TITLE</title>
+            <content mode="base64" type=""TYPE"><ENCODED</content>
+          </entry>
+        '''
+
+    describe 'multiple elements', ->
+      it 'works', ->
+        Fotolife = require '../src/fotolife'
+        json =
+          entry:
+            $:
+              xmlns: 'http://purl.org/atom/ns#'
+            title:
+              [
+                $:
+                  attr: 'ATTR1'
+                _: '<TITLE1'
+              ,
+                $:
+                  attr: 'ATTR2'
+                _: '<TITLE2'
+              ]
+        xml = Fotolife.prototype._toXml json
+        assert xml is '''
+          <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+          <entry xmlns="http://purl.org/atom/ns#">
+            <title attr="ATTR1"><TITLE1</title>
+            <title attr="ATTR2"><TITLE2</title>
+          </entry>
+        '''


add testing for plugins causing errors

add testing for plugins causing errors

--- test/lib/test.coffee
+++ test/lib/test.coffee
@@ -11,6 +11,22 @@
 helperFile = (name) -> corepath.resolve __dirname, '..', 'helpers', name

 describe 'test cio', ->
+
+  describe 'errors', ->
+
+    it 'should return an error because string is an invalid plugin', ->
+
+      badCio = buildCio plugins: [ '/invalid/plugin' ]
+      assert badCio.error
+      assert badCio.reason
+      assert.equal badCio.reason.reason, 'Cannot find module \'/invalid/plugin\''
+
+    it 'should return an error because a function returning a string is an invalid plugin', ->
+
+      badCio = buildCio plugins: [ -> 'invalid plugin' ]
+      assert badCio.error
+      assert.equal badCio.reason.error, 'Elements must be functions'
+

   describe 'client and server', ->


update parsley error msg

update parsley error msg

--- assets/coffeescript/pupil_store.coffee
+++ assets/coffeescript/pupil_store.coffee
@@ -633,7 +633,7 @@
             .prop("disabled",true)

         return validity
-      )).addMessage 'en', 'countryvalidator', 'Please select a country from the datalist'
+      )).addMessage 'en', 'countryvalidator', 'Please select a country from the list'

   divisionValidator: ->
     if $(@cartPage).length > 0


Fix require('source-map-support') in prod

--- tasks/compile.coffee
+++ tasks/compile.coffee
@@ -43,7 +43,7 @@
       .split('\n').filter((rule) -> !!rule).map((rule) -> '!' + rule.trim())
     excludeHeaderFilter = filter ['**/*'].concat(loggerIgnore), {restore: true}
     sourceMapHeader = "if (process.type === 'browser') { " +
-      "var ___smp = require('source-map-support'); if (___smp) { ___smp.install(); } }"
+      "try { require('source-map-support').install(); } catch(ignored) {} }"
     loggerHeader = [
       "var log = require('common/utils/logger').debugLogger(__filename);"
       "var logError = require('common/utils/logger').errorLogger(__filename, false);"


missing twinkle

missing twinkle

--- assets/scripts/controllers/task.coffee
+++ assets/scripts/controllers/task.coffee
@@ -67,7 +67,8 @@
         lightDom = document.createElement "li"
         lightDom.id = wid
         _div = document.createElement 'div'
-        _div.setAttribute 'class',"twinkle twinkle-on"
+        twinkleOn = (if worker.active then 'twinkle-on' else '')
+        _div.setAttribute 'class',"twinkle #{twinkleOn}"
         lightDom.appendChild _div
         _input = document.createElement 'input'
         _input.type = 'checkbox'


Fix timeline video references

Fix timeline video references

Fixes #2496 a victim of coffeescript scoping rules

--- app/assets/volume/slot.coffee
+++ app/assets/volume/slot.coffee
@@ -10,6 +10,8 @@
     $scope.editing = editing
     $scope.mode = if editing then 'edit' else 'view'
     $scope.form = {}
+
+    video = undefined

     searchLocation = (url) ->
       url
@@ -270,7 +272,5 @@
     $scope.playing = 0
     $scope.position = undefined

-    video = undefined
-
     return
 ])


Don't display off-screen bars

Don't display off-screen bars

--- app/assets/volume/slot.coffee
+++ app/assets/volume/slot.coffee
@@ -149,19 +149,25 @@
       style: ->
         style = {}
         l = @lt.p
+        r = @ut.p
         if l < 0
-          style.left = '0px'
-          style['border-left'] = '0px'
-          style['border-top-left-radius'] = '0px'
-          style['border-bottom-left-radius'] = '0px'
+          if r < 0
+            style.display = 'none'
+          else
+            style.left = '0px'
+            style['border-left'] = '0px'
+            style['border-top-left-radius'] = '0px'
+            style['border-bottom-left-radius'] = '0px'
         else if l < 1
           style.left = 100*l + '%'
-        r = @ut.p
         if r > 1
-          style.right = '0px'
-          style['border-right'] = '0px'
-          style['border-top-right-radius'] = '0px'
-          style['border-bottom-right-radius'] = '0px'
+          if l > 1
+            style.display = 'none'
+          else
+            style.right = '0px'
+            style['border-right'] = '0px'
+            style['border-top-right-radius'] = '0px'
+            style['border-bottom-right-radius'] = '0px'
         else if r > 0
           style.right = 100*(1-r) + '%'
         style


Skip unimplemented XHR feature (parsing XML)

--- test/xhr_test.coffee
+++ test/xhr_test.coffee
@@ -207,7 +207,7 @@
       assert.equal "Text", browser.document.text

-  describe "HTML document", ->
+  describe.skip "HTML document", ->
     before (done)->
       brains.get "/xhr/get-html", (req, res)->
         res.send """
@@ -216,7 +216,7 @@
           <body>
             <script>
               $.get("/xhr/html", function(response, status, xhr) {
-                window.html = xhr.responseXML;
+                document.body.appendChild(xhr.responseXML);
               });
             </script>
           </body>
@@ -224,14 +224,14 @@
         """
       brains.get "/xhr/html", (req, res)->
         res.type("text/html")
-        res.send "<foo><bar id='bar'></foo>"
+        res.send("<foo><bar id='bar'></foo>")
       brains.ready done

     before (done)->
       browser.visit("http://localhost:3003/xhr/get-html", done)

     it "should parse HTML document", ->
-      assert browser.document.body.querySelectorAll("foo > bar#bar")
+      browser.assert.element "foo > bar#bar"

   after ->


Load build scripts before running build

Load build scripts before running build

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -43,8 +43,9 @@
 configuration.flavors = [configuration.mode, configuration.network].concat(argv['flavor'])

 checkoutAndRun = (script, conf = configuration) ->
+  gitFinalize = require('./compilation/script-git-finalize')
   require('./compilation/script-git-setup')(conf).then -> 12 #require("./compilation/#{script}")(conf)
-  .then -> require('./compilation/script-git-finalize')(conf)
+  .then -> gitFinalize(conf)

 gulp.task 'build', -> checkoutAndRun('script-build')
 gulp.task 'zip', -> checkoutAndRun('script-zip')


hub, etc. -- hopefully workaround bug that prevents reconnect when database dies

--- src/smc-hub/postgres-base.coffee
+++ src/smc-hub/postgres-base.coffee
@@ -321,7 +321,15 @@
                 # disable doing so!
                 @_connect_time = new Date()
                 locals.i = 0
+
+                # Weird and unfortunate fact -- this query can and does **HANG** never returning
+                # in some edge cases.  That's why we have to be paranoid about this entire _connect
+                # function...
                 f = (client, cb) =>
+                    it_hung = =>
+                        cb("hung")
+                        cb = undefined
+                    timeout = setTimeout(it_hung, 15000)
                     dbg("now connected; disabling nestloop query planning for client #{locals.i}")
                     locals.i += 1
                     client.query "SET enable_nestloop TO off", (err) =>
@@ -329,6 +337,7 @@
                             dbg("disabling nestloop failed for client #{locals.i}")
                         else
                             dbg("disabling nestloop done for client #{locals.i}")
+                        clearTimeout(timeout)
                         cb(err)
                 async.map(locals.clients, f, cb)
             (cb) =>


[Gutter] Rename TextEditorPresenter::updateCustomGutterState -> ::updateGutterOrderState

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -90,7 +90,7 @@
     @updateOverlaysState() if @shouldUpdateOverlaysState
     @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState
     @updateLineNumbersState() if @shouldUpdateLineNumbersState
-    @updateCustomGutterState() if @shouldUpdateCustomGutterState
+    @updateGutterOrderState() if @shouldUpdateGutterOrderState
     @updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState
     @updating = false

@@ -109,7 +109,7 @@
       @updateLinesState()
       @updateLineNumberGutterState()
       @updateLineNumbersState()
-      @updateCustomGutterState()
+      @updateGutterOrderState()
       @updateCustomGutterDecorationState()
     @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this))
     @disposables.add @model.onDidChangePlaceholderText(@updateContentState.bind(this))
@@ -122,12 +122,12 @@
       @updateLineNumberGutterState()
       @updateLineNumbersState()
       @updateCommonGutterState()
-      @updateCustomGutterState()
+      @updateGutterOrderState()
       @updateCustomGutterDecorationState()
     @disposables.add @model.onDidChangeLineNumberGutterVisible =>
       @updateLineNumberGutterState()
       @updateCommonGutterState()
-      @updateCustomGutterState()
+      @updateGutterOrderState()
     @disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
     @disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
     @disposables.add @model.onDidChangeScrollTop(@setScrollTop.bind(this))
@@ -163,14 +163,14 @@
       @showLineNumbers = newValue
       @updateLineNumberGutterState()
       @updateCommonGutterState()
-      @updateCustomGutterState()
+      @updateGutterOrderState()

   didChangeGrammar: ->
     @observeConfig()
     @updateContentState()
     @updateLineNumberGutterState()
     @updateCommonGutterState()
-    @updateCustomGutterState()
+    @updateGutterOrderState()

   buildState: ->
     @state =
@@ -210,7 +210,7 @@
     @updateLineNumberGutterState()
     @updateLineNumbersState()
     @updateCommonGutterState()
-    @updateCustomGutterState()
+    @updateGutterOrderState()
     @updateCustomGutterDecorationState()

   updateFocusedState: -> @batch "shouldUpdateFocusedState", ->
@@ -395,21 +395,21 @@
   didAddGutter: (gutter) ->
     gutterDisposables = new CompositeDisposable
     gutterDisposables.add gutter.onDidChangeVisible =>
-      @updateCustomGutterState()
+      @updateGutterOrderState()
       @updateCustomGutterDecorationState()
     gutterDisposables.add gutter.onDidDestroy =>
       @disposables.remove(gutterDisposables)
       gutterDisposables.dispose()
-      @updateCustomGutterState()
+      @updateGutterOrderState()
       # It is not necessary to @updateCustomGutterDecorationState here.
       # The destroyed gutter will be removed from the list of gutters in @state,
       # and thus will be removed from the DOM.
     @disposables.add(gutterDisposables)
-    @updateCustomGutterState()
+    @updateGutterOrderState()
     @updateCustomGutterDecorationState()

-  updateCustomGutterState: ->
-    @batch "shouldUpdateCustomGutterState", ->
+  updateGutterOrderState: ->
+    @batch "shouldUpdateGutterOrderState", ->
       @state.gutters.sortedDescriptions = []
       if @model.isMini()
         return
@@ -808,14 +808,14 @@
       @updateContentState()
       @updateLineNumberGutterState()
       @updateCommonGutterState()
-      @updateCustomGutterState()
+      @updateGutterOrderState()

   setGutterBackgroundColor: (gutterBackgroundColor) ->
     unless @gutterBackgroundColor is gutterBackgroundColor
       @gutterBackgroundColor = gutterBackgroundColor
       @updateLineNumberGutterState()
       @updateCommonGutterState()
-      @updateCustomGutterState()
+      @updateGutterOrderState()

   setLineHeight: (lineHeight) ->
     unless @lineHeight is lineHeight


Include more fields in line state

Include more fields in line state

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -50,6 +50,9 @@
       text: line.text
       tokens: line.tokens
       endOfLineInvisibles: line.endOfLineInvisibles
+      indentLevel: line.indentLevel
+      tabLength: line.tabLength
+      fold: line.fold
       top: row * @getLineHeight()
       width: @getScrollWidth()


:bug: Move accidentally leftover statement

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -89,6 +89,8 @@
     @state

   getPostMeasurementState: ->
+    @updating = true
+
     @updateHorizontalDimensions()
     @commitPendingLogicalScrollLeftPosition()
     @commitPendingScrollLeftPosition()
@@ -116,8 +118,6 @@
   # Public: Gets this presenter's state, updating it just in time before returning from this function.
   # Returns a state {Object}, useful for rendering to screen.
   getState: ->
-    @updating = true
-
     @linesYardstick.prepareScreenRowsForMeasurement()

     @getPostMeasurementState()


Hide line decorations for invalid markers again

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -628,6 +628,8 @@
     marker = decoration.getMarker()
     properties = decoration.getProperties()

+    return unless marker.isValid()
+
     if range.isEmpty()
       return if properties.onlyNonEmpty
     else


disable edit attributes for elements with no attributes to edit

--- src/textorum/tinymce/tree.coffee
+++ src/textorum/tinymce/tree.coffee
@@ -167,6 +167,10 @@
       )
       submenu = _submenuItemsForAction(validNodes)
       siblingSubmenu = _submenuItemsForAction(siblingNodes)
+      if (x for own x of schema.defs?[node.attr('data-xmlel')]?.attr).length
+        editDisabled = false
+      else
+        editDisabled = true
       items =
         before:
           label: "Insert Tag Before"
@@ -194,8 +198,9 @@
           submenu: siblingSubmenu("change")

         edit:
-          label: "Edit Tag"
+          label: "Edit Attributes"
           icon: "img/tag_edit.png"
+          _disabled: editDisabled
           action: (obj) ->
             editor.execCommand "editSchemaTag", true, obj


Load stylesheet when attached

Load stylesheet when attached

--- src/trix/elements/element.coffee
+++ src/trix/elements/element.coffee
@@ -12,12 +12,12 @@
       Object.defineProperty(@prototype, name, descriptor)

     createdCallback: ->
-      @loadStylesheet()
       @loadDefaultContent()
       handleEvent "element-attached", onElement: this, withCallback: (event) =>
         @childAttachedCallback(event.target) unless event.target is this

     attachedCallback: ->
+      @loadStylesheet()
       triggerEvent("element-attached", onElement: this)

     childAttachedCallback: ->


Search completion; disable on redundant whitespace.

--- pages/vomnibar.coffee
+++ pages/vomnibar.coffee
@@ -214,8 +214,13 @@

     return unless completion

-    # Fetch the query and suggestion.
-    query = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase()
+    # Bail on leading whitespace or on redundant whitespace.  This provides users with a way to force this
+    # feature off.
+    value = @input.value
+    return if /^\s/.test(value) or /\s\s/.test value
+
+    # Fetch the query and the suggestion texts.
+    query = value.ltrim().split(/\s+/).join(" ").toLowerCase()
     suggestion = completion.title

     index = suggestion.toLowerCase().indexOf query


Fix various vomnibar clicks...

Fix various vomnibar clicks...

Click:
- in the vomnibar focuses the input
- anywhere else (such as in the space below the vomnibar) hides the
  vomnibar.

This makes clicks in the space below the vomnibar behave the same as
clicks in the page itself...  Which makes sense, because the difference
is not apparent to the user.

--- pages/vomnibar.coffee
+++ pages/vomnibar.coffee
@@ -182,6 +182,12 @@
     @completionList.style.display = ""

     window.addEventListener "focus", => @input.focus()
+    # A click in the vomnibar itself refocuses the input.
+    @box.addEventListener "click", (event) =>
+      @input.focus()
+      event.stopImmediatePropagation()
+    # A click anywhere else hides the vomnibar.
+    document.body.addEventListener "click", => @hide()

 #
 # Sends filter and refresh requests to a Vomnibox completer on the background page.


Added problems

Added problems

--- server/materials/data/topics/binary_trees_and_trie.coffee
+++ server/materials/data/topics/binary_trees_and_trie.coffee
@@ -9,7 +9,11 @@
     return {
         topic: topic("Бинарные деревья и бор", "Задачи на бинарные деревья и бор", [
             label("Теории тут пока нет"),
-            label("TODO"),
+            problem(757),
+            problem(760),
+            problem(761),
+            problem(111730),
+            problem(3302),
             problem(111729),
         ]),
         advancedProblems: [


Allow changing environment from browser

Allow changing environment from browser

--- app/api/config.coffee
+++ app/api/config.coffee
@@ -1,15 +1,19 @@
-DEFAULT_ENV = '_cam'
+DEFAULT_ENV = 'staging'

 API_HOSTS =
   production: '' # Same domain!
   staging: 'https://panoptes-staging.zooniverse.org'
   development: 'http://localhost:3000'
   test: 'http://localhost:7357'
-  _cam: 'http://172.17.2.87:3000'
+  cam: 'http://172.17.2.87:3000'

 API_APPLICATION_IDS =
-  _cam: '05fd85e729327b2f71cda394d8e87e042e0b77b05e05280e8246e8bdb05d54ed'
+  cam: '05fd85e729327b2f71cda394d8e87e042e0b77b05e05280e8246e8bdb05d54ed'
+
+BROWSER_ENV_PARAM = location.search.match(/\W?env=(\w+)/)?[1]
+
+env = BROWSER_ENV_PARAM ? process.env.NODE_ENV ? DEFAULT_ENV

 module.exports =
-  host: API_HOSTS[process.env.NODE_ENV ? DEFAULT_ENV]
-  clientAppID: API_APPLICATION_IDS[process.env.NODE_ENV ? DEFAULT_ENV]
+  host: API_HOSTS[env]
+  clientAppID: API_APPLICATION_IDS[env]


fix unit tests

fix unit tests

--- services/clsi/test/unit/coffee/ResourceWriterTests.coffee
+++ services/clsi/test/unit/coffee/ResourceWriterTests.coffee
@@ -7,7 +7,9 @@
 describe "ResourceWriter", ->
 	beforeEach ->
 		@ResourceWriter = SandboxedModule.require modulePath, requires:
-			"fs": @fs = { mkdir: sinon.stub().callsArg(1) }
+			"fs": @fs =
+				mkdir: sinon.stub().callsArg(1)
+				unlink: sinon.stub().callsArg(1)
 			"wrench": @wrench = {}
 			"./UrlCache" : @UrlCache = {}
 			"mkdirp" : @mkdirp = sinon.stub().callsArg(1)


Update Excerpts

Update Excerpts

--- app/controllers/editor.coffee
+++ app/controllers/editor.coffee
@@ -23,7 +23,7 @@

   enable: (note) =>
     # Put back into the right mode
-    @toggleMode() if @mode = "edit"
+    @toggleMode() if @mode is "edit"

     # Loads note
     Note.current = note
@@ -62,8 +62,24 @@
       # Save it
       if Note.current isnt undefined
         currentNote = Note.find(Note.current.id)
-        currentNote.updateAttribute("name", @title.val())
-        currentNote.updateAttribute("date", Math.round(new Date()/1000))
+
+        # Excerpts nicely
+        info = noteText
+        if info.length > 90
+          info = info.substring(0, 100)
+          lastIndex = info.lastIndexOf(" ")
+          info = info.substring(0, lastIndex) + "…"
+        info = $(marked(info)).text()
+        info.split("\n").join(" ")
+
+        # Update Spine
+        currentNote.updateAttributes {
+          "name": @title.val()
+          "excerpt": info
+          "date": Math.round(new Date()/1000)
+        }
+
+        # Update IndexedDB
         currentNote.saveNote(noteText)

       # The opposite


refactoring jquery method: css('height') to height()

--- app/assets/javascripts/map.coffee
+++ app/assets/javascripts/map.coffee
@@ -70,12 +70,12 @@
           zoom: 1
         $('#panorama').show()
         that.pano.setVisible true
-        $('#map').css('height', $('#mapview').height() - $('#panorama').height())
+        $('#map').height($('#mapview').height() - $('#panorama').outerHeight(true))
         google.maps.event.trigger that.map, 'resize'
       else
         $('#panorama').hide()
         that.pano.setVisible false
-        $('#map').css('height', $('#mapview').height())
+        $('#map').height($('#mapview').height())
         google.maps.event.trigger that.map, 'resize'
       that.map.setCenter LL(user.lat, user.lon)


Add tests for the main controller

Add tests for the main controller

--- test/spec/controllers/main.coffee
+++ test/spec/controllers/main.coffee
@@ -15,5 +15,5 @@
       $scope: scope
     }

-  it 'should attach a list of awesomeThings to the scope', ->
-    expect(scope.awesomeThings.length).toBe 3
+  it 'should create an instance of the game', ->
+    expect(scope.game instanceof Game).toBe true


fix improper saving of ymapper state

fix improper saving of ymapper state

--- static/coffee/common/plot.coffee
+++ static/coffee/common/plot.coffee
@@ -283,7 +283,7 @@

     have_new_mapper_state = false
     xms = @xmapper.get('mapper_state')[0]
-    yms = @xmapper.get('mapper_state')[0]
+    yms = @ymapper.get('mapper_state')[0]
     if Math.abs(@old_mapper_state.x-xms) > 1e-8 or Math.abs(@old_mapper_state.y - yms) > 1e-8
       @old_mapper_state.x = xms
       @old_mapper_state.y = yms


branches test is testing too low to be of much use - removing

--- test/branches.coffee
+++ test/branches.coffee
@@ -1,166 +0,0 @@
-zombie    = require 'zombie'
-assert    = require 'assert'
-fs        = require 'fs'
-path      = require 'path'
-rimraf    = require 'rimraf'
-detective = require 'detective'
-utils     = require './../lib/utils'    # hook in to this
-analysis  = require './../lib/analysis' # hook in to this
-resolver  = require './../lib/resolver' # hook in to this
-modul8    = require './../index.js' # public interface
-dir       = __dirname
-
-{isLegalRequire, Resolver} = resolver
-
-
-makeBranch = (num, domain, folder, same=[], cross=[]) ->
-  for i in [0...num]
-    l = []
-    l.push "var a = b = c = d = e = {chain:true};" # initalize safes in case we do not use them
-
-    l.push "a = require('./#{i+1}');" if i < num-1  # a checks the basic chain up to num
-
-    # mixin sub-branches from other folders to spice things up a bit (see the dependency tree)
-
-    # b, c are basic mixins
-    l.push "b = require('./#{same[0]}#{i}.js');" if same[0]? and i is 3
-    l.push "c = require('./#{same[1]}#{i}.js');" if same[1]? and i is 2
-
-    # d, e are cross domain mixins
-    l.push "d = require('#{cross[0].dom}::#{cross[0].branch}#{i}');" if cross[0]? and i is 3
-    l.push "e = require('#{cross[1].dom}::#{cross[1].branch}#{i}');" if cross[1]? and i is 2
-
-    l.push "exports.chain = a.chain && b.chain && c.chain && d.chain && e.chain;" # everything this module requires must resolve!
-    fs.writeFileSync(dir+'/input/'+domain+'/'+folder+i+'.js', l.join('\n'))
-
-
-options =
-  domains:
-    'app'       : dir+'/input/app/'
-    'internal'  : dir+'/input/internal/'
-    'shared'    : dir+'/input/shared/'
-    'crazy'     : dir+'/input/crazy/'
-
-  chains :
-    'app' : [
-      { folder: 'branch1/' }#, cross: [{dom:'shared', branch:''}] }
-      { folder: 'branch2/', mix: ['../branch1/', '../branch1/'] }
-    ]
-    'shared' : [
-      { folder: '', mix: ['branch1/'] } # shared::k <- shared::branch1::k for some k
-      { folder: 'branch1/'}
-    ]
-    'internal' : [
-      { folder: ''}#, mix: ['']} # mixin of self is circular and does throw
-      { folder: 'control/', mix: ['../']}
-    ]
-    'crazy'   : [
-      {folder: 'waa/', cross: [{dom:'shared', branch: 'branch1/'}, {dom:'internal', branch:''}] }
-      {folder: ''}
-    ]
-  size : 6
-
-hasRun = false
-generateApp = -> # dont call this with size < 4 otherwise we wont get the mixins
-  return if hasRun
-  hasRun = true
-  l = [] # generate an entry point simultaneously with module generation
-
-  # clean out old directory
-  try rimraf.sync(dir+'/input')
-  catch e
-  fs.mkdirSync(dir+'/input', 0755)
-  size = options.size
-  for domain, domChains of options.chains
-    fs.mkdirSync(dir+'/input/'+domain, 0755)
-    prefix = if domain is 'app' then '' else domain+'::'
-    for chain in domChains
-      fs.mkdirSync(dir+'/input/'+domain+'/'+chain.folder, 0755) if chain.folder # only truth test this (do not want to create dir with empty name)
-      makeBranch(size, domain, chain.folder, chain.mix, chain.cross)
-      l.push "exports.#{domain}_#{chain.folder[0...-1]} = require('#{prefix}#{chain.folder}0');" #exports name slices away the trailing slash from folder if it exists
-
-  fs.writeFileSync(dir+'/input/app/entry.js', l.join('\n')) # write entry point
-
-  modul8(dir+'/input/app/entry.js')
-    .set('force', true)
-    #.analysis(console.log)
-    .domains()
-      .add('internal', dir+'/input/internal/')
-      .add('shared', dir+'/input/shared/')
-      .add('crazy', dir+'/input/crazy/')
-    .compile(dir+'/output/output.js')
-
-
-exports["test require#branches"] = ->
-  generateApp()
-  num = options.size
-
-  compile = utils.makeCompiler()
-
-  browser = new zombie.Browser()
-  browser.visit 'file:///'+dir+"/empty.html", (err, browser, status) ->
-    throw err if err
-    mainCode = compile(dir+'/output/output.js')
-
-    assert.isUndefined(browser.evaluate(mainCode), ".compile() result evaluates successfully") # will throw if it fails
-    assert.isDefined(browser.evaluate("M8"), "global namespace is defined")
-    assert.isDefined(browser.evaluate("M8.require"), "require is globally accessible")
-    assert.type(browser.evaluate("M8.require"), 'function', "require is a function")
-    assert.ok(browser.evaluate("M8.require('entry')"), 'require resolves entrypoint')
-    testCount = 5
-
-    for domain, domChains of options.chains
-      assert.includes(browser.evaluate("M8.domains()"), domain, "domains() contain #{domain}")
-      testCount++
-      prefix = if domain is 'app' then '' else domain+'::'
-
-      for chain in domChains
-        for i in [0...num]
-          assert.isDefined(browser.evaluate("M8.require('#{domain}::#{chain.folder}#{i}.js')"), "require('#{domain}::#{chain.folder}#{i}.js') is defined with full specifier")
-          assert.isDefined(browser.evaluate("M8.require('#{domain}::#{chain.folder}#{i}')"), "require('#{domain}::#{chain.folder}#{i}') is defined with full specifier no ext")
-          assert.isDefined(browser.evaluate("M8.require('#{prefix}#{chain.folder}#{i}.js')"), "require('#{prefix}#{chain.folder}#{i}.js') is defined without domain specifier (when applicable)")
-          assert.isDefined(browser.evaluate("M8.require('#{prefix}#{chain.folder}#{i}')"), "require('#{prefix}#{chain.folder}#{i}') is defined without ext and domain specifier (when applicable)")
-          testCount+=4
-
-        assert.ok(browser.evaluate("M8.require('entry').#{domain}_#{chain.folder[0...-1]}"), "require entrys domain_chain: #{domain}_#{chain.folder[0...-1]} works")
-        testCount++
-    console.log 'require#branches - completed:', testCount
-  return
-
-exports["test analyzer#order"] = ->
-  generateApp()
-  exts = ['','.js','.coffee']
-  resolver = new Resolver(options.domains, {}, exts) # no arbiters
-  compile = utils.makeCompiler()
-  ca = analysis
-    entryPoint : 'entry.js'
-    domains    : options.domains
-    arbiters   : {}
-    ignoreDoms : []
-    exts       : exts
-    (a) -> a # before
-    compile  # argument 3
-  order = ca.sorted()
-
-  assert.ok(order.length, "order is a non-empty array") # we are testing the giant app
-  testCount = 1
-
-  included = []
-  for [domain, name] in order
-    assert.ok(domain, "domain found for #{name}")
-    assert.ok(utils.exists(options.domains[domain]+name), "file #{domain}::#{name} resolves on #{options.domains[domain]}")
-    testCount+=2
-
-    # now detective directly and hook into resolver to see if requirements have been included previously!
-
-    extraPath = path.dirname(name)
-    code = compile(options.domains[domain]+name)
-    deps = (resolver.locate(dep, extraPath, domain) for dep in detective(code) when isLegalRequire(dep)) # should trivially resolve because we know full string and domain
-
-    for [n,d,fake] in deps when !fake
-      assert.includes(included, d+'::'+n, "#{domain}::#{name} requires #{d}::#{n}, which should have been included before")
-      testCount+=1
-
-    included.push domain+'::'+name
-
-  console.log "analyzer#order - completed:", testCount


remove unbalanced bracket in zootorial steps

--- app/lib/tutorial_steps.coffee
+++ app/lib/tutorial_steps.coffee
@@ -37,7 +37,7 @@
     className: 'arrow right'

     onEnter: ->
-      @toHighlight = $('button[value="like"][name="characteristic"], [button[value="likeAntelopeDeer"][name="characteristic-value"]')
+      @toHighlight = $('button[value="like"][name="characteristic"], button[value="likeAntelopeDeer"][name="characteristic-value"]')
       @toHighlight.css 'background-color': '#490'

     onExit: ->


removing outdated line about how to add subject sets from visability help (#3558)

Now you can directly link and unlink subject sets from the workflows section of the project builder for active workflows. So removing this help text as it's not needed to make the workflow inactive anymore.

--- app/pages/lab/visibility.cjsx
+++ app/pages/lab/visibility.cjsx
@@ -321,7 +321,6 @@

Status

In a live project active workflows are available to volunteers and cannot be edited. Inactive workflows can be edited if a project is live or in development.

If an active workflow is the default workflow for the project and is made inactive, then it will be removed as the default workflow.

-
On a live project, if you want to switch which subjects sets are associated with an active workflow: set the workflow to inactive, next change which subject sets are linked in the Workflow Section within the Project Builder, then return the workflow to active.

Completeness statistic

Use this option to change how each workflow's completeness is calculated on the public statistics page.


clarify a TODO

clarify a TODO

--- source/javascripts/index/announcement.coffee
+++ source/javascripts/index/announcement.coffee
@@ -11,7 +11,7 @@
   nullify: -> @_nullify ?= "Make sure no חָמֵץ is in your possesion and say כָּל חֲמִירָא before #{@latestTimeToBurn()} on שַׁבָּת"
   announcement: -> @_announcement ?=if @hebrewDate.isShabbat()
         "#{@burn()}
#{@stopEating()}
#{@nullify()}"
-        # TODO: Need to decide what to do announce about Seudat Shelishit
+        # TODO: Need to decide what to announce about Seudat Shelishit by 2021.  See github issue #34
         # TODO: This could be more accurate if we generate the burn announcement using Friday's time instead of Saturday's
       else
         "#{@stopEating()}
#{@burn()}"


:white_check_mark: Add tests for boolean state methods

--- specs/units/mixins/state_machine.spec.coffee
+++ specs/units/mixins/state_machine.spec.coffee
@@ -55,6 +55,7 @@

   it 'initialize the instance in the initial state', ->
     expect(dummy.state).toEqual('birth')
+    expect(dummy.birth()).toBeTruthy()
     expect(dummy.talk()).toEqual('...waaahahaahaAAHAAAHAAA')
     expect(dummy.age).toEqual(0)

@@ -63,6 +64,7 @@
       dummy.live()

       expect(dummy.state).toEqual('life')
+      expect(dummy.live()).toBeTruthy()
       expect(dummy.talk()).toEqual('I am alive')
       expect(dummy.age).toEqual(20)

@@ -71,6 +73,7 @@
         dummy.disappear()

         expect(dummy.state).toEqual('void')
+        expect(dummy.void()).toBeTruthy()
         expect(dummy.talk).toBeUndefined()
         expect(dummy.age).toBeUndefined()


Get out of there.

Get out of there.

--- src/stdlib/settings.coffee
+++ src/stdlib/settings.coffee
@@ -19,7 +19,6 @@

   constructor: ->
     @load "~/.atomicity/settings.coffee"
-    console.log @settings

   load: (path) ->
     path = require.resolve path


fixed zipping of arbitrarily deep directory structures

--- src/steroids/fs/zip.coffee
+++ src/steroids/fs/zip.coffee
@@ -8,7 +8,7 @@
     @zipRecursively callback

   zipRecursively: (callback)->
-    child = childProcess.exec "zip -R -FS #{@to} ./* ./**/* ./**/**/* ./**/**/**/*", {
+    child = childProcess.exec "find . -print | zip -@ -FS #{@to}", {
       cwd: @from
     }, (error, stdout, stderr)->
       throw error if error?


adapt tests to rewrite, add a bunch more tests for correctness and 100% coverage

--- test/lib/test.coffee
+++ test/lib/test.coffee
@@ -5,6 +5,66 @@
 Path     = require 'fspath'

 describe 'test kevas', ->
+
+  it 'should use a noop function when not given values', ->
+
+    string = 'test {{noop}} function'
+    transform = kevas()
+
+    calledData = []
+    transform.on 'data', (data) -> calledData.push data
+    transform.end string
+
+    assert calledData.length > 0
+    for each, index in calledData
+      assert Buffer.isBuffer(each), 'index ' + index + ' isnt a buffer'
+    assert.equal Buffer.concat(calledData).toString(), string
+
+
+  it 'should use a values function when given', ->
+
+    string = 'test {{value}} function'
+    transform = kevas values: (key) -> if key is 'value' then 'VALUE'
+
+    calledData = []
+    transform.on 'data', (data) -> calledData.push data
+    transform.end string
+
+    assert calledData.length > 0
+    for each, index in calledData
+      assert Buffer.isBuffer(each), 'index ' + index + ' isnt a buffer'
+    assert.equal Buffer.concat(calledData).toString(), 'test VALUE function'
+
+
+  it 'should use a noop function when given an invalid values type', ->
+
+    string = 'test {{noop}} function'
+    transform = kevas values: 123
+
+    calledData = []
+    transform.on 'data', (data) -> calledData.push data
+    transform.end string
+
+    assert calledData.length > 0
+    for each, index in calledData
+      assert Buffer.isBuffer(each), 'index ' + index + ' isnt a buffer'
+    assert.equal Buffer.concat(calledData).toString(), string
+
+
+  it 'should leave key wrapped with braces when there\'s no value for the key', ->
+
+    string = 'test {{unknown}} function'
+    transform = kevas values: {}
+
+    calledData = []
+    transform.on 'data', (data) -> calledData.push data
+    transform.end string
+
+    assert calledData.length > 0
+    for each, index in calledData
+      assert Buffer.isBuffer(each), 'index ' + index + ' isnt a buffer'
+    assert.equal Buffer.concat(calledData).toString(), string
+

   shouldReplaceKeyWithValue = 'should replace the key with its value'
   for test in [
@@ -31,7 +91,7 @@
     {
       desc: 'with a single key surrouned by space'
       output:['{{ key }}']
-      values: key:'value'
+      values: ' key ':'value'
       result:'value'
     }
     {
@@ -86,10 +146,17 @@
     }
     {
       desc: 'with escaped opening brace'
+      should:'should include the braces without the slash'
+      output:['some \\{{ value']
+      values: key:'good'
+      result:'some {{ value'
+    }
+    {
+      desc: 'with escaped closing brace'
       should:'should include the brace without the slash'
-      output:['some \\{{ value']
-      values: key:'good'
-      result:'some {{ value'
+      output:['some {{key\\}}} value']
+      values: 'key}':'good'
+      result:'some good value'
     }
     {
       desc: 'with an escape slash before first opening brace which voids key'
@@ -99,18 +166,48 @@
       result:'some {{key}} value'
     }
     {
-      desc: 'with two escape slashes before first opening brace which doesnt affect the key'
+      desc: 'with escaped slash before first opening brace which doesnt affect the key'
       output:['some \\\\{{key}} value']
       values: key:'good'
-      result:'some \\\\good value'
+      result:'some \\good value'
+    }
+    {
+      desc: 'brace found at >1 w two slashes before it'
+      output:[' \\\\{']
+      values: key:'good'
+      result:' \\\\{'
+    }
+    {
+      desc: 'brace found at 1 w/out slash before it'
+      output:[' {']
+      values: key:'good'
+      result:' {'
+    }
+    {
+      desc: 'brace found at 1 with slash before it'
+      output:['\\{']
+      values: key:'good'
+      result:'\\{'
+    }
+    {
+      desc: 'slash found last w/out slash before it'
+      output:[' \\']
+      values: key:'good'
+      result:' \\'
+    }
+    {
+      desc: 'dangling key gets flushed at end()'
+      output:['some dangling {{key']
+      values: key:'good'
+      result:'some dangling {{key'
     }

     # # # # # # # # # # # # # # # # # # # # # # # #
-      # now redo escape test with split output  #
+    #   now redo escape test with split output    #
     # # # # # # # # # # # # # # # # # # # # # # # #
     {
-      desc: 'with two escape slashes next to each other'
-      should:'should include a single slash'
+      desc: 'with two escape slashes next to each other without a pair after it'
+      should:'should remain two slashes'
       output:['some \\', '\\ value']
       values: key:'good'
       result:'some \\\\ value'
@@ -123,26 +220,50 @@
       result:'some {{ value'
     }
     {
-      desc: 'with an escape slash before first opening brace which voids key'
+      desc: 'with an escape slash before first opening brace which voids key (1)'
       should:'should match input without key replacement'
       output:['some \\', '{{key}} value']
       values: key:'good'
       result:'some {{key}} value'
     }
     {
-      desc: 'with an escape slash before first opening brace which voids key'
+      desc: 'with an escape slash before first opening brace which voids key (2)'
       should:'should match input without key replacement'
       output:['some \\{', '{key}} value']
       values: key:'good'
       result:'some {{key}} value'
     }
     {
-      desc: 'with two escape slashes before first opening brace which doesnt affect the key'
+      desc: 'with two escape slashes before first opening brace which doesnt affect the key (1)'
       output:['some \\\\', '{{key}} value']
       values: key:'good'
       result:'some \\\\good value'
     }
-
+    {
+      desc: 'with two escape slashes before first opening brace which doesnt affect the key (2)'
+      output:['some \\', '\\{{key}} value']
+      values: key:'good'
+      result:'some \\good value'
+    }
+    {
+      desc: 'with two escape slashes before first closing brace which becomes part of the key'
+      output:['some {{ke\\', '\\y}} value']
+      values: 'ke\\\\y':'good'
+      result:'some good value'
+    }
+    {
+      desc: 'with escape slash at start of second chunk escaping opening braces'
+      output:['some ', '\\{{key}} value']
+      values: key:'good'
+      result:'some {{key}} value'
+    }
+    {
+      desc: 'with short chunk, previous slash, and slash at start of second chunk'
+      should:'should escape slash and still match key'
+      output:['some \\', '\\{', '{key}} value']
+      values: key:'good'
+      result:'some \\good value'
+    }
     {
       desc: 'with lots of output chunks'
       should:'should still escape a brace and match proper key'
@@ -150,86 +271,87 @@
       values: key:'good'
       result:'some good value'
     }
-
+    {
+      desc: 'dangling key gets flushed at end()'
+      output:['some dangling {', '{k', 'ey']
+      values: key:'good'
+      result:'some dangling {{key'
+    }
   ]
     do (test) ->
-      describe test.desc, ->
-
-        it test.should ? shouldReplaceKeyWithValue, (done) ->
-
-          # create a through stream as a pass-through to pipe to our stream
-          input = through()
-
-          # create our stream with values provided by the test
-          stream = kevas values:test.values
-
-          # create a sink stream to collect all it receives into a string
-          output = strung()
-
-          # when all writes are finished then verify the results
-          output.on 'finish', ->
-            assert.equal output?.string, test.result
-            done() # tell Mocha the test is done
-
-          # send any errors to Mocha's callback
-          input.on  'error', done
-          stream.on 'error', done
-          output.on 'error', done
-
-          # pipe our test input to our stream and then to our output collector.
-          input.pipe(stream).pipe(output)
-
-          # asynchronously write chunks to the input stream
-          # allow for pause/resume with on-drain.
-          index = 0
-          ready = true
-          writeout = (error) ->
-            if error? then return done error
-            unless ready then return input.once('drain', writeout)
-            if index < test.output.length
-              ready = input.write test.output[index], 'utf8', writeout
-              index++
-            else
-              input.end()
-
-          writeout()
+      it test.desc + ' ' + (test.should ? shouldReplaceKeyWithValue), (done) ->
+
+        # create a through stream as a pass-through to pipe to our stream
+        input = through()
+
+        # create our stream with values provided by the test
+        stream = kevas values:test.values
+
+        # create a sink stream to collect all it receives into a string
+        output = strung()
+
+        # when all writes are finished then verify the results
+        output.on 'finish', ->
+          assert.equal output?.string, test.result
+          done() # tell Mocha the test is done
+
+        # send any errors to Mocha's callback
+        input.on  'error', done
+        stream.on 'error', done
+        output.on 'error', done
+
+        # pipe our test input to our stream and then to our output collector.
+        input.pipe(stream).pipe(output)
+
+        # asynchronously write chunks to the input stream
+        # allow for pause/resume with on-drain.
+        index = 0
+        ready = true
+        writeout = (error) ->
+          if error? then return done error
+          unless ready then return input.once('drain', writeout)
+          if index < test.output.length
+            ready = input.write test.output[index], 'utf8', writeout
+            index++
+          else
+            input.end()
+
+        writeout()

 # TODO: set of tests using a file stream as input (use filed? or paths?)
-  describe 'with json file input stream', ->
-
-    it 'should convert unescaped keys to values', (done) ->
-
-      dir = new Path 'test/helpers'
-
-      file = dir.to 'kevas.json'
-
-      expected = JSON.parse dir.to('expected.kevas.json').read()
-
-      stream = kevas values:key:'good'
-
-      output = strung()
-
-      # when all writes are finished then verify the results
-      output.on 'finish', (error) ->
-        if error? then return done error
-        result = JSON.parse output.string
-        assert.deepEqual result, expected
-        done() # tell Mocha the test is done
-
-      # send any errors to Mocha's callback
-      stream.on 'error', done
-      output.on 'error', done
-
-      options = # send errors to `done`
-        reader:
-          events:
-            'error': done
-        writer:
-          events:
-            'error': done
-
-      # pipe our test input to our stream and then to our output collector.
-      file.pipe(stream, options).pipe(output)
+  it 'with json file input stream should convert unescaped keys to values', (done) ->
+
+    dir = new Path 'test/helpers'
+
+    file = dir.to 'kevas.json'
+
+    expected = dir.to('expected.kevas.json').read encoding:'utf8'
+
+    stream = kevas values:key:'good'
+
+    output = strung()
+
+    # when all writes are finished then verify the results
+    output.on 'finish', (error) ->
+      if error? then return done error
+      result = output.string
+      assert.equal result, expected
+      done() # tell Mocha the test is done
+
+    # send any errors to Mocha's callback
+    stream.on 'error', done
+    output.on 'error', done
+
+    options = # send errors to `done`
+      reader:
+        events:
+          'error': done
+      writer:
+        events:
+          'error': done
+
+    # pipe our test input to our stream and then to our output collector.
+    file.pipe(stream, options).pipe(output)


reset _resolvedSystemPostId on props change.

The entire page re-renders anyway

--- resources/assets/coffee/react/beatmap-discussions/discussion.coffee
+++ resources/assets/coffee/react/beatmap-discussions/discussion.coffee
@@ -34,6 +34,10 @@
     @state =
       collapsed: false
       highlighted: false
+
+
+  componentDidUpdate: =>
+    @_resolvedSystemPostId = null

   componentWillMount: =>


Navigate to resource after form submit

Navigate to resource after form submit

--- app/assets/javascripts/hyper_admin/angularjs/controllers/form_ctrl.js.coffee
+++ app/assets/javascripts/hyper_admin/angularjs/controllers/form_ctrl.js.coffee
@@ -18,7 +18,7 @@
         @errors = response.data

       onSuccess = (resource) =>
-        # Do nothing yet
+        $state.go "^.show", id: resource.id

       if method == "post"
         Restangular.all(target).post(@resource).then onSuccess, onError


fix(draft-account-switching): Need to totally delete draft threadId and replyToMessageId

--- src/flux/tasks/syncback-draft.coffee
+++ src/flux/tasks/syncback-draft.coffee
@@ -116,5 +116,9 @@
     newDraft = new Message(draft)
     newDraft.id = generateTempId()
     newDraft.accountId = newAccountId if newAccountId
+
+    delete newDraft.threadId
+    delete newDraft.replyToMessageId
+
     DatabaseStore.swapModel(oldModel: draft, newModel: newDraft, localId: @draftLocalId).then =>
       Promise.resolve(newDraft)


Simplify FifoBuffer

Simplify FifoBuffer

--- src/util/fifobuffer.coffee
+++ src/util/fifobuffer.coffee
@@ -1,6 +1,6 @@
 "use strict"
 # Pre dependencies
-# (none)
+require("util/primitive")

 TR = ArrayBuffer.transfer or (oldBuffer, newByteLength = oldBuffer.byteLength) ->
   oldByteLength = oldBuffer.byteLength
@@ -12,114 +12,122 @@
 MIN_CAPACITY = 256

 module.exports = class FifoBuffer
-  SB = Symbol("Buffer")
-  SF = Symbol("FirstPart")
-  SS = Symbol("SecondPart")
+  null

-  Object.defineProperty(@prototype, "byteLength", get: ->
-    return (@[SF]?.byteLength or 0) + (@[SS]?.byteLength or 0)
-  )
+  ###*
+  @property {number} byteLength
+    Length of stored data in bytes
+  @readonly
+  ###
+  @property("byteLength", get: -> @_byteLength)

-  Object.defineProperty(@prototype, "capacity", get: ->
-    return @[SB].byteLength
-  )
+  ###*
+  @property {number} capacity
+    Length of buffer in bytes
+  @readonly
+  ###
+  @property("capacity", get: -> @_buffer.byteLength)

+  ###*
+  @method constructor
+    Constructor of FifoBuffer class
+  ###
   constructor: ->
-    @[SB] = new ArrayBuffer(0)
-    @[SF] = null
-    @[SS] = null
+    @_byteLength = 0
+    @_buffer = new ArrayBuffer(MIN_CAPACITY)
     return

+  ###*
+  @method
+    Append new data to buffer
+  @param {TypedArray/ArrayBuffer} data
+    Data to add
+  @return {undefined}
+  ###
   push: (data) ->
-    data = new Uint8Array(data) unless data instanceof Uint8Array
-    alen = data.byteLength
-    return if alen == 0
-
-    # Extend buffer size
-    @extend(@byteLength + alen)
-
-    foff = @[SF]?.byteOffset or 0
-    flen = @[SF]?.byteLength or 0
-    slen = @[SS]?.byteLength or 0
-    if slen > 0
-      # Extend second part only
-      (@[SS] = new Uint8Array(@[SB], 0, slen + alen)).set(data, slen)
+    if data instanceof ArrayBuffer
+      array = new Uint8Array(data)
     else
-      # Extend first part
-      frem = @[SB].byteLength - (foff + flen)
-      fadd = Math.min(alen, frem)
-      (@[SF]= new Uint8Array(@[SB], foff, flen + fadd)).set(data, foff + flen)
-      alen -= fadd
-      if alen > 0
-        # Create second part
-        (@[SS] = new Uint8Array(@[SB], 0, alen)).set(data.subarray(fadd), 0)
+      array = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
+    alen = array.byteLength
+    return unless alen > 0
+    nlen = @_byteLength + alen
+    @allocate(nlen)
+    new Uint8Array(@_buffer, @_byteLength).set(array)
+    @_byteLength = nlen
     return

-  pop: (byteLength) ->
-    if byteLength?
-      byteLength = Math.min(byteLength, @byteLength)
-    else
-      byteLength = @byteLength
-    array = new Uint8Array(byteLength)
-    if byteLength > 0
-      flen = @[SF].byteLength
-      fpop = Math.min(byteLength, flen)
-      # Copy from first part
-      array.set(@[SF].subarray(0, fpop), 0)
-      byteLength -= fpop
-      if byteLength > 0
-        # Switch to second part
-        array.set(@[SS].subarray(0, byteLength), fpop)
-        slen = @[SS].byteLength - byteLength
-        if slen > 0
-          @[SF] = @[SS].subarray(byteLength, slen)
-        else
-          @[SF] = null
-        @[SS] = null
-      else
-        flen -= fpop
-        if flen > 0
-          @[SF] = @[SF].subarray(fpop, flen)
-        else
-          @[SF] = null
-    return array.buffer
+  ###*
+  @method
+    Pick out data from buffer
+  @param {number} byteLength
+    Read length in bytes
+  @return {ArrayBuffer}
+    Read data
+  ###
+  shift: (byteLength) ->
+    data = @peek(byteLength)
+    rlen = data.byteLength
+    @_byteLength -= rlen
+    if @_byteLength > 0 and rlen > 0
+      new Uint8Array(@_buffer).set(new Uint8Array(@_buffer, rlen, @_byteLength))
+      @compact()
+    return data

+  ###*
+  @method
+    Read data from buffer
+  @param {number} byteLength
+    Read length in bytes
+  @return {ArrayBuffer}
+    Read data
+  ###
   peek: (byteLength) ->
     if byteLength?
-      byteLength = Math.max(0, Math.min(byteLength, @byteLength))
+      byteLength = Math.min(@_byteLength, byteLength)
     else
-      byteLength = @byteLength
-    array = new Uint8Array(byteLength)
-    if byteLength > 0
-      flen = @[SF].byteLength
-      fpek = Math.min(byteLength, flen)
-      # Copy from first part
-      array.set(@[SF].subarray(0, fpek), 0)
-      byteLength -= fpek
-      if byteLength > 0
-        # Copy from second part
-        array.set(@[SS].subarray(0, byteLength), fpek)
-    return array.buffer
+      byteLength = @_byteLength
+    return @_buffer.slice(0, byteLength)

-  extend: (byteLength) ->
-    newCapacity = @[SB].byteLength
-    return if byteLength <= newCapacity
-    newCapacity or= MIN_CAPACITY
-    newCapacity *= 2 while newCapacity < byteLength
-    foff = @[SF]?.byteOffset
-    flen = @[SF]?.byteLength or 0
-    slen = @[SS]?.byteLength or 0
-    @[SF] = null
-    @[SS] = null
-    @[SB] = TR(@[SB], newCapacity)
-    @[SF] = new Uint8Array(@[SB], foff, flen + slen) if flen > 0
-    @[SF].set(new Uint8Array(@[SB], 0, slen), foff + flen) if slen > 0
+  ###*
+  @method
+    Allocate (growth or reduce) buffer
+  @param {number} capacity
+    New capacity
+  @return {undefined}
+  ###
+  allocate: (capacity) ->
+    newCapacity = Math.max(@_byteLength, capacity) - 1
+    newCapacity |= (newCapacity >>> 1)
+    newCapacity |= (newCapacity >>> 2)
+    newCapacity |= (newCapacity >>> 4)
+    newCapacity |= (newCapacity >>> 8)
+    newCapacity |= (newCapacity >>> 16)
+    newCapacity = Math.max(MIN_CAPACITY, newCapacity + 1)
+    oldCapacity = @_buffer.byteLength
+    return if newCapacity == oldCapacity
+    newBuffer = new ArrayBuffer(newCapacity)
+    new Uint8Array(newBuffer).set(new Uint8Array(@_buffer, 0, @_byteLength))
+    @_buffer = newBuffer
     return

-  clear: ->
-    @[SB] = TR(@[SB], 0)
-    @[SF] = null
-    @[SS] = null
+  ###*
+  @method
+    Compactize buffer
+  @return {undefined}
+  ###
+  compact: ->
+    @allocate(0)
+    return
+
+  ###*
+  @method
+    Reset buffer
+  @return {undefined}
+  ###
+  reset: ->
+    @_byteLength = 0
+    @compact()
     return

 # Post dependencies


Add bargraph to quench

Add bargraph to quench

--- app/config/projects_config.coffee
+++ app/config/projects_config.coffee
@@ -10,7 +10,7 @@
     name: 'Quench'
     talk: true
     sources: ["Quench", "Zooniverse", "Sky Server"]
-    tools: ["Histogram", "Scatterplot", "Mapper", "Statistics", "SubjectViewer", "Spectra", "Table", "ColorMagnitudeChart"]
+    tools: ["BarGraph", "Histogram", "Scatterplot", "Mapper", "Statistics", "SubjectViewer", "Spectra", "Table", "ColorMagnitudeChart"]
     defaults: ["Table", "SubjectViewer"]

   spacewarp:


enforce one artifact at a time

enforce one artifact at a time

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -237,8 +237,20 @@

   onCreateTool: (tool) =>
     surfaceIndex = +@markingSurfaceList.indexOf tool.surface
-    if @state is 'asteroidTool' then tool.setMarkType 'asteroid'
-    if @state is 'artifactTool' then tool.setMarkType 'artifact'
+    tool.mark.id = @currSighting.id
+    tool.mark.on 'change', =>
+      @removeGhostMarks()
+      @addGhostMark(tool.mark)
+
+    switch @state
+      when 'asteroidTool'
+        tool.setMarkType 'asteroid'
+        @doneButton.prop 'disabled', false if @currSighting.annotations.length is @numFrames
+      when 'artifactTool'
+        tool.setMarkType 'artifact'
+        otherFrames = [0...@numFrames].filter (num) -> num isnt surfaceIndex
+        @destroyMarksInFrame(frame) for frame in otherFrames
+        @doneButton.prop 'disabled', false if @currSighting.annotations

     if @asteroidMarkedInFrame[surfaceIndex]
       @currSighting.clearSightingsInFrame surfaceIndex
@@ -247,15 +259,6 @@
       @el.find(".asteroid-frame-complete-#{surfaceIndex}").prop 'checked', true
       @asteroidMarkedInFrame[surfaceIndex] = true
     @updateIconsForCreateMark(surfaceIndex)
-
-    if @state is 'asteroidTool' and @currSighting.annotations.length is @numFrames \
-      or @state is 'artifactTool' and @currSighting.annotations.length > 0 and @artifactSelector.filter(':checked').length > 0
-        @doneButton.prop 'disabled', false
-
-    tool.mark.id = @currSighting.id
-    tool.mark.on 'change', =>
-      @removeGhostMarks() # remove unworthy ghosts
-      @addGhostMark(tool.mark)

   onChangeFrameSlider: =>
     frame = document.getElementById('frame-slider').value


reset fav and zoom buttons on next subject

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -316,7 +316,9 @@
     # @noTransitsButton.hide()
     @finishedMarkingButton.hide()

-    # @el.find("#zoom-button").removeClass("zoomed")
+    @el.find("#zoom-button").removeClass("zoomed")
+    @el.find("#toggle-fav").removeClass("toggled")
+
     @isZoomed = false
     @zoomLevel = 0
     @recordedClickEvents = []


dont load tutorial unless current lightcurve has finished loading

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -182,6 +182,7 @@
     @loadSubjectData()

   loadSubjectData: (jsonFile) ->
+    $('#graph-container').addClass 'loading-lightcurve'
     unless jsonFile?
       jsonFile = jsonFile = @subject.selected_light_curve.location
       # jsonFile = 'https://s3.amazonaws.com/demo.zooniverse.org/planet_hunter/beta_subjects/2442084_13-2.json' # TRAINING FILE
@@ -207,6 +208,7 @@
       @canvasGraph = new CanvasGraph(@canvas, data)
       @canvasGraph.plotPoints()
       @el.find('#loading-screen').hide()
+      $('#graph-container').removeClass 'loading-lightcurve'
       @canvasGraph.enableMarking()
       @zoomRanges = [@canvasGraph.largestX, 10, 2]
       @magnification = [ '1x (all days)', '10 days', '2 days' ]
@@ -362,6 +364,9 @@
     @course.showPrompt()

   onClickTutorial: ->
+    if $('#graph-container').hasClass 'loading-lightcurve'
+      @notify 'Please wait until current lightcurve is loaded.'
+      return
     # do stuff after tutorial complete/aborted
     addEventListener "zootorial-end", =>
       $('.tutorial-annotations.x-axis').removeClass('visible')


Remove logging from fits viewer

Remove logging from fits viewer

--- app/controllers/fitsviewer.coffee
+++ app/controllers/fitsviewer.coffee
@@ -42,7 +42,6 @@
     @createStretchButtons()

   requestFITS: (survey, id) =>
-    console.log 'requesting FITS from Portsmouth'
     window.addEventListener("message", @receiveFITS, false)
     msg =
       survey: survey
@@ -69,7 +68,6 @@
       # Initialize the FITS object
       p1 = p1.pipe (obj) =>
         band = obj.band
-        console.log 'Initializing FITS object for', band
         @images[band] = new FITS.File(obj.arraybuffer)

         # Select the dataunit
@@ -90,7 +88,6 @@

       # Compute statistics
       p1 = p1.pipe ([dataunit, band]) =>
-       console.log 'Computing statistics ', dataunit

        # Set up message to pass to worker
        msg =
@@ -131,7 +128,6 @@
         band = obj.band
         image = obj.image
         dataunit = image.getDataUnit()
-        console.log 'Creating texture for', band, dataunit.width, dataunit.height
         address = "TEXTURE#{@textureCount}"
         @gl.activeTexture(@gl[address])

@@ -194,7 +190,6 @@

   # Sets up everything except for textures
   setupWebGL: =>
-    console.log 'setupWebGL'

     @canvas = WebGL.setupCanvas(@container, FITSViewer.viewportWidth, FITSViewer.viewportHeight)
     @gl     = WebGL.create3DContext(@canvas)


Revert "removed setTimeOut in done() call-back helper in @saveClassification()"

setTimeout considered good here because of async nature of web service call

This reverts commit 2da6b6e4c70eb8f9178709e559e4a0c91f439138.

--- app/controllers/interfaces.coffee
+++ app/controllers/interfaces.coffee
@@ -45,20 +45,23 @@

     done = =>
-
-      #refresh other data
-      Institute.fetch()
-      Project.fetch()
-
-      #refresh User data, primarily to up the badges
-      unless User.current then return
-      badges = User.current.badges
-      userFetch = User.fetch()
+      #throttle to reduce abuse of endpoints
+      setTimeout =>
+        #refresh other data
+        Institute.fetch()
+        Project.fetch()
+
+        #refresh User data, primarily to up the badges
+        unless User.current then return
+        badges = User.current.badges
+        userFetch = User.fetch()

-      userFetch.done =>
-        User.current.badges = badges
-        @archive?.checkBadges()
-
+        userFetch.done =>
+          User.current.badges = badges
+          @archive?.checkBadges()
+
+      , 500
+
     @classification.send done

     Subject.next()


Fixed Notes infinite loop & change to "Notes"

--- app/controllers/tasks.item.coffee
+++ app/controllers/tasks.item.coffee
@@ -97,14 +97,13 @@
       @name.blur()

   notesEdit: =>
-    if @notes.text() is "Notes:" then @notes.text("")
+    if @notes.text() is "Notes" then @notes.text("")
     @notes.removeClass("placeholder")
-    @notes.focus()

   notesSave: =>
     text = @notes.text()
     if text is ""
-      @notes.text("Notes:")
+      @notes.text("Notes")
       @notes.addClass("placeholder")
     else
       @task.updateAttribute "notes", text


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/number_extensions.coffee
+++ test/number_extensions.coffee
@@ -110,7 +110,8 @@
   equals 0.5.turns, Math.TAU / 2

 test "#circularPoints", ->
-  points = []
+  points = [
+  ]
   4.circularPoints (p) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/expirable.coffee
+++ test/expirable.coffee
@@ -27,6 +27,6 @@
   5.times ->
     obj.update()

-  equals obj.I.alpha, 0.8 * (obj.I.age + 1 / obj.I.duration)
+  equals obj.I.alpha, 0.8 * (obj.I.age - 1 / obj.I.duration)

 module()


Fixed EC2 tests

Fixed EC2 tests

--- test/services/ec2.spec.coffee
+++ test/services/ec2.spec.coffee
@@ -24,13 +24,13 @@
           'Action=CopySnapshot',
           'DestinationRegion=mock-region',
           'PresignedUrl=https%3A%2F%2Fec2.src-region.amazonaws.com%2F%3F' +
-            "Action%3DCopySnapshot%26DestinationRegion%3Dmock-region%26SourceRegion%3Dsrc-region" +
-            "%26SourceSnapshotId%3Dsnap-123456789%26Version%3D#{ec2.api.apiVersion}" +
-            "%26X-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Credential%3Dakid%252F19700101" +
-            "%252Fsrc-region%252Fec2%252Faws4_request%26X-Amz-Date%3D19700101T000000Z" +
-            "%26X-Amz-Expires%3D3600%26X-Amz-Security-Token%3Dsession" +
-            "%26X-Amz-Signature%3D37f45901157b4599bfcd4bf42b26f958d503f357aed899488a78f494825d34be" +
-            "%26X-Amz-SignedHeaders%3Dhost",
+          'Action%3DCopySnapshot%26DestinationRegion%3Dmock-region%26SourceRegion%3Dsrc-region' +
+          '%26SourceSnapshotId%3Dsnap-123456789%26Version%3D2016-04-01' +
+          '%26X-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Credential%3Dakid%252F19700101' +
+          '%252Fsrc-region%252Fec2%252Faws4_request%26X-Amz-Date%3D19700101T000000Z' +
+          '%26X-Amz-Expires%3D3600%26X-Amz-Security-Token%3Dsession' +
+          '%26X-Amz-Signature%3D49bbd1016e6e7dd7eafd12ff8773ef56c4f63bbdc8edb767452eaa6a144ef98c' +
+          '%26X-Amz-SignedHeaders%3Dhost',
           'SourceRegion=src-region',
           'SourceSnapshotId=snap-123456789'
         ].forEach (i) ->


Updated test-html5-writer

Updated test-html5-writer

--- test/test-html5-writer.coffee
+++ test/test-html5-writer.coffee
@@ -6,6 +6,8 @@
 { CohtmlToHtml5Converter } = require './../src/cohtml-to-html5-converter'

 { Html5Writer } = require './../src/html5-writer'
+
+{ Seetoken } =  require 'seetoken'

 fs = require 'fs'

@@ -23,7 +25,14 @@

     html5Scope = converter.convert scope[0]

+    # console.log (require 'util').inspect html5Scope, {depth:50, colors: true}
+
     writer = new Html5Writer

-    console.log writer.write html5Scope
+    html5 = writer.write html5Scope

+    console.log html5
+
+    # console.log Seetoken.tokenize html5, { color: true }
+
+


add remove / DELETE test

add remove / DELETE test

--- test/src/mysql.coffee
+++ test/src/mysql.coffee
@@ -82,6 +82,8 @@
         }
       )

+    joe = null
+
     it 'should be a Pipelet', ->
       expect( mysql_users ).to.be.a RS.Pipelet

@@ -99,5 +101,15 @@
       mysql_users._fetch_all ( _users ) ->
         check done, () ->
           expect( _users.length ).to.be.eql 1
+
+          joe = _users[ 0 ]
+
           expect( _users ).to.be.eql users._fetch_all()
-          +
+    it 'should allow to remove previously added user', ( done ) ->
+      input._remove [ joe ]
+
+      mysql_users._fetch_all ( _users ) ->
+        check done, () ->
+          expect( _users.length ).to.be.eql 0
+


Modify json structure

Modify json structure

--- src/lib/commands/init-command.coffee
+++ src/lib/commands/init-command.coffee
@@ -233,8 +233,9 @@
         fs.mkdirSync path.dirname(result)

       setting = {}
-      setting.fileId = project.fileId
-      setting[env] = files
+      setting[env] =
+        fileId : project.fileId
+        files : files

       fs.writeFileSync result, JSON.stringify(setting, "", 2)


Add Sao Paulo database condition

Add Sao Paulo database condition

--- server/commons/database.coffee
+++ server/commons/database.coffee
@@ -23,6 +23,8 @@
 module.exports.generateMongoConnectionString = ->
   if not testing and config.tokyo
     address = config.mongo.mongoose_tokyo_replica_string
+  else if not testing and config.saoPaulo
+    address = config.mongo.mongoose_saoPaulo_replica_string
   else if not testing and config.mongo.mongoose_replica_string
     address = config.mongo.mongoose_replica_string
   else


better error handling in DataAdapter

better error handling in DataAdapter

--- server/lib/data_adapter.coffee
+++ server/lib/data_adapter.coffee
@@ -26,11 +26,12 @@
     requestApi = @apiDefaults(req)
     start = new Date().getTime()
     request requestApi, (err, response, body) =>
+      return callback(err) if err?
       end = new Date().getTime()
       debug('%s %s %s %sms', requestApi.method.toUpperCase(), requestApi.url, response.statusCode, end - start)
       debug('%s', inspect(response.headers))
       if options.convertErrorCode
-        err ||= @getErrForResponse(response, {allow4xx: options.allow4xx})
+        err = @getErrForResponse(response, {allow4xx: options.allow4xx})
       if ~(response.headers['content-type'] || '').indexOf('application/json')
         try
           body = JSON.parse(body)


minor

minor

--- server/lib/server/index.coffee
+++ server/lib/server/index.coffee
@@ -514,7 +514,6 @@
       if loggedIn
         # go to koding activity
         findUsernameFromSession req, res, (err, username)->
-          console.log "username:", username
           JName.fetchModels username, (err, models)->
             user = models.last
             user.fetchAccount "koding", (err, account)->


Routes: remove not necessary redirect route

--- smsgw/static/src/routes.coffee
+++ smsgw/static/src/routes.coffee
@@ -44,7 +44,6 @@
     <Route handler={Wrapper} path="/">
         <Redirect from="/" to="dashboard" />
         <Redirect from="directory" to="contacts" />
-        <Redirect from="application" to="application-overview" />

         <Route name="sign-in" path="sign/in" handler={SignIn} />
         <Route name="sign-up" path="sign/up" handler={SignUp} />


"gulp dev" shouldn't startup  karma testrunner

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -119,7 +119,8 @@
   server = new karma.Server(config)
   server.start()

-# same as TDD, not sure if anyone uses 'dev' ?
-gulp.task 'dev', ['tdd']
+# clean out the dist directory before running since otherwise stale files might be served from there.
+# The _webserver task builds and serves from memory with a fallback to files in dist
+gulp.task 'dev', ['_cleanDist', '_webserver']

 gulp.task 'tdd', ['_cleanDist', '_webserver', '_karma']


Use name instead of productName on Bintray

--- tasks/publish.coffee
+++ tasks/publish.coffee
@@ -40,8 +40,8 @@
       filePath = path.basename(srcPath)

       if dist == 'deb'
-        poolPath = 'pool/main/' + manifest.productName[0] + '/'
-        filePath = poolPath + manifest.productName + '/' + filePath
+        poolPath = 'pool/main/' + manifest.name[0] + '/'
+        filePath = poolPath + manifest.name + '/' + filePath

       opts =
         url: host + '/content/' + subject + '/' + dist + '/' + filePath
@@ -49,7 +49,7 @@
           user: subject
           pass: process.env.BINTRAY_API_KEY
         headers:
-          'X-Bintray-Package': manifest.productName
+          'X-Bintray-Package': manifest.name
           'X-Bintray-Version': manifest.version
           'X-Bintray-Publish': 1
           'X-Bintray-Debian-Distribution': 'stable'


Fix module stub in incorrect scope

Fix module stub in incorrect scope

--- services/web/test/unit/coffee/infrastructure/RateLimterTests.coffee
+++ services/web/test/unit/coffee/infrastructure/RateLimterTests.coffee
@@ -24,20 +24,15 @@
 		@RedisWrapper =
 			client: sinon.stub().returns(@rclient)

-		@limiterFn = sinon.stub()
-		@RollingRateLimiter = (opts) =>
-			return @limiterFn
-
-		@limiter = SandboxedModule.require modulePath, requires:
-			"rolling-rate-limiter": @RollingRateLimiter
-			"settings-sharelatex":@settings
-			"logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()}
-			"./RedisWrapper": @RedisWrapper
-
 		@endpointName = "compiles"
 		@subject = "some-project-id"
 		@timeInterval = 20
 		@throttleLimit = 5
+
+		@requires =
+			"settings-sharelatex":@settings
+			"logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()}
+			"./RedisWrapper": @RedisWrapper

 		@details =
 			endpointName: @endpointName
@@ -52,7 +47,9 @@
 	describe 'when action is permitted', ->

 		beforeEach ->
-			@limiterFn = sinon.stub().callsArgWith(1, null, 0, 22)
+			@requires["rolling-rate-limiter"] = (opts) =>
+				return sinon.stub().callsArgWith(1, null, 0, 22)
+			@limiter = SandboxedModule.require modulePath, requires: @requires

 		it 'should not produce and error', (done) ->
 			@limiter.addCount {}, (err, should) ->
@@ -67,7 +64,9 @@
 	describe 'when action is not permitted', ->

 		beforeEach ->
-			@limiterFn = sinon.stub().callsArgWith(1, null, 4000, 0)
+			@requires["rolling-rate-limiter"] = (opts) =>
+				return sinon.stub().callsArgWith(1, null, 4000, 0)
+			@limiter = SandboxedModule.require modulePath, requires: @requires

 		it 'should not produce and error', (done) ->
 			@limiter.addCount {}, (err, should) ->
@@ -82,7 +81,9 @@
 	describe 'when limiter produces an error', ->

 		beforeEach ->
-			@limiterFn = sinon.stub().callsArgWith(1, new Error('woops'))
+			@requires["rolling-rate-limiter"] = (opts) =>
+				return sinon.stub().callsArgWith(1, new Error('woops'))
+			@limiter = SandboxedModule.require modulePath, requires: @requires

 		it 'should produce and error', (done) ->
 			@limiter.addCount {}, (err, should) ->


exclude other likely URI schemes from file regex and update comment

--- parsers/file_parser.coffee
+++ parsers/file_parser.coffee
@@ -13,13 +13,13 @@
   failures: []

 RegExplorer =
-  # not beginning with http or mailto or a ? (? bc `marked` parses some regex in their code as links…)
+  # not beginning with any likely URI schemes or a ? (? bc `marked` parses some regex in their code as links…)
   # not beginning with "// (markdown shortcut link)
   # capture below
   #  any chars
   #  capture the anchor link (which is optional)
   #  capture the final double quote
-  filePath: /href="(?!\?|http|mailto|ftp)(?!\/\/)((?!#)(.*?)((#.*?)?)("))/
+  filePath: /href="(?!\?|http|mailto|ftp|sftp|git|smtp|file)(?!\/\/)((?!#)(.*?)((#.*?)?)("))/

 FileParser =
   imagePaths: []


don't combine realtime + final outputs

don't combine realtime + final outputs

--- js/inner/action_log.coffee
+++ js/inner/action_log.coffee
@@ -137,6 +137,8 @@
         type: "GET"
         success: (data) =>
           @retrieved_output(true)
+          ## reset the converter
+          @outputConverter = CI.terminal.ansiToHtmlConverter()
           @append_output(data)
         complete: (data, status) =>
           @retrieving_output(false)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/animated.coffee
+++ test/animated.coffee
@@ -463,7 +463,15 @@

   equals animation.I.activeAnimation.name, animation.I.activeAnimation.complete, "After the bite cycle we should end up in the next state specified by the animation"

-  50.times -> animation.update()
+test "should loop on looping animation states", ->
+  animation = GameObject
+    data: animationData
+    includedModules: ["Animated"]
+
+  animation.I.activeAnimation.frames.length.times ->
+    animation.update()
+
+  30.times -> animation.update()

   equals animation.I.activeAnimation.name, "Idle1", "The idle1 state loops, so after any number of updates we should still be there"

@@ -488,6 +496,7 @@

   animation.update()

+  equal animation.I.currentFrameIndex, 0
   ok whiteParticlesFired, "White particle effect fired"

   animation.update()


Actually use foreign relations as expected

--- api/main.coffee
+++ api/main.coffee
@@ -13,6 +13,7 @@

 sqlite = require 'sqlite3'
 db = new sqlite.Database settings.database
+db.exec "PRAGMA foreign_keys = ON;"

 app = express()
 server = http.createServer app


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/tweening.coffee
+++ test/tweening.coffee
@@ -4,7 +4,8 @@
   o = GameObject
     x: 0

-  o
+  o.tween 10,
+

   # Test for equality of two objects
   equals one, 1


Tell Finder to empty instead of rm -rf

Tell Finder to empty instead of rm -rf

--- Eva.widget/index.coffee
+++ Eva.widget/index.coffee
@@ -698,7 +698,7 @@
     $(domEl).on 'click', '.iTunesNext', => @run "osascript -e 'tell application \"iTunes\" to next track'"
     $(domEl).on 'click', '.iTunesPause', => @run "osascript -e 'tell application \"iTunes\" to pause'"
     $(domEl).on 'click', '.iTunesPlay', => @run "osascript -e 'tell application \"iTunes\" to play'"
-    $(domEl).on 'click', '#TrashCell', => @run "rm -rf ~/.Trash/* && rm -rf ~/.Trash/.*"
+    $(domEl).on 'click', '#TrashCell', => @run "osascript -e 'tell application \"Finder\" to empty'"
 #   Command to open up mounted volumes
     $(domEl).on 'click', '#66', => @run "ls /Volumes/ | awk -F'\t' '{ print $0}' > tmp.txt;i=1; cat tmp.txt | while read line; do if [ \"$i\" -eq 1 ]; then open /Volumes/\"${line}\"; fi; let i=i+1; done; rm tmp.txt
 "


add signature interpretation

add signature interpretation

--- src/renderer/components/signatures/doc-signatures-type-component.coffee
+++ src/renderer/components/signatures/doc-signatures-type-component.coffee
@@ -21,11 +21,12 @@
           # type.name
           #
           # TODO
+          # serach type declaration and reflection system
           #
           name = ''
           if @props.type.declaration?.signatures?
             name = 'function'
-          else if @props.type.declaration?.children?
+          else if @props.type.declaration?.children? || @props.type.declaration?.indexSignature?
             name = 'object'
           elm.push {name}
         else


Removing `console.log` call

Removing `console.log` call

--- tests/www/run.coffee
+++ tests/www/run.coffee
@@ -26,5 +26,4 @@
         browser_conf.name = browser_name
         browser = if conf then wd.remote conf else do wd.remote
         test = (require file).test
-        console.log 'test', browser?, browser_conf?, base_url?, 10000, mark_as_passed?
         test browser, browser_conf, base_url, 10000, mark_as_passed


Wrapping top-level title inside brackets

--- tests/www/run.coffee
+++ tests/www/run.coffee
@@ -8,8 +8,8 @@
 exports.test = ( name, conf, base_url, mark_as_passed )->

   timeout = if name is 'local' then 3000 else 10000
-
-  describe name.toUpperCase(), ->
+  title =  "[#{do name.toUpperCase}]"
+  describe title, ->

     for file in files
       for browser_name, browser_conf of browsers


moving config so it loads earlier

moving config so it loads earlier

--- app/server/config/accounts.coffee
+++ app/server/config/accounts.coffee
@@ -1,14 +0,0 @@
-
-
-Accounts.onCreateUser (options, user) ->
-    if options.profile
-        user.profile = options.profile
-        if user.profile.name?
-            user.tag = createUserTag(user.profile.name)
-            console.log("New User", user.profile.name, user.tag)
-        else
-            console.log("No profile.name for new user", user)
-    else
-        console.log("No profile for new user", user)
-    user
-


style: remove newlines

style: remove newlines

--- app/routes/box.coffee
+++ app/routes/box.coffee
@@ -12,5 +12,3 @@
           ip: ip

         newBox.save()
-
-


fix: calculate offset of the collision products correctly

--- tools/collider.coffee
+++ tools/collider.coffee
@@ -236,8 +236,8 @@
     return collision

   console.log "#### colliding:"
-  console.log "#### 1)  #{normalizedRle p1} at x=0, y=0, t=0"
-  console.log "#### 2)  #{normalizedRle p2} at x=#{offset[0]}, y=#{offset[1]}, t=#{offset[2]}"
+  console.log "#### 1)  #{normalizedRle p1} at [0, 0, 0]"
+  console.log "#### 2)  #{normalizedRle p2} at {JSON.stringify offset}"
   console.log "####     collision at T=#{coll.time}  (give up at #{timeGiveUp})"
   collision.timeStart = coll.time
   collision.collision = true
@@ -275,8 +275,8 @@
 #simulate patern until it decomposes into several simple patterns
 # Return list of collision products
 finishCollision = (rule, field, time) ->
-  console.log "#### Collision RLE: #{normalizedRle field}"
-  growthLimit = field.length + 15 #when to try to separate pattern
+  #console.log "#### Collision RLE: #{normalizedRle field}"
+  growthLimit = field.length*3 + 8  #when to try to separate pattern

   while true
     field = evaluateCellList rule, field, mod2(time)
@@ -285,7 +285,7 @@
     size = Math.max (x1-x0), (y1-y0)
     #console.log "####    T:#{time}, s:#{size}, R:#{normalizedRle field}"
     if size > growthLimit
-      console.log "#### At time #{time}, pattern growed to #{size}: #{normalizedRle field}"
+      #console.log "#### At time #{time}, pattern grew to #{size}: #{normalizedRle field}"
       break
   #now try to split result into several patterns
   parts = separatePatternParts field
@@ -397,17 +397,21 @@
       [dx1, dy1, tfm] = Cells._find_normalizing_rotation dx, dy
       pattern1 = Cells.transform pattern, tfm, false #no need to normalize
       result = @_classifyNormalizedPattern pattern1, time, dx1, dy1, period
-      result.transform = tfm
+      result.transform = inverseTfm tfm
     else
       #it is not a spaceship - no way to find a normal rotation. Check all 4 rotations.
       for tfm in Cells._rotations
         pattern1 = Cells.transform pattern, tfm, false
         result = @_classifyNormalizedPattern pattern1, time, 0, 0, period
         if result.found
-          result.transform = tfm
+          result.transform = inverseTfm tfm
           break
     #reverse-transform position
-    [[result.x0, result.y0]] = Cells.transform [[result.x, result.y]], inverseTfm(result.transform)
+    [x,y,t] = result.pos
+    #console.log "#### Original pos: #{JSON.stringify result.pos}"
+    [x,y] = Cells.transformVector [x,y], result.transform, false #no normalize!
+    #console.log "#### after inverse rotate: #{JSON.stringify [x,y,t]}"
+    result.pos = [x,y,t]
     return result

   _classifyNormalizedPattern: (pattern, time, dx, dy, period) ->
@@ -422,9 +426,7 @@
       #console.log "####    T:#{t}, rle:#{rle}"
       if self.rle2pattern.hasOwnProperty rle
         #console.log "####      found! #{rle}"
-        result.x = dx
-        result.y = dy
-        result.t = t
+        result.pos = [dx, dy, t]
         result.rle = rle
         result.found=true
         return false


fix padding in mock engine

fix padding in mock engine

--- js/src/engine/MockKryptnosticEngine.coffee
+++ js/src/engine/MockKryptnosticEngine.coffee
@@ -5,9 +5,18 @@

   BinaryUtils = require 'kryptnostic.binary-utils'

+  SPACE = ' '
+
+  pad = (string) ->
+    if string.length % 2 is 0
+      return string
+    else
+      return string + SPACE
+
   #
   # Stand-in for the FHE engine which returns mocked values.
   # This class does not provide any security guarantees.
+  # Some of the static data is padded with whitespace to ensure divisibility by 2.
   #
   # Author: rbuckheit
   #
@@ -17,32 +26,32 @@
     # ===========

     getFhePrivateKey: ->
-      return BinaryUtils.stringToUint8('fhe.priv')
+      return BinaryUtils.stringToUint8(pad('fhe.priv'))

     getSearchPrivateKey: ->
-      return BinaryUtils.stringToUint8('search.pvt')
+      return BinaryUtils.stringToUint8(pad('search.pvt'))

     getClientHashFunction: ->
-      return BinaryUtils.stringToUint8('client.hashfun')
+      return BinaryUtils.stringToUint8(pad('client.hashfn'))

     # indexing
     # ========

     getObjectSearchKey: (id) ->
-      return BinaryUtils.stringToUint8('doc.search')
+      return BinaryUtils.stringToUint8(pad('doc.search'))

     getObjectAddressFunction: (id) ->
-      return BinaryUtils.stringToUint8('doc.addressfun')
+      return BinaryUtils.stringToUint8(pad('doc.addressfn'))

     getObjectConversionMatrix: (id) ->
-      return BinaryUtils.stringToUint8('doc.conversion')
+      return BinaryUtils.stringToUint8(pad('doc.conversion'))

     getObjectIndexPair: (id) ->
-      return BinaryUtils.stringToUint8('doc.index')
+      return BinaryUtils.stringToUint8(pad('doc.index'))

     # pair of docSearchKey, docAddressFunction
     getObjectSharingPair: (id) ->
-      return BinaryUtils.stringToUint8('doc.sharing')
+      return BinaryUtils.stringToUint8(pad('doc.sharing'))

     # search
     # ======


Setup redirect for Planet Hunters TESS (#5359)

--- app/router.cjsx
+++ app/router.cjsx
@@ -140,6 +140,7 @@
       <IndexRoute component={FilteredProjectsList} />
     </Route>

+    <Route path="/projects/nora-dot-eisner/planet-hunters-tess/classify" component={() => <RELOAD newUrl='https://fe-project.zooniverse.org/projects/nora-dot-eisner/planet-hunters-tess/classify' />} />
     <Route path="projects/:owner/:name" component={require('./pages/project').default}>
       <IndexRoute component={ProjectHomePage} />
       <Route path="home" component={ONE_UP_REDIRECT} />