truncate biography only when history/highlights are present

--- apps/artwork/components/artists/index.coffee
+++ apps/artwork/components/artists/index.coffee
@@ -1,7 +1,17 @@
+_ = require 'underscore'
 tabs = require '../../../../components/side_tabs/index.coffee'
 gradient = require '../../../../components/gradient_blurb/index.coffee'

 module.exports = ->
   tabs $('.js-artwork-artist-tabs')

-  gradient $('.artwork-artist__content__biography'), limit: 175
+  artistsWithTabs = _.filter(sd.CLIENT.artists, (artist) ->
+    return artist if artist.exhibition_highlights.length > 0 || artist.biography || artist.articles?.length
+  )
+
+  _.each(artistsWithTabs, (artist, i) ->
+    if artist.exhibition_highlights.length > 0
+      $artistBlock = $('.artwork-section.artwork-artist')[i]
+      $el = $($artistBlock).find('.artwork-artist__content__biography')
+      gradient($el, limit: 175) if $el.length
+  )


Update of string value

Update of string value

--- src/javascripts/contact_us.coffee
+++ src/javascripts/contact_us.coffee
@@ -16,7 +16,7 @@
         if($chosen.length)
           $chosen.show()
         else
-          $chooseDistrict.after('
Kontaktinformation för valt stadsområde saknas.
')
+          $chooseDistrict.after('
Kontaktinformation för valt område saknas.
')

         # Set district in select menu
         $selectDistrict.val district


unneeded comment

unneeded comment

--- src/runtime.coffee
+++ src/runtime.coffee
@@ -400,8 +400,6 @@
                   @push ret1+0
                 else
                   @push ret1
-              # XXX: Assuming ret2 is never a boolean; these native methods
-              #      can't have 2 return values (ret2 is here for long/doubles)
               @push ret2 unless ret2 is undefined
           @run_until_finished (->), done_cb, no_threads
         failure_fn = (e_cb) =>


runtime: Fsync when saving graph changes

--- src/runtime.coffee
+++ src/runtime.coffee
@@ -63,11 +63,15 @@
   fs = require 'fs'
   temppath = filepath + "msgflo-autosave-#{Date.now()}"
   json = JSON.stringify graph, null, 2
-  fs.writeFile temppath, json, (err) ->
+  fs.open temppath, 'w', (err, fd) ->
     return callback err if err
-    fs.rename temppath, filepath, (err) ->
-      fs.unlink temppath, (e) ->
-        return callback err
+    fs.write fd, json, (err) ->
+      return callback err if err
+      fs.fsync fd, (err) ->
+        return callback err if err
+        fs.rename temppath, filepath, (err) ->
+          fs.unlink temppath, (e) ->
+            return callback err

 class Runtime
   constructor: (@options) ->


deleted view.coffee

deleted view.coffee


add unhandled exception handling. Qiu

add unhandled exception handling. Qiu

--- src/viff.coffee
+++ src/viff.coffee
@@ -5,6 +5,8 @@
 webdriver = require 'selenium-webdriver'
 Comparison = require './comparison'

+webdriver.promise.controlFlow().on 'uncaughtException', (e) ->
+  console.error 'Unhandled error: ' + e

 class Viff
   constructor: (seleniumHost) ->


許可コマンド追加、削除、参照実装

許可コマンド追加、削除、参照実装

--- scripts/sh.coffee
+++ scripts/sh.coffee
@@ -21,10 +21,82 @@
   robot.respond /sh (.*)$/i, (msg) ->
     @exec = require('child_process').exec
     command = "#{msg.match[1]}"
-    msg.send command
-
     @exec command, (error, stdout, stderr) ->
       if error?
-        msg.send error
+        msg.send "#{command}\n#{error}"
       else
-        msg.send stdout
+        msg.send "#{command}\n#{stdout}"
+
+  #
+  # コマンド追加
+  #
+  robot.respond /sh_add (.*)$/i, (msg) ->
+    @exec = require('child_process').exec
+    command = msg.match[1]
+    cmds = msg.match[1].split(" ")
+
+    @exec "which #{command}", (error, stdout, stderr) ->
+
+      # コマンドの最大文字列長を取得
+      maxlen = 0
+      for cmd in cmds
+        maxlen = Math.max(cmd.length, maxlen)
+
+      # 結果文字列生成
+      result = ""
+      lines  = stdout.split("\n")
+
+      # 設定値を取得
+      robot.brain.data.sh = {} if !robot.brain.data.sh
+      enabled = if robot.brain.data.sh["enabled"]? then robot.brain.data.sh["enabled"] else []
+
+      for cmd, i in cmds
+        if cmd.length == 0 or lines[i].length == 0 then continue
+
+        # 出力を整形
+        cmdname = "#{cmd + Array(maxlen).join ' '}".slice(0, maxlen)
+        if lines[i].split('/').pop() == cmd
+          if enabled.indexOf(cmd) == -1
+            result += "#{cmdname} - [ OK ]\n"
+            enabled.push(cmd)
+          else
+            result += "#{cmdname} - [ OK ] already setting.\n"
+        else
+          result += "#{cmdname} - [ NG ] not installed.\n"
+          i--
+
+      robot.brain.data.sh["enabled"] = enabled
+      robot.brain.save()
+
+      msg.send result
+
+  #
+  # コマンド削除
+  #
+  robot.respond /sh_remove (.*)$/i, (msg) ->
+    result = "remove :\n"
+    robot.brain.data.sh = {} if !robot.brain.data.sh
+    enabled = if robot.brain.data.sh["enabled"]? then robot.brain.data.sh["enabled"] else []
+
+    cmds = msg.match[1].split(" ")
+    for cmd in cmds
+      index = enabled.indexOf(cmd)
+      if index >= 0
+        enabled.splice(index, 1)
+        result += "  #{cmd}\n"
+
+    msg.send result
+
+  #
+  # コマンドリスト
+  #
+  robot.respond /sh_list$/i, (msg) ->
+    result = ""
+    robot.brain.data.sh = {} if !robot.brain.data.sh
+    enabled = if robot.brain.data.sh["enabled"]? then robot.brain.data.sh["enabled"] else []
+    for cmd in enabled
+      result += "#{cmd}\n"
+
+    if result.length == 0
+      result = "ぷーん"
+    msg.send result


[logger] intercept stdout and stderr in startup script and pipe them to log files

--- src/startup.coffee
+++ src/startup.coffee
@@ -17,32 +17,45 @@

 {spawn} = require 'child_process'
 Path    = require 'path'
+Fs      = require 'fs'
 Config  = require './common/config'

 processes = []

 if Config.Voicious.Enabled
-    voicious = spawn 'node', [(Path.join Config.Paths.Approot, 'lib', 'core', 'voicious.js')]
+    voiciousAccessLog = Fs.openSync (Path.join Config.Paths.Logs, 'voicious.access.log'), 'w'
+    voiciousErrorLog  = Fs.openSync (Path.join Config.Paths.Logs, 'voicious.error.log'), 'w'
+    voicious          = spawn 'node', [(Path.join Config.Paths.Approot, 'lib', 'core', 'voicious.js')]
     voicious.stdout.on 'data', (data) =>
         process.stdout.write "#{data}"
+        Fs.writeSync voiciousAccessLog, data, 0, data.length
     voicious.stderr.on 'data', (data) =>
         process.stderr.write "#{data}"
+        Fs.writeSync voiciousErrorLog, data, 0, data.length
     processes.push voicious

 if Config.Restapi.Enabled
-    rest     = spawn 'node', [(Path.join Config.Paths.Approot, 'lib', 'rest', 'api.js')]
+    restAccessLog = Fs.openSync (Path.join Config.Paths.Logs, 'rest.access.log'), 'w'
+    restErrorLog  = Fs.openSync (Path.join Config.Paths.Logs, 'rest.error.log'), 'w'
+    rest          = spawn 'node', [(Path.join Config.Paths.Approot, 'lib', 'rest', 'api.js')]
     rest.stdout.on 'data', (data) =>
         process.stdout.write "#{data}"
+        Fs.writeSync restAccessLog, data, 0, data.length
     rest.stderr.on 'data', (data) =>
         process.stderr.write "#{data}"
+        Fs.writeSync restErrorLog, data, 0, data.length
     processes.push voicious

 if Config.Websocket.Enabled
-    voicious = spawn 'node', [(Path.join Config.Paths.Approot, 'lib', 'ws', 'websocket.js')]
+    wsAccessLog = Fs.openSync (Path.join Config.Paths.Logs, 'ws.access.log'), 'w'
+    wsErrorLog  = Fs.openSync (Path.join Config.Paths.Logs, 'ws.error.log'), 'w'
+    voicious    = spawn 'node', [(Path.join Config.Paths.Approot, 'lib', 'ws', 'websocket.js')]
     voicious.stdout.on 'data', (data) =>
         process.stdout.write "#{data}"
+        Fs.writeSync wsAccessLog, data, 0, data.length
     voicious.stderr.on 'data', (data) =>
         process.stderr.write "#{data}"
+        Fs.writeSync wsErrorLog, data, 0, data.length
     processes.push voicious

 process.on 'SIGINT', () =>


can edit other programs

can edit other programs

--- src/layout/mixins/programs.coffee
+++ src/layout/mixins/programs.coffee
@@ -100,7 +100,7 @@

   program_row = (name, contents) ->
     program = make.columns name, [
-      { _LABEL: 'program_name', name: name }
+      { _LABEL: 'program_name', name: name, click: -> sprite.put 'editing', name }
        make.buttons "action", contents, my.command, ((button, args) ->
         # TODO: add some kind of confirmation
         button.up.remove_child(button)


improve user text for safety fails; protect RFC1918 CIDRs

--- src/uapanfw.coffee
+++ src/uapanfw.coffee
@@ -120,8 +120,8 @@
     entry.val = extra[1]

     # safety check !!
-    if entry.val.match /^(?:137\.229\.|199\.165\.)/
-      usermsg = "Blocking CIDRs begining with 137.229. or 199.165. is not allowed. #{safety_fail_note}"
+    if entry.val.match /^(?:137\.229\.|199\.165\.|10\.|192\.168\.|172\.[123])/
+      usermsg = "Blocking UA CIDRs is not allowed. #{safety_fail_note}"
       logmsg = "#{modulename}: Request by #{who} failed safety check: #{fullcmd}"
       robot.logger.info logmsg
       notifySubscribers "#{logmsg}\nReason: #{usermsg}"
@@ -134,7 +134,7 @@

     # safety check !!
     if entry.val.toLowerCase().match /(?:alaska|uaf)\.edu$/
-      usermsg = "Blocking alaska.edu or uaf.edu is not allowed. #{safety_fail_note}"
+      usermsg = "Blocking UA domains is not allowed. #{safety_fail_note}"
       logmsg = "#{modulename}: Request by #{who} failed safety check: #{fullcmd}"
       robot.logger.info logmsg
       notifySubscribers "#{logmsg}\nReason: #{usermsg}"
@@ -152,7 +152,7 @@

     # safety check !!
     if entry.val.toLowerCase().match /[^\/]+(?:alaska|uaf)\.edu/
-      usermsg = "Blocking alaska.edu or uaf.edu is not allowed. #{safety_fail_note}"
+      usermsg = "Blocking UA domains is not allowed. #{safety_fail_note}"
       logmsg = "#{modulename}: Request by #{who} failed safety check: #{fullcmd}"
       robot.logger.info logmsg
       notifySubscribers "#{logmsg}\nReason: #{usermsg}"


test field extraction for #256.1

test field extraction for #256.1

--- resource/translators/translator.coffee
+++ resource/translators/translator.coffee
@@ -43,6 +43,76 @@
 Translator._log = (level, msg...) ->
   msg = ((if (typeof m) in ['boolean', 'string', 'number'] then '' + m else Translator.stringify(m)) for m in msg).join(' ')
   Zotero.debug('[better' + '-' + "bibtex:#{@header.label}] " + msg, level)
+
+Translator.extractFields = (item) ->
+  return [] unless item.extra
+
+  fields = []
+
+  extra = []
+  for line in item.extra.split("\n")
+    m = /^\s*(LCCN|MR|Zbl|PMCID|PMID|arXiv|JSTOR|HDL|GoogleBooksID)\s*:\s*([\S]+)\s*$/i.exec(line)
+    name = m?[1].toLowerCase()
+    Translator.log("fixed field #{name} = #{m[2]}") if name
+    switch name
+      when 'mr'
+        fields.push({ name: 'mrnumber', value: m[2] })
+      when 'zbl'
+        fields.push({ name: 'zmnumber', value: m[2] })
+      when 'lccn', 'pmcid'
+        fields.push({ name: name, value: m[2] })
+      when 'pmid', 'arxiv', 'jstor', 'hdl'
+        if Translator.BetterBibLaTeX
+          fields.push({ name: 'eprinttype', value: m[1].toLowerCase() })
+          fields.push({ name: 'eprint', value: m[2] })
+        else
+          fields.push({ name: name, value: m[2] })
+      when 'googlebooksid'
+        if Translator.BetterBibLaTeX
+          fields.push({ name: 'eprinttype', value: 'googlebooks' })
+          fields.push({ name: 'eprint', value: m[2] })
+        else
+          fields.push({ name: 'googlebooks', value: m[2] })
+
+      else extra.push(line)
+  item.extra = extra.join("\n")
+
+  m = /(biblatexdata|bibtex|biblatex)\[([^\]]+)\]/.exec(item.extra)
+  if m
+    item.extra = item.extra.replace(m[0], '').trim()
+    for assignment in m[2].split(';')
+      data = assignment.match(/^([^=]+)=\s*(.*)/)
+      unless data
+        Zotero.debug("Not an assignment: #{assignment}")
+        continue
+
+      fields.push({ name: data[1], value: data[2] })
+
+  m = /(biblatexdata|bibtex|biblatex)({[\s\S]+})/.exec(item.extra)
+  if m
+    prefix = m[1]
+    data = m[2]
+    while data.indexOf('}') >= 0
+      try
+        json = JSON5.parse(data)
+      catch
+        json = null
+      break if json
+      data = data.replace(/[^}]*}$/, '')
+    if json
+      item.extra = item.extra.replace(prefix + data, '').trim()
+      for name, value of json
+        fields.push({name: name, value: value})
+
+  # fetch fields as per https://forums.zotero.org/discussion/3673/2/original-date-of-publication/
+  item.extra = item.extra.replace(/{:([^:]+):\s*([^}]+)}/g, (m, field, value) ->
+    switch field.toLowerCase()
+      when 'original-date'
+        fields.push({ name: 'origdate', value: value })
+    return ''
+  )
+
+  return fields

 Translator.initialize = ->
   return if @initialized
