fixed an issue with the notice where the option buttons weren't always being hidden when not used

--- src/chat/notice.coffee
+++ src/chat/notice.coffee
@@ -32,11 +32,10 @@
     @_show()

   _parseRepresentation: (representation) ->
+    @_setMessageText representation
     options = representation.match /\[.+?\]/g
-    if options
-      @_setOptionText @$option1, options[0]
-      @_setOptionText @$option2, options[1]
-    @_setMessageText representation
+    @_setOptionText @$option1, options?[0]
+    @_setOptionText @$option2, options?[1]

   _setMessageText: (representation) ->
     representation = representation.split('[')[0]


Remove the Rest of the Test Content

Remove the Rest of the Test Content

--- src/coffee/game.coffee
+++ src/coffee/game.coffee
@@ -363,67 +363,3 @@
         bluebacks: consq.increase 2

     return
-
-    front choice 'body-building',
-      'Bodybuilding Class'
-      'Well, this is unpleasant…'
-      visible:
-        punchiness: reqs.lt 6
-
-    storylet 'body-building',
-      'Bodybuilding Class'
-      """
-      You work out!
-      """
-      choices: [
-        choice 'body-hard',
-          'Hard!'
-          'Let no muscle be spared!'
-        choice 'body-soft',
-          'Lightly'
-          'I don\'t want to muss my moustache.'
-        choice 'to-the-death',
-          'To the death!',
-          'Wait, actually, that sounds like a terrible idea.'
-          active:
-            deathWish: reqs.exists
-      ]
-
-    storylet 'body-hard',
-      'You work out hard'
-      'You ache, but it was worth it.'
-      consequences:
-        punchiness: consq.increase 4
-
-    storylet 'body-soft',
-      'You work out softly'
-      'You don\'t even need to shower, but did it do anything?'
-      consequences:
-        punchiness: consq.increase 1
-
-    front choice 'harrowing-carriage',
-      'A Harrowing Carriage-Voyage'
-      'Well, this is unpleasant…'
-
-    storylet 'harrowing-carriage',
-      'A Harrowing Carriage-Voyage'
-      """
-      This looks unsafe in a profound and painful way.
-      """
-      choices: [
-        choice 'ride-carriage',
-          'Do It'
-          'And damn the consequences!'
-      ]
-
-    storylet 'ride-carriage',
-      'A Harrowing Carriage-Voyage'
-      (punchiness) ->
-        desc = if punchiness > 5 then 'sexy beast' else 'blob'
-        """
-        Insert purple prose here, you #{desc}.
-        """
-      consequences:
-        punchiness: consq.decrease 1
-
-    return


--cache is boolean only, not configurable.

The intention was to allow --cache [/some/path] where the path is
optional. I don't see how to make it optional, it's either a boolean, or
requires a path to be specified. I think `os.tmpdir()` is safe enough to
just not make this configurable.

--- src/commandline.coffee
+++ src/commandline.coffee
@@ -256,12 +256,8 @@

 else
     # Initialize cache, if enabled
-    cacheDir = options.argv.cache
-    if cacheDir
-        coffeelint.setCache new Cache if typeof cacheDir is 'string'
-            path.resolve(cacheDir)
-        else
-            path.join(os.tmpdir(), 'coffeelint')
+    if options.argv.cache
+        coffeelint.setCache new Cache(path.join(os.tmpdir(), 'coffeelint'))

     # Load configuration.
     config = null


Remove handles on destroy

Remove handles on destroy

--- src/commentList.coffee
+++ src/commentList.coffee
@@ -9,7 +9,7 @@
   Meteor.subscribe 'comments', series: series

 TemplateClass.rendered = ->
-  Collections.observe getCursor(), delayedScrollToBottom.bind(@, @)
+  @autorun => Collections.observe getCursor(), delayedScrollToBottom.bind(@, @)
   delayedScrollToBottom(@)

 TemplateClass.helpers


make menus useful

make menus useful

--- menus/atom-flow.cson
+++ menus/atom-flow.cson
@@ -11,8 +11,17 @@
       'label': 'flow'
       'submenu': [
         {
-          'label': 'Toggle'
-          'command': 'flow:toggle'
+          'label': 'Status'
+          'command': 'flow:status'
+        },
+        {
+          'label': 'Set target'
+          'command': 'flow:set-target'
+        },
+        { type: 'separator' },
+        {
+          'label': 'Toggle log view'
+          'command': 'flow:toggle-log-view'
         }
       ]
     ]


new bill nye link on education page

new bill nye link on education page

--- app/translations/en_us.coffee
+++ app/translations/en_us.coffee
@@ -786,6 +786,7 @@

 TropicMind.com: Ecosystem and Food Chain (aimed for younger children)

+
Bill Nye: The Science Guy - Biodiversity

We also recommend checking out the Urban Wildlife Institute webpage for more information on the organization.


LPZ Educator's Resources paragraph

LPZ Educator's Resources paragraph

--- app/translations/en_us.coffee
+++ app/translations/en_us.coffee
@@ -785,8 +785,10 @@

 TropicMind.com: Ecosystem and Food Chain (aimed for younger children)

-
We also recommend checking out the Urban Wildlife Institute webpage for more information on the organization

-
+
Please visit Lincoln Park Zoo's Educator's Resources  webpage to access curriculum guides, Zoo field trip and self-tour information, accesssibility kits, and links to several multi-media apps.
+

+
We also recommend checking out the Urban Wildlife Institute webpage for more information on the organization.
+

       """
     aside:
       content: """


Fix issue where mentions which are substrings of other mentions don't auto link correctly (due to index overlap between the two)

fixes #18

--- assets/javascripts/helpers/url.js.coffee
+++ assets/javascripts/helpers/url.js.coffee
@@ -63,7 +63,7 @@
     return url if url.match /^[a-z]+:\/\//i
     'http://' + url

-  substringIndices: (string, substring) ->
+  substringIndices: (string, substring, invalid_after) ->
     return [] unless string and substring

     _indices = []
@@ -74,6 +74,7 @@
       break if i == -1
       _start_index = i + _offset
       _end_index = _start_index + _length
+      break if string.substr(_end_index, 1).match(invalid_after) if invalid_after
       _offset += i + _length
       _indices.push _start_index, _end_index

@@ -121,7 +122,7 @@
         _offset += _text.indexOf(matched) + matched.length
         original_text = entity
         _length = original_text.length
-        _indices = TentStatus.Helpers.substringIndices(text, matched)
+        _indices = TentStatus.Helpers.substringIndices(text, matched, /[a-z0-9]/)
         if entity.match(new RegExp(TentStatus.config.tent_host_domain))
           entity = "https://#{entity}"
         else


Fix messages count on initial page load

Fix messages count on initial page load

--- assets/javascripts/mailcatcher.js.coffee
+++ assets/javascripts/mailcatcher.js.coffee
@@ -123,7 +123,6 @@

     @refresh()
     @subscribe()
-    @updateMessagesCount()

   # Only here because Safari's Date parsing *sucks*
   # We throw away the timezone, but you could use it for something...
@@ -287,44 +286,12 @@
         text = text.replace(/\n/g, "
")
         message_iframe.find("html").html("#{text}")

-  loadMessageAnalysis: (id) ->
-    id ||= @selectedMessage()
-
-    $("#message .views .analysis.tab:not(.selected)").addClass("selected")
-    $("#message .views :not(.analysis).tab.selected").removeClass("selected")
-
-    if id?
-      # Makes a new iframe and populate it with a quick intro and a button
-      $iframe = $("#message iframe").contents().children()
-        .html("""
-
-
-          <title>Analysis</title>
-          #{$('link[rel="stylesheet"]')[0].outerHTML}
-
-
-
Analyse your email with Fractal

-
Fractal is a really neat service that applies common email design and development knowledge from Email Standards Project to your HTML email and tells you what you've done wrong or what you should do instead.

-
Please note that this sends your email to the Fractal service for analysis. Read their terms of service if you're paranoid.

-          <form>
-          <input type="submit" value="Analyse" />Analysing…
-          </form>
-
-
-        """)
-      $form = $iframe.find("form")
-        .submit (e) ->
-          e.preventDefault()
-          $(this)
-            .find("""input[type="submit"]""").attr("disabled", "disabled").end()
-            .find(".loading").show()
-          $("#message iframe").contents().find("body").xslt("/messages/#{id}/analysis.xml", "/stylesheets/analysis.xsl")
-
   refresh: ->
     $.getJSON "/messages", (messages) =>
       $.each messages, (i, message) =>
         unless @haveMessage message
           @addMessage message
+      @updateMessagesCount()

   subscribe: ->
     if WebSocket?


Fixed an issue with one of the specs

Fixed an issue with one of the specs

--- test/coffee/aaDatepickerSpec.coffee
+++ test/coffee/aaDatepickerSpec.coffee
@@ -105,12 +105,13 @@
         scope = $rootScope
         scope.myDate = new Date()
         element = $compile("<aa-datepicker placeholder='Choose a Date' ng-model='myDate' />")(scope)
-        scope.$digest()
+        scope.$apply()
       )

       it "Should add a 'today' class to the today td", ->
         expect($(element).find('.today').length).toEqual(1)
         element.scope().nextMonth()
+        element.scope().nextMonth()
         scope.$apply()
         expect($(element).find('.today').length).toEqual(0)


Add callback to buildClient

Add callback to buildClient

--- source/graphite.coffee
+++ source/graphite.coffee
@@ -7,7 +7,7 @@

 module.exports.registry = require './registry/registry'

-module.exports.buildClient = ->
+module.exports.buildClient = (callback)->
   browserify   = require 'browserify'
   FS           = require 'fs'
   coffeeify    = require 'coffeeify'
@@ -33,8 +33,7 @@
   b.bundle {standalone: 'Components'}, (err,src)->
     result = UglifyJS.minify(src, {fromString: true})
     FS.unlink './tmp.js'
-    console.log result.code
-    process.exit()
+    callback result.code

 # Builds a browser bundle
 #


Fix getSupportedLanguages test name

Fix getSupportedLanguages test name

--- test/get-supported-languages.coffee
+++ test/get-supported-languages.coffee
@@ -5,7 +5,7 @@
 fs = require "fs"
 path = require "path"

-describe "getLanguageSupport", ->
+describe "getSupportedLanguages", ->

   it "lists all supported languages", ->


SVG renderer obeys text wrapping

SVG renderer obeys text wrapping

--- src/core/shapes.coffee
+++ src/core/shapes.coffee
@@ -498,9 +498,13 @@
   fromJSON: (data) -> createShape('Text', data)

   toSVG: ->
+    # fallback: don't worry about auto-wrapping
     widthString = if @forcedWidth then "width='#{@forcedWidth}px'" else ""
     heightString = if @forcedHeight then "height='#{@forcedHeight}px'" else ""
     textSplitOnLines = @text.split(/\r\n|\r|\n/g)
+
+    if @renderer
+      textSplitOnLines = @renderer.lines

     "
     <text x='#{@x}' y='#{@y}'
@@ -517,9 +521,6 @@

 HANDLE_SIZE = 10
 MARGIN = 4
-handles = [
-  {}
-]
 defineShape 'SelectionBox',
   constructor: (args={}) ->
     @shape = args.shape


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/core_object.coffee
+++ src/core_object.coffee
@@ -159,7 +159,7 @@
                 moduleName = key

           if moduleName
-            unless I.included_modules.include moduleName
+            unless I.includedModules.include moduleName
               I.included_modules.push moduleName
               self.extend Module(I, self)
           else


SpecHelper: fix a description

SpecHelper: fix a description

--- spec/SpecHelper.coffee
+++ spec/SpecHelper.coffee
@@ -70,7 +70,7 @@
         events.push(toValue(event))), extraCheck

 verifyPLateEval = (srcF, expectedEvents) ->
-  verifyPropertyWith "(single subscriber)", srcF, expectedEvents, (src, events, done) ->
+  verifyPropertyWith "(late eval)", srcF, expectedEvents, (src, events, done) ->
     src.subscribe (event) ->
       if event.isEnd()
         done()


Perhaps more testing of the facade would be good

--- spec/blisp_spec.coffee
+++ spec/blisp_spec.coffee
@@ -14,3 +14,6 @@
   it "exposes a module", ->
     expect(blisp).toBeDefined()

+  it "has a method to generate moz parser AST", ->
+    expect(blisp.generate).toBeDefined()
+


Typo.

Typo.

--- test/unit/dredd-command-test.coffee
+++ test/unit/dredd-command-test.coffee
@@ -233,7 +233,7 @@
         assert.equal dc.dreddInstance.configuration.server, "http://localhost:#{PORT}"

-  describe "when called w/ OR wo/ exitting arguments", () ->
+  describe "when called w/ OR wo/ exiting arguments", () ->
     describe '--help', ->
       before (done) ->
         execCommand argv: ['--help'], ->


async server start/close

async server start/close

--- spec/stars-spec.coffee
+++ spec/stars-spec.coffee
@@ -31,18 +31,27 @@
       response.sendFile path.join(__dirname, 'fixtures', 'test-module2-2.0.0.tgz')
     app.get '/packages/test-module', (request, response) ->
       response.sendFile path.join(__dirname, 'fixtures', 'install-test-module.json')
+
     server =  http.createServer(app)
-    server.listen(3000)

-    atomHome = temp.mkdirSync('apm-home-dir-')
-    process.env.ATOM_HOME = atomHome
-    process.env.ATOM_API_URL = "http://localhost:3000"
-    process.env.ATOM_ELECTRON_URL = "http://localhost:3000/node"
-    process.env.ATOM_PACKAGES_URL = "http://localhost:3000/packages"
-    process.env.ATOM_ELECTRON_VERSION = 'v0.10.3'
+    live = false
+    server.listen 3000, '127.0.0.1', ->
+      atomHome = temp.mkdirSync('apm-home-dir-')
+      process.env.ATOM_HOME = atomHome
+      process.env.ATOM_API_URL = "http://localhost:3000"
+      process.env.ATOM_ELECTRON_URL = "http://localhost:3000/node"
+      process.env.ATOM_PACKAGES_URL = "http://localhost:3000/packages"
+      process.env.ATOM_ELECTRON_VERSION = 'v0.10.3'
+      process.env.npm_config_registry = 'http://localhost:3000/'
+
+      live = true
+
+    waitsFor -> live

   afterEach ->
-    server.close()
+    closed = false
+    server.close -> closed = true
+    waitsFor -> closed

   describe "when no user flag is specified", ->
     it 'lists your starred packages', ->


Store all subjects in an object

Store all subjects in an object

--- src/scripts/collections/subjects.coffee
+++ src/scripts/collections/subjects.coffee
@@ -8,19 +8,21 @@
   return new class Subjects extends Backbone.Collection
     url: () -> "#{SERVER}/extras"

+    list:
+      'Arts'                      : '/images/subjects/art.jpg'
+      'Business'                  : '/images/subjects/business.jpg'
+      'Humanities'                : '/images/subjects/humanities.jpg'
+      'Mathematics and Statistics': '/images/subjects/math.jpg'
+      'Science and Technology'    : '/images/subjects/science.jpg'
+      'Social Sciences'           : '/images/subjects/social_science.jpg'
+
     initialize: () ->
       @fetch({reset: true})

     parse: (response, options) ->
       subjects = response.subjects

-      _.each subjects, (subject) ->
-        switch subject.name
-          when 'Arts' then subject.image = '/images/subjects/art.jpg'
-          when 'Business' then subject.image = '/images/subjects/business.jpg'
-          when 'Humanities' then subject.image = '/images/subjects/humanities.jpg'
-          when 'Mathematics and Statistics' then subject.image = '/images/subjects/math.jpg'
-          when 'Science and Technology' then subject.image = '/images/subjects/science.jpg'
-          when 'Social Sciences' then subject.image = '/images/subjects/social_science.jpg'
+      _.each subjects, (subject) =>
+        subject.image = @list[subject.name]

       return subjects


sqlite -- basic working wrapper

sqlite -- basic working wrapper

--- salvus/sqlite.coffee
+++ salvus/sqlite.coffee
@@ -85,6 +85,8 @@
             opts.cb?(err, rows)

     _format: (x, cond) =>
+        if not cond or misc.len(cond) == 0
+            return {query:"", vals:[]}
         q = []
         vals = []
         for k, v of cond
@@ -161,11 +163,24 @@
             table : undefined
             where : {}
             cb    : undefined
+        w = @_where(opts.where)
+        @sql
+            query : "DELETE FROM #{opts.table} #{w.query}"
+            vals  : w.vals
+            cb    : opts.cb

     select: (opts={}) =>
         opts = defaults opts,
             table   : required    # string -- the table to query
-            columns : required    # list -- columns to extract
+            columns : undefined   # list -- columns to extract
             where   : undefined   # object -- conditions to impose; undefined = return everything
             cb      : required    # callback(error, results)
-
+        w = @_where(opts.where)
+        if opts.columns?
+            columns = opts.columns.join(',')
+        else
+            columns = '*'
+        @sql
+            query : "SELECT #{columns} FROM #{opts.table} #{w.query}"
+            vals  : w.vals
+            cb    : opts.cb


be safe and always return id until slugs work

--- models/person.coffee
+++ models/person.coffee
@@ -91,7 +91,7 @@
         promise

 # instance methods
-PersonSchema.method 'toString', -> @slug or @id
+PersonSchema.method 'toString', -> @id
 ROLES.forEach (t) ->
   PersonSchema.virtual(t).get -> @role == t
 PersonSchema.virtual('login').get ->


organise widgets svc

organise widgets svc

--- src/services/widgets/widgets.svc.coffee
+++ src/services/widgets/widgets.svc.coffee
@@ -1,51 +1,23 @@
 angular
   .module('impac.services.widgets', [])
-  .service 'ImpacWidgetsSvc', ($q, $http, $log, ImpacRoutes, ImpacMainSvc, ImpacDashboardsSvc, ImpacDeveloper) ->
+  .service 'ImpacWidgetsSvc', ($q, $http, $log, $timeout, ImpacRoutes, ImpacMainSvc, ImpacDashboardsSvc, ImpacDeveloper) ->

     _self = @
-
+# ====================================
+# Getters
+# ====================================
     # Simply forward the getter for objects that remain stored in other services
     @getSsoSessionId = ImpacMainSvc.getSsoSessionId

+
+# ====================================
+# Load and initialize
+# ====================================
     @load = (force=false) ->
       if !_self.getSsoSessionId()? || force
         $q.all([ImpacMainSvc.loadUserData(force), ImpacDashboardsSvc.load(force)])
       else
         $q.resolve()
-
-
-    @create = (opts) ->
-      deferred = $q.defer()
-
-      _self.load().then(
-        ->
-          dashboard = ImpacDashboardsSvc.getCurrentDashboard()
-          data = { widget: opts }
-
-          # form a http request or a stubbed request which returns a promise.
-          if ImpacDeveloper.isWidgetStubbed(data.widget)
-            request = ImpacDeveloper.createWidgetStub(data.widget, dashboard)
-          else
-            request = $http.post(ImpacRoutes.widgets.create(dashboard.id), data)
-
-          request.then(
-            (success) ->
-              newWidget = success.data
-              dashboard.widgets.push(newWidget)
-              ImpacDashboardsSvc.callbacks.widgetAdded.notify(newWidget)
-              deferred.resolve(newWidget)
-
-            (error) ->
-              $log.error("Impac! - WidgetsSvc: Cannot create widget on dashboard #{dashboard.id}")
-              deferred.reject(error)
-          )
-
-        (error) ->
-          $log.error("Impac! - WidgetsSvc: Error while trying to load the service")
-          deferred.reject(error)
-      )
-
-      return deferred.promise

     isWidgetInCurrentDashboard = (widgetId) ->
@@ -150,9 +122,9 @@
           )
       )

-    #====================================
-    # CRUD methods
-    #====================================
+# ====================================
+# CRUD methods
+# ====================================

     @show = (widget, refreshCache=false) ->
       deferred = $q.defer()
@@ -164,13 +136,12 @@
             deferred.reject("trying to load a widget (id: #{widget.id}) that is not in currentDashboard")

           else
-            data = {
+            data =
               owner: widget.owner
               sso_session: _self.getSsoSessionId()
               metadata: widget.metadata
               engine: widget.category
-            }
-            angular.extend(data, {refresh_cache: true}) if refreshCache
+            data.refresh_cache = true if refreshCache

             dashboard = ImpacDashboardsSvc.getCurrentDashboard()

@@ -188,7 +159,8 @@
                   setting.initialize() if angular.isDefined(setting.initialize)

                 # Formats the chart when necessary
-                widget.format() if angular.isDefined(widget.format)
+                if angular.isDefined(widget.format)
+                  widget.format()

                 deferred.resolve widget

@@ -202,6 +174,40 @@
                 widget.processError(errorResponse.data.error) if angular.isDefined(widget.processError) && errorResponse.data? && errorResponse.data.error
                 deferred.reject(errorResponse)
             )