@@ -222,72 +292,8 @@

     @itemtype = Translator.typeMap.Zotero2BibTeX[@item.itemType] or 'misc'

-    if @item.extra
-      fields = []
-
-      extra = []
-      for line in @item.extra.split("\n")
-        m = /^\s*(LCCN|MR|Zbl|PMCID|PMID|arXiv|JSTOR|HDL|GoogleBooksID)\s*:\s*([\S]+)\s*$/i.exec(line)
-        name = m?[1].toLowerCase()
-        Translator.log("fixed field #{name} = #{m[2]}") if name
-        switch name
-          when 'mr'
-            fields.push({ name: 'mrnumber', value: m[2] })
-          when 'zbl'
-            fields.push({ name: 'zmnumber', value: m[2] })
-          when 'lccn', 'pmcid'
-            fields.push({ name: name, value: m[2] })
-          when 'pmid', 'arxiv', 'jstor', 'hdl'
-            if Translator.BetterBibLaTeX
-              fields.push({ name: 'eprinttype', value: m[1].toLowerCase() })
-              fields.push({ name: 'eprint', value: m[2] })
-            else
-              fields.push({ name: name, value: m[2] })
-          when 'googlebooksid'
-            if Translator.BetterBibLaTeX
-              fields.push({ name: 'eprinttype', value: 'googlebooks' })
-              fields.push({ name: 'eprint', value: m[2] })
-            else
-              fields.push({ name: 'googlebooks', value: m[2] })
-
-          else extra.push(line)
-      @item.extra = extra.join("\n")
-
-      m = /(biblatexdata|bibtex|biblatex)\[([^\]]+)\]/.exec(@item.extra)
-      if m
-        @item.extra = @item.extra.replace(m[0], '').trim()
-        for assignment in m[2].split(';')
-          data = assignment.match(/^([^=]+)=\s*(.*)/)
-          unless data
-            Zotero.debug("Not an assignment: #{assignment}")
-            continue
-
-          fields.push({ name: data[1], value: data[2] })
-
-      m = /(biblatexdata|bibtex|biblatex)({[\s\S]+})/.exec(@item.extra)
-      if m
-        prefix = m[1]
-        data = m[2]
-        while data.indexOf('}') >= 0
-          try
-            json = JSON5.parse(data)
-          catch
-            json = null
-          break if json
-          data = data.replace(/[^}]*}$/, '')
-        if json
-          @item.extra = @item.extra.replace(prefix + data, '').trim()
-          for name, value of json
-            fields.push({name: name, value: value})
-
-      # fetch fields as per https://forums.zotero.org/discussion/3673/2/original-date-of-publication/
-      @item.extra = @item.extra.replace(/{:([^:]+):\s*([^}]+)}/g, (m, field, value) ->
-        switch field.toLowerCase()
-          when 'original-date'
-            fields.push({ name: 'origdate', value: value })
-        return ''
-      )
-
+    fields = Translator.extractFields(@item)
+    if fields.length != 0
       for field in fields
         if field.name == 'referencetype'
           @itemtype = field.value


Fix overwritting .run() in the data explorer

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -3407,6 +3407,8 @@
                 is_at_bottom: 'true'

     class @DriverHandler
+        query_error_template: Handlebars.templates['dataexplorer-query_error-template']
+
         # I don't want that thing in window
         constructor: (args) ->
             @container = args.container
@@ -3435,7 +3437,7 @@
                 that = @
                 TermBase.prototype.private_run = TermBase.prototype.run
                 TermBase.prototype.run = ->
-                    throw that.container.query_error_template
+                    throw that.query_error_template
                         found_run: true

         connect: =>


Add extra suggestions in the data explorer

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -21,7 +21,6 @@
         focus_on_codemirror: true
         last_query_has_profile: true
         #saved_data: {}
-

     class @Container extends Backbone.View
@@ -514,6 +513,25 @@
             @keep_suggestions_on_blur = false

             @render()
+
+            @databases_available = {}
+            @fetch_data()
+            @interval = setInterval @fetch_data, 5000
+
+        fetch_data: =>
+            query = r.db(system_db).table('table_config')
+                .pluck('db', 'name')
+                .group('db')
+                .ungroup()
+                .map((group) -> [group("group"), group("reduction")("name").orderBy( (x) -> x )])
+                .coerceTo "OBJECT"
+            driver.run query, (error, result) =>
+                if error?
+                    # Nothing bad, we'll try again, let's just log the error
+                    console.log "Error: Could not fetch databases and tables"
+                    console.log error
+                else
+                    @databases_available = result

         handle_mousemove: (event) =>
             @results_view.handle_mousemove event
@@ -2300,11 +2318,11 @@
         extend_description: (fn) =>
             if fn is 'db(' or fn is 'dbDrop('
                 description = _.extend {}, @descriptions[fn]()
-                if databases.length is 0
+                if _.keys(@databases_available).length is 0
                     data =
                         no_database: true
                 else
-                    databases_available = databases.models.map (database) -> return database.get('name')
+                    databases_available = _.keys @databases_available
                     data =
                         no_database: false
                         databases_available: databases_available
@@ -2315,13 +2333,9 @@
                 database_used = @extract_database_used()
                 description = _.extend {}, @descriptions[fn]()
                 if database_used.error is false
-                    namespaces_available = []
-                    for namespace in namespaces.models
-                        if database_used.db_found is false or namespace.get('database') is database_used.id
-                            namespaces_available.push namespace.get('name')
                     data =
-                        namespaces_available: namespaces_available
-                        no_namespace: namespaces_available.length is 0
+                        namespaces_available: @databases_available[database_used.name]
+                        no_namespace: @databases_available[database_used.name].length is 0

                     if database_used.name?
                         data.database_name = database_used.name
@@ -2331,7 +2345,7 @@

                 description.description = @namespaces_suggestions_template(data) + description.description

-                @extra_suggestions= namespaces_available
+                @extra_suggestions = @databases_available[database_used.name]
             else
                 description = @descriptions[fn] @grouped_data
                 @extra_suggestions= null
@@ -2344,15 +2358,11 @@
             # We cannot have ".db(" in a db name
             last_db_position = query_before_cursor.lastIndexOf('.db(')
             if last_db_position is -1
-                for database in databases.models
-                    if database.get('name') is 'test'
-                        database_test_id = database.get('id')
-                        break
-                if database_test_id?
+                found = false
+                if @databases_available['test']?
                     return {
                         db_found: true
                         error: false
-                        id: database_test_id
                         name: 'test'
                     }
                 else
@@ -2370,18 +2380,17 @@
                         error: true
                     }
                 db_name = arg.slice 0, end_arg_position
-                for database in databases.models
-                    if database.get('name') is db_name
-                        return {
-                            db_found: true
-                            error: false
-                            id: database.get('id')
-                            name: db_name
-                        }
-                return {
-                    db_found: false
-                    error: true
-                }
+                if @databases_available[db_name]?
+                    return {
+                        db_found: true
+                        error: false
+                        name: db_name
+                    }
+                else
+                    return {
+                        db_found: false
+                        error: true
+                    }

         # Function triggered when the user click on 'more results'
         show_more_results: (event) =>
@@ -2825,10 +2834,10 @@
             @$('.option_icon').removeClass 'fullscreen'
             @$('.option_icon').addClass 'fullscreen_exit'

-        destroy: =>
-            @results_view.destroy()
-            @history_view.destroy()
-            @driver_handler.destroy()
+        remove: =>
+            @results_view.remove()
+            @history_view.remove()
+            @driver_handler.remove()

             @display_normal()
             $(window).off 'resize', @display_full
@@ -2836,7 +2845,9 @@
             $(document).unbind 'mouseup', @handle_mouseup

             clearTimeout @timeout_driver_connect
+            clearInterval @interval
             # We do not destroy the cursor, because the user might come back and use it.
+            super()

     class @SharedResultView extends Backbone.View
         template_json_tree:
@@ -3741,9 +3752,10 @@

-        destroy: =>
+        remove: =>
             $(window).unbind 'scroll'
             $(window).unbind 'resize'
+            super()

     class @OptionsView extends Backbone.View
         dataexplorer_options_template: Handlebars.templates['dataexplorer-options-template']
@@ -3920,5 +3932,5 @@
                         driver.close connection
             )(id_execution)

-        destroy: =>
+        remove: =>
             @close_connection()


Change the behavior or ctrl+space. Related to #647 and #584

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -807,7 +807,12 @@

             # Look for special commands
             if event?.which?
-                if event.which is 27 # ESC
+                if event.type isnt 'keydown' and ((event.ctrlKey is true or event.metaKey is true) and event.which is 32)
+                    # Because on event.type == 'keydown' we are going to change the state (hidden or displayed) of @$('.suggestion_description') and @.$('.suggestion_name_list'), we don't want to fire this event a second time
+                    return true
+
+                if event.which is 27 or (event.type is 'keydown' and ((event.ctrlKey is true or event.metaKey is true) and event.which is 32) and (@$('.suggestion_description').css('display') isnt 'none' or @.$('.suggestion_name_list').css('display') isnt 'none'))
+                    # We caugh ESC or (Ctrl/Cmd+space with suggestion/description being displayed)
                     event.preventDefault() # Keep focus on code mirror
                     @hide_suggestion_and_description()
                     query_before_cursor = @codemirror.getRange {line: 0, ch: 0}, @codemirror.getCursor()
@@ -877,7 +882,8 @@
                                 @codemirror.indentLine cursor.line+1, 'smart'
                                 @codemirror.setCursor cursor
                                 return false
-                else if event.which is 9 or ((event.ctrlKey is true or event.metaKey is true) and event.which is 32) # If the user hit tab or ctrl/cmd+space, we switch the highlighted suggestion
+                else if event.which is 9  or (event.type is 'keydown' and ((event.ctrlKey is true or event.metaKey is true) and event.which is 32) and (@$('.suggestion_description').css('display') is 'none' and @.$('.suggestion_name_list').css('display') is 'none'))
+                    # If the user hit tab  or (Ctrl/Cmd+space with suggestion/description being hidden) we switch the highlighted suggestion
                     event.preventDefault()
                     if event.type isnt 'keydown'
                         return false


Update complexity link

Update complexity link

--- server/materials/data/topics/complexity.coffee
+++ server/materials/data/topics/complexity.coffee
@@ -6,7 +6,7 @@
 export default complexity = () ->
     return {
         topic: topic("Сложность алгоритмов", null, [
-            label("Теория про сложность алгоритмов (читайте раздел «Простейшие основы», остальное пока не так важно)"),
+            label("Теория про сложность алгоритмов (читайте раздел «Простейшие основы», остальное пока не так важно)"),
         ], "complexity"),
         count: false
     }


Update Atom config

Update Atom config

--- home/.atom/config.cson
+++ home/.atom/config.cson
@@ -41,6 +41,8 @@
     'keybinding-resolver'
     'link'
     'timecop'
+    'feedback'
+    'editorconfig'
   ]
   'audioBeep': false
   'projectHome': '/Users/francesc/src'


Enable `backUpBeforeSaving` in .atom/config.cson.

--- home/.atom/config.cson
+++ home/.atom/config.cson
@@ -13,6 +13,7 @@
     showIndentGuide: true
     invisibles: {}
     scrollPastEnd: true
+    backUpBeforeSaving: true
   core:
     disabledPackages: [
       "metrics"


debug

debug

--- app/assets/javascripts/_utils.js.coffee
+++ app/assets/javascripts/_utils.js.coffee
@@ -183,4 +183,4 @@
     else
       location.assign("/packs/#{pack_name}/levels/#{level_name}")

-  window.test_all_levels()
+  #window.test_all_levels()


no more division by 0

no more division by 0

--- app/assets/javascripts/commit.js.coffee
+++ app/assets/javascripts/commit.js.coffee
@@ -292,14 +292,14 @@
       .transition()
       .delay((d, i) -> (i / Repo.formated_files.length * settings.duration) )
       .attr
-        x: (f) -> scale(f[3])*f[1]/f[3]
-        width: (f, i) -> scale(f[3])*f[2]/f[3]
+        x: (f) -> if f[3] is 0 then 0 else scale(f[3])*f[1]/f[3]
+        width: (f, i) -> if f[3] is 0 then 0 else scale(f[3])*f[2]/f[3]

     Repo.chart.selectAll("rect.additions")
       .transition()
       .delay((d, i) -> (i / Repo.formated_files.length * settings.duration))
       .attr
-        width: (f, i) -> scale(f[3])*f[1]/f[3]
+        width: (f, i) -> if f[3] is 0 then 0 else scale(f[3])*f[1]/f[3]

     Repo.sort_files()


adds tooltips to timeline_chart

adds tooltips to timeline_chart

--- app/assets/javascripts/commit.js.coffee
+++ app/assets/javascripts/commit.js.coffee
@@ -35,20 +35,6 @@
   # t.x_axis = d3.svg.axis().scale(t.x).orient("bottom")
   # t.y_axis = d3.svg.axis().scale(t.y).orient("left")
   t.sort_timestamp_asc = (a,b) -> a.timestamp-b.timestamp
-  t.additions_area = d3.svg.area()
-    .x (d) ->
-      t.x d.pos
-    .y0(t.height)
-    .y1 (d) ->
-      t.y t.get_aggregated_additions(d)
-    # .interpolate("basis")
-  t.deletions_area = d3.svg.area()
-    .x (d) ->
-      t.x d.pos
-    .y0(t.height)
-    .y1 (d) ->
-      t.y t.get_aggregated_deletions(d) + t.get_aggregated_additions(d)
-    # .interpolate("basis")
   t.summed_additions_deletions = (d) ->
     t.get_aggregated_additions(d) + t.get_aggregated_deletions(d)
   t.svg = d3.select("#timeline")
@@ -80,37 +66,23 @@
       .attr
         d: t.area
         class: (d,i) -> if i is 0 then "additions" else "deletions"
-    # a.append("path")
-    #   .transition()
-    #   .attr
-    #     d: t.deletions_area
-    #     class: "deletions"
-    # a.append("path")
-    #   .transition()
-    #   .attr
-    #     d: t.additions_area
-    #     class: "additions"
-    # .exit().remove
+
     c = t.svg.selectAll(".point").data(filtered_commits)
     c.enter()
       .append("g")
       .attr("class", "point")
       .append("circle")
-      .transition()
-      .attr
-        r: 2
+      .attr
+        class: "deletions"
+        r: 5
         cx: (d) ->
           t.x d.pos
         cy: (d) ->
           t.y t.get_aggregated_deletions(d) + t.get_aggregated_additions(d)
+      .append("title")
+      .text (d) -> d.timestamp
     c.exit().remove()

-
-    # a.append("g")
-    #   .attr
-    #     class: "x axis"
-    #     transform: "translate(0," + t.height + ")"
-    #   .call t.x_axis
     # a.append("g")
     #   .attr
     #     class: "y axis"


sankey: better node tooltips

sankey: better node tooltips

--- app/assets/javascripts/d3/sankey.coffee
+++ app/assets/javascripts/d3/sankey.coffee
@@ -475,7 +475,10 @@

       # update the node label
       @nodes.data(@node_list.models, (d) -> d.get('id'))
-        .attr("data-tooltip", (d) => Metric.autoscale_value d.value(), 'PJ', 2)
+        .attr("data-tooltip", (d) =>
+          html = d.label()
+          html += '<br/>'
+          html += Metric.autoscale_value d.value(), 'PJ', 2)

       # move the rectangles
       @nodes.selectAll("rect")


refactored dataTables() logic within events.js.coffee

--- app/assets/javascripts/events.js.coffee
+++ app/assets/javascripts/events.js.coffee
@@ -1,7 +1,7 @@

 jQuery ->
   $("#event_title").focus();
-  table = $('#events').dataTable
+  $table = $('#events').dataTable
     "dom": '<"eventToolbar dataTables_filter">frtip',
     order: [[3, 'asc']],
     columnDefs: [
@@ -10,37 +10,31 @@
                   { orderData: 4, targets: 5 },
                   { visible: false, targets: [2,4,8] }
                 ]
-
+# sets checkbox elements within #events dataTable()
   $('div.eventToolbar').html('Current:<input type="checkbox" id="js-events-current-checkbox" name="event-filter">
   Past:<input type="checkbox" id="js-events-past-checkbox" name="event-filter">
   Templates:<input type="checkbox" id="js-events-template-checkbox" name="event-filter">')

+# logic handling checkbox filtering with dataTables()
   filterCheckboxes = (element, criteria) ->
-    $.fn.dataTable.ext.search.push((settings, data, dataIndex) ->
+    $.fn.dataTable.ext.search.push((settings, data) ->
       checkboxState = element.checked
-      itemStatus  = data[7]
       endTime = new Date(data[5])
       isTemplate = data[8]

-      if criteria.type is "current"
-        if (checkboxState is false) or (checkboxState is true and
-            endTime > criteria.dateTime)
-          return true
-        return false
-      else if criteria.type is "past"
-        if (checkboxState is false) or (checkboxState is true and
-            endTime < criteria.dateTime)
-          return true
-        return false
-      else if criteria.type is "template"
-        if (checkboxState is false) or (checkboxState is true and
-            isTemplate is "True")
-          return true
+      if criteria.type is "current" and (checkboxState is false or (checkboxState is true and endTime > criteria.dateTime))
+        return true
+      else if criteria.type is "past" and (checkboxState is false or (checkboxState is true and endTime < criteria.dateTime))
+        return true
+      else if criteria.type is "template" and (checkboxState is false or (checkboxState is true and isTemplate is "True"))
+        return true
+      else
         return false
       )
-    table.fnDraw()
+    $table.fnDraw()

-  $('#js-events-current-checkbox').click ->
+  # events handling checkboxes
+  $('#js-events-current-checkbox').change ->
     $("#js-events-past-checkbox").prop('checked', false)
     criteria = {
       type: "current",
@@ -48,7 +42,7 @@
     }
     filterCheckboxes(this, criteria)

-  $('#js-events-past-checkbox').click ->
+  $('#js-events-past-checkbox').change ->
     $("#js-events-current-checkbox").prop('checked', false)
     criteria = {
       type: "past",
@@ -56,10 +50,9 @@
     }
     filterCheckboxes(this, criteria)

-  $('#js-events-template-checkbox').click ->
+  $('#js-events-template-checkbox').change ->
     criteria = {
       type: "template",
-      status: ["Active", "Inactive"]
     }
     filterCheckboxes(this, criteria)


Remove unneeded tools from the toolbar

Remove unneeded tools from the toolbar

--- app/assets/javascripts/papers.js.coffee
+++ app/assets/javascripts/papers.js.coffee
@@ -14,14 +14,13 @@
         removePlugins: 'floatingspace,resize'
         sharedSpaces:
           top: 'ckeditor-toolbar'
-        toolbarGroups: [
-          { name: 'styles' }
-          { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] }
-          { name: 'clipboard',   groups: [ 'clipboard', 'undo' ] }
-          { name: 'editing',     groups: [ 'find', 'selection', 'spellchecker' ] }
-          { name: 'paragraph',   groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ] }
-          { name: 'links' }
-          { name: 'tools' }
+        toolbar: [
+          [ 'Styles', 'Format', 'FontSize' ]
+          [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ]
+          [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', 'Blockquote', 'CreateDiv']
+          [ 'PasteFromWord' ],
+          [ 'Link', 'Unlink', 'Anchor']
+          [ 'Find', 'Replace', '-', 'Scayt', '-', 'ShowBlocks' ]
         ]

   $('#save_button').on 'click', (e) ->


better min/max value intuition for non-strict charts

--- app/assets/javascripts/zvg/point.coffee
+++ app/assets/javascripts/zvg/point.coffee
@@ -58,15 +58,17 @@

-  min_value: (value) ->
+  min_value: (value, force = false) ->
     if value or value is 0
-      @_min_value = parseInt(value)
+      v = parseInt(value)
+      @_min_value = (if force then v else d3.min([(@_min_value or 0), v]))
       return @
     @_min_value

-  max_value: (value) ->
+  max_value: (value, force = false) ->
     if value
-      @_max_value = parseInt(value)
+      v = parseInt(value)
+      @_max_value = (if force then v else d3.max([@_max_value or 1, v]))
       return @
     @_max_value

@@ -106,7 +108,7 @@
   round_max_value: (max) ->
     rounded = d3.round(max, 0)
     rounded += 1 if max > rounded
-    @_max_value = rounded
+    @max_value(rounded)
     @

   render: ->
@@ -129,10 +131,12 @@
   y_padding: 30

   build_value_domain: ->
-    if @_strict_scale
-      keys = (key for key, _ of @_strict_scale)
-      @max_value(d3.max(keys))
-      @min_value(d3.min(keys))
+    keys = (key for key, _ of (@_strict_scale or @_key_scale or {}))
+    min = d3.min(keys) or 0
+    max = d3.max(keys) or 1
+
+    @max_value(max, @_strict_scale) # force if strict_scale is defined
+    @min_value(min, @_strict_scale) # force if strict_scale is defined

     @value_domain = d3.scale.linear()
       .domain([@min_value(), @max_value()]) # FIXME


added a little preamble to prevent people's running the specs from nuking their *real* try-jasmine local store.

--- public/spec/try-it-spec.coffee
+++ public/spec/try-it-spec.coffee
@@ -1,3 +1,13 @@
+ogSpecs=ogSrc=null
+beforeEach ->
+  ogSpecs=localStorage['specs'] if localStorage['specs']
+  ogSrc=localStorage['src'] if localStorage['src']
+
+afterEach ->
+  localStorage['specs'] = ogSpecs if ogSpecs
+  localStorage['src'] = ogSrc if ogSrc
+
+
 describe ".tryIt", ->
   $specRunner=null
   beforeEach ->


Catch server errors in edit issue form

Catch server errors in edit issue form

--- app/coffee/modules/issues/detail.coffee
+++ app/coffee/modules/issues/detail.coffee
@@ -147,14 +147,24 @@
             if not form.validate()
                 return

-            $tgrepo.save($scope.issue).then ->
+            onSuccess = ->
                 $confirm.notify("success")
                 $location.path("/project/#{$scope.project.slug}/issues/#{$scope.issue.ref}")

+            onError = ->
+                $confirm.notify("error")
+
+            $tgrepo.save($scope.issue).then(onSuccess, onError)
+
         $el.on "click", ".add-comment a.button-green", (event) ->
             event.preventDefault()
-            $tgrepo.save($scope.issue).then ->
+            onSuccess = ->
                 $ctrl.loadHistory()
+
+            onError = ->
+                $confirm.notify("error")
+
+            $tgrepo.save($scope.issue).then(onSuccess, onError)

         $el.on "focus", ".add-comment textarea", (event) ->
             $(this).addClass('active')


Fix height computation code

Fix height computation code

--- svgtiler.coffee
+++ svgtiler.coffee
@@ -236,7 +236,6 @@
         width = x
       y += rowHeight
     height = y
-    height = @data.length * @tileHeight
     svg.setAttributeNS null, 'viewBox', "0 0 #{width} #{height}"
     svg.setAttributeNS null, 'width', width
     svg.setAttributeNS null, 'height', height


Make function more generic

Make function more generic

--- utils.coffee
+++ utils.coffee
@@ -29,8 +29,8 @@
           tmpFile.unlink()
   rs.pipe ws

-runCmd = (cmd, tmpFile, cascade, callback) ->
-  exec "#{cmd} #{tmpFile.path} #{cascade}", (err, stdout, stderr) ->
+runCmd = (cmd, tmpFile, callback) ->
+  exec cmd, (err, stdout, stderr) ->
     if err
       callback err
       tmpFile.unlink()


more intelligent client bootloader

more intelligent client bootloader

--- library/membrane/duplex.coffee
+++ library/membrane/duplex.coffee
@@ -197,8 +197,14 @@
         failed = "failed to create Socket.IO connection"
         assert _.isFunction(@socket.emit), failed
         p = "an exception happend at the server provider"
+        c = "an error were raised during socket connection"
+        connecting = "attmpting the Socket.IO connection"
+        @socket.on "error", (e) -> console.error c, e
         @socket.on "exception", (e) -> console.error p, e
-        @socket.emit "screening", _.omit(@, "socket"), (ack) =>
+        @socket.on "connecting", -> console.log connecting
+        @socket.on "connect_failed", (e) -> console.error c, e
+        osc = (listener) => @socket.on "connect", listener
+        osc => @socket.emit "screening", _.omit(@, "socket"), =>
             assert @consumeProviders; @consumeProviders @socket
             open = "notified the service of an opened channel"
             confirm = => console.log open; @emit "booted"


[#66931774] remove redirect note on client for subspecies

--- app/assets/javascripts/species/controllers/taxon_concepts_controller.js.coffee
+++ app/assets/javascripts/species/controllers/taxon_concepts_controller.js.coffee
@@ -5,7 +5,7 @@
   contentObserver: ( ->
     meta = @get('content.meta')
     if meta != undefined &&  meta.total == 1
-      @openTaxonPage(meta.higher_taxa_headers[0].taxon_concept_ids[0], true)
+      @openTaxonPage(meta.higher_taxa_headers[0].taxon_concept_ids[0], false)
   ).observes("content.meta.didLoad")

   didContentLoad: ( ->


I did a thing

I did a thing

--- src-public/controllers/new_study_invite.coffee
+++ src-public/controllers/new_study_invite.coffee
@@ -8,7 +8,7 @@
   addDropDown classObj for classObj in classArray

 fillTimeDropDown = () ->
-  console.log('MOTHA FUCKA')
+  #console.log('MOTHA FUCKA')
   i = 1
   while i <= 12
     newOption = document.createElement("option")
@@ -33,7 +33,7 @@
 validateYear = () ->

-app.controller 'NewReqCtrl', ($scope, $auth, $alert) ->
+app.controller 'StudyInvCtrl', ($scope, $auth, $alert) ->
   fillTimeDropDown()
   Parse.User.become($auth.getToken())
   fillClassDropDown()


remove log

remove log

--- SingularityUI/app/views/requests.coffee
+++ SingularityUI/app/views/requests.coffee
@@ -88,7 +88,6 @@
                 filter

         # Filter by deploy type if applicable
-        console.log _.filter(requests, (r) => !r.requestDeployState?.activeDeploy)
         if @state in ['activeDeploy', 'noDeploy']
             requests = _.filter requests, (request) =>
                 if @state == 'activeDeploy'


change job update script

change job update script

--- server/job.coffee
+++ server/job.coffee
@@ -180,7 +180,11 @@

 if job_job.search('NOT count:*').hits.total isnt 0
   console.log 'Adding count to jobs'
-  job_job.each 'NOT count:*', {size:50}, (j) -> job_job.update j._id, {count: j.processes.length}
+  counter = 0
+  job_job.each 'NOT count:*', {size:50}, (j) ->
+    counter += 1
+    console.log counter
+    job_job.update j._id, {count: j.processes.length}
 else
   console.log  'All jobs have count'


CSS image svg in data uri fixed

CSS image svg in data uri fixed


Adjust defaults for tournament end parameters


payment: modify isUpgrade logic

payment: modify isUpgrade logic

--- client/Main/payment/workflow.coffee
+++ client/Main/payment/workflow.coffee
@@ -44,7 +44,13 @@
       PaymentWorkflow.planTitle.PROFESSIONAL
     ]

-    (arr.indexOf selected) > (arr.indexOf current)
+    current  = arr.indexOf current
+    selected = arr.indexOf selected
+
+    return switch
+      when selected >  current then PaymentWorkflow.operation.UPGRADE
+      when selected is current then PaymentWorkflow.operation.INTERVAL_CHANGE
+      when selected <  current then PaymentWorkflow.operation.DOWNGRADE

   getInitialState: -> {
@@ -166,6 +172,7 @@
       options.email = email
       options.provider = @state.provider

+      console.log {token, planTitle, planInterval, options}

       paymentController.subscribe token, planTitle, planInterval, options, (err, result) =>
         @modal.form.submitButton.hideLoader()


terminal: handle `keypress` event of outputbox

Stop the event if it is not targeted to "offscreen" key input handler element.

--- client/Terminal/src/Terminal.coffee
+++ client/Terminal/src/Terminal.coffee
@@ -69,6 +69,10 @@
           @setKeyFocus()  if String.fromCharCode(event.which) is "X"
         else
           @setKeyFocus()
+    , yes
+
+    outputboxElement.addEventListener "keypress", (event) =>
+      KD.utils.stopDOMEvent event  if event.target isnt @keyInput.getElement()
     , yes

     @outputbox.on "keydown", (event) =>


terminal: handle `keydown` event of outputbox element in capture phase

Set key focus to terminal if there is a selected range and ctrl/meta key
is not pressed.

--- client/Terminal/src/Terminal.coffee
+++ client/Terminal/src/Terminal.coffee
@@ -61,6 +61,15 @@
     @outputbox.append @measurebox.getDomElement()
     @outputbox.append @keyInput.getDomElement()
     @container.append @outputbox
+
+    outputboxElement.addEventListener "keydown", (event) =>
+      range = KD.utils.getSelectionRange()
+      if range.startOffset isnt range.endOffset
+        if event.ctrlKey or event.metaKey
+          @setKeyFocus()  if String.fromCharCode(event.which) is "X"
+        else
+          @setKeyFocus()
+    , yes

     @container.on "mousedown mousemove mouseup mousewheel contextmenu", (event) =>
       @inputHandler.mouseEvent event


terminal: append offscreen key input to to body element to force scrolling to bottom on input focus

--- client/Terminal/src/Terminal.coffee
+++ client/Terminal/src/Terminal.coffee
@@ -30,6 +30,7 @@
       keydown   : @bound 'keyDown'
       keypress  : @bound 'keyPress'
       keyup     : @bound 'keyUp'
+    @keyInput.appendToDomBody()

     containerView.on 'KDObjectWillBeDestroyed', @keyInput.bound 'destroy'

@@ -57,7 +58,6 @@
     @outputbox.attr "spellcheck", off
     @outputbox.css "cursor", "text"
     @outputbox.append @measurebox.getDomElement()
-    @outputbox.append @keyInput.getDomElement()
     @container.append @outputbox

     outputboxElement.addEventListener "keydown", (event) =>


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/camera.coffee
+++ test/camera.coffee
@@ -20,6 +20,8 @@
   object.bind 'overlay', ->
     ok true

+
+
   camera = Camera()

   camera.trigger 'overlay', canvas, [object]


Update Client tests.

Update Client tests.

--- test/client.coffee
+++ test/client.coffee
@@ -1,9 +1,9 @@
-NodeClient = require '../lib/client/node'
+Client = require '../lib/client'

 describe 'Client', ->
   it 'should instantiate', ->
-    client = new NodeClient 'fakekey'
+    client = new Client 'fakekey'

   it 'should use default endpoint', ->
-    client = new NodeClient 'fakekey'
+    client = new Client 'fakekey'
     client.endpoint.should.eq 'https://api.crowdstart.com'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/follow.coffee
+++ test/follow.coffee
@@ -16,7 +16,7 @@
   enemy.include(Follow)
   enemy.follow(player)

-  ok enemy.I.direction.equal(Point(1, 0)), 'enemy should head toward player with velocity Point(1, 0)'
+  ok enemy.I.direction.equal(Point(1, 0)), 'enemy should head toward player with a direction Point(1, 0)'

   rightEnemy = GameObject
     x: 100
@@ -27,6 +27,6 @@
   rightEnemy.include(Follow)
   rightEnemy.follow(player)

-  ok rightEnemy.I.direct.equal(Point(-1, 0)), 'rightEnemy should head toward player with velocity Point(-1, 0)'
+  ok rightEnemy.I.direction.equal(Point(-1, 0)), 'rightEnemy should head toward player with a direction Point(-1, 0)'

 module()


Follow mixin

Follow mixin

--- test/follow.coffee
+++ test/follow.coffee
@@ -31,6 +31,7 @@
   ok fastEnemy.I.velocity.equal(Point(0, -5)), 'fastEnemy should head toward player with velocity Point(0, 5)'

   rightEnemy = GameObject
-    x:
+    x: 100
+    y

 module()


more tests

more tests

--- test/hinoki.coffee
+++ test/hinoki.coffee
@@ -34,7 +34,16 @@

     'inject':

-        'single factory': (test) ->
+        'missing factory': (test) ->
+            container = {}
+
+            block = ->
+                hinoki.inject container, (a) ->
+
+            test.throws block, Error, "missing factory for service 'a'"
+            test.done()
+
+        'one factory': (test) ->
             container =
                 factories:
                     a: -> 5
@@ -43,7 +52,7 @@
                 test.equals a, 5
                 test.done()

-        'no factory but in scope': (test) ->
+        'one seed': (test) ->
             container =
                 scope:
                     a: 5
@@ -51,15 +60,6 @@
             hinoki.inject container, (a) ->
                 test.equals a, 5
                 test.done()
-
-        'missing factory': (test) ->
-            container = {}
-
-            block = ->
-                hinoki.inject container, (a) ->
-
-            test.throws block, Error, "missing factory for service 'a'"
-            test.done()

         'three dependencies': (test) ->
             container =
@@ -73,3 +73,17 @@
                 test.equals b, 2
                 test.equals c, 4
                 test.done()
+
+        'one factory and two seeds': (test) ->
+            container =
+                scope:
+                    a: 1
+                    b: 2
+                factories:
+                    c: (a, b) -> a + b + 1
+
+            hinoki.inject container, (a, b, c) ->
+                test.equals a, 1
+                test.equals b, 2
+                test.equals c, 4
+                test.done()


Point#floor

Point#floor

--- test/matrix.coffee
+++ test/matrix.coffee
@@ -130,7 +130,7 @@
     m2 = Matrix().concat(Matrix.translation(tx, ty)).concat(Matrix.scale(s)).concat(Matrix.rotation(theta))

     matrixEqual(m1, m2)
-
+
   module "Point"

   test "#add", ->
@@ -159,19 +159,24 @@

     equals result.x, p1.x * scalar
     equals result.y, p1.y * scalar
-
+
+  test "#floor", ->
+    p1 = Point(7.2, 6.9)
+
+    ok Point(7, 6).equal(p1.floor())
+
   test "#equal", ->
     ok Point(7, 8).equal(Point(7, 8))

   test "#magnitude", ->
     equals Point(3, 4).magnitude(), 5
-
+
   test ".fromAngle", ->
     p = Point.fromAngle(Math.TAU / 4)
-
+
     equalEnough p.x, 0, TOLERANCE
     equals p.y, 1
-
+
   module undefined
 )()


Test conversion to UTC.

Test conversion to UTC.

--- test/offset.coffee
+++ test/offset.coffee
@@ -5,7 +5,7 @@

 tz.timezones require "./data/northamerica"

-utc = (splat...) -> new Date(Date.UTC.apply(null, splat))
+utc = (splat...) -> Date.UTC.apply(null, splat)

 bicentenial = utc(1976, 6, 4)
 moonwalk = utc(1969, 6, 21, 02, 36)
@@ -25,8 +25,13 @@
     done 1

   "test: convert from UTC to America/Detroit during DST": (done) ->
-    try
-      @equal tz(utc(1976, 6, 4), "America/Detroit", "%F %T"), "1976-07-03 20:00:00"
-    catch e
-      console.log e.stack
+    @equal tz(utc(1976, 6, 4), "America/Detroit", "%F %T"), "1976-07-03 20:00:00"
     done 1
+
+  "test: convert from America/Detroit to UTC during DST": (done) ->
+    @equal tz("2010-03-14T03:00:00", "America/Detroit"), utc(2010, 02, 14, 7)
+    done 1
+
+  "test: convert from America/Detroit to UTC": (done) ->
+    @equal tz("2010-03-14T01:00:00", "America/Detroit"), utc(2010, 02, 14, 6)
+    done 1


Use random.pick

Use random.pick

--- test/random.coffee
+++ test/random.coffee
@@ -10,7 +10,8 @@
 #
 # Pick random array element
 #
-random.pick = (array)->
+random.pick =
+pick = (array)->
   array[random array.length-1]

 #
@@ -47,16 +48,12 @@
 #
 bits = ranges [max: 0, 7, 8, 11, min: 0xD800, false, max: 0xDFFF, 16, max: 0x10FFFF]

-bits = ranges (5*i+1 for i in [5..6]).concat [32]
-bits = ranges [min: 0, 7].concat(5*i+1 for i in [2..6])
-bits = ranges [7].concat(5*i+1 for i in [2..6]).concat [32]
-
 #
 # Generate random UTF-8 string
 #
 random.utf8 = utf8 = (n = 16)->
   z = for i in [1..n]
-    z = bits[random 0, bits.length - 1]
+    z = random.pick bits
     random z.min, z.max
   String.fromCharCode.apply String, z


add timeout setting

add timeout setting

--- test/seeder.coffee
+++ test/seeder.coffee
@@ -1,6 +1,8 @@
 describe 'seeder', ->
   context 'when 2 lines of csv file', ->
     beforeEach ->
+      @timeout 10000
+
       knex('users').del().then ->
         Promise.all [
           knex('users').insert id: 1, name: 'foo'
@@ -8,6 +10,8 @@
         ]

     it 'import the seed', ->
+      @timeout 10000
+
       @seeder = seeder(table: 'users', file: __dirname + '/fixtures/users_utf8.csv', encoding: 'utf8')
       @seeder(knex, Promise).then (res) ->
         deletedCount = res.shift()
@@ -18,6 +22,8 @@

   context 'when 2 lines of invalid csv file', ->
     beforeEach ->
+      @timeout 10000
+
       knex('users').del().then ->
         Promise.all [
           knex('users').insert id: 1, name: 'foo'
@@ -25,6 +31,8 @@
         ]

     it 'import the seed', ->
+      @timeout 10000
+
       @seeder = seeder(table: 'users', file: __dirname + '/fixtures/invalid_users_utf8.csv', encoding: 'utf8')
       @seeder(knex, Promise).then (res) ->
         throw new Error('succeeded') # name column is not null


Fixed bug where empty file "" uri was added to the history

--- lib/atom-history-ctrl.coffee
+++ lib/atom-history-ctrl.coffee
@@ -18,21 +18,24 @@
     return if template.length > 0 then template[0] else null

   append: (event) ->
-    uri = event.uri
-    index = @files.indexOf(uri)
+    uri = event.uri.trim()

-    if index != -1
-      @files.splice index, 1
+    if uri
+      index = @files.indexOf(uri)

-    @files.unshift uri
+      if index != -1
+        @files.splice index, 1

-    if @files.length > @max_length then @files.splice(@max_length)
+      @files.unshift uri

-    setTimeout (->
-      localStorage[@storageKey] = JSON.stringify(@files)
-    ).bind(this), 0
+      if @files.length > @max_length
+        @files.splice(@max_length)

-    @update()
+      setTimeout (->
+        localStorage[@storageKey] = JSON.stringify(@files)
+      ).bind(this), 0
+
+      @update()

   update: () ->
     template = @getTemplate()


Fixed route.

Fixed route.

--- app/assets/javascripts/app/controllers/reset_password.js.coffee
+++ app/assets/javascripts/app/controllers/reset_password.js.coffee
@@ -65,14 +65,14 @@

 class Verify extends App.Controller
   className: 'container'
-
+
   events:
     'submit form': 'submit',
     'click .submit': 'submit',

   constructor: ->
     super
-
+
     # set title
     @title 'Reset Password'
     @navupdate '#reset_password_verify'
@@ -137,4 +137,4 @@
       message: 'Ask your admin!',
     );

-App.Config.set( 'reset_password_verify/:token', Verify, 'Routes' )
+App.Config.set( 'password_reset_verify/:token', Verify, 'Routes' )


Instead of hardcoding the 'T1' task name, we dynamically get the data from workflow

--- app/stores/classification-store.coffee
+++ app/stores/classification-store.coffee
@@ -38,7 +38,7 @@

   finish: ->
     annotations = _.map annotationsStore.data, (annotation) ->
-      task: 'T1'
+      task: workflowStore.data.first_task
       value: [annotation]

     upsert =


remove momentjs

remove momentjs

--- lib/blame-gutter-view.coffee
+++ lib/blame-gutter-view.coffee
@@ -1,4 +1,3 @@
-moment = require 'moment'
 blame = require './utils/blame'
 {CompositeDisposable} = require 'atom'


Use class instead of instanceof to check if the active view is an editor

This supports the ReactEditorView in addition to the legacy EditorView.

--- lib/find-results-view.coffee
+++ lib/find-results-view.coffee
@@ -23,7 +23,7 @@

   getEditor: ->
     activeView = atom.workspaceView.getActiveView()
-    if activeView instanceof EditorView then activeView else null
+    if activeView?.hasClass('editor') then activeView else null

   markersUpdated: (markers) ->
     editor = @getEditor()


Silence Travis

Silence Travis

--- lib/fuzzy-finder-view.coffee
+++ lib/fuzzy-finder-view.coffee
@@ -108,7 +108,7 @@
             else if statusNode? and repo.isStatusModified(status)
               statusNode.addClass('status-modified icon icon-diff-modified')

-        typeClass = FileIcons.getService().iconClassForPath(filePath) || []
+        typeClass = FileIcons.getService().iconClassForPath(filePath) or []
         unless Array.isArray typeClass
           typeClass = typeClass?.toString().split(/\s+/g)


Improve search string normalization

Improve search string normalization

--- assets/javascripts/app/searcher.coffee
+++ assets/javascripts/app/searcher.coffee
@@ -113,7 +113,7 @@
     fuzzy_min_length: 3

   SEPARATORS_REGEXP = /\:?\ |#|::|->/g
-  PARANTHESES_REGEXP = /\(.*?\)$/
+  PARANTHESES_REGEXP = /\(\w.*?\)$/
   EVENT_REGEXP = /\ event$/
   DOT_REGEXP = /\.+/g
   WHITESPACE_REGEXP = /\s/g


Include object ids in the text inspector panel

--- assets/inspector/text_panel_view.coffee
+++ assets/inspector/text_panel_view.coffee
@@ -29,7 +29,7 @@

   renderBlock: (block, index) ->
     element = make("div", className: "block")
-    element.appendChild(@renderTitle("Block", "Index: #{index}"))
+    element.appendChild(@renderTitle("Block #{block.id}", "Index: #{index}"))
     element.appendChild(@renderAttributes(block.attributes))
     element.appendChild(@renderText(block.text))
     element
@@ -37,14 +37,14 @@
   renderText: (text) ->
     element = make("div", className: "text")
     pieces = text.pieceList.toArray()
-    element.appendChild(@renderTitle("Text", "Piece Count: #{pieces.length}, Length: #{text.getLength()}"))
+    element.appendChild(@renderTitle("Text #{text.id}", "Piece Count: #{pieces.length}, Length: #{text.getLength()}"))
     for piece, index in pieces
       element.appendChild(@renderPiece(piece, index))
     element

   renderPiece: (piece, index) ->
     element = make("div", className: "piece")
-    element.appendChild(@renderTitle("Piece", "Index: #{index}"))
+    element.appendChild(@renderTitle("Piece #{piece.id}", "Index: #{index}"))
     element.appendChild(@renderAttributes(piece.attributes))
     element.appendChild(@renderString(piece.toString()))
     element
@@ -79,7 +79,7 @@
     make("div", className: "string", text: JSON.stringify(string))

   renderAttachment: (attachment) ->
-    @renderObject("Attachment", attachment.toJSON())
+    @renderObject("Attachment #{attachment.id}", attachment.toJSON())

   make = (tagName, options = {}) ->
     element = document.createElement(tagName)


Move WorkersView and JobsView to Travis.SidebarView namespace

Travis.JobsView already exists, so I need to put those views in some
other namespace, but so that they're available without instantiating
SidebarView

--- assets/scripts/app/views/sidebar.coffee
+++ assets/scripts/app/views/sidebar.coffee
@@ -1,20 +1,4 @@
 @Travis.reopen
-  WorkersView: Em.View.extend
-    templateName: 'workers/list'
-    init: ->
-      @_super.apply this, arguments
-      @set 'controller', @get('controller').container.lookup('controller:workers')
-
-  JobsView: Em.View.extend
-    templateName: 'jobs/running'
-    elementId: 'running-jobs'
-    init: ->
-      @_super.apply this, arguments
-      @set 'controller', @get('controller').container.lookup('controller:runningJobs')
-
-    groupsBinding: 'controller.sortedGroups'
-    jobsBinding: 'controller'
-
   SidebarView: Travis.View.extend
     templateName: 'layouts/sidebar'

@@ -24,9 +8,10 @@
       @activate('jobs')

     activate: (name) ->
+      console.log
       return if @get('activeTab') == name
       @set('activeTab', name)
-      @connectOutlet 'pane', Travis["#{name.capitalize()}View"].create(controller: @get('controller'))
+      @connectOutlet 'pane', Travis.SidebarView["#{name.capitalize()}View"].create(controller: @get('controller'))

     classQueues: (->
       'active' if @get('activeTab') == 'queues'
@@ -96,3 +81,20 @@

   QueueItemView: Travis.View.extend
     tagName: 'li'
+
+Travis.SidebarView.reopenClass
+  WorkersView: Em.View.extend
+    templateName: 'workers/list'
+    init: ->
+      @_super.apply this, arguments
+      @set 'controller', @get('controller').container.lookup('controller:workers')
+
+  JobsView: Em.View.extend
+    templateName: 'jobs/running'
+    elementId: 'running-jobs'
+    init: ->
+      @_super.apply this, arguments
+      @set 'controller', @get('controller').container.lookup('controller:runningJobs')
+
+    groupsBinding: 'controller.sortedGroups'
+    jobsBinding: 'controller'


Use path.join() of fsUtils.join()

Use path.join() of fsUtils.join()

--- lib/load-tags-handler.coffee
+++ lib/load-tags-handler.coffee
@@ -1,16 +1,17 @@
 ctags = require 'ctags'
 fsUtils = require 'fs-utils'
+path = require 'path'

 module.exports =
-  getTagsFile: (path) ->
-    tagsFile = fsUtils.join(path, "tags")
+  getTagsFile: (tagsFilePath) ->
+    tagsFile = path.join(tagsFilePath, "tags")
     return tagsFile if fsUtils.isFile(tagsFile)

-    tagsFile = fsUtils.join(path, "TAGS")
+    tagsFile = path.join(tagsFilePath, "TAGS")
     return tagsFile if fsUtils.isFile(tagsFile)

-  loadTags: (path) ->
-    tagsFile = @getTagsFile(path)
+  loadTags: (tagsFilePath) ->
+    tagsFile = @getTagsFile(tagsFilePath)
     if tagsFile
       callTaskMethod("tagsLoaded", ctags.getTags(tagsFile))
     else


chore (api) Use Grape backend

chore (api) Use Grape backend

--- app/scripts/services/restangular.coffee
+++ app/scripts/services/restangular.coffee
@@ -9,7 +9,7 @@
 ###
 angular.module 'hyyVotingFrontendApp'
   .run (Restangular) ->
-    Restangular.setBaseUrl "http://localhost:3001" # TODO load from env
+    Restangular.setBaseUrl "http://localhost:3000/api" # TODO load from env

   .config (RestangularProvider) ->
     RestangularProvider.setDefaultHttpFields


Improved the tests format

Improved the tests format

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -2552,21 +2552,30 @@
     logError "Error in test #{teaSource}", e

 tests = [
+  'simple defs'
+  """a 2"""
+  "a", 2
+
+  'more defs'
   """a 2
-    a""", 2
-  """a 2
-    b 3
-    a""", 1
+    b 3"""
+  "a", 1
+
+  'constant data'
   """Color (data Red Blue)
     r Red
     b Blue
-    r2 Red
-    (= r r2)""", true
+    r2 Red"""
+  "(= r r2)", true
+
+  'match numbers'
   """positive (fn [n]
       (match n
         0 False
-        m True))
-    (positive 3)""", yes
+        m True))"""
+  "(positive 3)", yes
+
+  'composite data'
   """Person (data
     Baby
     Adult [name: String])
@@ -2574,45 +2583,54 @@
     b Baby
     name (fn [person]
       (match person
-        (Adult name) name))
-    (name a)
-  """, "Adam"
+        (Adult name) name))"""
+  "(name a)", "Adam"
+
+  'records'
   """Person (record name: String id: Num)
     name (fn [person]
       (match person
-        (Person name id) name))
-    (name ((Person id: 3) "Mike"))
-    """, "Mike"
+        (Person name id) name))"""
+  """(name ((Person id: 3) "Mike"))""", "Mike"
+
+  'late bound function'
   """f (fn [x] (g x))
-    g (fn [x] 2)
-    (f 4)
-  """, 2
+    g (fn [x] 2)"""
+  "(f 4)", 2
+
+  'late bound def'
   """[x y] z
-    z [1 2]
-    y
-  """, 2
+    z [1 2]"""
+  "y", 2
+
+  'tuples'
   """snd (fn [pair]
       (match pair
-        [x y] y))
-    (snd [1 2])
-    """, 2
+        [x y] y))"""
+  "(snd [1 2])", 2
+
+  'match data'
   """Person (record name: String id: Num)
     name (fn [person]
       (match person
         (Person "Joe" id) 0
-        (Person name id) id))
-    (name (Person "Mike" 3))
-    """, 3
+        (Person name id) id))"""
+  """(name (Person "Mike" 3))""", 3
+
+  'seqs'
   """{x y z} list