+
+        (error) ->
+          $log.error("Impac! - WidgetsSvc: Error while trying to load the service")
+          deferred.reject(error)
+      )
+
+      return deferred.promise
+
+
+    @create = (opts) ->
+      deferred = $q.defer()
+
+      _self.load().then(
+        ->
+          dashboard = ImpacDashboardsSvc.getCurrentDashboard()
+          data = { widget: opts }
+
+          # form a http request or a stubbed request which returns a promise.
+          if ImpacDeveloper.isWidgetStubbed(data.widget)
+            request = ImpacDeveloper.createWidgetStub(data.widget, dashboard)
+          else
+            request = $http.post(ImpacRoutes.widgets.create(dashboard.id), data)
+
+          request.then(
+            (success) ->
+              newWidget = success.data
+              dashboard.widgets.push(newWidget)
+              ImpacDashboardsSvc.callbacks.widgetAdded.notify(newWidget)
+              deferred.resolve(newWidget)
+
+            (error) ->
+              $log.error("Impac! - WidgetsSvc: Cannot create widget on dashboard #{dashboard.id}")
+              deferred.reject(error)
+          )

         (error) ->
           $log.error("Impac! - WidgetsSvc: Error while trying to load the service")


webapp: let copy operation write to clipboard (if allowed)

--- src/smc-webapp/copy-paste-buffer.coffee
+++ src/smc-webapp/copy-paste-buffer.coffee
@@ -15,4 +15,7 @@

 exports.set_buffer = (s) ->
     buffer = s ? ''
+    try
+        # https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
+        document.execCommand('copy')
     return


change exit confirmation to not use "unsynced docs", since it doesn't work right now anyways.  Maybe someday (or maybe not really necessary, since right now refreshing always looses something, namely what docs you have open).

--- src/smc-webapp/exit_confirmation.coffee
+++ src/smc-webapp/exit_confirmation.coffee
@@ -30,23 +30,19 @@
 ###

 {top_navbar} = require('./top_navbar')
-{unsynced_docs} = require('./syncdoc')
+#{unsynced_docs} = require('./syncdoc')

 {redux} = require('./smc-react')

 window.onbeforeunload = (e) ->
-    mesg = undefined
-    if not unsynced_docs()
-        if redux.getStore('account').get_confirm_close()
-            mesg = "Your data is saved, but you asked for confirmation before leaving SageMathCloud (in account settings)."
-        else
-            return
-
-    e.cancelBubble = true  # e.cancelBubble is supported by IE - this will kill the bubbling process.
-    if not mesg?
-        mesg = "Some documents haven't successfully synchronized with the server yet.  Leaving now may result in lost work."
-    e.returnValue = mesg
-    if e.stopPropagation
-        e.stopPropagation()
-        e.preventDefault()
+    if redux.getStore('account').get_confirm_close()
+        mesg = ""
+    else
+        mesg = undefined
+    if mesg?
+        e.cancelBubble = true  # e.cancelBubble is supported by IE - this will kill the bubbling process.
+        e.returnValue = mesg
+        if e.stopPropagation
+            e.stopPropagation()
+            e.preventDefault()
     return mesg


chore(tests): Use temp folder based on operation system.

--- src/spec/integration/export.spec.coffee
+++ src/spec/integration/export.spec.coffee
@@ -1,4 +1,5 @@
 _ = require 'underscore'
+path = require 'path'
 _.mixin require('underscore-mixins')
 Promise = require 'bluebird'
 fs = Promise.promisifyAll require('fs')
@@ -7,6 +8,9 @@
 {Export} = require '../../lib/main'
 extract = require 'extract-zip'
 extractArchive = Promise.promisify(extract)
+tmp = require 'tmp'
+# will clean temporary files even when an uncaught exception occurs
+tmp.setGracefulCleanup()

 describe 'Export integration tests', ->

@@ -148,7 +152,8 @@

   it 'should do a full export', (done) ->
-    file = '/tmp/output.zip'
+    tempDir = tmp.dirSync({ unsafeCleanup: true })
+    file = path.join tempDir.name, 'output.zip'
     expectedHeader = '_published,_hasStagedChanges,productType,variantId,id,sku,prices,tax,categories,images,name.en,description.en,slug.en,metaTitle.en,metaDescription.en,metaKeywords.en,searchKeywords.en,attr-text-n,attr-ltext-n.en,attr-enum-n,attr-lenum-n,attr-number-n,attr-boolean-n,attr-money-n,attr-date-n,attr-time-n,attr-datetime-n,attr-ref-product-n,attr-ref-product-type-n,attr-ref-channel-n,attr-ref-state-n,attr-ref-zone-n,attr-ref-shipping-method-n,attr-ref-category-n,attr-ref-review-n,attr-ref-key-value-n,attr-set-text-n,attr-set-ltext-n.en,attr-set-enum-n,attr-set-lenum-n,attr-set-number-n,attr-set-boolean-n,attr-set-money-n,attr-set-date-n,attr-set-time-n,attr-set-datetime-n,attr-set-ref-product-n,attr-set-ref-product-type-n,attr-set-ref-channel-n,attr-set-ref-state-n,attr-set-ref-zone-n,attr-set-ref-shipping-method-n,attr-set-ref-category-n,attr-set-ref-review-n,attr-set-ref-key-value-n,attr-text-u,attr-ltext-u.en,attr-enum-u,attr-lenum-u,attr-number-u,attr-boolean-u,attr-money-u,attr-date-u,attr-time-u,attr-datetime-u,attr-ref-product-u,attr-ref-product-type-u,attr-ref-channel-u,attr-ref-state-u,attr-ref-zone-u,attr-ref-shipping-method-u,attr-ref-category-u,attr-ref-review-u,attr-ref-key-value-u,attr-set-text-u,attr-set-ltext-u.en,attr-set-enum-u,attr-set-lenum-u,attr-set-number-u,attr-set-boolean-u,attr-set-money-u,attr-set-date-u,attr-set-time-u,attr-set-datetime-u,attr-set-ref-product-u,attr-set-ref-product-type-u,attr-set-ref-channel-u,attr-set-ref-state-u,attr-set-ref-zone-u,attr-set-ref-shipping-method-u,attr-set-ref-category-u,attr-set-ref-review-u,attr-set-ref-key-value-u,attr-text-cu,attr-ltext-cu.en,attr-enum-cu,attr-lenum-cu,attr-number-cu,attr-boolean-cu,attr-money-cu,attr-date-cu,attr-time-cu,attr-datetime-cu,attr-ref-product-cu,attr-ref-product-type-cu,attr-ref-channel-cu,attr-ref-state-cu,attr-ref-zone-cu,attr-ref-shipping-method-cu,attr-ref-category-cu,attr-ref-review-cu,attr-ref-key-value-cu,attr-set-text-cu,attr-set-ltext-cu.en,attr-set-enum-cu,attr-set-lenum-cu,attr-set-number-cu,attr-set-boolean-cu,attr-set-money-cu,attr-set-date-cu,attr-set-time-cu,attr-set-datetime-cu,attr-set-ref-product-cu,attr-set-ref-product-type-cu,attr-set-ref-channel-cu,attr-set-ref-state-cu,attr-set-ref-zone-cu,attr-set-ref-shipping-method-cu,attr-set-ref-category-cu,attr-set-ref-review-cu,attr-set-ref-key-value-cu,attr-text-sfa,attr-ltext-sfa.en,attr-enum-sfa,attr-lenum-sfa,attr-number-sfa,attr-boolean-sfa,attr-money-sfa,attr-date-sfa,attr-time-sfa,attr-datetime-sfa,attr-ref-product-sfa,attr-ref-product-type-sfa,attr-ref-channel-sfa,attr-ref-state-sfa,attr-ref-zone-sfa,attr-ref-shipping-method-sfa,attr-ref-category-sfa,attr-ref-review-sfa,attr-ref-key-value-sfa,attr-set-text-sfa,attr-set-ltext-sfa.en,attr-set-enum-sfa,attr-set-lenum-sfa,attr-set-number-sfa,attr-set-boolean-sfa,attr-set-money-sfa,attr-set-date-sfa,attr-set-time-sfa,attr-set-datetime-sfa,attr-set-ref-product-sfa,attr-set-ref-product-type-sfa,attr-set-ref-channel-sfa,attr-set-ref-state-sfa,attr-set-ref-zone-sfa,attr-set-ref-shipping-method-sfa,attr-set-ref-category-sfa,attr-set-ref-review-sfa,attr-set-ref-key-value-sfa'
     expectedProduct = 'false,false,ImpEx with all types,1,MONGO_ID,,,,,,Foo,,foo,,,,'
     expectedVariant =   ',,,2,,123,,,,,,,,,,,,,,,lenum1,,,,,,,,,,,,,,,,,,,lenum1;lenum2'
@@ -164,18 +169,18 @@
         return Promise.reject "Archive was not generated"
     .then ->
       console.log "Archive was generated successfully"
-      extractArchive(file, {dir: '/tmp'})
+      extractArchive(file, {dir: tempDir.name})
     .then ->

       try
-        fs.statSync '/tmp/products'
-        exportedFolder = '/tmp/products/'
+        exportedFolder = path.join tempDir.name, 'products'
+        fs.statSync exportedFolder # test if file was created
         files = fs.readdirSync(exportedFolder)
         expect(files.length).toBe 1
-        csvFile = exportedFolder + files[0]
-        fs.statSync csvFile
+        csvFile = path.join exportedFolder, files[0]
+        fs.statSync csvFile # test if file was created
       catch e
-        return Promise.reject 'Archive was not successfully parsed'
+        return Promise.reject 'Archive was not successfully created or parsed'

       fs.readFileAsync csvFile, {encoding: 'utf8'}
     .then (content) =>
@@ -197,4 +202,6 @@
         expect(csv[1]).toBe expectedProductLine
     .then ->
       done()
-    .catch (err) -> done _.prettify(err)+    .catch (err) -> done _.prettify(err)
+    .finally ->
+      tempDir.removeCallback()


Add more expectations for price import. Refs #34.

--- src/spec/integration/import.spec.coffee
+++ src/spec/integration/import.spec.coffee
@@ -74,6 +74,16 @@
       .then (result) ->
         expect(_.size result).toBe 1
         expect(result[0]).toBe '[row 2] New product created.'
+        @client.products.where("productType(id=\"#{@productType.id}\")").fetch()
+      .then (result) ->
+        expect(_.size result.body.results).toBe 1
+        p = result.body.results[0].masterData.staged
+        expect(_.size p.masterVariant.prices).toBe 3
+        prices = p.masterVariant.prices
+        expect(prices[0]).toEqual { value: { currencyCode: 'EUR', centAmount: 899 } }
+        expect(prices[1]).toEqual { country: 'CH', value: { currencyCode: 'EUR', centAmount: 999 } }
+        expect(prices[2].channel.typeId).toBe 'channel'
+        expect(prices[2].channel.id).toBeDefined()
         done()
       .fail (err) ->
         done(_.prettify err)


Forgot user agent in w3c logger

Forgot user agent in w3c logger

--- src/streammachine/log_controller.coffee
+++ src/streammachine/log_controller.coffee
@@ -109,7 +109,7 @@
             #console.log "in w3c logger with ", level, msg, meta, cb

             # for a valid w3c log, level should == "request", meta.
-            logline = "#{meta.ip} #{meta.ip} #{strftime(meta.time,"%F %T")} #{meta.path} 200 #{meta.bytes} #{meta.seconds}\n"
+            logline = "#{meta.ip} #{meta.ip} #{strftime(meta.time,"%F %T")} #{meta.path} 200 #{escape(meta.ua)} #{meta.bytes} #{meta.seconds}\n"

             if @file
                 # make sure there aren't any queued writes


ping: work around failures; better logs

ping: work around failures; better logs

--- client/app/MainApp/monitor_status.coffee
+++ client/app/MainApp/monitor_status.coffee
@@ -83,9 +83,6 @@
       @printReport()
       @reset()

-    @on "webtermDown", (channel) ->
-      @emit "onlyChannelDown", "webterm"
-
     @on "internetDown", ->
       status = KD.getSingleton "status"
       status.internetDown()
@@ -96,12 +93,9 @@
     notifications =
       internetUp : "All systems go!"
       internetDown: "Your internet is down."
-      kodingDown: "Koding is down."
+      brokerDown: "Broker is down."
       kitesDown: "Kites are down."
-      sharedHostingDown: "SharedHosting is down."
-      webtermDown: "Webterm is down."
-      bongoDown: "Bongo is down"
-      brokerDown: "Broker is Down."
+      bongoDown: "Bongo is down."
       undefined: "Sorry, something went wrong."

     msg = notifications[reason] or notifications["undefined"]
@@ -127,11 +121,9 @@
   deductReasonForFailure: ->
     reasons = {}
     reasons.internetDown      = ["bongo", "broker", "external"]
-    reasons.kodingDown        = ["bongo", "broker"]
-    reasons.kitesDown         = ["sharedHosting", "webterm"]
     reasons.brokerDown        = ["broker"]
+    reasons.kitesDown         = ["os"]
     reasons.bongoDown         = ["bongo"]
-    reasons.webtermDown       = ["webterm"]

     for reason, items of reasons
       intersection = _.intersection items, @failedPings
@@ -180,18 +172,24 @@

   KD.troubleshoot = (showNotifications=true)->
     monitorItems = KD.getSingleton("monitorItems").items
+
+    if monitorItems.length == 1
+      log "no services connected; possible auth/social worker down"
+      return
+
     monitor = new MonitorStatus monitorItems
     monitor.showNotifications = showNotifications
     monitor.run()

-  window.jsonp = ->
-    KD.externalPong()
-
-  brokerInterval  = null
-  failureCallback = null
-  lastPong        = null
+  window.jsonp =-> KD.externalPong()
+
+  brokerInterval       = null
+  failureCallback      = null
+  lastPong             = null
+  timesTroubleshootRan = 0

   # use broker ping to determine internet connection
+  # TODO: refactor this ugliness
   pingBrokerOnInterval = ->
     brokerInterval = setInterval ->
       clearTimeout failureCallback
@@ -215,23 +213,25 @@
         lastPong = Date.now()

       failureCallback = setTimeout ->
-        log 'broker ping failed, running troubleshoot', failureCallback
-        KD.troubleshoot(false)
+        if timesTroubleshootRan > 3
+          log "broker ping failed too many times, stopping troubleshoot"
+          return
+
+        log 'broker ping failed, running troubleshoot'
+        KD.troubleshoot false
+        timesTroubleshootRan++
       , 3000

       KD.remote.mq.ping -> brokerPong()
-
     , 5000

   KD.remote.on 'connected', ->
+    resetIntervals()
     pingBrokerOnInterval()
-    log 'connected, starting broker ping', brokerInterval
-
-  KD.remote.on 'disconnected', ->
-    return unless brokerInterval?
-
-    log 'disconnected, stopping broker ping'
-
+
+  KD.remote.on 'disconnected', -> resetIntervals()
+
+  resetIntervals =->
     clearInterval brokerInterval
     clearTimeout failureCallback
     brokerInterval  = null


Auto contact autocomplete

Auto contact autocomplete

--- client/app/components/mails_input.coffee
+++ client/app/components/mails_input.coffee
@@ -102,19 +102,17 @@

     onQuery: ->
         query = @refs.contactInput.getDOMNode().value.split(',').pop().trim()
-        if query.length > 2
+        if query.length > 3
             ContactActionCreator.searchContactLocal query
             @setState open: true
             return true
         else
+            if @state.open
+                @setState contacts: null, open: false
             return false

     onKeyDown: (evt) ->
         switch evt.key
-            when "Tab"
-                if @onQuery()
-                    evt.preventDefault()
-                    return false
             when "Enter"
                 if @state.contacts?.count() > 0
                     @onContact
@@ -134,6 +132,11 @@
                 node.value = node.value.trim()
             when "Escape"
                 @setState contacts: null, open: false
+            else
+                if (evt.key.toString().length is 1)
+                    if @onQuery()
+                        evt.preventDefault()
+                        return false

     onContact: (contact) ->


realtime: add defensive error check in handleError

--- client/app/lib/realtimecontroller.coffee
+++ client/app/lib/realtimecontroller.coffee
@@ -378,6 +378,9 @@

   handleError: (err) ->
+
+    return  unless err
+
     {message, payload} = err

     if @isDisconnected err


Fix reversed events hash for ConceptSearch, add autocomplete handler

--- coffee/cilantro/ui/concept/search.coffee
+++ coffee/cilantro/ui/concept/search.coffee
@@ -9,7 +9,8 @@
         className: 'concept-search search'

         events:
-            'input typeahead:selected': 'focusConcept'
+            'typeahead:selected input': 'focusConcept'
+            'typeahead:autocompleted input': 'focusConcept'

         options: ->
             url = c.data.concepts.url()


fix error message for missing module

fix error message for missing module

--- nacl/js/aroma.coffee
+++ nacl/js/aroma.coffee
@@ -119,11 +119,11 @@
     require: (msg, callback) ->
       [_, module] = msg
       @show_loading "Loading module `#{module}`"
-      @file_loader.get_module module, (code) ->
+      @file_loader.get_module module, (code, error) ->
         if code?
           callback ["success", code]
         else
-          callback ["error", "Failed to find module: tried #{url}"]
+          callback ["error", "Failed to find module: tried #{error}"]

     image: (msg, callback) ->
       [_, path] = msg
@@ -407,7 +407,7 @@

   _get: (url, callback) ->
     log ">> getting: #{url}"
-    get url, callback, @fail_fn || ->
+    get url, callback, => @fail_fn? url

   real_path: (path) ->
     if @root?
@@ -432,7 +432,7 @@
     loader = @loaders[loader] if loader
     loader = @default_loader unless loader

-    @on_fail = -> callback null
+    @fail_fn = (url) -> callback null, url

     @pending++
     loader.call this, path, (result) =>


[feat] Raise upload max size to 50MB

[feat] Raise upload max size to 50MB

That way it matches the server limit.

--- client/app/models/photoprocessor.coffee
+++ client/app/models/photoprocessor.coffee
@@ -1,7 +1,7 @@
 # read the file from photo.file using a FileReader
 # create photo.img : an Image object
 readFile = (photo, next) ->
-    if photo.file.size > 10 * 1024 * 1024
+    if photo.file.size > 50 * 1024 * 1024
         return next t 'is too big (max 10Mo)'

     if not photo.file.type.match /image\/.*/


Delay deploy success rate number by 5 minutes

--- client/models/flow-deploy-status.coffee
+++ client/models/flow-deploy-status.coffee
@@ -40,8 +40,10 @@

   query: =>
     yesterday = moment().subtract(1, 'day')
+    five_minutes_ago = moment().subtract(5, 'minutes')
     query = _.cloneDeep FLOW_DEPLOY_STATUS_QUERY
     query.aggs.filter_by_timestamp.filter.range.beginTime.gte = yesterday.valueOf()
+    query.aggs.filter_by_timestamp.filter.range.beginTime.lte = five_minutes_ago.valueOf()
     query

 module.exports = FlowDeployStatus


Adding favorites started

Adding favorites started

--- scripts/src/controllers/Classifier.coffee
+++ scripts/src/controllers/Classifier.coffee
@@ -8,6 +8,7 @@
   Pager = require 'zooniverse/controllers/Pager'

   Subject = require 'models/Subject'
+  Favorite = require 'zooniverse/models/Favorite'
   Classification = require 'models/Classification'

   Tutorial = require 'controllers/Tutorial'
@@ -32,6 +33,7 @@
       'click .species .toggles button': 'changeSpecies'
       'click .species .other-creatures button': 'changeOther'
       'click .species .finished': 'finishSpecies'
+      'click .summary .favorite': 'addFavorite'
       'click .map-toggle img': 'toggleMap'
       'click .talk [value="yes"]': 'goToTalk'
       'click .talk [value="no"]': 'nextSubject'
@@ -151,6 +153,11 @@
     finishSpecies: =>
       @picker.setDisabled true
       @steps.addClass 'finished'
+      @saveClassification()
+
+    addFavorite: =>
+      favorite = Favorite.create subjectIds: [Subject.current.id]
+      favorite.persist()

     toggleMap: (show) =>
       unless typeof show is 'boolean' then show = (do -> arguments[0])


ensuring that if a date before a patients birth date is evaluated for calculateAge the result will be 0

--- Src/coffeescript/cql-execution/src/elm/clinical.coffee
+++ Src/coffeescript/cql-execution/src/elm/clinical.coffee
@@ -49,6 +49,7 @@
     @

 calculateAge = (date1, date2, precision) ->
+  if date1.getTime() - date2.getTime() > 0 then return 0
   value = if precision is "Year"
     monthsDiff(date1,date2) / 12
   else if  precision is "Month"


[#20] When paginating to a next sequence of Transcribe items, apply all relevant filters including active=true


Features [#40423729] and [#40423797]

Features [#40423729] and [#40423797]

Client names and project names now autocomplete.

Meaning that when you start typing a client's name you get a list of all
the existing clients partially matching that client name.
Same for projects.

Additionally, when the user ends up selecting an existing client, the
list of possible typeaheads for projects is subsequently restricted to
the client's projects.

--- app/assets/javascripts/views/staffplans/show.js.coffee
+++ app/assets/javascripts/views/staffplans/show.js.coffee
@@ -5,7 +5,17 @@
   events:
     "click a[data-change-page]": "changePage"
     "click a.add-client": "onAddClientClicked"
+    "change input[data-model=\"Client\"]": "updateProjectTypeAheadOptionList"

+  updateProjectTypeAheadOptionList: (event) ->
+    clientName = $(event.target).val()
+    client = StaffPlan.clients.detect (client) ->
+      client.get('name') is clientName
+    if client?
+      @$el.find("input[data-model=\"Project\"]").typeahead
+        source: client.getProjects().pluck("name")
+        items: 12
+
   onAddClientClicked: (event) =>
     event.preventDefault()
     event.stopPropagation()
@@ -15,6 +25,12 @@
       model: new StaffPlan.Models.Client()

     @appendChild clientView
+    @$el.find('input[data-model="Client"]').typeahead
+      source: StaffPlan.clients.pluck 'name'
+      items: 12
+    @$el.find('input[data-model="Project"]').typeahead
+      source: StaffPlan.projects.pluck 'name'
+      items: 12

   gatherClientsByAssignments: ->
     _.uniq @model.getAssignments().pluck( 'client_id' ).map (clientId) -> StaffPlan.clients.get clientId


Fix header cells string cell className spec

--- spec/header-cells/string-cell-spec.coffee
+++ spec/header-cells/string-cell-spec.coffee
@@ -12,4 +12,4 @@
     expect(showView().$el).toHaveText 'header cell label'

   it 'has a className', ->
-    expect(showView().$el).toHaveClass 'string-cell'
+    expect(showView().$el).toHaveClass 'header-cells-string-cell'


fixes see more + save bug

fixes see more + save bug

--- components/feed/client/feed_item.coffee
+++ components/feed/client/feed_item.coffee
@@ -58,7 +58,7 @@
           @$seeMore.remove()

         @$('.feed-large-artworks-columns').html artworkColumns artworkColumns: artworks.groupByColumnsInOrder(4)
-        @setupArtworkSaveControls artworks
+        @setupArtworkSaveControls artworks.models
     false

   setupArtworkImpressionTracking: (artworks=@model.artworks().models) ->


chore($tests): fix typo in test description

--- specs/support/helpers/spec_helpers.coffee
+++ specs/support/helpers/spec_helpers.coffee
@@ -124,7 +124,7 @@
       describe 'foo', -> spectacular.global[method]()

   cannotBeCalledWithoutPreviousSubject: ->
-    runningSpecs('call in contex without previous subject')
+    runningSpecs('call in context without previous subject')
     .shouldStopWith /called in context without a previous subject/, ->
       describe 'foo', -> spectacular.global[method]()


Fix DE ship for Sabine's Masterpiece.

Fix DE ship for Sabine's Masterpiece.

fixes #404

--- coffeescripts/cards-de.coffee
+++ coffeescripts/cards-de.coffee
@@ -1775,6 +1775,7 @@
             text: '''Nur für Jagdschiffe der Lanzen-Klasse.%LINEBREAK%Nachdem du einen Angriff durchgeführt hast, der getroffen hat, darfst du dem Verteidiger 1 Traktorstrahlmarker zuordnen, falls er innerhalb deines mobilen Feuerwinkels in Reichweite 1-2 ist.'''
         # Wave X
         '''Sabine's Masterpiece''':
+            ship: "TIE-Jäger"
             text: '''TIE Fighter only.%REBELONLY%%LINEBREAK%Your upgrade bar gains the %CREW% and %ILLICIT% upgrade icons.'''
         '''Kylo Ren's Shuttle''':
             text: '''Upsilon-class Shuttle only.%LINEBREAK%At the end of the Combat phase, choose an unstressed enemy ship at Range 1-2.  Its owner must assign a stress token to it or assign a stress token to another ship at Range 1-2 of you that that player controls.'''


Wave 11

Wave 11

-Translated (and added) all the cards from the last wave.
-Added 'ship' tags to the new Starviper and Khiraxz pilots.

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -85,6 +85,9 @@
         "Sabine's TIE Fighter Expansion Pack": "Pack de Expansión Caza TIE de Sabine"
         "Quadjumper Expansion Pack": "Pack de Expansión Saltador Quad"
         "C-ROC Cruiser Expansion Pack": "Pack de Expansión Crucero C-ROC"
+        "TIE Aggressor Expansion Pack": "Pack de Expansión Tie Agresor"
+        "Scurrg H-6 Bomber Expansion Pack": "Pack de Expansión Bombardero Scurrg H-6"
+        "Auzituck Gunship Expansion Pack": "Pack de Expansión Cañonera Auzituck"
     ui:
         shipSelectorPlaceholder: "Selecciona una nave"
         pilotSelectorPlaceholder: "Selecciona un piloto"
@@ -198,6 +201,7 @@
     exportObj.renameShip 'TIE/sf Fighter', 'Caza TIE/sf'
     exportObj.renameShip 'TIE Striker', 'Fustigador TIE'
     exportObj.renameShip 'Upsilon-class Shuttle', 'Lanzadera clase Ípsilon'
+    exportObj.renameShip 'TIE Aggressor', 'TIE Agresor'
     exportObj.renameShip 'A-Wing', 'Ala-A'
     exportObj.renameShip 'B-Wing', 'Ala-B'
     exportObj.renameShip 'E-Wing', 'Ala-E'
@@ -211,6 +215,7 @@
     exportObj.renameShip 'GR-75 Medium Transport', 'Transporte mediano GR-75'
     exportObj.renameShip 'T-70 X-Wing', 'T-70 Ala-X'
     exportObj.renameShip 'U-Wing', 'Ala-U'
+    exportObj.renameShip 'Auzituck Gunship', 'Cañonera Auzituck'
     exportObj.renameShip 'M3-A Interceptor', 'Interceptor M3-A'
     exportObj.renameShip 'StarViper', 'Víbora Estelar'
     exportObj.renameShip 'Aggressor', 'Agresor'
@@ -221,6 +226,7 @@
     exportObj.renameShip 'Lancer-class Pursuit Craft', 'Nave de persecución clase Lancero'
     exportObj.renameShip 'Quadjumper', 'Saltador Quad'
     exportObj.renameShip 'C-ROC Cruiser', 'Crucero C-ROC'
+    exportObj.renameShip 'Scurrg H-6 Bomber', 'Bombardero Scurrg H-6'

     pilot_translations =
         "Wedge Antilles":
@@ -992,9 +998,18 @@
             ship: "Crucero C-ROC"
             name: "Crucero C-ROC"
         'Lieutenant Kestal':
-            text: '''When attacking, you may spend 1 focus token to cancel all of the defender's blank and %FOCUS% results.'''
+            ship: "TIE Agresor"
+            text: '''Cuando ataques, puedes gastar 1 ficha de Concentración para anular todos los resultados %FOCUS% y de cara vacía del defensor.'''
         '"Double Edge"':
-            text: '''Once per round, after you perform a secondary weapon attack that does not hit, you may perform an attack with a different weapon.'''
+            ship: "TIE Agresor"
+            name: "Doble Filo"
+            text: '''Una vez por ronda, después de que efectúes un ataque con un armamento secundario que no impacte, puedes efectuar un ataque con un arma diferente.'''
+        'Onyx Squadron Escort':
+            ship: "TIE Agresor"
+            name: "Escolta del Escuadrón Ónice"
+        'Sienar Specialist':
+            ship: "TIE Agresor"
+            name: "Especialista de Sienar"
         'Viktor Hel':
             ship: "Caza Kihraxz"
             text: '''After defending, if you did not roll exactly 2 defense dice, the attacker receives 1 stress token.'''
@@ -1003,16 +1018,30 @@
         'Wullffwarro':
             text: '''When attacking, if you have no shields and at least 1 Damage card assigned to you, roll 1 additional attack die.'''
         'Captain Nym (Scum)':
-            text: '''You may ignore friendly bombs. When a friendly ship is defending, if the attacker measures range through a friendly bomb token, the defender may add 1 %EVADE% result.'''
+            name: "Capitán Nym (Scum)"
+            ship: "Bombardero Scurrg H-6"
+            text: '''Puedes ignorar las bombas aliadas. Cuando una nave aliada se está defendiendo, si el atacante mide el alcance a través de una ficha de Bomba aliada, el defensor puede añadir 1 resultado%EVADE%.'''
         'Captain Nym (Rebel)':
-            text: '''Once per round, you may prevent a friendly bomb from detonating.'''
+            name: "Capitán Nym (Rebelde)"
+            ship: "Bombardero Scurrg H-6"
+            text: '''Una vez por ronda, puedes impedir que una bomba aliada detone.'''
+        'Lok Revenant':
+            name: "Aparecido de Lok"
+            ship: "Bombardero Scurrg H-6"
+        'Karthakk Pirate ':
+            name: "Pirata de Karthakk"
+            ship: "Bombardero Scurrg H-6"
         'Sol Sixxa':
-            text: '''When dropping a bomb, you may use the (%TURNLEFT% 1) or (%TURNRIGHT% 1) template instead of the (%STRAIGHT% 1) template.'''
+            ship: "Bombardero Scurrg H-6"
+            text: '''Cuando sueltes una bomba, puedes utilizar la plantilla de maniobra (%TURNLEFT% 1) o (%TURNRIGHT% 1) en vez de la plantilla de (%STRAIGHT% 1).'''
         'Dalan Oberos':
+            ship: 'Víbora Estelar'
             text: '''If you are not stressed, when you reveal a turn, bank, or Segnor's Loop maneuver, you may instead treat it as a red Tallon Roll maneuver of the same direction (left or right) using the template of the original revealed maneuver.'''
         'Thweek':
+            ship: 'Víbora Estelar'
             text: '''During setup, before the "Place Forces" step, you may choose 1 enemy ship and assign the "Shadowed" or "Mimicked" Condition card to it.'''
         'Captain Jostero':
+            ship: "Caza Kihraxz"
             text: '''Once per round, after an enemy ship that is not defending against an attack suffers damage or critical damage, you may perform an attack against that ship.'''

     upgrade_translations =
@@ -1652,11 +1681,14 @@
         'Wookiee Commandos':
             text: '''When attacking, you may reroll your %FOCUS% results.'''
         'Synced Turret':
-            text: '''Attack (Target Lock): Attack 1 ship (even a ship outside your firing arc).%LINEBREAK%If the defender is inside your primary firing arc, you may reroll a number of attack dice up to your primary weapon value.'''
+            name: "Torreta sincronizada"
+            text: '''Ataque (Blanco fijado: Ataca a 1 nave (aunque esté fuera de tu arco de fuego).%LINEBREAK%Si el defensor está dentro de tu arco de fuego principal, puedes volver a tirar tantos dados de ataque como tu valor de Armamento principal.'''
         'Unguided Rockets':
-            text: '''Attack (focus): Attack 1 Ship.%LINEBREAK%Your attack dice can be modified only by spending a focus token for its standard effect.'''
+            name: "Cohetes no guiados"
+            text: '''Ataque (Concentración): Ataca a 1 nave.%LINEBREAK%Tus dados de ataque solo pueden ser modificados mediante el gasto de una ficha de Concentración para su efecto normal.'''
         'Intensity':
-            text: '''%SMALLSHIPONLY% %DUALCARD%%LINEBREAK%Side A: After you perform a boost or barrel roll action, you may assign 1 focus or evade token to your ship. If you do, flip this card.%LINEBREAK%Side B: (Exhausted) At the end of the Combat phase, you may spend 1 focus or evade token to flip this card.'''
+            name: "Ímpetu"
+            text: '''%SMALLSHIPONLY% %DUALCARD%%LINEBREAK%Cara A: Después de que realices una acción de impulso o de tonel volado, puedes asignar 1 ficha de Concentración o de Evasión a tu nave. Si lo haces, dale la vuelta a esta carta.%LINEBREAK%Cara B: (Agotada) Al final de la fase de Combate, puedes gasta 1 ficha de Concentración o de Evasión para darle la vuelta a esta carta.'''
         'Jabba the Hutt':
             name: "Jabba el Hutt"
             text: '''%SCUMONLY%%LINEBREAK%Cuando equipes esta carta, coloca 1 ficha de Material Ilícito encima de cada carta de Mejora %ILLICIT% en tu escuadrón. Cuando debas descartar una carta de Mejora, en vez de eso puedes descartar 1 ficha de Material ilícito que esté encima de esa carta.'''


Reorder expectations in binary-search-tree tests

This matches the jasmine conventions, and makes error messages less
confusing.

--- binary-search-tree/binary_search_tree_test.spec.coffee
+++ binary-search-tree/binary_search_tree_test.spec.coffee
@@ -1,4 +1,4 @@
-Bst = require './example'
+Bst = require './bst'

 recordAllData = (bst) ->
   out = []
@@ -14,22 +14,22 @@
     four = new Bst(4)
     four.insert(2)

-    expect(4).toEqual four.data
-    expect(2).toEqual four.left.data
+    expect(four.data).toEqual 4
+    expect(four.left.data).toEqual 2

   xit 'inserting same', ->
     four = new Bst(4)
     four.insert(4)

-    expect(4).toEqual four.data
-    expect(4).toEqual four.left.data
+    expect(four.data).toEqual 4
+    expect(four.left.data).toEqual 4

   xit 'inserting right', ->
     four = new Bst(4)
     four.insert(5)

-    expect(4).toEqual four.data
-    expect(5).toEqual four.right.data
+    expect(four.data).toEqual 4
+    expect(four.right.data).toEqual

   xit 'complex tree', ->
     four = new Bst(4)
@@ -40,28 +40,28 @@
     four.insert(7)
     four.insert(5)

-    expect(4).toEqual four.data
-    expect(2).toEqual four.left.data
-    expect(1).toEqual four.left.left.data
-    expect(3).toEqual four.left.right.data
-    expect(6).toEqual four.right.data
-    expect(5).toEqual four.right.left.data
-    expect(7).toEqual four.right.right.data
+    expect(four.data).toEqual 4
+    expect(four.left.data).toEqual 2
+    expect(four.left.left.data).toEqual 1
+    expect(four.left.right.data).toEqual 3
+    expect(four.right.data).toEqual 6
+    expect(four.right.left.data).toEqual 5
+    expect(four.right.right.data).toEqual 7

   xit 'iterating one element', ->
-    expect([4]).toEqual recordAllData(new Bst(4))
+    expect(recordAllData(new Bst(4))).toEqual [4]

   xit 'iterating over smaller element', ->
     four = new Bst(4)
     four.insert(2)

-    expect([2, 4]).toEqual recordAllData(four)
+    expect(recordAllData(four)).toEqual [2, 4]

   xit 'iterating over larger element', ->
     four = new Bst(4)
     four.insert(5)

-    expect([4, 5]).toEqual recordAllData(four)
+    expect(recordAllData(four)).toEqual [4, 5]

   xit 'iterating over complex tree', ->
     four = new Bst(4)
@@ -72,5 +72,4 @@
     four.insert(7)
     four.insert(5)

-    expect([1, 2, 3, 4, 5, 6, 7]).toEqual recordAllData(four)
-
+    expect(recordAllData(four)).toEqual [1, 2, 3, 4, 5, 6, 7]


Remove using FactlinkApp.Overlay in the tour. This creates a regression, but a regression to something we didn't consider a bug for a long time, and which will be fixed when moving the semi-transparent layer.

--- core/app/assets/javascripts/tour.coffee
+++ core/app/assets/javascripts/tour.coffee
@@ -115,12 +115,6 @@
           @popoverRemove '.factlink.fl-first'
           @state.transition()

-        onopen_modal: =>
-          FactlinkApp.Overlay.show()
-
-        onclose_modal: =>
-          FactlinkApp.Overlay.hide()
-
   factlinkFirstExists: ->
     @$('.factlink.fl-first').length > 0


removed the mitsuku bypass of commands

removed the mitsuku bypass of commands

--- scripts/chat.coffee
+++ scripts/chat.coffee
@@ -63,7 +63,7 @@
 		txt = msg.match[1]
 		isCmd = catchCmd txt
 		isJ = catchJ txt
-		if !_.size isCmd
+# 		if !_.size isCmd
 			if _.size isJ then jtalk dict[isJ], msg, robot else mtalk txt, msg, robot

 	# greeting for when new member enters the room


Remove test code

Remove test code

--- scripts/goal.coffee
+++ scripts/goal.coffee
@@ -98,9 +98,6 @@
       rate = getRate achieved, attempted
       user_rates[_user] = rate

-    user_rates["test"] = 87
-    user_rates["test2"] = 33
-
     chart_url = getBarCharUrl user_rates, embed_user

     unless data and data.attempted


Make Resize synchronous

Make Resize synchronous

--- components/Resize-node.coffee
+++ components/Resize-node.coffee
@@ -31,9 +31,8 @@
     in: ['path']
     params: ['width', 'height']
     out: ['canvas']
-    async: true
     forwardGroups: true
-  , (payload, groups, out, callback) ->
+  , (payload, groups, out) ->
     width = if c.params.width? then c.params.width else null
     height = if c.params.height? then c.params.height else null
     if width is null and height is null
@@ -60,8 +59,7 @@
           canvas.originalWidth = metadata.width
           canvas.originalHeight = metadata.height
           out.send canvas
-          do callback
     catch err
-      callback err
+      out.error err

   c


arxiv: option for showing abstracts

arxiv: option for showing abstracts

--- scripts/arxiv.coffee
+++ scripts/arxiv.coffee
@@ -6,6 +6,8 @@
 module.exports = (robot) ->
     robot.respond /arxiv (.*)/i, (msg) ->
         keyWords = msg.match[1].split(' ')
+        showAbstract = 'showabstract' in keyWords
+        keyWords = (x for x in keyWords when x != 'showabstract')
         url = 'http://export.arxiv.org/rss/astro-ph'
         parser url, (err,rss)->
             text = []
@@ -24,7 +26,8 @@
                 if keyWords.every(inText)
                     text.push '*' + title + '*'
                     text.push link
-                    # text.push description
+                    if showAbstract
+                        text.push description
                     text.push ''
                     gotNone = false
             if gotNone


left in a debugger

left in a debugger

Hours of sleep: 25
Cups of coffee: 0
Cans of soda: 0
Cans of redbull: 0

--- public/javascripts/people.coffee
+++ public/javascripts/people.coffee
@@ -41,7 +41,6 @@
   $('#page.people-show .votes form.vote').submit (e) ->
     $form = $(this)
     $inputs = $form.find('input, textarea')
-    debugger
     $.ajax
       type: $form.attr 'method'
       url: $form.attr 'action'


colours, colours, everywhere!

colours, colours, everywhere!

--- public/javascripts/tetris.coffee
+++ public/javascripts/tetris.coffee
@@ -20,6 +20,12 @@
       x: 0
       y: 0
     shapes: {}
+    colors: []
+    playerBlockColor:
+      h: Math.floor(Math.random()*360)
+      s:'60%'
+      l:'50%'
+    abandonedBlockColor: "#FF0080"
     playerID: makeGuid()
     viewportSpace: (x,y) ->
       ret = {x: x-(@viewportOffset.x*@gridSize), y: y-(@viewportOffset.y*@gridSize)}
@@ -56,9 +62,9 @@
       for id, shape of @shapes
         for i in [0,1,2,3]
           block = shape.blockPosition(i)
-          @drawBlock(c, block.x, block.y, '#FF6FCF')
+          @drawBlock(c, block.x, block.y, shape.colorString())
     drawBlocks: (c) ->
-      @drawBlock(c, block.x, block.y, "#FF0080") for block in @blocks
+      @drawBlock(c, block.x, block.y, Tetris.abandonedBlockColor) for block in @blocks
     drawBlock: (c, x, y, color) ->
       c.fillStyle = color
       x = @x + (@gridSize * x)
@@ -113,7 +119,7 @@
     draw: (c) ->
       vs = Tetris.viewportSpace(@x*Tetris.gridSize+1, @y*Tetris.gridSize+1)
       return unless vs.visible
-      c.fillStyle = "#FF6FCF"
+      c.fillStyle = Tetris.abandonedBlockColor
       c.fillRect(vs.x, vs.y, Tetris.blockSize, Tetris.blockSize)

@@ -162,17 +168,21 @@
         return false if x >= Tetris.levelSize
         return false if y >= Tetris.viewport.y
       return true
-
-    draw: (c) ->
-      rgba = (o, alpha=1) ->
-        "rgba(#{o.r},#{o.g},#{o.b},#{alpha})"
+
+    colorString: ->
+      alpha = if @owned then 1 else 0.4
+      if @color.r?
+        "rgba(#{@color.r},#{@color.g},#{@color.b},#{alpha})"
+      else
+        "hsla(#{@color.h},#{@color.s},#{@color.l},#{alpha})"
+
+    draw: (c) ->
       drawBlock = (blockDef) =>
         x = (@x*Tetris.gridSize) + (blockDef[0]*Tetris.gridSize) + 1
         y = (@y*Tetris.gridSize) + (blockDef[1]*Tetris.gridSize) + 1
-        alpha = if @owned then 1 else 0.4
         vs = Tetris.viewportSpace(x,y)
         return unless vs.visible
-        c.fillStyle = rgba(@color, alpha)
+        c.fillStyle = @colorString()
         c.fillRect(vs.x, vs.y, Tetris.blockSize, Tetris.blockSize)
       definition = @shapeDefinition(this.rotation)
       drawBlock(definition[0])
@@ -194,7 +204,7 @@
       Tetris.blocks.add(new Tetris.Block(@blockPosition(3)))
       @remove()
       if @owned
-        gs.addEntity(Tetris.Shape.randomShape(x:Tetris.initialShapeOffset(), y:0, color: {r:0, g:0, b:0}, owned: true))
+        gs.addEntity(Tetris.Shape.randomShape(x:Tetris.initialShapeOffset(), y:0, color: Tetris.playerBlockColor, owned: true))

     created: ->
       # console.log('creating a new piece')
@@ -342,7 +352,7 @@
       gs.addEntity( new shapeClass(id: data.id, x:data.x, y:data.y, rotation: data.rotation, color: data.color) )
     for block in info.blocks
       Tetris.blocks.add(new Tetris.Block(x:block.x, y:block.y), false)
-    gs.addEntity(Tetris.Shape.randomShape(x:Tetris.initialShapeOffset(), y:0, color: {r:0, g:0, b:0}, owned: true))
+    gs.addEntity(Tetris.Shape.randomShape(x:Tetris.initialShapeOffset(), y:0, color: Tetris.playerBlockColor, owned: true))

   channel.bind 'refreshLines', (blocks) ->
     Tetris.blocks.reset()


Fixed selection rate limit

Fixed selection rate limit

--- lib/atom_pair.coffee
+++ lib/atom_pair.coffee
@@ -216,10 +216,6 @@

     @pairingChannel.bind 'client-broadcast-initial-marker', (data) => @receiveFriendInfo(data)

-    @pairingChannel.bind 'client-text-select', (data) =>
-      @clearMarkers(data.colour)
-      @markRows(data.rows, data.colour)
-
     @pairingChannel.bind 'client-grammar-sync', (syntax) =>
       grammar = atom.grammars.grammarForScopeName(syntax)
       @editor.setGrammar(grammar)
@@ -235,7 +231,13 @@
       buffer.append(chunk)
       @triggerPush = true

-    @pairingChannel.bind 'client-change', (events) => _.each(events, (event) => @changeBuffer(event) )
+    @pairingChannel.bind 'client-change', (events) =>
+      _.each(events, (event) =>
+        @changeBuffer(event) if event.eventType is 'buffer-change'
+        if event.eventType is 'buffer-selection'
+          console.log 'hello'
+          @updateCollaboratorMarker(event)
+      )

     @pairingChannel.bind 'client-disconnected', (data) =>
       @clearMarkers(data.colour)
@@ -247,19 +249,16 @@
     @editorListeners.add @listenToBufferChanges()
     @editorListeners.add @syncSelectionRange()

+  updateCollaboratorMarker: (data) ->
+    @clearMarkers(data.colour)
+    @markRows(data.rows, data.colour)
+
   listenToBufferChanges: ->
     @buffer.onDidChange (event) =>
       return unless @triggerPush
       deletion = !(event.newText is "\n") and (event.newText.length is 0)
-      event = {deletion: deletion, event: event, colour: @markerColour}
+      event = {deletion: deletion, event: event, colour: @markerColour, eventType: 'buffer-change'}
       @events.push(event)
-
-  triggerEventQueue: ->
-    @eventInterval = setInterval(=>
-      if @events.length > 0
-        @pairingChannel.trigger 'client-change', @events
-        @events = []
-    , 180)

   changeBuffer: (data) ->
     newRange = Range.fromObject(data.event.newRange)
@@ -288,7 +287,14 @@
     @editor.onDidChangeSelectionRange (event) =>
       rows = event.newBufferRange.getRows()
       return unless rows.length > 1
-      @pairingChannel.trigger 'client-text-select', {colour: @markerColour, rows: rows}
+      @events.push {eventType: 'buffer-selection', colour: @markerColour, rows: rows}
+
+  triggerEventQueue: ->
+    @eventInterval = setInterval(=>
+      if @events.length > 0
+        @pairingChannel.trigger 'client-change', @events
+        @events = []
+    , 120)

   syncGrammars: ->
     return @editor.on 'grammar-changed', => @sendGrammar()


removed unnecessary changes part 2

removed unnecessary changes part 2

--- src/application.coffee
+++ src/application.coffee
@@ -102,7 +102,7 @@
 		knex('config').select('value').where(key: 'uuid')
 		knex('app').select()
 	])
-	.then ([[apiKey], [uuid], [version], apps]) ->
+	.then ([[apiKey], [uuid], apps]) ->
 		apiKey = apiKey.value
 		uuid = uuid.value
 		resinAPI.get(


simplify some code / docs

simplify some code / docs

--- src/application.coffee
+++ src/application.coffee
@@ -20,73 +20,19 @@
 			that.controller = {}

 			options = that.options
-
-			configureInstance = ->
-				# The following configuration options are available:
-				#
-				# ### variables
-				#
-				# See the section on MITHGrid.initInstance#addVariable.
-				#
-
-
-				# ### dataStores
-				#
-				# See the section on #addDataStore.
-				#
-				if options?.dataStores?
-					for storeName, config of options.dataStores
-						that.addDataStore storeName, config
-
-				# ### dataViews
-				#
-				# See the section on #addDataView.
-				#
-				if options?.dataViews?
-					for viewName, viewConfig of options.dataViews
-						that.addDataView viewName, viewConfig
-
-				# ### controllers
-				#
-				# See the section on #addController.
-				#
-				if options?.controllers?
-					for cName, cconfig of options.controllers
-						that.addController cName, cconfig
-
-				# ### facets
-				#
-				# See the section on #addFacet.
-				#
-				if options?.facets?
-					for fName, fconfig of options.facets
-						that.addFacet fName, fconfig
-
-				# ### components
-				#
-				# See the section on #addComponent
-				#
-				if options?.components?
-					for cName, cconfig of options.components
-						that.addComponent cName, cconfig
-
-				# ### presentations
-				#
-				# See the section on #addPresentation.
-				#
-				if options?.presentations?
-					for pName, pconfig of options.presentations
-						that.addPresentation pName, pconfig
-
-				# ### plugins
-				#
-				# See the section on #addPlugin.
-				#
-				if options?.plugins?
-					for pconfig in options.plugins
-						that.addPlugin pconfig

 			that.ready = (fn) -> onReady.push fn
+
+			# ### #run
+			#
+			# Finishes configuring the application by running all queued or pending functions registered through
+			# the #ready() method. The #ready() method will be redefined to run functions after the current thread
+			# finishes.
+			that.run = () ->
+				$(document).ready () ->
+					fn() for fn in onReady
+					onReady = []
+					that.ready = (fn) -> fn()

 			# ### #addDataStore
 			#
@@ -103,14 +49,12 @@

 			that.addDataStore = (storeName, config) ->
 				#
-				# The data store automatically has an "Item" type and the "label", "type", and "id" properties.
+				# The data store automatically has an "Item" type and the "type" and "id" properties.
 				#
 				if !that.dataStore[storeName]?
 					store = MITHGrid.Data.Store.initInstance()
 					that.dataStore[storeName] = store
 					store.addType 'Item'
-					store.addProperty 'label',
-						valueType: 'text'
 					store.addProperty 'type',
 						valueType: 'text'
 					store.addProperty 'id',
@@ -317,9 +261,9 @@
 				plugin = pconfig.type.initInstance(pconfig)
 				if plugin?
 					if pconfig?.dataView?
-						# hook plugin up with dataView requested by app configuration
+
 						plugin.dataView = that.dataView[pconfig.dataView]
-						# add
+
 						plugin.dataView.addType type for type, typeInfo of plugin.getTypes()

 						plugin.dataView.addProperty prop, propOptions for prop, propOptions of plugin.getProperties()
@@ -342,15 +286,64 @@
 								presentation.selfRender()

-			configureInstance()
-
-			# ### #run
-			#
-			# Finishes configuring the application by running all queued or pending functions registered through
-			# the #ready() method. The #ready() method will be redefined to run functions after the current thread
-			# finishes.
-			that.run = () ->
-				$(document).ready () ->
-					fn() for fn in onReady
-					onReady = []
-					that.ready = (fn) -> fn()+
+			# In addition to the configuration options for generic MITHGrid object instances,
+			# the following configuration options are available:
+			#
+
+			# ### dataStores
+			#
+			# See the section on #addDataStore.
+			#
+			if options?.dataStores?
+				for storeName, config of options.dataStores
+					that.addDataStore storeName, config
+
+			# ### dataViews
+			#
+			# See the section on #addDataView.
+			#
+			if options?.dataViews?
+				for viewName, viewConfig of options.dataViews
+					that.addDataView viewName, viewConfig
+
+			# ### controllers
+			#
+			# See the section on #addController.
+			#
+			if options?.controllers?
+				for cName, cconfig of options.controllers
+					that.addController cName, cconfig
+
+			# ### facets
+			#
+			# See the section on #addFacet.
+			#
+			if options?.facets?
+				for fName, fconfig of options.facets
+					that.addFacet fName, fconfig
+
+			# ### components
+			#
+			# See the section on #addComponent
+			#
+			if options?.components?
+				for cName, cconfig of options.components
+					that.addComponent cName, cconfig
+
+			# ### presentations
+			#
+			# See the section on #addPresentation.
+			#
+			if options?.presentations?
+				for pName, pconfig of options.presentations
+					that.addPresentation pName, pconfig
+
+			# ### plugins
+			#
+			# See the section on #addPlugin.
+			#
+			if options?.plugins?
+				for pconfig in options.plugins
+					that.addPlugin pconfig
+


Don't tell me what to do :princess:

Don't tell me what to do :princess:

--- scripts/vote.coffee
+++ scripts/vote.coffee
@@ -50,7 +50,7 @@
     # We're not done unless we get three votes
     return if (@votes.yes.length + @votes.no.length) < 3
     # We're not done if there's no difference between the votes
-    return if (Math.abs(@votes.yes.length - @votes.no.length) < 0)
+    return if (Math.abs(@votes.yes.length - @votes.no.length) <= 0)
     # Is this still being contested?
     return if (Date.now() - @votes.prev) < 10000
     @finish()


teams: activity: bind sidebar social events only for koding

--- client/app/lib/activity/sidebar/activitysidebar.coffee
+++ client/app/lib/activity/sidebar/activitysidebar.coffee
@@ -86,17 +86,18 @@
     router
       .on "RouteInfoHandled",          @bound 'deselectAllItems'

-    notificationController
-      .on 'AddedToChannel',            @bound 'accountAddedToChannel'
-      .on 'RemovedFromChannel',        @bound 'accountRemovedFromChannel'
-      .on 'MessageAddedToChannel',     @bound 'messageAddedToChannel'
-      .on 'MessageRemovedFromChannel', @bound 'messageRemovedFromChannel'
-      .on 'ReplyAdded',                @bound 'replyAdded'
-
-      .on 'MessageListUpdated',        @bound 'setPostUnreadCount'
-      .on 'ParticipantUpdated',        @bound 'handleGlanced'
-      # .on 'ReplyRemoved',              (update) -> log update.event, update
-      # .on 'ChannelUpdateHappened',     @bound 'channelUpdateHappened'
+    if isKoding()
+      notificationController
+        .on 'AddedToChannel',            @bound 'accountAddedToChannel'
+        .on 'RemovedFromChannel',        @bound 'accountRemovedFromChannel'
+        .on 'MessageAddedToChannel',     @bound 'messageAddedToChannel'
+        .on 'MessageRemovedFromChannel', @bound 'messageRemovedFromChannel'
+        .on 'ReplyAdded',                @bound 'replyAdded'
+
+        .on 'MessageListUpdated',        @bound 'setPostUnreadCount'
+        .on 'ParticipantUpdated',        @bound 'handleGlanced'
+        # .on 'ReplyRemoved',              (update) -> log update.event, update
+        # .on 'ChannelUpdateHappened',     @bound 'channelUpdateHappened'

     computeController
       .on 'MachineDataModified',       @bound 'updateMachines'


Removed unnecessary resize event handler.

--- client/landing/app/MainApp/maintabs/maintabview.coffee
+++ client/landing/app/MainApp/maintabs/maintabview.coffee
@@ -22,7 +22,6 @@
     @getSingleton("mainView").on "mainViewTransitionEnd", (e) =>
       if e.target is @getSingleton("contentPanel").domElement[0]
         @tabHandleContainer.setWidth @getWidth()
-        @resizeTabHandles()

   # temp fix sinan 27 Nov 12
   # not calling @removePane but @_removePane


routes: disabled login routes for subdomains

--- client/landing/site.landing/coffee/login/routes.coffee
+++ client/landing/site.landing/coffee/login/routes.coffee
@@ -1,7 +1,14 @@
 do ->

-  handler = (callback)-> (options) ->
+  handler = (callback) -> (options) ->
     cb = (app) -> callback app, options
+    groupName = KD.utils.getGroupNameFromLocation()
+
+    # we need to remove this and make it selectively only
+    # for login and register routes
+    # redeem/reset etc should work for groups - SY
+    return KD.singletons.router.handleRoute '/'  if groupName isnt 'koding'
+
     KD.singletons.router.openSection 'Login', null, null, cb


load before quit

load before quit

--- lib/commander.coffee
+++ lib/commander.coffee
@@ -64,7 +64,7 @@
             @panelModel.set 'Loading'
             @loaded = true

-    quit: ->
+    quit: -> if @loaded
         @loaded = false
         @executable.quit()
         @panel.hide()


if an element doesn't have animators, reduce back to pre-persistant-animator overhead

--- src/art/engine/core/epoched_object.coffee
+++ src/art/engine/core/epoched_object.coffee
@@ -508,14 +508,16 @@
   ###
   _applyStateChanges: ->
     @__stateChangeQueued = false
-    pendingAnimators = @_pendingState._animators
-    {frameSecond} = stateEpoch
-    for prop, v of @_pendingState
-      do (v) =>
-        currentValue = @[prop]
-        if pendingAnimators && (animator = pendingAnimators[prop]) && (animator.active || !propsEq currentValue, v)
-          v = animator.animateAbsoluteTime @, @[prop], v, frameSecond
-        @[prop] = v
+    if pendingAnimators = @_pendingState._animators
+      {frameSecond} = stateEpoch
+      for prop, v of @_pendingState
+        do (v) =>
+          currentValue = @[prop]
+          if pendingAnimators && (animator = pendingAnimators[prop]) && (animator.active || !propsEq currentValue, v)
+            v = animator.animateAbsoluteTime @, @[prop], v, frameSecond
+          @[prop] = v
+    else
+      mergeInto @, @_pendingState

   ######################
   #


:lipstick:

:lipstick:

--- src/atom/editor.coffee
+++ src/atom/editor.coffee
@@ -463,7 +463,7 @@
     editor.setCursorScreenPosition(@getCursorScreenPosition())

     this[insertMethod](editor)
-    @parents('#root-view').view().adjustSplitPanes()
+    @rootView().adjustSplitPanes()
     editor

   remove: (selector, keepData) ->


Send output when no gifs match tags.

Send output when no gifs match tags.

--- src/giphy-gifme.coffee
+++ src/giphy-gifme.coffee
@@ -17,6 +17,8 @@
 api_key = process.env.HUBOT_GIPHY_API_KEY or 'dc6zaTOxFJmzC' # <== Giphy's public API key, please request your own!
 rating_limit = process.env.HUBOT_GIPHY_RATING or 'pg'

+Array::empty = -> @.length == 0
+
 getRandomGiphyGif = (msg, tags) ->
   url = "http://api.giphy.com/v1/gifs/random?api_key=#{api_key}&rating=#{rating_limit}"
   if tags and tags[0] != ''
@@ -25,7 +27,12 @@
       url += ('+' + tags[i]) if tags[i].length > 0
   msg.http(url).get() (err, res, body) ->
     response = JSON.parse(body);
-    msg.send(response.data.image_url)
+    if response.data.empty()
+      message = "No gifs match your tags :("
+    else
+      message = response.data.image_url
+
+    msg.send(message)

 module.exports = (robot) ->
   robot.respond /gif me|giphy(.*)/i, (msg) ->


Show LearnMoreView in modal bottom region

--- core/app/backbone/controllers/client_controller.coffee
+++ core/app/backbone/controllers/client_controller.coffee
@@ -45,3 +45,5 @@
       # This is a lame hack. The .factlink-modal ($el.parent()) needs a class
       # to be positioned lower so the top popup can be shown.
       FactlinkApp.mainRegion.$el.parent().addClass('with-top-popup')
+
+      FactlinkApp.bottomRegion.show new LearnMoreView()


Clearer name to indicate temporal dependency

--- core/app/backbone/controllers/search_controller.coffee
+++ core/app/backbone/controllers/search_controller.coffee
@@ -1,8 +1,8 @@
 class window.SearchController extends Backbone.Marionette.Controller

   showSearch: (params={}) ->
-    @listenTo Backbone.history, 'route', (router, name) ->
-      return if name == 'showSearch'
+    @listenTo Backbone.history, 'route', (router, executed_route_name) ->
+      return if executed_route_name == 'showSearch'

       $('.js-navbar-search-box').val('')


Corrected sytax error

Corrected sytax error

--- scripts/bungie-data-helper.coffee
+++ scripts/bungie-data-helper.coffee
@@ -151,7 +151,7 @@

     if message
       attachment['text'] = message
-      attachment['mrkdwn_in']: ['text']
+      attachment['mrkdwn_in'] = ['text']

     return attachment


user-import: check mongo url env var existence

--- scripts/user-importer/main.coffee
+++ scripts/user-importer/main.coffee
@@ -11,7 +11,7 @@
   KONFIG    = require('koding-config-manager').load("main.#{argv.c}")
   mongo     = "mongodb://#{ KONFIG.mongo }"

-  if process.env.MONGO_URL isnt ""
+  if process.env.MONGO_URL
     mongo = process.env.MONGO_URL

   modelPath = '../../workers/social/lib/social/models'


Updated news

Updated news

--- server/materials/data/news.coffee
+++ server/materials/data/news.coffee
@@ -3,9 +3,9 @@

 export default allNews = () ->
     return news([
-        newsItem("Занятий 20 марта во ВШЭ и 23 марта в лицее 40 не будет", String.raw"""
-            Занятий 20 марта во ВШЭ и 23 марта в лицее 40 не будет. Занимайтесь из дома. Если хотите, я готов организовать консультации по zoom, пишите.
-            Далее следите за объявлениями на сайте.
+        newsItem("Очных заннятий до 10 апреля не будет. С 10 апреля планируется возобновление очных занятий.", String.raw"""
+            Очных заннятий до 10 апреля не будет. Занимайтесь из дома. Если хотите, я готов организовать консультации по zoom, пишите.
+            С 10 апреля планируется возобновление очных занятий.
         """),
         newsItem("Опрос про алгопрог", String.raw"""
             Ответьте, пожалуйста, на несколько вопросов. Тем, кто уже отвечал — я там добавил несколько вопросов, можете ответить еще раз.


Fixed passport lookup in req

Fixed passport lookup in req

--- server/packages/user/login.coffee
+++ server/packages/user/login.coffee
@@ -16,7 +16,7 @@

 	receiver: (req, fn) ->

-		{passport} = req
+		passport = req._passport.instance

 		loginPromise = switch req.body.method


Removing <show>chat</show> stanza to prevent Google Talk from setting default Hubot status to "Chatty".

--- src/hubot/gtalk.coffee
+++ src/hubot/gtalk.coffee
@@ -28,7 +28,6 @@

   online: =>
     @client.send new Xmpp.Element('presence')
-      .c('show').t('chat')

     # send raw whitespace for keepalive
     setInterval =>


Ignore also direct messages

Ignore also direct messages

--- src/hubot/gtalk.coffee
+++ src/hubot/gtalk.coffee
@@ -72,6 +72,11 @@
       return

   handleMessage: (stanza) =>
+    if @ignoreUser(stanza.attrs.from)
+      console.log "Ignoring user message because of whitelist: #{stanza.attrs.from}"
+      console.log "  Accepted Users: " + @options.acceptUsers.join(',')
+      console.log "  Accepted Domains: " + @options.acceptDomains.join(',')
+      return

     # Strip the from attribute to get who this is from
     [from, room] = stanza.attrs.from.split '/'
@@ -88,34 +93,37 @@
     @receive new Robot.TextMessage from, message

   handlePresence: (stanza) =>
+    # Check for buddy request
     return if stanza.attrs.type isnt 'subscribe'

+    if @ignoreUser(stanza.attrs.from)
+      console.log "Ignoring user subscribe because of whitelist: #{stanza.attrs.from}"
+      console.log "  Accepted Users: " + @options.acceptUsers.join(',')
+      console.log "  Accepted Domains: " + @options.acceptDomains.join(',')
+      return
+
+    # Accept
+    @client.send new Xmpp.Element('presence',
+      to:   stanza.attrs.from
+      id:   stanza.attrs.id
+      type: 'subscribed'
+    )
+
+  ignoreUser: (user) ->
     ignore = false
+
     if @options.acceptDomains.length > 0 or @options.acceptUsers.length > 0
       ignore = true
-      [from, room] = stanza.attrs.from.split '/'
+      [from, room] = user.split '/'
       if @options.acceptDomains.length > 0
-        [username, domain] = stanza.attrs.from.split '@'
+        [username, domain] = from.split '@'
         ignore = false if domain in @options.acceptDomains

       if @options.acceptUsers.length > 0
         ignore = false if from in @options.acceptUsers

-    # Check for buddy request
-    if stanza.attrs.type is 'subscribe'
-      if not ignore
-        @client.send new Xmpp.Element('presence',
-          to:   stanza.attrs.from
-          id:   stanza.attrs.id
-          type: 'subscribed'
-        )
-      else
-        console.log "We denied: " + stanza.attrs.from
-        # Lets see what we are trying to match
-        console.log "Accepted Users: " + @options.acceptUsers.join(',')
-        console.log "Accepted Domains: " + @options.acceptDomains.join(',')
-
-
+    return ignore
+
   send: (user, strings...) ->
     for str in strings
       message = new Xmpp.Element('message',


remove duplicate image

remove duplicate image

--- src/ihavenoidea.coffee
+++ src/ihavenoidea.coffee
@@ -35,7 +35,6 @@
   'http://imgur.com/xC0JCFK',
   'http://imgur.com/kFNAAeX',
   'http://imgur.com/9s62t7y',
-  'http://imgur.com/QJ4MoXP',
   'http://imgur.com/ugZ2tGn',
   'http://imgur.com/DeyWrC2',
   'http://imgur.com/tkbLyTG',


Scripts: added forgotten entrypoint

Scripts: added forgotten entrypoint

--- workers/social/lib/social/render/loggedout/kodinghome.coffee
+++ workers/social/lib/social/render/loggedout/kodinghome.coffee
@@ -3,6 +3,9 @@
   getStyles    = require './../styleblock'
   fetchScripts = require './../scriptblock'
   getGraphMeta = require './../graphmeta'
+
+  entryPoint         = { slug : "koding", type: "group" }
+  options.entryPoint = entryPoint

   prepareHTML = (scripts)->
     """
@@ -17,7 +20,7 @@
       <!--[if IE]>
       <script>(function(){window.location.href='/unsupported.html'})();</script>
       <![endif]-->
-      #{KONFIG.getConfigScriptTag { roles: ['guest'], permissions: [] } }
+      #{KONFIG.getConfigScriptTag { entryPoint, roles: ['guest'], permissions: [] } }
       #{scripts}
     </body>
     </html>


minor

minor

--- release/lib/texton-linter.coffee
+++ release/lib/texton-linter.coffee
@@ -1,3 +1,5 @@
+# Should this just be about errors?
+linter = window.atom.config.get("texton.UseLinter")
 textOnCore = require './texton-core'
 path = require 'path'

@@ -6,7 +8,6 @@
   scope: "file"
   lintOnFly: true
   lint: (textEditor) =>
-    linter = window.atom.config.get("texton.UseLinter")
     return [] unless linter
     isTextOn = textOnCore.isTextOnEditor textEditor
     return [] unless isTextOn


fixed ui bug

fixed ui bug

--- shared/ironTableController.coffee
+++ shared/ironTableController.coffee
@@ -141,7 +141,7 @@
                 deleteOk: @collection().deleteOk?(record)

         theData =
-            haveData: records? and @_sess("recordCount") > 0
+            haveData: records? and (records.length > 0 or @_sess("recordCount") > 0)
             tableTitle: @_tableTitle()
             recordDisplayStart: @skip() + 1
             recordDisplayStop: @skip() + @increment


[About: #26492497] points collection phase

--- lib/ephemeris.coffee
+++ lib/ephemeris.coffee
@@ -5,6 +5,7 @@
 _       = require("massagist")._
 cliff   = require("cliff")
 degrees = require("lin").degrees
+Points  = require("lin").Points

 class Ephemeris
@@ -25,7 +26,7 @@
     "time": null
     "geo": {"lat": null, "lon": null}
     "dms": false
-    "stuff": [ [0, 3]
+    "stuff": [ [0, 1, 3]
              , [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 15, 17, 18, 19, 20]
              , [136199, 7066, 50000, 90377, 20000]
              ]
@@ -53,6 +54,21 @@
     if treats?
       massage = new Massage treats
       massage.pipe ephemeris.stdout, stream, "ascii"
+    else if @settings.out is "points"
+      # NOTE: temporarily here for deveopment of precious points import.
+      # This needs to be part of treats / massage as it's a json data collection.
+      # Futhermore: this is just about data, not presentation.
+      # The presentation code will be moved to the new "phase".
+      ephemeris.stdout.on "data", (precious) ->
+        points = new Points [], {data: JSON.parse precious}
+        json = points.toJSON()
+        if json.length > 0
+          colors = []
+          colors.push "white" for count in [0.._.size(json[0])]
+          (colors ?= []).push "white" for count in [0.._.size(json[0])]
+          stream.write cliff.stringifyObjectRows json, _.keys(json[0]), colors
+        else stream.write "Given no data."
+        stream.write "\n\n"
     else if @settings.out is "phase"
       # this is a bit ugly because it's easier to not change the precious output
       # will need to at least add an input method to lin's itemerge (soon)
@@ -78,19 +94,20 @@
                 " ": lead + rpad
                 "what": what + rpad
               for key, val of it
-                label = labels[key] ? key
-                switch key
-                  when "0"
-                    objs[idx][label] = degrees.lon(val).rep('str') + rpad
-                  when "3"
-                    rows.push '~' if idx is 0
-                    objs[idx]['~'] = if val < 0 then '℞'.red else ''
-                    # precision, rounding and alignment (if negative not <= -10?)
-                    val = val.toFixed 3
-                    val = (if val < 0 or val >=10  then val else " " + val)
-                    objs[idx][label] = val + rpad
-                  else objs[idx][label] = val + rpad
-                rows.push labels[key] if idx is 0
+                if key is '0' or key is '3' # process just the longitude / speed
+                  label = labels[key] ? key
+                  switch key
+                    when "0"
+                      objs[idx][label] = degrees.lon(val).rep('str') + rpad
+                    when "3"
+                      rows.push '~' if idx is 0
+                      objs[idx]['~'] = if val < 0 then '℞'.red else ''
+                      # precision, rounding and alignment (if negative not <= -10?)
+                      val = val.toFixed 3
+                      val = (if val < 0 or val >=10  then val else " " + val)
+                      objs[idx][label] = val + rpad
+                    else objs[idx][label] = val + rpad
+                  rows.push labels[key] if idx is 0
               idx++
         objs = _.sortBy objs, (obj) -> obj[labels['0']] # longitude-sorted
         colors.push "white" for row in rows


notes about resizer and added flexibility for throttle time

--- src/components/resize-listener-mixin.cjsx
+++ src/components/resize-listener-mixin.cjsx
@@ -12,7 +12,9 @@
     $window: {}

   componentWillMount: ->
-    @makeDebouncedResizer()
+    # need to define @resizeListener so that we can throttle resize effect
+    # and have access to @state.resizeThrottle or @props.resizeThrottle
+    @resizeListener = _.throttle(@resizeEffect, @state.resizeThrottle or @props.resizeThrottle)

   componentDidMount: ->
     @_setSizeState()
@@ -20,9 +22,6 @@

   componentWillUnmount: ->
     window.removeEventListener('resize', @resizeListener)
-
-  makeDebouncedResizer: ->
-    @resizeListener = _.throttle(@resizeEffect, @props.resizeThrottle)

   resizeEffect: (resizeEvent) ->
     @_setSizeState(resizeEvent)


unnecessary new removed

unnecessary new removed

--- server/server.coffee
+++ server/server.coffee
@@ -15,7 +15,7 @@
     workers = {}

     constructor: (config) ->
-        @redis = new redis.createClient
+        @redis = redis.createClient()

         pusher = zeromq.createSocket 'push'
         controller = zeromq.createSocket 'pub'


Remove some references to workspaceView in find view

--- lib/find-view.coffee
+++ lib/find-view.coffee
@@ -93,8 +93,9 @@

   didHide: ->
     @hideAllTooltips()
-    atom.workspaceView.focus()
-    atom.workspaceView.removeClass('find-visible')
+    workspaceElement = atom.views.getView(atom.workspace)
+    workspaceElement.focus()
+    workspaceElement.classList.remove('find-visible')

   hideAllTooltips: ->
     @tooltipSubscriptions.dispose()
@@ -134,7 +135,8 @@

     @on 'focus', (e) => @findEditor.focus()
     @find('button').on 'click', =>
-      atom.workspaceView.focus()
+      workspaceElement = atom.views.getView(atom.workspace)
+      workspaceElement.focus()

   handleFindEvents: ->
     @findEditor.getModel().onDidStopChanging => @liveSearch()
@@ -200,7 +202,8 @@
       if fieldToFocus
         fieldToFocus.focus()
       else if focusEditorAfter
-        atom.workspaceView.focus()
+        workspaceElement = atom.views.getView(atom.workspace)
+        workspaceElement.focus()
       else
         @findEditor.focus()


Remove log line

Remove log line

--- lib/find-view.coffee
+++ lib/find-view.coffee
@@ -300,7 +300,6 @@
     ranges = for marker in @markers
       marker.getBufferRange()
     scrollMarker = @markers[@firstMarkerIndexAfterCursor()]
-    console.log(scrollMarker)
     editor = @findModel.getEditor()
     editor.setSelectedBufferRanges(ranges, flash: true)
     editor.scrollToBufferPosition(scrollMarker.getStartBufferPosition(), center: true)


Add refresh alias

Add refresh alias

--- BackofficeBundle/Resources/public/coffee/adminFormView.coffee
+++ BackofficeBundle/Resources/public/coffee/adminFormView.coffee
@@ -1,5 +1,7 @@
 adminFormView = Backbone.View.extend(
   el: '#OrchestraBOModal'
+  events:
+    'blur input#node_name' : 'refreshAlias'
   initialize: (options) ->
     @url = options.url
     @method = if options.method then options.method else 'GET'
@@ -39,4 +41,7 @@
             )
             Backbone.history.loadUrl(Backbone.history.fragment)
     return
+  refreshAlias: (event) ->
+    if $('input#node_alias').val() is ''
+        $('input#node_alias').val(event.target.value)
 )


MultiInput: Added own implementation for updateData method so it updates the inner inputs. See #59006

--- src/elements/MultiInput/MultiInput.coffee
+++ src/elements/MultiInput/MultiInput.coffee
@@ -164,6 +164,16 @@
 		for input in @__inputs
 			input.displayValue()
 		@
+
+	updateData: (data) ->
+		super(data)
+		value = @getValue()
+		for input in @__inputs
+			input.updateData(value)
+		@displayValue()
+		@__setUserSelectedData()
+		@setInputVisibility()
+		return @

 	__initInputs: ->
 		if @__inputs


Node rotation

Node rotation

--- src/coffee/node.coffee
+++ src/coffee/node.coffee
@@ -24,6 +24,9 @@

   update: () ->
     mat4.translate(this.matrix, this._parentMatrix(), this.position)
+    mat4.rotateX(this.matrix, this.matrix, this.rotation[0])
+    mat4.rotateY(this.matrix, this.matrix, this.rotation[1])
+    mat4.rotateZ(this.matrix, this.matrix, this.rotation[2])

     for child, index in this.children
       if child.parent == this


last_worked_at is a date

last_worked_at is a date

--- tutor/src/components/late-icon.cjsx
+++ tutor/src/components/late-icon.cjsx
@@ -10,7 +10,7 @@
       due_at:          React.PropTypes.oneOfType([
         React.PropTypes.string, React.PropTypes.instanceOf(Date)
       ])
-      last_worked_at:  React.PropTypes.string
+      last_worked_at:  React.PropTypes.date
       type:            React.PropTypes.string
       status:          React.PropTypes.string
     ).isRequired


Fixes for intermittently broken tests (custom assertion bug and sinon.js bug workaround).

Rewrote custom assertion to eliminate timing issue that caused intermittent invalid test failures.

--- test/coffee/Deft/promise/Promise.coffee
+++ test/coffee/Deft/promise/Promise.coffee
@@ -64,17 +64,18 @@

 		assert.eventuallyThrows = ( error, done, timeout = 50 ) ->
 			originalHandler = window.onerror
-			onErrorStub = sinon.stub().returns( true )
-			window.onerror = onErrorStub
-			setTimeout(
+			restored = false
+			window.onerror = ( message ) ->
+				window.onerror = originalHandler
+				restored = true
+				expect( message ).to.contain( error.message )
+				done()
+				return
+			setTimeout(
 				->
-					window.onerror = originalHandler
-					try
-						expect( onErrorStub ).to.be.calledOnce
-						expect( onErrorStub.lastCall.args[ 0 ] ).to.contain( error.message )
-						done()
-					catch error
-						done( error )
+					if not restored
+						window.onerror = originalHandler
+						done( new Error( 'expected ' + error + ' to be thrown within ' + timeout + 'ms' ) )
 				timeout
 			)
 			return
@@ -1074,7 +1075,7 @@
 				promise.should.be.an.instanceof( Deft.Promise )
 				promise = promise.then(
 					( value ) ->
-						expect( now() - start ).to.be.closeTo( 100, 10 )
+						expect( now() - start ).to.be.closeTo( 100, 50 )
 						return value
 				)
 				return promise.should.eventually.equal( undefined )
@@ -1108,7 +1109,7 @@
 				promise.should.be.an.instanceof( Deft.Promise )
 				promise = promise.then(
 					( value ) ->
-						expect( now() - start ).to.be.closeTo( 100, 10 )
+						expect( now() - start ).to.be.closeTo( 100, 50 )
 						return value
 				)
 				return promise.should.eventually.equal( 'expected value' )
@@ -1124,7 +1125,7 @@
 				promise.should.be.an.instanceof( Deft.Promise )
 				promise = promise.then(
 					( value ) ->
-						expect( now() - start ).to.be.closeTo( 100, 10 )
+						expect( now() - start ).to.be.closeTo( 100, 50 )
 						return value
 				)
 				return promise.should.eventually.equal( 'expected value' )
@@ -1146,7 +1147,7 @@
 					( value ) ->
 						return value
 					( error ) ->
-						expect( now() - start ).to.be.closeTo( 100, 10 )
+						expect( now() - start ).to.be.closeTo( 100, 50 )
 						throw error
 				)
 				return promise.should.be.rejected.with( Error, 'error message' )
@@ -2946,14 +2947,14 @@
 				promise = Deft.Promise.reduce( [], sumFunction )

 				promise.should.be.an.instanceof( Deft.Promise )
-				return promise.should.be.rejected.with( TypeError, 'Reduce of empty array with no initial value' )
+				return promise.should.be.rejected.with( TypeError )
 			)

 			specify( 'Promise of an empty Array', ->
 				promise = Deft.Promise.reduce( Deft.Deferred.resolve( [] ), sumFunction )

 				promise.should.be.an.instanceof( Deft.Promise )
-				return promise.should.be.rejected.with( TypeError, 'Reduce of empty array with no initial value' )
+				return promise.should.be.rejected.with( TypeError )
 			)

 			return
@@ -2996,15 +2997,16 @@
 		describe( 'attaches a callback that will be called if this Promise is rejected', ->
 			specify( 'called if rejected', ( done ) ->
 				onRejected = sinon.spy()
-
-				promise = Deft.Deferred.reject( new Error( 'error message' ) )
+				error = new Error( 'error message' )
+
+				promise = Deft.Deferred.reject( error )
 				promise.otherwise( onRejected )

 				promise.then(
 					null
 					->
 						try
-							expect( onRejected ).to.be.calledOnce.and.calledWith( new Error( 'error message' ) )
+							expect( onRejected ).to.be.calledOnce.and.calledWith( error )
 							done()
 						catch error
 							done( error )
@@ -3016,15 +3018,16 @@
 			specify( 'called in specified scope if rejected', ( done ) ->
 				targetScope = {}
 				onRejected = sinon.spy()
-
-				promise = Deft.Deferred.reject( new Error( 'error message' ) )
+				error = new Error( 'error message' )
+
+				promise = Deft.Deferred.reject( error )
 				promise.otherwise( onRejected, targetScope )

 				promise.then(
 					null
 					->
 						try
-							expect( onRejected ).to.be.calledOnce.and.calledWith( new Error( 'error message' ) ).and.calledOn( targetScope )
+							expect( onRejected ).to.be.calledOnce.and.calledWith( error ).and.calledOn( targetScope )
 							done()
 						catch error
 							done( error )


Serialise settings for MessagePanelView

Serialise settings for MessagePanelView

Basically this is just to preserve the maxHeight of the message panel

--- lib/ltconsole.coffee
+++ lib/ltconsole.coffee
@@ -47,18 +47,41 @@
 class LTConsole
   constructor: (state) ->
     setupLogCss()
-    @messages = new MessagePanelView
-      title: 'LaTeXTools Console'
-      rawTitle: true
-      closeMethod: 'hide'
-      autoScroll: true
-      className: 'latextools-console'

+    unless state?.messagePanelView?
+      @messages = new MessagePanelView
+        title: 'LaTeXTools Console'
+        rawTitle: true
+        closeMethod: 'hide'
+        autoScroll: true
+        className: 'latextools-console'
+    else
+      @messages = new MessagePanelView state.messagePanelView
+
+    # don't display a summary
     @messages.setSummary summary: ''
+
+  destroy: ->
+    @messages.panel?.destroy()
+    @messages.panel = undefined
+
+  serialize: ->
+    messagePanelView:
+      title: @messages.title
+      rawTitle: @messages.rawTitle
+      speed: @messages.speed
+      autoScroll: @messages.autoScroll
+      closeMethod: @messages.closeMethod
+      recentMessagesAtTop: @messages.recentMessagesAtTop
+      position: @messages.position
+      className: @messages.className
+      # use the actual element height
+      maxHeight: @messages.body[0].style.maxHeight

   # Public API:
   show: ->
     @messages.attach()
+    @messages.show()

   hide: ->
     @messages.hide()
@@ -74,7 +97,7 @@

     message = new PlainMessageView
       message: message
-      raw: is_html,
+      raw: is_html
       className: className

     if file?


Don't use deprecated APIs in specs

Don't use deprecated APIs in specs

--- spec/grammar-selector-spec.coffee
+++ spec/grammar-selector-spec.coffee
@@ -31,8 +31,11 @@
     waitsForPromise ->
       atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-with-no-name'))

+
+    waitsForPromise ->
+      atom.workspace.open('sample.js')
+
     runs ->
-      atom.workspaceView.openSync('sample.js')
       editorView = atom.workspaceView.getActiveView()
       {editor} = editorView
       textGrammar = atom.syntax.grammarForScopeName('text.plain')


update the regexes for apologize

update the regexes for apologize

--- scripts/apologize.coffee
+++ scripts/apologize.coffee
@@ -22,6 +22,6 @@
   apologizeTo = (robot) ->
     robot.send "I'm sorry @${robot.match[1]}"

-  robot.respond /apologize/i, apologize
-  robot.respond /that's not nice/i, apologize
   robot.respond /^apologize to (@?.+)$/i, apologizeTo
+  robot.respond /^apologize$/i, apologize
+  robot.respond /^that's not nice$/i, apologize


Error messages for tests

Error messages for tests

--- test/generators/flat/convenience.coffee
+++ test/generators/flat/convenience.coffee
@@ -119,19 +119,19 @@

           Flat.exists {name: model.get('name')}, (err, exists) ->
             assert.ok(!err, "No errors: #{err}")
-            assert.ok(exists, 'the model exists by name')
+            assert.ok(exists, "the model exists by name. Expected: #{true}. Actual: #{exists}")

             Flat.exists {name: "#{model.get('name')}_thingy"}, (err, exists) ->
               assert.ok(!err, "No errors: #{err}")
-              assert.ok(!exists, 'the model does not exist by bad name')
+              assert.ok(!exists, "the model does not exist by bad name. Expected: #{false}. Actual: #{exists}")

               Flat.exists {created_at: model.get('created_at')}, (err, exists) ->
                 assert.ok(!err, "No errors: #{err}")
-                assert.ok(exists, 'the model exists by created_at')
+                assert.ok(exists, "the model exists by created_at. Expected: #{true}. Actual: #{exists}")

                 Flat.exists {created_at: moment('01/01/2001').toDate()}, (err, exists) ->
                   assert.ok(!err, "No errors: #{err}")
-                  assert.ok(!exists, 'the model does not exist by bad created_at')
+                  assert.ok(!exists, "the model does not exist by bad created_at. Expected: #{false}. Actual: #{exists}")
                   done()