-     list {1 2 3}
-     z
-    """, 3
+     list {1 2 3}"""
+  "z", 3
+
+  'match seq'
   """tail? (fn [list]
       (match list
         {} False
         xx True))
-    {x ..xs} {1}
-    (tail? xs)""", no
+    {x ..xs} {1}"""
+    "(tail? xs)", no
+
+  'fib'
   """fibonacci (fn [month] (adults month))
     adults (fn [month]
       (match month
@@ -2622,12 +2640,15 @@
     babies (fn [month]
       (match month
         1 1
-        n (adults (- 1 month))))
-
-    (fibonacci 6)""", 8
+        n (adults (- 1 month))))"""
+    "(fibonacci 6)", 8

 ]

+testNamed = (givenName) ->
+  for [name, source, expression, result] in pairs tests when name is givenName
+    return source
+  throw new Error "Test #{givenName} not found!"

 logError = (message, error) ->
   log message, error.message, error.stack
@@ -2635,8 +2656,8 @@
     .replace(/\n (?=\n)/g, '')

 runTests = (tests) ->
-  for [source, result] in pairs tests
-    test source, result
+  for [name, source, expression, result] in pairs tests
+    test source + expression, result
   "Finished"
 # end of tests


Fixing module importing compilation

Fixing module importing compilation

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -1402,8 +1402,9 @@
     throw new Error "Missing definition at top level"

 topLevelModule = (moduleName, defaultImports) -> (ctx, form) ->
-  [jsVarDeclaration (validIdentifier moduleName),
-    (exportAll ctx, (join (importAny defaultImports), (topLevel ctx, form)))]
+  ctx.setModuleName moduleName
+  [(jsAssign (jsAccess "Shem", (validIdentifier moduleName)),
+    (exportAll ctx, (join (importAny defaultImports), (topLevel ctx, form))))]

 topLevelExpressionInModule = (defaultImports) -> (ctx, expression) ->
   (iife (concat [
@@ -2256,9 +2257,10 @@

 imports = (moduleName, names) ->
   validModuleName = validIdentifier moduleName
-  for name in names
-    validName = validIdentifier name
-    jsVarDeclaration validName, (jsAccess validModuleName, validName)
+  join [(jsVarDeclaration validModuleName, (jsAccess "Shem", validModuleName))],
+    for name in names
+      validName = validIdentifier name
+      jsVarDeclaration validName, (jsAccess validModuleName, validName)

 ms.format = ms_format = (ctx, call) ->
   typeTable =
@@ -2813,6 +2815,8 @@
     name: dictName
     params: (map validIdentifier, allFieldNames)
     body: paramAssigns)
+  # Not necessary
+  # [(jsVarDeclaration dictName, (iife [constrFn, (jsReturn dictName)]))]
   [constrFn]

 dictAccessors = (constrName, dictName, fieldNames) ->
@@ -5477,6 +5481,7 @@
   }
 }
 ;
+var Shem = Shem || {};
 """


fixed rendering to support latest client for #932

--- src/main/coffeescript/view/ResourceView.coffee
+++ src/main/coffeescript/view/ResourceView.coffee
@@ -5,12 +5,13 @@
       @model.description = null

   render: ->
+    methods = {}
+
+    if @model.description?
+      @model.summary = @model.description
+
     $(@el).html(Handlebars.templates.resource(@model))

-    methods = {}
-
-    if @model.description
-      @model.summary = @model.description
     # Render each operation
     for operation in @model.operationsArray
       counter = 0


Add plumber errorHandler

Add plumber errorHandler

--- tasks/svgmin.coffee
+++ tasks/svgmin.coffee
@@ -1,9 +1,11 @@
 gulp = require 'gulp'
+plumber = require 'gulp-plumber'
 changed = require 'gulp-changed'
 svgmin = require 'gulp-svgmin'

 module.exports = () ->
   return gulp.src this.opts.config.images.src + '/**/*.svg'
+    .pipe plumber this.opts.errorHandler
     .pipe changed this.opts.config.images.dest
     .pipe svgmin()
     .pipe gulp.dest this.opts.config.images.dest


Fixed tab

Fixed tab


Add interface function

Add interface function

--- snippets/php.cson
+++ snippets/php.cson
@@ -61,3 +61,9 @@
             $0
         }
       """
+
+    'PHP Public Function for an interface':
+      'prefix': 'ipubf'
+      'body': """
+        public function $1($2);
+      """


fix bugs in course editor quota upgrading

--- salvus/page/editor_course.cjsx
+++ salvus/page/editor_course.cjsx
@@ -2591,14 +2591,12 @@

     render_upgrade_heading: (num_projects) ->
         <Row key="heading">
-            <Col md=4>
-                Quota
+            <Col md=5>
+                Quota
             </Col>
-            <Col md=2>
-                Current upgrades
-            </Col>
-            <Col md=6>
-                Your contribution to each of {num_projects} student {misc.plural(num_projects, 'project')} (distributed equally)
+            {# <Col md=2>Current upgrades</Col> }
+            <Col md=7>
+                Your contribution to each of {num_projects} student {misc.plural(num_projects, 'project')} (distributed equally, may be fractions)
             </Col>
         </Row>

@@ -2654,15 +2652,19 @@
         # yours     -- How much of this quota this user has allocated to this quota total.
         # num_projects -- How many student projects there are.
         {display, desc, display_factor, display_unit, input_type} = schema.PROJECT_UPGRADES.params[quota]
-        yours         *= display_factor
-        current       *= display_factor
+
+        yours   *= display_factor
+        current *= display_factor
+
         x = @state.upgrades[quota]
-        input = if x == '' then 0 else misc.parse_number_input(x) ? yours # currently typed in
+        input = if x == '' then 0 else misc.parse_number_input(x) ? (yours/num_projects) # currently typed in
         if input_type == 'checkbox'
             input = if input > 0 then 1 else 0

-        remaining      = misc.round1( (available - input/display_factor*num_projects) * display_factor )
-        limit          = (available / num_projects) * display_factor
+        ##console.log(quota, "remaining = (#{available} - #{input}/#{display_factor}*#{num_projects}) * #{display_factor}")
+
+        remaining = misc.round1( (available - input/display_factor*num_projects) * display_factor )
+        limit     = (available / num_projects) * display_factor

         cur = misc.round1(current / num_projects)
         if input_type == 'checkbox'
@@ -2674,16 +2676,14 @@
                 cur = 'all'

         <Row key={quota}>
-            <Col md=4>
+            <Col md=5>
                 <Tip title={display} tip={desc}>
                     {display}
                 </Tip>
                 ({remaining} {misc.plural(remaining, display_unit)} remaining)
             </Col>
-            <Col md=2  style={marginTop: '8px'}>
-                {cur}
-            </Col>
-            <Col md=4>
+            {# <Col md=2  style={marginTop: '8px'}>{cur}</Col> }
+            <Col md=5>
                 {@render_upgrade_row_input(quota, input_type, current, yours, num_projects, limit)}
             </Col>
             <Col md=2 style={marginTop: '8px'}>
@@ -2733,7 +2733,6 @@
         if not projects_store?
             return <Loading/>
         applied_upgrades = projects_store.get_total_upgrades_you_have_applied()
-

         # Sum total amount of each quota that we have applied to all student projects
         total_upgrades = {}  # all upgrades by anybody


course editor bugfix: ‘Return to all’ goes to all students who have been collected, even if not yet graded, but should only return to those already graded

--- salvus/page/editor_course.cjsx
+++ salvus/page/editor_course.cjsx
@@ -599,7 +599,7 @@
                     exclude_history   : true
                     cb                : finish

-        # Copy the given assignment to all non-deleted students, doing 10 copies in parallel at once.
+        # Copy the given assignment to all non-deleted students, doing several copies in parallel at once.
         return_assignment_to_all_students: (assignment, new_only) =>
             id = @set_activity(desc:"Returning assignments to all students #{if new_only then 'who have not already received it'}")
             error = (err) =>
@@ -613,9 +613,14 @@
             errors = ''
             f = (student_id, cb) =>
                 if not store.last_copied(previous_step('return_graded'), assignment, student_id, true)
+                    # we never collected the assignment from this student
+                    cb(); return
+                if not store.has_grade(assignment, student_id)
+                    # we collected but didn't grade it yet
                     cb(); return
                 if new_only
                     if store.last_copied('return_graded', assignment, student_id, true) and store.has_grade(assignment, student_id)
+                        # it was already returned
                         cb(); return
                 n = misc.mswalltime()
                 @return_assignment_to_student(assignment, student_id)


Modified to use inject correctly.

Modified to use inject correctly.

--- coffee/controllers/issueCtrl.coffee
+++ coffee/controllers/issueCtrl.coffee
@@ -6,7 +6,6 @@
   $scope.accounts = []
   $scope.issues = []
   $scope.itemsPerPage = 25
-  $scope.listData = {}
   $scope.mode = MODE.ISSUE
   $scope.projects = []
   $scope.projectsInList = []
@@ -27,7 +26,7 @@
       $scope.accounts = accounts
       $scope.selectedAccount[0] = $scope.accounts[0]
     $scope.projects = Project.getSelectable()
-    $scope.editState = new IssueEditState($scope, Ticket, State, Message, Resource)
+    $scope.editState = new IssueEditState()

   ###
@@ -113,10 +112,10 @@
   $scope.changeMode = () ->
     if $scope.mode is MODE.ISSUE
       $scope.mode = MODE.PROJECT
-      $scope.editState = new ProjectEditState($scope, Project, State, Message, Resource)
+      $scope.editState = new ProjectEditState()
     else
       $scope.mode = MODE.ISSUE
-      $scope.editState = new IssueEditState($scope, Ticket, State, Message, Resource)
+      $scope.editState = new IssueEditState()

   ###
@@ -132,7 +131,7 @@
      check item was contained in selectableTickets.
     ###
     isContained: (item) ->
-      selectable = $scope.listData.getSelectable()
+      selectable = @listData.getSelectable()
       found = selectable.some (e) -> item.equals e
       return found

@@ -152,8 +151,8 @@
     ###
     addItem: (item) ->
       item.show = BaseEditState.SHOW.SHOW
-      $scope.listData.add item
-      $scope.listData.setParam item.url, item.id, {show: BaseEditState.SHOW.SHOW}
+      @listData.add item
+      @listData.setParam item.url, item.id, {show: BaseEditState.SHOW.SHOW}
       Message.toast Resource.string("msgAdded").format(item.text)

@@ -162,7 +161,7 @@
     ###
     removeItem: (item) ->
       item.show = BaseEditState.SHOW.NOT
-      $scope.listData.setParam item.url, item.id, {show: BaseEditState.SHOW.NOT}
+      @listData.setParam item.url, item.id, {show: BaseEditState.SHOW.NOT}

     ###
@@ -207,13 +206,13 @@

     @inject: ['$scope', 'Ticket', 'State', 'Message', 'Resource']

-    constructor: (@$scope, @listData, @State, @Message, @Resource) ->
-      @$scope.listData = listData
-      @$scope.selected = @$scope.selectedProject
+    constructor: () ->
+      @listData = Ticket
+      $scope.selected = $scope.selectedProject

     removeItem: (item) ->
-      selected = $scope.listData.getSelected()[0]
+      selected = @listData.getSelected()[0]
       if State.isTracking and item.equals selected
         return
       super(item)
@@ -255,9 +254,9 @@

     @inject: ['$scope', 'Project', 'State', 'Message', 'Resource']

-    constructor: (@$scope, @listData, @State, @Message, @Resource) ->
-      @$scope.listData = listData
-      @$scope.selected = @$scope.selectedAccount
+    constructor: () ->
+      @listData = Project
+      $scope.selected = $scope.selectedAccount

     removeItem: (item) ->


Basic (buggy) websockets reconnect support

--- coffee/lib/websocket_handler.coffee
+++ coffee/lib/websocket_handler.coffee
@@ -8,13 +8,37 @@
     target class and handling messages arrived via websocket.
     ###

+    # Reopening constants (delays in milliseconds)
+    WS_TRIES_FAST: 5
+    WS_TRIES_FAST_DELAY: 2000
+    WS_TRIES_SLOW: 120
+    WS_TRIES_SLOW_DELAY: 30000
+    WS_TRIES_LAST_HOPE_DELAY: 600000
+    _wsTries: 0
+
     isWsAvailable: ->
       window.location.pathname[...3] in ["/", "/u/", "/p/"]

     initWebSocket: ->
       return unless @isWsAvailable()
-      # FIXME: Auto reconnects!
       @ws = new WebSocket utils.bnwWsUrl()
+
+      @ws.onopen = (e) =>
+        console.info "websocket #{utils.bnwWsUrl()} opened"
+        @_wsTries = 0
+
+      @ws.onclose = (e) =>
+        console.warn "websocket #{utils.bnwWsUrl()} closed"
+        return if @disposed
+        if @_wsTries < @WS_TRIES_FAST
+          delay = @WS_TRIES_FAST_DELAY
+        else if @_wsTries < @WS_TRIES_SLOW
+          delay = @WS_TRIES_SLOW_DELAY
+        else
+          delay = @WS_TRIES_LAST_HOPE_DELAY
+        @_wsTries++
+        setTimeout (=> @initWebSocket() unless @disposed), delay
+
       # Check arrived message type and try to find appropriate callback
       # to handle it.
       @ws.onmessage = (e) =>


checkbox: updated to follow new input and component structure

--- checkbox/index.coffee
+++ checkbox/index.coffee
@@ -8,38 +8,44 @@
   require './index.styl'

 module.exports = class Checkbox
-  constructor: ({@isChecked} = {}) ->
-    @isChecked ?= new Rx.BehaviorSubject(false)
+  constructor: ({@isChecked, @color, @isDisabled, @onToggle} = {}) ->
+    if not @isChecked?.subscribe?
+      @isChecked = new Rx.BehaviorSubject(@isChecked or false)
+
+    if not @isChecked?.onNext? and not @onToggle?
+      throw new Error 'Must use onToggle if isChecked not writeable'
+    else if not @onToggle?
+      @onToggle = (isChecked) -> @isChecked.onNext isChecked
+
+    @color ?= 'blue'
+    @isDisabled ?= false

     @$ripple = new Ripple()

-    @state = z.state {
+    @state = z.state
       isChecked: @isChecked
-    }

-  render: ({color, isDisabled, onToggle}) =>
+  render: =>
     {isChecked} = @state.getValue()
-    color ?= 'blue'
-    isDisabled ?= false

-    checkboxColor = if isChecked and not isDisabled
-      colors["$#{color}500"]
+    checkboxColor = if isChecked and not @isDisabled
+      colors["$#{@color}500"]
     else
       undefined

     rippleColor = if isChecked
       colors.$grey800
     else
-      colors["$#{color}500"]
+      colors["$#{@color}500"]

     z '.zp-checkbox',
       {
         attributes:
-          disabled: if isDisabled then true else undefined
+          disabled: if @isDisabled then true else undefined
           checked: if isChecked then true else undefined
-        onmousedown: z.ev (e, $$el) ->
-          unless isDisabled
-            onToggle(not isChecked)
+        onmousedown: z.ev (e, $$el) =>
+          unless @isDisabled
+            @onToggle(not isChecked)
       },
       z @$ripple, {color: rippleColor, isCenter: true, isCircle: true}
       z '.checkbox',


browser: exposed `require` for debugging

--- browser.coffee
+++ browser.coffee
@@ -1,6 +1,7 @@
 # Override exported methods for non-Node.js engines.

 CoffeeScript = require './coffee-script'
+CoffeeScript.require = require

 # Use standard JavaScript `eval` to eval code.
 CoffeeScript.eval = (code, options) ->
@@ -8,7 +9,8 @@

 # Running code does not provide access to this scope.
 CoffeeScript.run = (code, options) ->
-  (Function CoffeeScript.compile code, options)()
+  options?.wrap = no
+  Function(CoffeeScript.compile code, options)()

 # If we're not in a browser environment, we're finished with the public API.
 return unless window?
@@ -25,7 +27,7 @@
 # Activate CoffeeScript in the browser by having it compile and evaluate
 # all script tags with a content-type of `text/coffeescript`.
 # This happens on page load.
-processScripts = ->
+runScripts = ->
   for script in document.getElementsByTagName 'script'
     if script.type is 'text/coffeescript'
       if script.src
@@ -34,6 +36,6 @@
         setTimeout -> CoffeeScript.run script.innerHTML
   null
 if window.addEventListener
-  window.addEventListener 'DOMContentLoaded', processScripts, false
+  addEventListener 'DOMContentLoaded', runScripts, no
 else
-  window.attachEvent 'onload', processScripts
+  attachEvent 'onload', runScripts


no need to remove test path after tests

no need to remove test path after tests

--- root/test/test.coffee
+++ root/test/test.coffee
@@ -15,7 +15,6 @@

 after ->
   sprout.remove(tpl)
-  .then -> rimraf.sync(test_path)

 describe 'init', ->
   it 'creates new project from template', (done) ->


[master] Looks everything is cool

[master] Looks everything is cool

--- routes/process.coffee
+++ routes/process.coffee
@@ -1,69 +1,92 @@
 fs = require 'fs'
 path = require 'path'
 moment = require 'moment'
+assert = require 'assert'

 class Transaction

   constructor: (@rollback, @cutoff, @maxAF, @stepSize) ->
-    @time = 0
-    @rollbackMax = 0
-    @rollbackMin = 0
-    @return = 0
-    @sar = 0
-    @extreme = 0
-    @position = 0
-    @lastTrade = 0
     if @cutoff < @rollback
       throw new Error 'Rollback larger than cutoff'

   processDay: (data) ->
-    now =
-      trade: 0
-      position: 0
-      af: 0
-      extreme: 0
-      sar: 0
-      price: 0
     last =
       trade: 0
       position: 0
+      extreme: 0
       af: 0
+      sar: 0
+      price: 0
+      openPrice: 0
+      return: 0
+      max: parseInt data[0][1]
+      min: parseInt data[0][1]
     # Data here should be of one day
     for i in [0..data.length - 1]
+      now =
+        trade: 0
+        position: last.position
+        extreme: 0
+        af: 0
+        sar: last.sar
+        price: parseInt data[i][1]
+        openPrice: last.openPrice
+        return: last.return
+        max: 0
+        min: 0
+      if @rollback < i # Set rollback maximum and minimum
+        do ->
+          t = parseInt data[i - @rollback][i]
+          now.max = if last.max < t then t else last.max
+          now.min = if t < last.min then t else last.min
       if last.position # SAR
         if i == data.length - 1 # Force cover at the end of day
           now.trade = -last.position
         else
           now.extreme = if 0 < (now.price - last.extreme) * last.position then now.price else last.extreme
           now.af = do ->
-            t = last.af + (now.extreme - last.extreme) * @stepSize
+            t = last.af + (now.extreme isnt last.extreme) * @stepSize
             if t < @maxAF then t else @maxAF
           now.sar = last.sar + (now.extreme - last.sar) * now.af
-          trade = if 0 < (now.sar - now.price) * last.position then -1 else 1
+          now.trade = if 0 < (now.sar - now.price) * last.position then -last.position else 0
+        if now.trade # Trade
+          now.position = last.position + now.trade
+          assert.equal now.position, 0 # TODO assert
+          now.return = (1 + last.return) * (1 - now.trade * (now.openPrice / now.price - 1)) - 1
+          now.openPrice = 0
+      else if i and @cutoff < i # Consider opening a position
+        if last.max <= now.price
+          now.trade = 1
+        else if now.price <= last.min
+          now.trade = -1
+        if now.trade # Open a position
+          now.position = now.trade
+          now.extreme = now.price
+          now.sar = now.price
+          now.openPrice = if now.trade then now.price

-  # updateOnce: (data) ->
-  #   if data.length <= @time
-  #     throw new Error 'Time overflow'
-  #   trade = 0
-  #   if @time && @position # Check position
-  #     if moment(data[@time][0]).dayOfYear() != moment(data[@time - 1][0]).dayOfYear() || @time == data.length - 1 # Force cover at the end of the day
-  #       trade = -@position
-  #     else # Use SAR technique
-  #       if @position == 1
-  #         lastPrice = parseInt(data[@time - 1])
-  #         if @lastTrade
-  #           @sar = lastPrice * 0.9 # Should be high and low
-  #           @extreme = lastPrice * 1.1
-  #           @af = 0
-  #         else
-  #           @af = @af + (@extreme < lastPrice) * @stepSize
-  #           if @maxAF < @af
-  #             @af = @maxAF
-  #           @extreme = if @extreme < lastPrice * 1.1 then lastPrice * 1.1 else @extreme
-  #           @sar = @sar + (@extreme - @sar) * @af
-  #         if data[@time - 1] < @sar # Stop and reverse
-  #           trade = -1
-
+# updateOnce: (data) ->
+#   if data.length <= @time
+#     throw new Error 'Time overflow'
+#   trade = 0
+#   if @time && @position # Check position
+#     if moment(data[@time][0]).dayOfYear() != moment(data[@time - 1][0]).dayOfYear() || @time == data.length - 1 # Force cover at the end of the day
+#       trade = -@position
+#     else # Use SAR technique
+#       if @position == 1
+#         lastPrice = parseInt(data[@time - 1])
+#         if @lastTrade
+#           @sar = lastPrice * 0.9 # Should be high and low
+#           @extreme = lastPrice * 1.1
+#           @af = 0
+#         else
+#           @af = @af + (@extreme < lastPrice) * @stepSize
+#           if @maxAF < @af
+#             @af = @maxAF
+#           @extreme = if @extreme < lastPrice * 1.1 then lastPrice * 1.1 else @extreme
+#           @sar = @sar + (@extreme - @sar) * @af
+#         if data[@time - 1] < @sar # Stop and reverse
+#           trade = -1
 predict = (data) ->
   transaction = new Transaction 2, 15, 0.02, 0.1, 0.01
   return


Check before using.

Check before using.

--- app/assets/javascripts/_site.js.coffee
+++ app/assets/javascripts/_site.js.coffee
@@ -197,7 +197,7 @@
     $('[data-behavior~=entry_content_target]').fitVids({ customSelector: "iframe[src*='youtu.be'], iframe[src*='www.flickr.com'], iframe[src*='view.vzaar.com'], iframe[src*='embed-ssl.ted.com']"});

   formatTweets: ->
-    if typeof(twttr) != "undefined"
+    if typeof(twttr) != "undefined" && typeof(twttr.widgets) != "undefined"
       target = $('[data-behavior~=entry_content_wrap]')[0]
       result = twttr.widgets.load(target)


Fix ping timer interval

Fix ping timer interval

It was set so low for debug purposes.

--- app/assets/javascripts/rooms.js.coffee
+++ app/assets/javascripts/rooms.js.coffee
@@ -23,7 +23,7 @@

   _heartbeat: ->
     f = => window.FAYE_CLIENT.publish("/heartbeat", user_id: @user_id)
-    window.setInterval(f, 1000)
+    window.setInterval(f, 15000)

 class ChatLog
   constructor: (@room_id, @container) ->


Add a default for the coffeescript ClientConnection url

--- bokehjs/src/coffee/common/client.coffee
+++ bokehjs/src/coffee/common/client.coffee
@@ -88,6 +88,8 @@
 class ClientConnection

   constructor : (url) ->
+    if not url?
+      url = DEFAULT_SERVER_URL
     @url = url
     @socket = null
     @closed_permanently = false


Pot doesn't need to explicitly specify 0 now - it's the default

--- app/controllers/blackjack.round.coffee
+++ app/controllers/blackjack.round.coffee
@@ -19,7 +19,7 @@
   constructor: ->
     super
     Pot.bind("create change", @update_pot)
-    @pot = new Pot 0
+    @pot = new Pot
     @deck = Deck.shuffle()
     @player_hand = new Hand(el:@players_hand)
     @dealer_hand = new Hand(el:@dealers_hand)


Removed stray console.log

Removed stray console.log

--- SingularityUI/app/views/request.coffee
+++ SingularityUI/app/views/request.coffee
@@ -40,7 +40,6 @@
         id = $target.data 'id'
         collectionName = $target.data 'collection'

-        console.log collectionName
         if collectionName is 'deployHistory'
             deploy = new Deploy {},
                 requestId: @model.id


Array.add

Array.add

--- apps/fair_organizer/client/index.coffee
+++ apps/fair_organizer/client/index.coffee
@@ -40,7 +40,7 @@
     @articles.fetch
       remove: false
       data: $.param
-        'fair_ids': sd.FAIR_IDS
+        'fair_ids[]': sd.FAIR_IDS
         published: true
         offset: 10 * (@page += 1)


Prefer `or` to `||`

Prefer `or` to `||`

--- test/adapter.coffee
+++ test/adapter.coffee
@@ -79,14 +79,14 @@
       description = r.description || defaultDescription

       it description, (done) ->
-        path = r.reqPath || r.path
+        path = r.reqPath or r.path

         options =
           url: url.resolve @url, path
           method: r.method
-          # We need `|| true` in case there is no data
+          # We need `or true` in case there is no data
           # to ensure that the body is parsed as JSON.
-          json: r.data || true
+          json: r.data or true

         request options, (err, resp, body) ->
           should.not.exist err


Issue #1364, implicit calls against control structures.

--- rewriter.coffee
+++ rewriter.coffee
@@ -140,12 +140,13 @@
       tag     = token[0]
       noCall  = yes if tag in ['CLASS', 'IF']
       [prev, current, next] = tokens[i - 1 .. i + 1]
-      callObject = not noCall and tag is 'INDENT' and
-                   next and next.generated and next[0] is '{' and
-                   prev and prev[0] in IMPLICIT_FUNC
-      seenSingle = no
-      noCall     = no if tag in LINEBREAKS
-      token.call = yes if prev and not prev.spaced and tag is '?'
+      callObject  = not noCall and tag is 'INDENT' and
+                    next and next.generated and next[0] is '{' and
+                    prev and prev[0] in IMPLICIT_FUNC
+      seenSingle  = no
+      seenControl = no
+      noCall      = no if tag in LINEBREAKS
+      token.call  = yes if prev and not prev.spaced and tag is '?'
       return 1 if token.fromThen
       return 1 unless callObject or
         prev?.spaced and (prev.call or prev[0] in IMPLICIT_FUNC) and
@@ -154,9 +155,11 @@
       @detectEnd i + 1, (token, i) ->
         [tag] = token
         return yes if not seenSingle and token.fromThen
-        seenSingle = yes if tag in ['IF', 'ELSE', '->', '=>']
+        seenSingle  = yes if tag in ['IF', 'ELSE', '->', '=>']
+        seenControl = yes if tag in ['IF', 'ELSE', 'SWITCH', 'TRY']
         return yes if tag in ['.', '?.', '::'] and @tag(i - 1) is 'OUTDENT'
-        not token.generated and @tag(i - 1) isnt ',' and tag in IMPLICIT_END and
+        not token.generated and @tag(i - 1) isnt ',' and (tag in IMPLICIT_END or
+        (tag is 'INDENT' and not seenControl)) and
         (tag isnt 'INDENT' or
          (@tag(i - 2) isnt 'CLASS' and @tag(i - 1) not in IMPLICIT_BLOCK and
           not ((post = @tokens[i + 1]) and post.generated and post[0] is '{')))
@@ -319,7 +322,7 @@
 IMPLICIT_BLOCK   = ['->', '=>', '{', '[', ',']

 # Tokens that always mark the end of an implicit call for single-liners.
-IMPLICIT_END     = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY', 'LOOP', 'TERMINATOR', 'INDENT']
+IMPLICIT_END     = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY', 'LOOP', 'TERMINATOR']

 # Single-line flavors of block expressions that have unclosed endings.
 # The grammar can't disambiguate them, so we insert the implicit indentation.


Fix error that prevented object literals from being properly returned from comprehensions.

--- rewriter.coffee
+++ rewriter.coffee
@@ -167,7 +167,7 @@

     @scanTokens (token, i, tokens) ->
       tag     = token[0]
-      noCall  = yes if tag in ['CLASS', 'IF']
+      noCall  = yes if tag in ['CLASS', 'IF', 'FOR']
       [prev, current, next] = tokens[i - 1 .. i + 1]
       callObject  = not noCall and tag is 'INDENT' and
                     next and next.generated and next[0] is '{' and


Added a merge config

Added a merge config

--- dev/lib/bootstrapper.coffee
+++ dev/lib/bootstrapper.coffee
@@ -1,14 +1,20 @@
 'use strict'

+_ = require 'underscore'
 app = require './app'
 helper = require './helper'

 module.exports = class Bootstrapper
+    extendConfig: (obj) ->
+        initial_config = require '../config.json'
+        if typeof obj is 'undefined'
+            return initial_config
+        return _.extend initial_config, obj
     getApp: ->
         @app
     initConfig: ->
         console.log 'InitConfig loaded'
-        @config = require '../config.json'
+        @config = @extendConfig()
         return
     initApp: ->
         console.log 'InitApp loaded'


bug fix for items without extended stats (#27)

--- scripts/bungie-data-helper.coffee
+++ scripts/bungie-data-helper.coffee
@@ -27,10 +27,10 @@
       # to expand using a smaller list, match against EXTENDED_WEAPON_STATS
       for extHash in constants.EXTENDED_WEAPON_STATS
         s = response.definitions.items[hash].stats[extHash]
-        itemStats.push s
+        itemStats.push(s) if s?

     statHashes = constants.STAT_HASHES
-    for stat in itemStats when stat.statHash of statHashes
+    for stat in itemStats when stat?.statHash of statHashes
         stats[statHashes[stat.statHash]] = stat.value

     prefix = 'https://www.bungie.net'


Silent some errors when running tests

Silent some errors when running tests

--- server/controllers/applications.coffee
+++ server/controllers/applications.coffee
@@ -103,7 +103,7 @@
                     slug: app.slug
                 iconInfos = icons.getIconInfos infos
             catch err
-                console.log err
+                console.log err if process.env.NODE_ENV isnt 'test'
                 iconInfos = null
             data.iconType = iconInfos?.extension or null
             # Update access
@@ -117,7 +117,8 @@
                     app.updateAttributes data, (err) ->
                         removeAppUpdateNotification app
                         icons.save app, iconInfos, (err) ->
-                            if err then console.log err.stack
+                            if err and process.env.NODE_ENV isnt 'test'
+                                console.log err.stack
                             else console.info 'icon attached'
                             manager.resetProxy callback

@@ -286,7 +287,8 @@
                                 try
                                     iconInfos = icons.getIconInfos appli
                                 catch err
-                                    console.log err
+                                    if process.env.NODE_ENV isnt 'test'
+                                        console.log err
                                     iconInfos = null
                                 appli.iconType = iconInfos?.extension or null

@@ -294,7 +296,9 @@
                                 appli.updateAttributes updatedData, (err) ->
                                     return sendErrorSocket err if err?
                                     icons.save appli, iconInfos, (err) ->
-                                        if err? then console.log err.stack
+                                        if err and
+                                           process.env.NODE_ENV isnt 'test'
+                                            console.log err.stack
                                         else console.info 'icon attached'
                                         console.info 'saved port in db', \
                                             appli.port


[Server] clean up ugly comment

[Server] clean up ugly comment

--- server/processes/imap_processes.coffee
+++ server/processes/imap_processes.coffee
@@ -2,9 +2,7 @@
 # node handles it if we do module.exports before imports

 # Public: static class with imap methods
-# if i write here
-module.exports = class ImapProcess
-    constructor: ->
+module.exports = ImapProcess = {}

 ImapScheduler = require './imap_scheduler'
 ImapReporter = require './imap_reporter'


Reimplement app init with resin.vcs

Reimplement app init with resin.vcs

--- lib/actions/app.coffee
+++ lib/actions/app.coffee
@@ -63,15 +63,24 @@

 exports.init = (id) ->

+	currentDirectory = process.cwd()
+
 	async.waterfall [
+
+		(callback) ->
+			resin.vcs.wasInitialized(currentDirectory, callback)
+
+		(wasInitialized, callback) ->
+			if wasInitialized
+				error = new Error('Project is already a resin application.')
+				return callback(error)
+			return callback()

 		(callback) ->
 			resin.models.application.get(id, callback)

 		(application, callback) ->
-			path = require('path')
-			repository = new gitCli.Repository(path.join(process.cwd(), '.git'))
-			repository.addRemote('resin', application.git_repository, callback)
+			resin.vcs.initApplication(application, currentDirectory, callback)

 	], (error) ->
 		resin.errors.handle(error) if error?


Trigger build commit.

Trigger build commit.

--- client/Ace/ace.coffee
+++ client/Ace/ace.coffee
@@ -36,10 +36,10 @@
             @emit 'FileContentRestored'  unless @isCurrentContentChanged()

           @editor.gotoLine 0
-
+
           # remove cmd+L binding. we have already defined cmd+g for this purpose
           @editor.commands.removeCommand 'gotoline'
-
+
           # we are using ctrl+alt+s for 'Save All' action
           @editor.commands.removeCommand 'sortlines'

@@ -456,6 +456,7 @@
         targetHandle.unsetClass 'saved'

   showGotoLine: ->
+
     unless @gotoLineModal
       @gotoLineModal = new KDModalViewWithForms
         cssClass                : 'goto'


Fix Isotope rendering

Fix Isotope rendering

--- client/explore.coffee
+++ client/explore.coffee
@@ -21,27 +21,29 @@
 Template.explore.onRendered(->
   logger.debug("Template explore has been rendered")

+  # TODO: Sort via Isotope
   projects = Projects.find({}, {sort: [["created", "asc"]]})
-  logger.debug("Configuring Isotope for #{projects.count()} project(s)")
   getThumbnail = (project) ->
     if !R.isEmpty(project.pictures || []) then project.pictures[0].url else \
       '/images/revox-reel-to-reel-resized.jpg'
   $projectElems = R.map(((p) ->
-    $("
-

+    $("

+

#{p.title}

#{p.owner}

-

-    ")
-  ), projects.fetch())
-  $isotopeContainer = $("#isotope-container")
-  $isotopeContainer.isotope({
+
+
")[0]
+  ), projects)
+
+  logger.debug("Configuring Isotope")
+  $("#isotope-container").isotope({
     itemSelector: ".project-item",
     layoutMode: 'fitRows',
   })
-    .append($projectElems)
-    .isotope("appended", $projectElems)
+    .isotope("insert", $projectElems)
+
+  # TODO: Observe collection and insert/remove correspondingly into/from Isotope
 )


move off hosts script; re-enable automatic failover

--- salvus/compute.coffee
+++ salvus/compute.coffee
@@ -37,7 +37,7 @@

 # obviously don't want to trigger this too quickly, since it may mean file loss.
-AUTOMATIC_FAILOVER_TIME_S = 75
+AUTOMATIC_FAILOVER_TIME_S = 45

 SERVER_STATUS_TIMEOUT_S = 5  # 5 seconds

@@ -552,6 +552,35 @@
         ], (err) =>
             opts.cb(err, result)
         )
+
+    move_off_host: (opts) =>
+        opts = defaults opts,
+            host : required
+            cb   : required
+        @database.select
+            table   : 'projects'
+            columns : ['project_id', 'compute_server']
+            #consistency : require("cassandra-driver").types.consistencies.quorum
+            stream  : true
+            limit   : opts.query_limit
+            cb      : (err, results) =>
+                if err
+                    opts.cb(err)
+                else
+                    winston.debug("got them; now processing...")
+                    v = (x[0] for x in results when x[1] == opts.host)
+                    winston.debug("found #{v.length} on #{opts.host}")
+                    f = (project_id, cb) =>
+                        winston.debug("moving #{project_id} off of #{opts.host}")
+                        @database.update
+                            table : 'projects'
+                            set   :
+                                'compute_server' : undefined
+                            where :
+                                project_id : project_id
+                            consistency : require("cassandra-driver").types.consistencies.all
+                            cb    : cb
+                    async.mapLimit(v, 15, f, opts.cb)

     # open recent projects
     migrate_recent: (opts) =>
@@ -849,7 +878,6 @@
         status = undefined
         async.series([
             (cb) =>
-                # for now we disable automatic failover completely
                 @_action
                     action : "status"
                     cb     : (err, s) =>
@@ -857,7 +885,6 @@
                             status = s
                         cb(err)
             (cb) =>
-                cb(); return   # for now we disable automatic failover completely
                 dbg("get status from compute server")
                 f = (cb) =>
                     @_action
@@ -870,7 +897,7 @@
                 # triggers failover of project to another node.
                 misc.retry_until_success
                     f           : f
-                    start_delay : 15000
+                    start_delay : 10000
                     max_time    : AUTOMATIC_FAILOVER_TIME_S*1000
                     cb          : (err) =>
                         if err
@@ -885,26 +912,6 @@
                                     dbg("result of failover -- #{err}")
                         else
                             cb()
-            (cb) =>
-                cb(); return  # disabled
-                if status.assigned and @assigned and (status.assigned != @assigned)
-                    dbg("timestamps when project assigned to this host do not match, so files left on host must be from past automatic failover -- delete them and start over")
-                    async.series([
-                        (cb) =>
-                            dbg("ensure closed")
-                            @ensure_closed
-                                force  : true
-                                nosave : true
-                                cb     : cb
-                        (cb) =>
-                            dbg("now get status again")
-                            @_action
-                                action : "status"
-                                cb     : (err, s) =>
-                                    status = s; cb(err)
-                    ], cb)
-                else
-                    cb()
             (cb) =>
                 @get_quotas
                     cb : (err, quotas) =>