test(categoryOrderHints): add integration test for exporting

--- src/spec/integration/categoryOrderHint.spec.coffee
+++ src/spec/integration/categoryOrderHint.spec.coffee
@@ -1,10 +1,11 @@
 _ = require 'underscore'
 debug = require('debug')('spec-integration:categoryOrderHints')
 _.mixin require('underscore-mixins')
-{Import} = require '../../lib/main'
+{Import, Export} = require '../../lib/main'
 Config = require '../../config'
 TestHelpers = require './testhelpers'
 Promise = require 'bluebird'
+fs = Promise.promisifyAll require('fs')

 defaultProduct = (productTypeId, categoryId) =>
   name:
@@ -37,184 +38,223 @@
     en: uniqueId 'c'
   externalId: 'externalCategoryId'

-describe 'Import', ->
-
-  beforeEach (done) ->
-    jasmine.getEnv().defaultTimeoutInterval = 90000 # 90 sec
-    @importer = createImporter()
-    @importer.validator.suppressMissingHeaderWarning = true
-    @client = @importer.client
-
-    debug 'create a category to work with'
-    @client.categories.save(newCategory())
-    .then (results) =>
-      @category = results.body
-      debug "Created #{results.length} categories"
-
-      @productType = TestHelpers.mockProductType()
-      TestHelpers.setupProductType(@client, @productType)
-    .then (result) =>
-      @productType = result
-      @client.channels.ensure(CHANNEL_KEY, 'InventorySupply')
-    .then -> done()
-    .catch (error) -> done(_.prettify(error))
-
-  afterEach (done) ->
-    debug 'About to delete all categories'
-    @client.categories.process (payload) =>
-      debug "Deleting #{payload.body.count} categories"
-      Promise.map payload.body.results, (category) =>
-        @client.categories.byId(category.id).delete(category.version)
-    .then (results) =>
-      debug "Deleted #{results.length} categories"
-      debug "Delete all the created products"
-      @client.products.process (payload) =>
-        debug "Deleting #{payload.body.count} products"
-        Promise.map payload.body.results, (product) =>
-          @client.products.byId(product.id).delete(product.version)
-    .then (results) ->
-      debug "Deleted #{results.length} products"
-      done()
-    .catch (error) -> done(_.prettify(error))
-  , 60000 # 1min
-
-  it 'should add categoryOrderHints', (done) ->
-
-    @client.products.save(defaultProduct(@productType.id, @category.id))
-    .then (result) =>
-      @product = result.body
-      csv =
-        """
-        productType,id,version,slug,categoryOrderHints
-        #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},#{@category.id}:0.5
-        """
-      im = createImporter(
-        continueOnProblems: true
+prepareCategoryAndProduct = (done) ->
+  jasmine.getEnv().defaultTimeoutInterval = 90000 # 90 sec
+  @export = new Export client: Config
+  @importer = createImporter()
+  @importer.validator.suppressMissingHeaderWarning = true
+  @client = @importer.client
+
+  debug 'create a category to work with'
+  @client.categories.save(newCategory())
+  .then (results) =>
+    @category = results.body
+    debug "Created #{results.length} categories"
+
+    @productType = TestHelpers.mockProductType()
+    TestHelpers.setupProductType(@client, @productType)
+  .then (result) =>
+    @productType = result
+    @client.channels.ensure(CHANNEL_KEY, 'InventorySupply')
+  .then -> done()
+  .catch (error) -> done(_.prettify(error))
+
+describe 'categoryOrderHints', ->
+
+  describe 'Import', ->
+
+    beforeEach prepareCategoryAndProduct
+
+    afterEach (done) ->
+      debug 'About to delete all categories'
+      @client.categories.process (payload) =>
+        debug "Deleting #{payload.body.count} categories"
+        Promise.map payload.body.results, (category) =>
+          @client.categories.byId(category.id).delete(category.version)
+      .then (results) =>
+        debug "Deleted #{results.length} categories"
+        debug "Delete all the created products"
+        @client.products.process (payload) =>
+          debug "Deleting #{payload.body.count} products"
+          Promise.map payload.body.results, (product) =>
+            @client.products.byId(product.id).delete(product.version)
+      .then (results) ->
+        debug "Deleted #{results.length} products"
+        done()
+      .catch (error) -> done(_.prettify(error))
+    , 60000 # 1min
+
+    it 'should add categoryOrderHints', (done) ->
+
+      @client.products.save(defaultProduct(@productType.id, @category.id))
+      .then (result) =>
+        @product = result.body
+        csv =
+          """
+          productType,id,version,slug,categoryOrderHints
+          #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},#{@category.id}:0.5
+          """
+        im = createImporter(
+          continueOnProblems: true
+        )
+        im.import(csv)
+      .then (result) =>
+        expect(result[0]).toBe '[row 2] Product updated.'
+        @client.products.byId(@product.id).fetch()
+      .then (result) =>
+        expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.5'}
+        done()
+      .catch (err) -> done _.prettify(err)
+
+    it 'should add categoryOrderHints when using an external category id', (done) ->
+
+      @client.products.save(defaultProduct(@productType.id, @category.id))
+      .then (result) =>
+        @product = result.body
+        csv =
+          """
+          productType,id,version,slug,categoryOrderHints
+          #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},externalCategoryId:0.5
+          """
+        im = createImporter(
+          continueOnProblems: true
+        )
+        im.import(csv)
+      .then (result) =>
+        expect(result[0]).toBe '[row 2] Product updated.'
+        @client.products.byId(@product.id).fetch()
+      .then (result) =>
+        expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.5'}
+        done()
+      .catch (err) -> done _.prettify(err)
+
+    it 'should add categoryOrderHints when using an category name', (done) ->
+
+      @client.products.save(defaultProduct(@productType.id, @category.id))
+      .then (result) =>
+        @product = result.body
+        csv =
+          """
+          productType,id,version,slug,categoryOrderHints
+          #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},#{@category.name.en}:0.5
+          """
+        im = createImporter(
+          continueOnProblems: true
+        )
+        im.import(csv)
+      .then (result) =>
+        expect(result[0]).toBe '[row 2] Product updated.'
+        @client.products.byId(@product.id).fetch()
+      .then (result) =>
+        expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.5'}
+        done()
+      .catch (err) -> done _.prettify(err)
+
+    it 'should add categoryOrderHints when using an category slug', (done) ->
+
+      @client.products.save(defaultProduct(@productType.id, @category.id))
+      .then (result) =>
+        @product = result.body
+        csv =
+          """
+          productType,id,version,slug,categoryOrderHints
+          #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},#{@category.slug.en}:0.5
+          """
+        im = createImporter(
+          continueOnProblems: true
+        )
+        im.import(csv)
+      .then (result) =>
+        expect(result[0]).toBe '[row 2] Product updated.'
+        @client.products.byId(@product.id).fetch()
+      .then (result) =>
+        expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.5'}
+        done()
+      .catch (err) -> done _.prettify(err)
+
+    it 'should remove categoryOrderHints', (done) ->
+
+      @client.products.save(
+        _.extend {}, defaultProduct(@productType.id, @category.id),
+          categoryOrderHints:
+            "#{@category.id}": '0.5'
       )
-      im.import(csv)
-    .then (result) =>
-      expect(result[0]).toBe '[row 2] Product updated.'
-      @client.products.byId(@product.id).fetch()
-    .then (result) =>
-      expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.5'}
-      done()
-    .catch (err) -> done _.prettify(err)
-
-  it 'should add categoryOrderHints when using an external category id', (done) ->
-
-    @client.products.save(defaultProduct(@productType.id, @category.id))
-    .then (result) =>
-      @product = result.body
-      csv =
-        """
-        productType,id,version,slug,categoryOrderHints
-        #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},externalCategoryId:0.5
-        """
-      im = createImporter(
-        continueOnProblems: true
+      .then (result) =>
+        @product = result.body
+        csv =
+          """
+          productType,id,version,slug,categoryOrderHints
+          #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},
+          """
+        im = createImporter(
+          continueOnProblems: true
+        )
+        im.import(csv)
+      .then (result) =>
+        expect(result[0]).toBe '[row 2] Product updated.'
+        @client.products.byId(@product.id).fetch()
+      .then (result) =>
+        expect(result.body.masterData.staged.categoryOrderHints).toEqual {}
+        done()
+      .catch (err) -> done _.prettify(err)
+
+    it 'should change categoryOrderHints', (done) ->
+
+      @client.products.save(
+        _.extend {}, defaultProduct(@productType.id, @category.id),
+          categoryOrderHints:
+            "#{@category.id}": '0.5'
       )
-      im.import(csv)
-    .then (result) =>
-      expect(result[0]).toBe '[row 2] Product updated.'
-      @client.products.byId(@product.id).fetch()
-    .then (result) =>
-      expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.5'}
-      done()
-    .catch (err) -> done _.prettify(err)
-
-  it 'should add categoryOrderHints when using an category name', (done) ->
-
-    @client.products.save(defaultProduct(@productType.id, @category.id))
-    .then (result) =>
-      @product = result.body
-      csv =
-        """
-        productType,id,version,slug,categoryOrderHints
-        #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},#{@category.name.en}:0.5
-        """
-      im = createImporter(
-        continueOnProblems: true
+      .then (result) =>
+        @product = result.body
+        csv =
+          """
+          productType,id,version,slug,categoryOrderHints
+          #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},#{@category.id}: 0.9
+          """
+        im = createImporter(
+          continueOnProblems: true
+        )
+        im.import(csv)
+      .then (result) =>
+        expect(result[0]).toBe '[row 2] Product updated.'
+        @client.products.byId(@product.id).fetch()
+      .then (result) =>
+        expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.9'}
+        done()
+      .catch (err) -> done _.prettify(err)
+
+  describe 'Export', ->
+
+    beforeEach prepareCategoryAndProduct
+
+    it 'should export categoryOrderHints', (done) ->
+
+      @client.products.save(
+        _.extend {}, defaultProduct(@productType.id, @category.id),
+          categoryOrderHints:
+            "#{@category.id}": '0.5'
       )
-      debug csv
-      im.import(csv)
-    .then (result) =>
-      expect(result[0]).toBe '[row 2] Product updated.'
-      @client.products.byId(@product.id).fetch()
-    .then (result) =>
-      expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.5'}
-      done()
-    .catch (err) -> done _.prettify(err)
-
-  it 'should add categoryOrderHints when using an category slug', (done) ->
-
-    @client.products.save(defaultProduct(@productType.id, @category.id))
-    .then (result) =>
-      @product = result.body
-      csv =
-        """
-        productType,id,version,slug,categoryOrderHints
-        #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},#{@category.slug.en}:0.5
-        """
-      im = createImporter(
-        continueOnProblems: true
-      )
-      im.import(csv)
-    .then (result) =>
-      expect(result[0]).toBe '[row 2] Product updated.'
-      @client.products.byId(@product.id).fetch()
-    .then (result) =>
-      expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.5'}
-      done()
-    .catch (err) -> done _.prettify(err)
-
-  it 'should remove categoryOrderHints', (done) ->
-
-    @client.products.save(
-      _.extend {}, defaultProduct(@productType.id, @category.id),
-        categoryOrderHints:
-          "#{@category.id}": '0.5'
-    )
-    .then (result) =>
-      @product = result.body
-      csv =
-        """
-        productType,id,version,slug,categoryOrderHints
-        #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},
-        """
-      im = createImporter(
-        continueOnProblems: true
-      )
-      im.import(csv)
-    .then (result) =>
-      expect(result[0]).toBe '[row 2] Product updated.'
-      @client.products.byId(@product.id).fetch()
-    .then (result) =>
-      expect(result.body.masterData.staged.categoryOrderHints).toEqual {}
-      done()
-    .catch (err) -> done _.prettify(err)
-
-  it 'should change categoryOrderHints', (done) ->
-
-    @client.products.save(
-      _.extend {}, defaultProduct(@productType.id, @category.id),
-        categoryOrderHints:
-          "#{@category.id}": '0.5'
-    )
-    .then (result) =>
-      @product = result.body
-      csv =
-        """
-        productType,id,version,slug,categoryOrderHints
-        #{@productType.id},#{@product.id},#{@product.version},#{@product.slug},#{@category.id}: 0.9
-        """
-      im = createImporter(
-        continueOnProblems: true
-      )
-      im.import(csv)
-    .then (result) =>
-      expect(result[0]).toBe '[row 2] Product updated.'
-      @client.products.byId(@product.id).fetch()
-    .then (result) =>
-      expect(result.body.masterData.staged.categoryOrderHints).toEqual {"#{@category.id}": '0.9'}
-      done()
-    .catch (err) -> done _.prettify(err)
+      .then (result) =>
+        @product = result.body
+        @client.products.byId(@product.id).fetch()
+      .then (result) =>
+        template =
+          """
+          productType,id,variantId,categoryOrderHints
+          """
+        file = '/tmp/output.csv'
+        expectedCSV =
+          """
+          productType,id,variantId,categoryOrderHints
+          #{@productType.name},#{@product.id},#{@product.lastVariantId},#{@category.id}:0.5
+
+          """
+        @export.export(template, file)
+        .then (result) ->
+          expect(result).toBe 'Export done.'
+          fs.readFileAsync file, {encoding: 'utf8'}
+        .then (content) ->
+          expect(content).toBe expectedCSV
+          done()
+        .catch (err) -> done _.prettify(err)


course: another thing might not be defined

--- salvus/page/editor_course.cjsx
+++ salvus/page/editor_course.cjsx
@@ -370,7 +370,10 @@
         configure_project_visibility: (student_project_id) =>
             users_of_student_project = flux.getStore('projects').get_users(student_project_id)
             # Make project not visible to any collaborator on the course project.
-            flux.getStore('projects').get_users(course_project_id).map (_, account_id) =>
+            users = flux.getStore('projects').get_users(course_project_id)
+            if not users? # TODO: should really wait until users is defined, which is a supported thing to do on stores!
+                return
+            users.map (_, account_id) =>
                 x = users_of_student_project.get(account_id)
                 if x? and not x.get('hide')
                     flux.getActions('projects').set_project_hide(account_id, student_project_id, true)


update an example

update an example

--- salvus/bup_server.coffee
+++ salvus/bup_server.coffee
@@ -574,7 +574,7 @@

 (x.c.register_server(host:"10.1.#{i}.5",dc:1,cb:console.log) for i in [1..7])

-(x.c.register_server(host:"10.3.#{i}.4",dc:1,cb:console.log) for i in [1..8])
+(x.c.register_server(host:"10.3.#{i}.5",dc:2,cb:console.log) for i in [1..8])

 x.c.push_servers_files(cb:console.log)


teams: add tracker/campaign logic to team creation tokens

--- workers/social/lib/social/models/teamtoken.coffee
+++ workers/social/lib/social/models/teamtoken.coffee
@@ -14,6 +14,18 @@
 { secure, signature, dash } = Bongo

 emailsanitize = require './user/emailsanitize'
+
+getName = (delegate) ->
+
+  { nickname, firstName, lastName } = delegate.profile
+
+  name = nickname
+  name = firstName              if firstName
+  name = "#{name} #{lastName}"  if firstName and lastName
+
+  return name
+
+

 module.exports = class JTeamInvitation extends jraphical.Module

@@ -37,6 +49,8 @@
           (signature Object, Function)
         byCode:
           (signature String, Function)
+        sendInvitationEmails:
+          (signature [String], Function)

     sharedEvents    :
       static        : []
@@ -91,3 +105,25 @@
   @byCode: (code, callback) ->
     @one { code }, callback

+  @sendInvitationEmails: permit 'send invitations',
+    success: (client, emails, callback) ->
+
+      inviter     = getName client.connection.delegate
+      queue       = []
+      invitations = []
+
+      emails.forEach (email) =>
+        queue.push =>
+          @create client, {email}, (err, invitation) ->
+            return queue.fin()  if err
+
+            properties =
+              inviter  : inviter
+              invitee  : invitation.email
+              link     : "#{protocol}//#{hostname}/Teams/#{encodeURIComponent invitation.code}"
+
+            Tracker.identifyAndTrack invitation.email, { subject : Tracker.types.CREATE_TEAM }, properties
+            queue.fin()
+
+      dash queue, callback
+


gcloud: improved handling of detaching/attaching disks

--- salvus/smc_gcloud.coffee
+++ salvus/smc_gcloud.coffee
@@ -254,8 +254,11 @@
             read_only : false
             cb        : required
         if not (opts.vm instanceof VM)
-            opts.cb("vm must be an instance of VM")
-            return
+            if typeof(opts.vm) == 'string'
+                opts.vm = @gcloud.vm(name:opts.vm, zone:@zone)
+            else
+                opts.cb("vm must be an instance of VM")
+                return
         opts.vm.attach_disk
             disk      : @
             read_only : opts.read_only
@@ -264,7 +267,7 @@
     detach: (opts) =>
         opts = defaults opts,
             vm : undefined   # if not given, detach from all users of this disk
-            cb : required
+            cb : undefined
         dbg = @dbg("detach")
         vms = undefined
         async.series([
@@ -290,7 +293,7 @@
                         cb   : cb
                 async.map(vms, f, cb)

-            ], opts.cb)
+            ], (err) => opts.cb?(err))

 class Snapshot
     constructor: (@gcloud, @name) ->
@@ -341,8 +344,8 @@
         config =
             sourceSnapshot : "global/snapshots/#{@name}"
         config.sizeGb = opts.size_GB if opts.size_GB?
-        config.type = opts.type if opts.type?
-        @gcloud._gce.zone(@zone).createDisk opts.name, config, (err, disk, operation, apiResponse) =>
+        config.type = "zones/#{opts.zone}/diskTypes/#{opts.type}"
+        @gcloud._gce.zone(opts.zone).createDisk opts.name, config, (err, disk, operation, apiResponse) =>
             handle_operation(err, operation, (->dbg('done')), (err) => opts.cb?(err))

@@ -472,7 +475,7 @@
         opts = defaults opts,
             cb  : required
         @dbg("get_operations")()
-        @_gce.getOperations {filter:"status ne DONE", maxResults:500}, (err, operations) => opts.cb(err, operations)
+        @_gce.getOperations {filter:"status ne 'DONE'", maxResults:500}, (err, operations) => opts.cb(err, operations)

     _check_db: (cb) =>
         if not @db


Can add warnings, errors, and ignore rules in config. (+3 squashed commits)
Squashed commits:
[d4507f5] Check for and add ignored rules from config.cson
[44949a4] Stricter matching, and array check.
[9f4ce2d] Missed variables during refactor.

Stricter matching, and array check.

Check for and add ignored rules from config.cson

Can add warnings, errors, and ignore rules in config.

--- lib/linter/csslint.coffee
+++ lib/linter/csslint.coffee
@@ -10,11 +10,18 @@
     command = []

     userCSSLintPath = atom.config.get('atom-lint.csslint.path')
+    userCSSLintRules = atom.config.get('atom-lint.csslint.rules')

     if userCSSLintPath?
       command.push(userCSSLintPath)
     else
       command.push('csslint')
+
+    # add rule flags
+    if userCSSLintRules?
+      for flag, rules of userCSSLintRules
+        if /errors|ignore|warnings/.test(flag) and Array.isArray(rules)
+          command.push("--#{flag.toLowerCase()}=#{rules.join(',')}")

     command.push('--format=checkstyle-xml')
     command.push(@filePath)


Added expand and collapse buttons to actions panel.

--- client/ide/views/tabview/idefilestabview.coffee
+++ client/ide/views/tabview/idefilestabview.coffee
@@ -57,4 +57,18 @@
         style    : 'display: block; margin: 20px; width: 200px; '
       callback   : -> ideAppController.openFile()

+    actionsPane.addSubView new KDButtonView
+      title      : 'Collapse sidebar'
+      cssClass   : 'compact solid green'
+      attributes :
+        style    : 'display: block; margin: 20px; width: 200px; '
+      callback   : -> ideAppController.collapseSidebar()
+
+    actionsPane.addSubView new KDButtonView
+      title      : 'Expand sidebar'
+      cssClass   : 'compact solid green'
+      attributes :
+        style    : 'display: block; margin: 20px; width: 200px; '
+      callback   : -> ideAppController.expandSidebar()
+
     @tabView.addPane actionsPane


Refactor the magnum ci script

Refactor the magnum ci script

--- scripts/magnum-ci.coffee
+++ scripts/magnum-ci.coffee
@@ -20,23 +20,22 @@
 #   sergeylukin

 url = require('url')
-
+rooms = process.env.HUBOT_DEV_ROOMS.split(",")
 module.exports = (robot) ->

   robot.router.post "/hubot/magnum-ci", (req, res) ->
+    rooms.forEach (room, i) ->
+      try
+        payload = JSON.parse req.body.payload
+        user = {}
+        user.room = room
+        user.type = payload.type if payload.type

+        robot.messageRoom room, "#{payload.title}"

-    try
-      payload = JSON.parse req.body.payload
-      user = {}
-      user.room = payload.room if payload.room
-      user.type = payload.type if payload.type
+      catch error
+        console.log "travis hook error: #{error}. Payload: #{req.body.payload}"

-      robot.messageRoom '#tool-mobile', "#{payload.title}"
-
-    catch error
-      console.log "travis hook error: #{error}. Payload: #{req.body.payload}"
-
-    res.end JSON.stringify {
-      send: true #some client have problems with and empty response, sending that response ion sync makes debugging easier
-    }+      res.end JSON.stringify {
+        send: true #some client have problems with and empty response, sending that response ion sync makes debugging easier
+      }


[Featue] Always(play, pause, volume change) video volume controling by fade

--- src/renderer/scripts/services/Player/Player.coffee
+++ src/renderer/scripts/services/Player/Player.coffee
@@ -20,14 +20,14 @@

         app.config.observe "nco.player.enabled", (enabled) =>
             if enabled is no
-                @_mp4player.pause()
+                @_stopMovie()
             else
-                @_loadMovie()
+                @_loadMovie(true)

             return

         app.config.observe "nco.player.volume", (volume) =>
-            @_mp4player.volume = volume
+            $(@_mp4player).animate({volume}, 1000)

     _handleNsenEvents : ->
         channel = app.nsenStream.getStream()
@@ -38,7 +38,7 @@
             @_loadMovie()

-    _loadMovie : ->
+    _loadMovie : (fadeIn = false)->
         nicoSession = app.getSession()
         channel = app.nsenStream.getStream()
         movie = channel?.getCurrentVideo()
@@ -69,8 +69,9 @@
             playContent = live.get("stream.contents.0")
             elapsedFromStart = (Date.now() - playContent.startTime) / 1000 | 0

-            if @_mp4player.src is ""
+            if @_mp4player.src is "" or fadeIn
                 volume = app.config.get("nco.player.volume")
+                @_mp4player.volume = 0
                 $(@_mp4player).animate({volume}, 2000)

             @_mp4player.src = result.url
@@ -79,6 +80,11 @@

         .catch =>
             console.log arguments
+
+    _stopMovie : ->
+        $(@_mp4player).animate {volume: 0}, 2000, =>
+            @_mp4player.pause()
+            @_mp4player.src = ""

     _translateToughCookieToElectronSettable : (cookies) ->


removed automatic rlwrap

removed automatic rlwrap

--- natal.coffee
+++ natal.coffee
@@ -410,33 +410,28 @@

 startRepl = (name, autoChoose) ->
   log 'Starting REPL'
-  hasRlwrap =
-    try
-      exec 'type rlwrap'
-      true
-    catch
-      log '
-          Warning: rlwrap is not installed.\nInstall it to make the REPL a much
-          better experience with arrow key support.
-          ', 'red'
-      false
-
-  try
-    lein = child.spawn (if hasRlwrap then 'rlwrap' else 'lein'),
-      "#{if hasRlwrap then 'lein ' else ''}trampoline run -m clojure.main -e"
-        .split(' ').concat(
-          """
-          (require '[cljs.repl :as repl])
-          (require '[ambly.core :as ambly])
-          (let [repl-env (ambly.core/repl-env#{if autoChoose then ' :choose-first-discovered true' else ''})]
-          (cljs.repl/repl repl-env
-            :watch \"src\"
-            :watch-fn
-              (fn []
-                (cljs.repl/load-file repl-env
-                  \"src/#{toUnderscored name}/core.cljs\"))
-            :analyze-path \"src\"))
-          """),
+  try
+    exec 'type rlwrap'
+  catch
+    log '
+        Warning: rlwrap is not installed.\nInstall it to make the REPL a much
+        better experience with arrow key support.
+        ', 'red'
+
+  try
+    lein = child.spawn 'lein', 'trampoline run -m clojure.main -e'.split(' ').concat(
+      """
+      (require '[cljs.repl :as repl])
+      (require '[ambly.core :as ambly])
+      (let [repl-env (ambly.core/repl-env#{if autoChoose then ' :choose-first-discovered true' else ''})]
+      (cljs.repl/repl repl-env
+        :watch \"src\"
+        :watch-fn
+          (fn []
+            (cljs.repl/load-file repl-env
+              \"src/#{toUnderscored name}/core.cljs\"))
+        :analyze-path \"src\"))
+      """),
       cwd: process.cwd()
       env: process.env


extract Number::truncate from Number::bytesToString

Signed-off-by: Steven Hoffman <46f1a0bd5592a2f9244ca321b129902a06b53e03@fustrate.com>

--- vendor/assets/javascripts/fustrate/_prototypes.coffee
+++ vendor/assets/javascripts/fustrate/_prototypes.coffee
@@ -47,15 +47,17 @@
 Number::accountingFormat = ->
   if @ < 0 then "($#{(@ * -1).toFixed(2)})" else "$#{@.toFixed(2)}"

+Number::truncate = (digits = 2) ->
+  @toFixed(digits).replace(/\.?0+$/, '')
+
 Number::bytesToString = ->
   return "#{@} B" if @ < 1000

-  return "#{(@ / 1000).toFixed(2).replace(/\.?0+$/, '')} kB" if @ < 1000000
+  return "#{(@ / 1000).truncate()} kB" if @ < 1000000

-  if @ < 1000000000
-    return "#{(@ / 1000000).toFixed(2).replace(/\.?0+$/, '')} MB"
+  return "#{(@ / 1000000).truncate()} MB" if @ < 1000000000

-  "#{(@ / 1000000000).toFixed(2).replace(/\.?0+$/, '')} GB"
+  "#{(@ / 1000000000).truncate()} GB"

 Object.defineProperty Object::, 'tap',
   enumerable: false


Make the page admin-only while I am working on it

--- apps/consignments/index.coffee
+++ apps/consignments/index.coffee
@@ -15,7 +15,7 @@

 { data, edit, upload } = require('../../components/json_page/routes')(landing)

-app.get landing.paths.show, routes.landing
+app.get landing.paths.show, adminOnly, routes.landing
 app.get landing.paths.show + '/data', data
 app.get landing.paths.edit, adminOnly, edit
 app.post landing.paths.edit, adminOnly, upload


manage background_css better

manage background_css better

--- @client/banner.coffee
+++ @client/banner.coffee
@@ -556,7 +556,7 @@
               id: 'background_color'
               type: 'color'
               name: 'background_color'
-              defaultValue: customization('banner')?.background_css or DEFAULT_BACKGROUND_COLOR
+              value: edit_banner.background_css or customization('banner')?.background_css or DEFAULT_BACKGROUND_COLOR
               onChange: (e) =>
                 edit_banner.background_css = e.target.value
                 save edit_banner
@@ -577,6 +577,8 @@
               textDecoration: 'underline'

             onClick: ->
+              edit_banner.background_css = DEFAULT_BACKGROUND_COLOR
+              save edit_banner
               document.querySelector('button#delete_masthead').click()

             onKeyDown: (e) =>


fix issue #147

fix issue #147

--- lib/md.coffee
+++ lib/md.coffee
@@ -219,7 +219,7 @@
       ## this doesn't work
       # element = globalMathTypesettingData.mathjax_s.splice(0, 1)[0]
       # return "<div class=\"mathjax-exps\"> #{element.innerHTML} </div>"
-      return text
+      return text.escape()

 # inline [[]] rule
 # [[...]]


Open target file in the correct direction (ref #150)

--- lib/todo-item-view.coffee
+++ lib/todo-item-view.coffee
@@ -44,12 +44,24 @@
   openPath: =>
     return unless @todo and @todo.loc
     position = [@todo.position[0][0], @todo.position[0][1]]
-    pending = atom.config.get('core.allowPendingPaneItems') or false

-    atom.workspace.open(@todo.loc, {split: 'left', pending}).then ->
+    atom.workspace.open(@todo.loc, {
+      initialLine: position[0]
+      initialColumn: position[1]
+      split: @getSplitDirection()
+      pending: atom.config.get('core.allowPendingPaneItems') or false
+    }).then ->
+      # Setting initialColumn/Line does not always center view
       if textEditor = atom.workspace.getActiveTextEditor()
         textEditor.setCursorBufferPosition(position, autoscroll: false)
         textEditor.scrollToCursorPosition(center: true)
+
+  getSplitDirection: ->
+    switch atom.config.get('todo-show.openListInDirection')
+      when 'up' then 'down'
+      when 'down' then 'up'
+      when 'left' then 'right'
+      else 'left'

 class TodoEmptyView extends View
   @content: (showInTable = []) ->


use arrows for back/skip

use arrows for back/skip

--- src/layout/header.coffee
+++ src/layout/header.coffee
@@ -27,9 +27,9 @@

   make.rows 'header', [
     make.columns 'progress', [
-      anchor "back", 'previous_params'
+      anchor "<", 'previous_params'
       " Level #{level.get('level_index')} of #{level.get('level_count')}"
-      anchor "skip", 'next_params'
+      anchor ">", 'next_params'
     ]
     { name: 'Level Progress', _LABEL: 'level_progress' }
     make.columns 'stats', [


Report error if error raised whilst requesting dataset

--- server/code/index.coffee
+++ server/code/index.coffee
@@ -747,6 +747,8 @@
 addView = (req, resp) ->
   user = req.user.effective
   Dataset.findOneById req.params.dataset, (err, dataset) ->
+    if err?
+      resp.send 500, error: "Error creating view: #{err}"
     if not dataset
       return resp.send 404, error: "Error creating view: #{req.params.dataset} not found"
     body = req.body


Change error message wording

Change error message wording

--- server/controllers/indicators.coffee
+++ server/controllers/indicators.coffee
@@ -126,7 +126,7 @@
       Unable to load google doc with key: #{req.body.spreadsheetKey}\n
       Check that the document loads at:\n
       https://docs.google.com/spreadsheets/d/#{req.body.spreadsheetKey}\n
-      and that the document has been published for web
+      and that the document has been published to the web
     """
     res.send(500, {error: errMessage})
   )


isExcludedAtMinimum takes an exercise

isExcludedAtMinimum takes an exercise

--- src/components/questions/exercises-display.cjsx
+++ src/components/questions/exercises-display.cjsx
@@ -100,7 +100,7 @@

   onExerciseToggle: (ev, exercise) ->
     isSelected = not ExerciseStore.isExerciseExcluded(exercise.id)
-    if isSelected and ExerciseStore.isExcludedAtMinimum(ExerciseStore.get(@props.sectionIds))
+    if isSelected and ExerciseStore.isExcludedAtMinimum(exercise)
       Dialog.show(
         className: 'question-library-min-exercise-exclusions'
         title: '', body: @renderMinimumExclusionWarning()


fix scheduling option

fix scheduling option

--- HeartString/app/assets/javascripts/messages.coffee
+++ HeartString/app/assets/javascripts/messages.coffee
@@ -1,5 +1,5 @@
 change_visibility = (status) ->
-  if status == "Scheduled"
+  if status == "Schedule"
     $(".published-field").show()
   else
     $(".published-field").hide()


Fixed hashstate not to eat the page title

--- static/js/vendor/hashstate.coffee
+++ static/js/vendor/hashstate.coffee
@@ -48,10 +48,11 @@
         # the state at all if we give in an empty string, so we'll
         # have to resort to having an empty question mark
         new_hash = '?'
+    new_state = [History.getState().data, document.title, new_hash]
     if history
-        History.pushState null, null, new_hash
+        History.pushState new_state...
     else
-        History.replaceState null, null, new_hash
+        History.replaceState new_state...

 # TODO: Track (and signal) "pending" states
 class HashState


Fixed issue#3 - Wrong values in User last seen view.

--- app/assets/javascripts/app/controllers/navigation.js.coffee
+++ app/assets/javascripts/app/controllers/navigation.js.coffee
@@ -204,9 +204,8 @@
       NavBarRight['RecendViewed::' + ticket.id + '-' + prio ] = {
         prio:      prio,
         parent:    '#current_user',
-        name:      item.history_object.name + ' (' + ticket.title + ')',
+        name:      item.history_object + ' (' + ticket.title + ')',
         target:    '#ticket/zoom/' + ticket.id,
-#        role:      ['Agent', 'Customer'],
         divider:   divider,
         navheader: navheader
       }


Logs all incoming messages in the extension

Signed-off-by: Akash Manohar J <e4cf979cf717fd295f4e4edc427ee1aee78fa7dc@akash.im>

--- app/assets/javascripts/backbone/broadcasters/faye.js.coffee
+++ app/assets/javascripts/backbone/broadcasters/faye.js.coffee
@@ -9,6 +9,10 @@
           message['ext'] = {
             auth_token: Kandan.Helpers.Users.current_user().auth_token
           }
+        callback(message)
+
+      incoming: (message, callback)->
+        console.log "incoming", message
         callback(message)
     }
     @faye_client.bind "transport:down", ()->


ben stir me

ben stir me

--- scripts/simpleResponses.coffee
+++ scripts/simpleResponses.coffee
@@ -4,5 +4,9 @@

 module.exports = (robot) ->
+
   robot.respond /kait disapproves/i, (msg) ->
     msg.send "https://goo.gl/cTmPh7"
+
+  robot.respond /ben stir me/i, (msg) ->
+    msg.send "https://goo.gl/hl6DcH"


Fix student signup flow for non-US/EU/Israel countries (#5488)

--- app/views/core/CreateAccountModal/CreateAccountModal.coffee
+++ app/views/core/CreateAccountModal/CreateAccountModal.coffee
@@ -97,7 +97,7 @@
       else
         if /^\/play/.test(location.pathname) and me.showIndividualRegister()
           @signupState.set({ path: 'individual', screen: 'segment-check' })
-    if @signupState.get('screen') is 'segment-check' and not @segmentCheckRequiredInCountry()
+    if @signupState.get('screen') is 'segment-check' and not @signupState.get('path') is 'student' and not @segmentCheckRequiredInCountry()
       @signupState.set screen: 'basic-info'

     @listenTo @signupState, 'all', _.debounce @render


whoops, the post model defines a "create posts" permission, not a "create content" permission

--- workers/social/lib/social/models/messages/post.coffee
+++ workers/social/lib/social/models/messages/post.coffee
@@ -101,7 +101,7 @@
         kodingErr[prop] = err[prop]
     kodingErr

-  @create = permit 'create content'
+  @create = permit 'create posts'
     success: (client, data, callback)->
       constructor = @
       {connection:{delegate}} = client


Save group account

Save group account

--- workers/social/lib/social/models/recurly/index.coffee
+++ workers/social/lib/social/models/recurly/index.coffee
@@ -146,19 +146,32 @@
         callback yes, {}
       else

-        JRecurlyAccount.create client, (err, account)=>
-          return callback err  if err
-
-          if data.accountCode
+        if data.accountCode
+          JRecurlyAccount.create client, (err, account)=>
+            return callback err  if err
             userCode         = ""
             data.accountCode = account.recurlyId
             data.plan        = @code
-          else
-            userCode      = account.recurlyId
-            data          =
-              plan        : @code
-              quantity    : data.quantity
-              accountCode : userCode
+
+            payment.addUserSubscription userCode, data, (err, result)->
+              return callback err  if err
+              sub = new JRecurlySubscription
+                planCode : result.code
+                userCode : data.accountCode
+                uuid     : result.uuid
+                quantity : result.quantity
+                status   : result.status
+                datetime : result.datetime
+                expires  : result.expires
+                renew    : result.renew
+              sub.save ->
+                callback no, account
+        else
+          userCode      = "user_#{delegate._id}"
+          data          =
+            plan        : @code
+            quantity    : data.quantity
+            accountCode : userCode

           payment.addUserSubscription userCode, data, (err, result)->
             return callback err  if err


path method of memberships store falls back to restful store for non-special cases

--- src/node_modules/memberships-store/memberships-store.coffee
+++ src/node_modules/memberships-store/memberships-store.coffee
@@ -6,13 +6,10 @@
   class MembershipsStore extends RestfulStore
     path: (params = {}, op) ->
       debug("path", params, op)
-      if params.id?
-        "#{@baseUrl}/#{@plural}/#{params.id}"
-      else if op == "all"
+      if op == "all"
         "#{@baseUrl}/groups/#{params.groupId}/#{@plural}"
       else
-        # create
-        "#{@baseUrl}/#{@plural}"
+        super(params, op)

   new MembershipsStore(
     singular: "membership"


Fixed: variables were not defined

Fixed: variables were not defined

--- server/controllers/user.coffee
+++ server/controllers/user.coffee
@@ -1,9 +1,9 @@
 db = require('../helpers/db_connect_helper').db_connect()
 keys = require '../lib/encryption'

-# POST /user
+# POST /user/
 module.exports.create = (req, res, next) ->
-    delete body._attachments
+    delete req.body._attachments
     if req.params.id
         db.get req.params.id, (err, doc) -> # this GET needed because of cache
             if doc
@@ -11,7 +11,7 @@
                 err.status = 409
                 next err
             else
-                db.save params.id, req.body, (err, response) ->
+                db.save req.params.id, req.body, (err, response) ->
                     if err
                         err = new Error "The document exists"
                         err.status = 409


Migrated to modal display (vs old tab method)

--- src/Thumbnails.coffee
+++ src/Thumbnails.coffee
@@ -15,6 +15,7 @@
 ###

 DOM = require("./DOM.coffee")
+Modal = require("./Modal.coffee")
 Templates = require("./Templates.coffee")

 class Thumbnails
@@ -24,26 +25,19 @@
     #
     constructor: (BUS)->
         @BUS = BUS
-        @BUS.subscribe('thumbnails.run',
-            ()=>
-                @run()
-                return
-        )
+        @modal = new Modal()

     #
     # Show the rule thumbnails
     #
-    run: ()->
+    open: ()->
+        @modal.open(Templates.thumbMontage)
+
         # Setup the list of rules
         ruleList = [0..255]

-        template_options = {
-            ruleList:ruleList,
-            path:@BUS.get('thumbnails.path')
-        }
-
-        # Clear the current thumbnails and populate it via Mustache template
-        DOM.elemById('WOLFCAGE', 'MAIN_CONTAINER').innerHTML = Templates.thumbnails(template_options)
+        el = DOM.elemById("THUMBMONTAGE", "CONTAINER")
+        el.innerHTML = Templates.thumbnails(ruleList)

         thumbsElems = document.querySelectorAll('.' + DOM.getClass('THUMBNAILS', 'THUMB_BOX'))

@@ -60,7 +54,6 @@
         # Change the current rule
         @BUS.set('currentruledecimal', rule)

-        # Load the generator
-        @BUS.broadcast('tabs.show.generator')
+        @modal.close()

 module.exports = Thumbnails


Don't return bounds for unknown types

Don't return bounds for unknown types

--- server/models/indicator.coffee
+++ server/models/indicator.coffee
@@ -126,7 +126,6 @@
   return bounds

 boundAggregators.date = boundAggregators.integer
-boundAggregators.text = -> null

 indicatorSchema.statics.calculateBoundsForType = (fieldType, data, fieldName) ->
   if boundAggregators[fieldType]?


Allow users to claim plunks created while not logged in

--- servers/api/index.coffee
+++ servers/api/index.coffee
@@ -341,9 +341,14 @@
         plunk.tags = _.uniq(plunk.tags)
         plunk.updated_at = new Date
         plunk.description = json.description if json.description
+        plunk.user = req.user._id if req.user
         plunk.save (err) ->
           if err then next(new apiErrors.InternalServerError(err))
-          else res.json(preparePlunk(req.session, plunk))
+          else
+            populate = {}
+            populate.user = req.user.toJSON() if req.user
+
+            res.json(preparePlunk(req.session, plunk, populate))

 # Give a thumbs-up to a plunk
 app.post "/plunks/:id/thumb", (req, res, next) ->


correct mis-spelling

correct mis-spelling

--- bokehjs/src/coffee/models/widgets/slider.coffee
+++ bokehjs/src/coffee/models/widgets/slider.coffee
@@ -18,7 +18,7 @@
     html = @template(@model.attributes)
     @$el.html(html)
     @callbackWrapper = null
-    if @mget('callback_policy') == 'continious'
+    if @mget('callback_policy') == 'continuous'
       @callbackWrapper = () ->
         @mget('callback')?.execute(@model)
     if @mget('callback_policy') == 'throttle' and @mget('callback')


Implement buffered stream destroy

Implement buffered stream destroy

--- src/nack/buffered.coffee
+++ src/nack/buffered.coffee
@@ -40,6 +40,11 @@

   # Ignore requrests to pause the stream
   pause: ->
+
+  # Destroy `@stream` and clear queue
+  destroy: ->
+    @_queue = []
+    @stream.destroy()

   flush: ->
     # Tell the `@stream` to resume


Send components and graphs after project

--- components/GistToProject.coffee
+++ components/GistToProject.coffee
@@ -32,6 +32,7 @@
     request.on 'success', (res) ->
       unless res.body?.files
         return callback new Error "Gist #{data.payload.gist} didn't provide any files"
+      project.name = res.body.description
       for name, file of res.body.files
         basename = path.basename name, path.extname name
         if path.extname(name) is '.json'
@@ -41,11 +42,10 @@
               project: project.id
               id: "#{project.id}_#{basename}"
             project.main = graph.properties.id unless project.main
-            project.name = graph.properties.name unless project.name
-            project.graphs.push graph
+            project.name = graph.name unless project.name
             if graph.properties?.environment?.type
               project.type = graph.properties.environment.type unless project.type
-            out.graph.send graph
+            project.graphs.push graph
           continue
         # Component
         component =
@@ -54,10 +54,13 @@
           project: project.id
           code: file.content
           tests: ''
-        project.components.push graph
-        out.component.send component
+        project.components.push
         continue
       out.project.send project
+      for graph in project.graphs
+        out.graph.send graph
+      for component in project.components
+        components.push component
       do callback
     request.on 'error', (err) ->
       callback err.error or err.body


Actually setup split test in client

Actually setup split test in client

--- components/layout/client.coffee
+++ components/layout/client.coffee
@@ -18,6 +18,7 @@
 RecentConnections = require '../../collections/recent_connections.coffee'
 CurrentUser = require '../../models/current_user.coffee'
 analytics = require '../../lib/analytics.coffee'
+setupSplitTests = require '../split_test/setup.coffee'
 initNightMode = require '../night_mode/index.coffee'

 module.exports = ->
@@ -132,6 +133,7 @@
   return if sd.SAVE
   analytics ga: ga
   analytics.registerCurrentUser()
+  setupSplitTests()
   unless (sd.CHANNEL and sd.CHANNEL.status is 'private')
     analytics.trackPageview()


removed some unnecessary assignment; related: #1491

--- nodes.coffee
+++ nodes.coffee
@@ -659,14 +659,14 @@
     [lt, gt] = ["#{idx} <#{@equals}", "#{idx} >#{@equals}"]

     # Generate the condition.
-    if @stepNum
-      condPart = if +@stepNum > 0 then "#{lt} #{@toVar}" else "#{gt} #{@toVar}"
+    condPart = if @stepNum
+      if +@stepNum > 0 then "#{lt} #{@toVar}" else "#{gt} #{@toVar}"
     else if known
       [from, to] = [+@fromNum, +@toNum]
-      condPart   = if from <= to then "#{lt} #{to}" else "#{gt} #{to}"
+      if from <= to then "#{lt} #{to}" else "#{gt} #{to}"
     else
       cond     = "#{@fromVar} <= #{@toVar}"
-      condPart = "#{cond} ? #{lt} #{@toVar} : #{gt} #{@toVar}"
+      "#{cond} ? #{lt} #{@toVar} : #{gt} #{@toVar}"

     # Generate the step.
     stepPart = if @stepVar


quick fix to part of new issue in #1099:

`not in []` unconditionally compiled to `false`

--- nodes.coffee
+++ nodes.coffee
@@ -1408,11 +1408,11 @@
     @compileLoopTest o

   compileOrTest: (o) ->
+    return "#{!!@negated}" if @array.base.objects.length is 0
     [sub, ref] = @object.cache o, LEVEL_OP
     [cmp, cnj] = if @negated then [' !== ', ' && '] else [' === ', ' || ']
     tests = for item, i in @array.base.objects
       (if i then ref else sub) + cmp + item.compile o, LEVEL_ACCESS
-    return 'false' if tests.length is 0
     tests = tests.join cnj
     if o.level < LEVEL_OP then tests else "(#{tests})"


fixed behavioural change accidentally introduced by #1348, thanks @satyr

--- nodes.coffee
+++ nodes.coffee
@@ -1317,7 +1317,7 @@
       fst = @first
       ref = fst
     if @isExistentialEquals
-      new If(new Existence(fst).invert(), @second, type: 'if').compile o
+      new If(new Existence(fst).invert(), @second, type: 'if').addElse(fst).compile o
     else
       new If(new Existence(fst), ref, type: 'if').addElse(@second).compile o


Throwing an error when pattern matching has a non-identifier on the left-hand side.

--- nodes.coffee
+++ nodes.coffee
@@ -692,6 +692,8 @@
     for obj, i in @variable.base.objects
       idx: i
       [obj, idx]: [obj.value, obj.variable.base] if @variable.is_object()
+      if not (obj instanceof ValueNode or obj instanceof SplatNode)
+        throw new Error 'pattern matching must use only identifiers on the left-hand side.'
       is_string: idx.value and idx.value.match IS_STRING
       access_class: if is_string or @variable.is_array() then IndexNode else AccessorNode
       if obj instanceof SplatNode and not splat


Typo: schmd -> schomd

Typo: schmd -> schomd

I think this is the cause of ZotPlus/zotero-citations#11

--- chrome/content/zotero-better-bibtex/cayw.coffee
+++ chrome/content/zotero-better-bibtex/cayw.coffee
@@ -257,7 +257,7 @@
   'atom-zotero-citations': (citations, options = {}) ->
     citekeys = (citation.citekey for citation in citations)

-    items = (item for item in Zotero.BetterBibTeX.schmd.items(citekeys, options) when item)
+    items = (item for item in Zotero.BetterBibTeX.schomd.items(citekeys, options) when item)
     url = "http://www.zotero.org/styles/#{options.style ? 'apa'}"
     style = Zotero.Styles.get(url)
     cp = style.getCiteProc()


activity: set replies property of preview data as an empty array

--- client/Social/Activity/views/inputwidget.coffee
+++ client/Social/Activity/views/inputwidget.coffee
@@ -304,6 +304,7 @@
       account       : KD.whoami().bongo_
       body          : value
       typeConstant  : 'post'
+      replies       : []
       interactions  :
         like        :
           actorsCount : 0


Activity: scroll behavior on messageWasAdded improved

--- client/Social/Activity/views/messagepane.coffee
+++ client/Social/Activity/views/messagepane.coffee
@@ -45,7 +45,7 @@
       @listController.getListView().on 'ItemWasAdded', @bound 'scrollUp'

-  scrollDown: ->
+  scrollDown: (item) ->

     return  unless @active

@@ -53,6 +53,8 @@
     unless @separator
       @separator = new KDView cssClass : 'new-messages'
       listView.addSubView @separator
+
+    return  unless item is listView.items.last

     KD.utils.defer -> window.scrollTo 0, document.body.scrollHeight


getCanonical bugfix

getCanonical bugfix

--- fetching.coffee
+++ fetching.coffee
@@ -89,7 +89,7 @@
         if _.isArray x
             J.util.sortByKey(
                 @_getCanonical y for y in x
-                @_getCanonical
+                (y) => EJSON.stringify @_getCanonical y
             )
         else if J.util.isPlainObject x
             sortedKeys = J.util.sortByKey _.keys x


fixing _fast_path

fixing _fast_path

--- subtree/bokehjs/static/coffee/renderers/glyph/circle.coffee
+++ subtree/bokehjs/static/coffee/renderers/glyph/circle.coffee
@@ -97,6 +97,7 @@
           continue
         if use_selection == false and @selected_mask[i]
           continue
+        ctx.moveTo(@sx[i], @sy[i])
         ctx.arc(@sx[i], @sy[i], @radius[i], 0, 2*Math.PI, false)
       ctx.fill()

@@ -110,6 +111,7 @@
           continue
         if use_selection == false and  @selected_mask[i]
           continue
+        ctx.moveTo(@sx[i], @sy[i])
         ctx.arc(@sx[i], @sy[i], @radius[i], 0, 2*Math.PI, false)
       ctx.stroke()


Loading aws credentials in Gruntfile

Loading aws credentials in Gruntfile

--- frontend/Gruntfile.coffee
+++ frontend/Gruntfile.coffee
@@ -1,15 +1,15 @@
 module.exports = (grunt) ->
 	grunt.initConfig({})
 	require('load-grunt-tasks')(grunt)
-	grunt.loadTasks('grunt_tasks')
 	require('mflib/grunt/build')(grunt)
 	require('mflib/grunt/serve')(grunt)
+	require('mflib/grunt/deploy')(grunt)

 	grunt.config.merge
 		mf:
-			jsOut: 'main.js'
-			port: 9002
-			awsCredentials: 'grunt-aws.json'
+			aws: grunt.file.readJSON('grunt-aws.json')
+			# jsOut: 'app.js'
+			# port: 9001
 			deployPath: ''
 			buckets:
 				production: ''


add dispatcherCount to config file

add dispatcherCount to config file

--- config/settings.defaults.coffee
+++ config/settings.defaults.coffee
@@ -86,5 +86,7 @@

 	max_doc_length: 2 * 1024 * 1024 # 2mb

+	dispatcherCount: process.env["DISPATCHER_COUNT"]
+
 	mongo:
 		url: "mongodb://#{process.env["MONGO_HOST"] or "localhost"}/sharelatex"


Minor demo app enhancement

Minor demo app enhancement

--- demo/client/index.coffee
+++ demo/client/index.coffee
@@ -1,15 +1,16 @@
 Template.index.onCreated ->
+  self            = @
   @take           = new ReactiveVar 50
   @filesLength    = new ReactiveVar 0
-  @getFilesLenght = =>
-    Meteor.call 'filesLenght', (error, length) =>
+  @getFilesLenght = ->
+    Meteor.call 'filesLenght', (error, length) ->
       if error
         console.error error
       else
-        @filesLength.set length
+        self.filesLength.set length
   @getFilesLenght()

-  @autorun => _app.subs.subscribe 'latest', @take.get()
+  @autorun -> _app.subs.subscribe 'latest', self.take.get()

 Template.index.helpers
   take:        -> Template.instance().take.get()


Smooth scroll; optimization and more tidy up.

--- content_scripts/scroller.coffee
+++ content_scripts/scroller.coffee
@@ -38,49 +38,49 @@
 # Test whether element should be scrolled.
 shouldScroll = (element, direction) ->
   computedStyle = window.getComputedStyle(element)
-  # Elements with `overflow: hidden` should not be scrolled.
+  # Elements with `overflow: hidden` must not be scrolled.
   return false if computedStyle.getPropertyValue("overflow-#{direction}") == "hidden"
-  # Non-visible elements should not be scrolled.
+  # Elements which are not visible should not be scrolled.
   return false if computedStyle.getPropertyValue("visibility") in ["hidden", "collapse"]
   return false if computedStyle.getPropertyValue("display") == "none"
   true

-# Test whether element actually scrolls in the direction required when asked to do so.  Due to chrome bug
+# Test whether element does actually scroll in the direction required when asked to do so.  Due to chrome bug
 # 110149, scrollHeight and clientHeight cannot be used to reliably determine whether an element will scroll.
 # Instead, we scroll the element by 1 or -1 and see if it moved (then put it back).
 # Bug verified in Chrome 38.0.2125.104.
-isScrollPossible = (element, direction, amount, factor) ->
-  # amount, here, is treated as a relative amount, which is correct for relative scrolls. For absolute scrolls
-  # (only gg, G, and friends), amount can be either 'max' or zero. In the former case, we're definitely
-  # scrolling forwards, so any positive value will do for delta.  In the latter, we're definitely scrolling
-  # backwards, so a delta of -1 will do.
+doesScroll = (element, direction, amount, factor) ->
+  # amount is treated as a relative amount, which is correct for relative scrolls. For absolute scrolls (only
+  # gg, G, and friends), amount can be either 'max' or zero. In the former case, we're definitely scrolling
+  # forwards, so any positive value will do for delta.  In the latter, we're definitely scrolling backwards,
+  # so a delta of -1 will do.  For absolute scrolls, factor is always 1.
   delta = factor * getDimension(element, direction, amount) || -1
   delta = Math.sign delta # 1 or -1
   performScroll(element, direction, delta) and performScroll(element, direction, -delta)

-# Find the element which we should and can scroll (or document.body).
-findScrollableElement = (element, direction, amount, factor = 1) ->
+# From element and its parents, find the first which we should scroll and which does scroll.
+findScrollableElement = (element, direction, amount, factor) ->
   while element != document.body and
-    not (isScrollPossible(element, direction, amount, factor) and shouldScroll(element, direction))
+    not (doesScroll(element, direction, amount, factor) and shouldScroll(element, direction))
       element = element.parentElement || document.body
   element

 checkVisibility = (element) ->
-  # if the activated element has been scrolled completely offscreen, subsequent changes in its scroll
-  # position will not provide any more visual feedback to the user. therefore we deactivate it so that
-  # subsequent scrolls only move the parent element.
+  # If the activated element has been scrolled completely offscreen, then subsequent changes in its scroll
+  # position will not provide any more visual feedback to the user. Therefore, we deactivate it so that
+  # subsequent scrolls affect the parent element.
   rect = activatedElement.getBoundingClientRect()
   if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth)
     activatedElement = element

-# How scrolling is handled:
-#   - For non-smooth scrolling, the entire scroll happens immediately.
+# How scrolling is handled by CoreScroller.
+#   - For jump scrolling, the entire scroll happens immediately.
 #   - For smooth scrolling with distinct key presses, a separate animator is initiated for each key press.
 #     Therefore, several animators may be active at the same time.  This ensures that two quick taps on `j`
 #     scroll to the same position as two slower taps.
 #   - For smooth scrolling with keyboard repeat (continuous scrolling), the most recently-activated animator
-#     continues scrolling at least until its corresponding keyup event is received.  We never initiate a new
-#     animator on keyboard repeat.
+#     continues scrolling at least until its keyup event is received.  We never initiate a new animator on
+#     keyboard repeat.

 CoreScroller =
   init: (frontendSettings) ->
@@ -97,13 +97,16 @@
         @keyIsDown = false
         @time += 1

+  # Return true if CoreScroller would not initiate a new scroll right now.
+  wouldNotInitiateScroll: -> @lastEvent?.repeat and @settings.get "smoothScroll"
+
   # Calibration fudge factors for continuous scrolling.  The calibration value starts at 1.0.  We then
   # increase it (until it exceeds @maxCalibration) if we guess that the scroll is too slow, or decrease it
   # (until it is less than @minCalibration) if we guess that the scroll is too fast.  The cutoff point for
   # which guess we make is @calibrationBoundary. We require: 0 < @minCalibration <= 1 <= @maxCalibration.
-  minCalibration: 0.5 # Controls how much we're willing to slow scrolls down; smaller => more slow down.
-  maxCalibration: 1.6 # Controls how much we're willing to speed scrolls up; bigger => more speed up.
-  calibrationBoundary: 150 # Boundary between scrolls which are considered too slow, and those too fast.
+  minCalibration: 0.5 # Controls how much we're willing to slow scrolls down; smaller means more slow down.
+  maxCalibration: 1.6 # Controls how much we're willing to speed scrolls up; bigger means more speed up.
+  calibrationBoundary: 150 # Boundary between scrolls which are considered too slow, or too fast.

   # Scroll element by a relative amount (a number) in some direction.
   scroll: (element, direction, amount) ->
@@ -115,31 +118,28 @@
       checkVisibility element
       return

-    # We don't activate new animators on keyboard repeats.
+    # We don't activate new animators on keyboard repeats; rather, the most-recently activated animator
+    # continues scrolling.
     return if @lastEvent?.repeat

     activationTime = ++@time
-    isMyKeyStillDown = => @time == activationTime and @keyIsDown
-
-    # Store amount's sign and make amount positive; the logic is clearer when amount is positive.
+    myKeyIsStillDown = => @time == activationTime and @keyIsDown
+
+    # Store amount's sign and make amount positive; the arithmetic is clearer when amount is positive.
     sign = Math.sign amount
     amount = Math.abs amount

-    # Duration in ms. Allow a bit longer for longer scrolls.
+    # Initial intended scroll duration (in ms). We allow a bit longer for longer scrolls.
     duration = Math.max 100, 20 * Math.log amount

     totalDelta = 0
     totalElapsed = 0.0
     calibration = 1.0
     previousTimestamp = null
-    animatorId = null
-
-    advanceAnimation = -> animatorId = requestAnimationFrame animate
-    cancelAnimation = -> cancelAnimationFrame animatorId

     animate = (timestamp) =>
       previousTimestamp ?= timestamp
-      return advanceAnimation() if timestamp == previousTimestamp
+      return requestAnimationFrame(animate) if timestamp == previousTimestamp

       # The elapsed time is typically about 16ms.
       elapsed = timestamp - previousTimestamp
@@ -149,23 +149,24 @@
       # The constants in the duration calculation, above, are chosen to provide reasonable scroll speeds for
       # distinct keypresses.  For continuous scrolls, some scrolls are too slow, and others too fast. Here, we
       # speed up the slower scrolls, and slow down the faster scrolls.
-      if isMyKeyStillDown() and 50 <= totalElapsed and @minCalibration <= calibration <= @maxCalibration
+      if myKeyIsStillDown() and 75 <= totalElapsed and @minCalibration <= calibration <= @maxCalibration
         calibration *= 1.05 if 1.05 * calibration * amount < @calibrationBoundary # Speed up slow scrolls.
         calibration *= 0.95 if @calibrationBoundary < 0.95 * calibration * amount # Slow down fast scrolls.

       # Calculate the initial delta, rounding up to ensure progress.  Then, adjust delta to account for the
       # current scroll state.
       delta = Math.ceil amount * (elapsed / duration) * calibration
-      delta = if isMyKeyStillDown() then delta else Math.max 0, Math.min delta, amount - totalDelta
+      delta = if myKeyIsStillDown() then delta else Math.max 0, Math.min delta, amount - totalDelta

       if delta and performScroll element, direction, sign * delta
         totalDelta += delta
-        advanceAnimation()
+        requestAnimationFrame animate
       else
+        # We're done.
         checkVisibility element
-        cancelAnimation()
-
-    advanceAnimation()
+
+    # Launch animator.
+    requestAnimationFrame animate

 Scroller =
   init: (frontendSettings) ->
@@ -186,15 +187,18 @@
     activatedElement ||= document.body
     return unless activatedElement

-    element = findScrollableElement activatedElement, direction, amount, factor
-    elementAmount = factor * getDimension element, direction, amount
-    CoreScroller.scroll element, direction, elementAmount
+    # Avoid the expensive scroll calculation if it will not be used.  This reduces costs during smooth,
+    # continuous scrolls, and is just an optimization.
+    unless CoreScroller.wouldNotInitiateScroll()
+      element = findScrollableElement activatedElement, direction, amount, factor
+      elementAmount = factor * getDimension element, direction, amount
+      CoreScroller.scroll element, direction, elementAmount

   scrollTo: (direction, pos) ->
     return unless document.body or activatedElement
     activatedElement ||= document.body

-    element = findScrollableElement activatedElement, direction, pos
+    element = findScrollableElement activatedElement, direction, pos, 1
     amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName]
     CoreScroller.scroll element, direction, amount


added units

added units

--- grammars/geant4-macro.cson
+++ grammars/geant4-macro.cson
@@ -22,4 +22,9 @@
         'name': 'constant.boolean'
         'match': '(?<!\/)\\b(true|false|TRUE|FALSE|t|f|T|F)\\b(?!\/)'
     }
+    {
+        # units, this line is ugly!
+        'name': 'constant.numeric'
+        'match': '(?<!\/)\\b(millimeter|millimeter2|millimeter3|centimeter|centimeter2|centimeter3|meter|meter2|meter3|kilometer|kilometer2|kilometer3|parsec|micrometer|nanometer|angstrom|fermi|barn|millibarn|microbarn|nanobarn|picobarn|mm|um|nm|mm2|mm3|cm|cm2|cm3|liter|L|dL|cL|mL|m|m2|m3|km|km2|km3|pc|radian|milliradian|degree|steradian|rad|mrad|sr|deg|nanosecond|second|millisecond|microsecond|picosecond|hertz|kilohertz|megahertz|ns|s|ms|eplus|e_SI|coulomb|megaelectronvolt|electronvolt|kiloelectronvolt|gigaelectronvolt|teraelectronvolt|petaelectronvolt|joule|MeV|eV|keV|GeV|TeV|PeV|kilogram|gram|milligram|kg|g|mg|watt|newton|hep_pascal|bar|atmosphere|ampere|milliampere|microampere|nanoampere|megavolt|kilovolt|volt|ohm|farad|millifarad|microfarad|nanofarad|picofarad|weber|tesla|gauss|kilogauss|henry|kelvin|mole|becquerel|curie|gray|candela|lumen|lux|perCent|perThousand|perMillion)\\b(?!\/)'
+    }
 ]


Removed superflous line

Removed superflous line

--- test/spec/application_spec.coffee
+++ test/spec/application_spec.coffee
@@ -24,7 +24,6 @@
       derivedApp = new DerivedApplication()
       expect(derivedApp instanceof Application).toBe true

-      expect(app instanceof Application).toBe true
     it 'should initialize', ->
       expect(typeof app.initialize).toBe 'function'
       app.initialize()


bug fix

bug fix

--- client/landing/app/MainApp/book/bookview.coffee
+++ client/landing/app/MainApp/book/bookview.coffee
@@ -154,7 +154,8 @@
     if @pointer then @destroyPointer()

     # check if page has tutorial
-    if @page.getData().howToSteps.length < 1
+    if @page.getData().howToSteps.length < 1 and
+       KD.singletons.vmController.defaultVmName
       @showMeButton.hide()
     else
       @showMeButton.show()
@@ -164,7 +165,6 @@
     @pointer = new KDCustomHTMLView
       partial : ''
       cssClass : 'point'
-

     @pointer.bindTransitionEnd()

@@ -288,7 +288,8 @@
         @destroyPointer()

     user = KD.nick()
-    userVmName = "[koding~#{user}~0]/home/#{user}"
+    defaultVMName = KD.singletons.vmController.defaultVmName
+    userVmName = "[#{defaultVMName}]/home/#{user}"
     @utils.wait 500, =>
       @defaultVm = KD.getSingleton("finderController").treeController.nodes[userVmName]
       @defaultVm.setClass('selected')
@@ -410,7 +411,8 @@
       @navigateToFolder()

     user = KD.nick()
-    userVmName = "[koding~#{user}~0]/home/#{user}"
+    defaultVmName = KD.singletons.vmController.defaultVmName
+    userVmName = "[#{defaultVmName}]/home/#{user}"
     @utils.wait 500, =>
       @defaultVm = KD.getSingleton("finderController").treeController.nodes[userVmName]
       # find file tree's menu position
@@ -432,7 +434,8 @@

     # find user Web folder location
     user = KD.nick()
-    webFolder = "[koding~#{user}~0]/home/#{user}/Web"
+    defaultVMName = KD.singletons.vmController.defaultVmName
+    webFolder = "[#{defaultVMName}]/home/#{user}/Web"
     @webFolderItem = KD.getSingleton("finderController").treeController.nodes[webFolder]
     offsetTo = @webFolderItem.$().offset()
     # navigate to folder
@@ -454,7 +457,8 @@
     @utils.wait 3000, =>
       # find index.html position
       user = KD.nick()
-      indexFile = "[koding~#{user}~0]/home/#{user}/Web/index.html"
+      defaultVMName = KD.singletons.vmController.defaultVmName
+      indexFile = "[#{defaultVMName}]/home/#{user}/Web/index.html"
       @indexFileItem = KD.getSingleton("finderController").treeController.nodes[indexFile]
       # move cursor to index.html position
       offsetTo = @indexFileItem.$().offset()
@@ -539,6 +543,7 @@

   destroyPointer:->
     @unsetClass('aside')
+    @setKeyView()
     @utils.wait 500, =>
       @pointer.destroy()


Added check_box_collection input in form_helper.js

--- vendor/assets/javascripts/form_helper/form_helper.js.coffee
+++ vendor/assets/javascripts/form_helper/form_helper.js.coffee
@@ -5,6 +5,7 @@
 #= require './templates/date_field'
 #= require './templates/text_area'
 #= require './templates/check_box'
+#= require './templates/check_box_collection'

 class @FormHelper

@@ -36,6 +37,12 @@
     attrs = @basics4attr(attr, value, html_options)
     attrs.title = title
     JST['form_helper/templates/check_box'] attrs
+
+  check_box_collection: (attr, options, html_options={}) ->
+    attrs = @basics4attr(attr, null, html_options)
+    attrs.values = options.values
+    attrs.checked ||= options.checked
+    JST['form_helper/templates/check_box_collection'] attrs

   date_field: (attr, html_options={}) ->
     JST['form_helper/templates/date_field'] @basics4attr(attr, value?, html_options)


rule name is unique

rule name is unique

--- workers/social/lib/social/models/proxy/rule.coffee
+++ workers/social/lib/social/models/proxy/rule.coffee
@@ -12,6 +12,9 @@
     sharedMethods   :
       instance      : []
       static        : ['fetchRulesByDomain', 'fetchRuleByDomainAndMatch']
+
+    indexes         :
+      name          : 'unique'

     schema          :


social: send pin and jwtToken as part of event

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -1459,6 +1459,7 @@
     user             = null
     error            = null
     newToken         = null
+    jwtToken         = null
     invitation       = null
     foreignAuthType  = null

@@ -1555,7 +1556,7 @@
       ->
         subject             = Tracker.types.START_REGISTER
         { username, email } = user
-        Tracker.track username, { to : email, subject }
+        Tracker.track username, { to : email, subject, jwtToken, pin }
         queue.next()

       ->


HHP COP cost chart no longer recieves a curve

Instead of the electricity cost being a curve, receive a single value
(the cost when COP is 1.0) and generate values for all other COPs to
ensure a smooth line is drawn.

--- app/assets/javascripts/d3/hhp_cop_cost.coffee
+++ app/assets/javascripts/d3/hhp_cop_cost.coffee
@@ -1,6 +1,12 @@
 D3.hhp_cop_cost =
   View: class extends D3ChartView
-    COP_LIST = [1, 2, 3, 4, 5, 6]
+    MIN_COP = 1
+    MAX_COP = 6
+
+    COP_LIST = [MIN_COP]
+
+    while COP_LIST[COP_LIST.length - 1] < MAX_COP
+      COP_LIST.push(Math.round(10 * (COP_LIST[COP_LIST.length - 1] + 0.1)) / 10)

     initialize: ->
       D3ChartView.prototype.initialize.call(this)
@@ -85,8 +91,8 @@
         .attr('cx', (data) -> xScale(data.x))
         .attr('cy', (data) -> yScale(data.y))
         .attr('data-tooltip-text', (data) =>
-          "COP #{Metric.round_number(data.y, 2)}, " +
-          "#{Metric.round_number(data.x, 2)} #{@model.get('unit')} "
+          "COP #{Metric.round_number(data.x, 2)}, " +
+          "#{Metric.round_number(data.y, 2)} #{@model.get('unit')} "
         )

       @drawLegend(@model.non_target_series().concat(@model.target_series()), 2)
@@ -103,10 +109,13 @@
       )

     serieData: (serie) =>
-      values = serie.future_value()
-
-      if typeof values == 'number' || !values
-        values = Array.apply(null, Array(COP_LIST.length)).map(-> values || 0)
+      value = serie.future_value()
+
+      values =
+        if serie.get('gquery_key').match(/_electricity_cop/) && value
+          (value / num for num in COP_LIST)
+        else
+          Array.apply(null, Array(COP_LIST.length)).map(-> value || 0)

       {
         color:     serie.get('color'),
@@ -125,7 +134,7 @@
     maxYValue: ->
       d3.max(
         @model.target_series().map(
-          (serie) -> d3.max(serie.future_value())
+          (serie) -> serie.future_value()
         )
       )

@@ -136,9 +145,7 @@
       d3.scale.linear().domain([0, @maxYValue()]).range([@height, 0]).nice()

     createXScale: ->
-      d3.scale.linear().domain([
-        COP_LIST[COP_LIST.length - 1], COP_LIST[0]]
-      ).range([@width, 0])
+      d3.scale.linear().domain([MAX_COP, MIN_COP]).range([@width, 0])

     # Draws the Y axis onto charts, configuring the scaling and grey grid lines.
     drawYAxis: (scale) ->
@@ -156,15 +163,6 @@
         .style("text-anchor", "end")
         .text(@model.get('unit'))

-      yaxisg.selectAll("line").data(scale.ticks(8), (d) => d)
-        .enter()
-        .append("line")
-        .attr("class", "minor")
-        .attr("y1", scale)
-        .attr("y2", scale)
-        .attr("x1", 0)
-        .attr("x2", @width)
-
       scale

     createYAxis: (scale) ->
@@ -190,20 +188,11 @@
         .attr('y', @margins.bottom)
         .text('COP')

-      xaxisg.selectAll("line").data(scale.ticks(12), (d) => d)
-        .enter()
-        .append("line")
-        .attr("class", "minor")
-        .attr("y1", 0)
-        .attr("y2", -@height)
-        .attr("x1", scale)
-        .attr("x2", scale)
-
       scale

     createXAxis: (scale) ->
       d3.svg.axis().scale(scale)
-        .ticks(COP_LIST.length)
+        .ticks(COP_LIST.length / 10)
         .tickSize(-@height, 0)
         .tickFormat((v) => v)


making sure that the base model for the batman ems application gets loaded in before all the other models

--- app/assets/javascripts/ems/application.coffee
+++ app/assets/javascripts/ems/application.coffee
@@ -20,6 +20,7 @@

 #= require ems/ems

+#= require ./models/base
 #= require_tree ./models
 #= require_tree ./controllers
 #= require_tree ./helpers


move teapot closer to camera

move teapot closer to camera

--- app/assets/jax/controllers/lighting.js.coffee
+++ app/assets/jax/controllers/lighting.js.coffee
@@ -4,9 +4,9 @@
   index: ->
     @world.addObject new Jax.Framerate ema: no
     @light = @world.addLight new Jax.Light.Directional
-      direction: [-1, 0, 0]
+      direction: [-1, -2, -1]

     @world.addObject new Jax.Model
-      position: [0, 0, -5]
+      position: [0, 0, -2]
       mesh: new Jax.Mesh.Teapot
       update: (tc) -> @camera.rotate tc * 0.001, [1, 0.75, 0.5]


gradebook should not filter for deleted grading periods

when a grading period is selected on gradebook page, then that
grading period is removed, when the grade bookpage is refreshed
the drop down will no longer show that grading period and it will
not filter assignments for that grading period. after refreshing
the page, the page will either 1) filter assignments for another
grading period if the period is current (today's date falls between
the grading period's start and end dates) or 2) show all grading
periods. #1 takes precedence over #2.

closes CNVS-19937

test plan:

select a grading period in the gradebook, then delete that
grading period. next, go back to the gradebook page. if there
is a current grading period (today's date falls between
that grading period's start and end dates), the gradebook
should now be filtered for that grading period (and the dropdown
should show that grading period listed). Otherwise, if there
are no current grading periods, all grading periods should be
shown and the dropdown should show 'All Grading Periods'

Change-Id: I1626cb44bdba47fda26a53382d2151f0b7a584fb
Reviewed-on: https://gerrit.instructure.com/52793
Tested-by: Jenkins
Reviewed-by: Josh Simpson <bfb4987b134a00f73d881fff5113b1d5fa1876bc@instructure.com>
QA-Review: Amber Taniuchi <ef4bfd1e6d278f6bc7d4a478f02be2cb9581a0a5@instructure.com>
Product-Review: Spencer Olson <7cc0890d6f1f70810a023e7173eca5724d6c8e49@instructure.com>

--- app/coffeescripts/gradebook2/Gradebook.coffee
+++ app/coffeescripts/gradebook2/Gradebook.coffee
@@ -86,7 +86,7 @@
       @numberOfFrozenCols = if @totalColumnInFront then 3 else 2
       @mgpEnabled = ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled
       @gradingPeriods = ENV.GRADEBOOK_OPTIONS.active_grading_periods
-      @gradingPeriodToShow = userSettings.contextGet('gradebook_current_grading_period') || ENV.GRADEBOOK_OPTIONS.current_grading_period_id
+      @gradingPeriodToShow = @getGradingPeriodToShow()

       $.subscribe 'assignment_group_weights_changed', @handleAssignmentGroupWeightChange
       $.subscribe 'assignment_muting_toggled',        @handleAssignmentMutingChange
@@ -193,6 +193,17 @@

     gradingPeriodIsClosed: (gradingPeriod) ->
       new Date(gradingPeriod.end_date) < new Date()
+
+    gradingPeriodIsActive: (gradingPeriodId) ->
+      activePeriodIds = _.pluck(@gradingPeriods, 'id')
+      _.contains(activePeriodIds, gradingPeriodId)
+
+    getGradingPeriodToShow: () ->
+      currentPeriodId = userSettings.contextGet('gradebook_current_grading_period')
+      if currentPeriodId && @gradingPeriodIsActive(currentPeriodId)
+        currentPeriodId
+      else
+        ENV.GRADEBOOK_OPTIONS.current_grading_period_id

     getAssignmentsInClosedGradingPeriods: (gradingPeriods) ->
       latestEndDate = new Date(gradingPeriods[0]?.end_date)


Compare view cannot show the note button

data-can-create-note == “false”

--- app/assets/javascripts/files_comment_button.js.coffee
+++ app/assets/javascripts/files_comment_button.js.coffee
@@ -1,6 +1,7 @@
 class @FilesCommentButton
   constructor: (@filesContainerElement) ->
-    return if not @filesContainerElement and not @filesContainerElement.data 'can-create-note'
+    return if not @filesContainerElement
+    return if not @filesContainerElement.data 'can-create-note'

     @COMMENT_BUTTON_CLASS = '.add-diff-note'
     @COMMENT_BUTTON_TEMPLATE = _.template("<button name='button' type='submit' class='btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button' title='Add a comment to this line'><i class='fa fa-comment-o'></i></button>")


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/flickerable.coffee
+++ test/flickerable.coffee
@@ -1,10 +1,10 @@
 module "Flickerable"

 test 'it should change the alpha value when the flicker duration has elapsed', ->
-  window.flicker.player = GameObject
+  player = GameObject
     alpha: 0.9

-  flicker.player.include Flickerable
+  player.include Flickerable

   equals flicker.player.I.alpha, 0.9


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/game_object.coffee
+++ test/game_object.coffee
@@ -18,7 +18,7 @@

 test "#closest", ->
   o = GameObject()
-  equals ,
+  equals o.closest()

 test "[event] create", 2, ->
   GameObject


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/game_object.coffee
+++ test/game_object.coffee
@@ -69,7 +69,7 @@
   gameObject.update(timeStep)

 test "[event] destroy", 1, ->
-  o = GameObject
+  o = GameObject()

   o.bind "destroy", ->
     ok true, "destroyed event is fired on destroy"


Fix bright attribute support

Fix bright attribute support

--- app/assets/javascripts/player/terminal_view.js.coffee
+++ app/assets/javascripts/player/terminal_view.js.coffee
@@ -74,11 +74,13 @@

       if hasCursor or brush.fg isnt undefined or brush.bg isnt undefined or brush.bright or brush.underline
         span = "<span class=\""
-        brightOffset = (if brush.bright then 8 else 0)

         if brush.fg isnt undefined
-          span += " fg" + (brush.fg + brightOffset)
-        else if brush.bright
+          fg = brush.fg
+          fg += 8 if fg < 8 and brush.bright
+          span += " fg" + fg
+
+        if brush.bright
           span += " bright"

         if brush.underline


minor ja fix

minor ja fix

--- app/assets/javascripts/uploadcare/locale/ja.js.coffee
+++ app/assets/javascripts/uploadcare/locale/ja.js.coffee
@@ -15,7 +15,6 @@
       deleted: '削除されたファイルです'
     draghere: 'ここにファイルをドロップ'
     file:
-      one: '%1ファイル'
       other: '%1ファイル'
     buttons:
       cancel: 'キャンセル'


profile: blog stuff

profile: blog stuff

--- components/inputs/KDInputViewWithPreview.coffee
+++ components/inputs/KDInputViewWithPreview.coffee
@@ -1,8 +1,8 @@
 class KDInputViewWithPreview extends KDInputView

-  constructor:(options,data)->
-
-    options.preview               or= {}
+  constructor:(options={},data)->
+
+    options.preview               ?= {}
     options.preview.autoUpdate    ?= yes
     options.preview.language      or= "markdown"
     options.preview.showInitially ?= yes


Fixed bug with heroes not loading the proper amount of gems.

--- app/views/play/level/modal/HeroVictoryModal.coffee
+++ app/views/play/level/modal/HeroVictoryModal.coffee
@@ -68,7 +68,8 @@
     for thangTypeOriginal in thangTypeOriginals
       thangType = new ThangType()
       thangType.url = "/db/thang.type/#{thangTypeOriginal}/version"
-      thangType.project = ['original', 'rasterIcon', 'name', 'soundTriggers', 'i18n']
+      #thangType.project = ['original', 'rasterIcon', 'name', 'soundTriggers', 'i18n']  # This is what we need, but the PlayHeroesModal needs more, and so we load more to fill up the supermodel.
+      thangType.project = ['original', 'rasterIcon', 'name', 'slug', 'soundTriggers', 'featureImages', 'gems', 'heroClass', 'description', 'components', 'extendedName', 'unlockLevelName', 'i18n']
       @thangTypes[thangTypeOriginal] = @supermodel.loadModel(thangType, 'thang').model

     @newEarnedAchievements = []


Use coffee class syntax

Use coffee class syntax

--- app/javascript/src/legacy/browser-post-loading.coffee
+++ app/javascript/src/legacy/browser-post-loading.coffee
@@ -1,308 +1,308 @@
-window.PostLoader = ->
-  document.on 'viewer:need-more-thumbs', @need_more_post_data.bindAsEventListener(this)
-  document.on 'viewer:perform-search', @perform_search.bindAsEventListener(this)
-  @hashchange_tags = @hashchange_tags.bind(this)
-  UrlHash.observe 'tags', @hashchange_tags
-  @cached_posts = new Hash
-  @cached_pools = new Hash
-  @sample_preload_container = null
-  @preloading_sample_for_post_id = null
-  @load results_mode: 'center-on-current'
-  return
-
-PostLoader::need_more_post_data = ->
-
-  ### We'll receive this message often once we're close to needing more posts.  Only
-  # start loading more data the first time.
+class window.PostLoader
+  constructor: ->
+    document.on 'viewer:need-more-thumbs', @need_more_post_data.bindAsEventListener(this)
+    document.on 'viewer:perform-search', @perform_search.bindAsEventListener(this)
+    @hashchange_tags = @hashchange_tags.bind(this)
+    UrlHash.observe 'tags', @hashchange_tags
+    @cached_posts = new Hash
+    @cached_pools = new Hash
+    @sample_preload_container = null
+    @preloading_sample_for_post_id = null
+    @load results_mode: 'center-on-current'
+    return
+
+  need_more_post_data: ->
+
+    ### We'll receive this message often once we're close to needing more posts.  Only
+    # start loading more data the first time.
+    ###
+
+    if @loaded_extended_results
+      return
+    @load extending: true
+    return
+
   ###
-
-  if @loaded_extended_results
-    return
-  @load extending: true
-  return
-
-###
-# This is a response time optimization.  If we know the sample URL of what we want to display,
-# we can start loading it from the server without waiting for the full post.json response
-# to come back and tell us.  This saves us the time of a round-trip before we start loading the
-# image.  The common case is if the user was on post and clicked on a link with "use
-# post browser" enabled.  This allows us to start loading the image immediately, without waiting
-# for any other network activity.
-#
-# We only do this for the sample image, to get a head-start loading it.  This is safe because
-# the image URLs are immutable (or effectively so).  The rest of the post information isn't cached.
-###
-
-PostLoader::preload_sample_image = ->
-  post_id = UrlHash.get('post-id')
-  if @preloading_sample_for_post_id == post_id
-    return
-  @preloading_sample_for_post_id = post_id
-  if @sample_preload_container
-    @sample_preload_container.destroy()
-    @sample_preload_container = null
-  if !post_id?
-    return
-
-  ### If this returns null, the browser doesn't support this. ###
-
-  cached_sample_urls = Post.get_cached_sample_urls()
-  if !cached_sample_urls?
-    return
-  if !(String(post_id) of cached_sample_urls)
-    return
-  sample_url = cached_sample_urls[String(post_id)]
-
-  ### If we have an existing preload_container, just add to it and allow any other
-  # preloads to continue.
+  # This is a response time optimization.  If we know the sample URL of what we want to display,
+  # we can start loading it from the server without waiting for the full post.json response
+  # to come back and tell us.  This saves us the time of a round-trip before we start loading the
+  # image.  The common case is if the user was on post and clicked on a link with "use
+  # post browser" enabled.  This allows us to start loading the image immediately, without waiting
+  # for any other network activity.
+  #
+  # We only do this for the sample image, to get a head-start loading it.  This is safe because
+  # the image URLs are immutable (or effectively so).  The rest of the post information isn't cached.
   ###
-
-  debug 'Advance preloading sample image for post ' + post_id
-  @sample_preload_container = new PreloadContainer
-  @sample_preload_container.preload sample_url
-  return
-
-PostLoader::server_load_pool = ->
-  if !@result.pool_id?
-    return
-  if !@result.disable_cache
-    pool = @cached_pools.get(@result.pool_id)
-    if pool
-      @result.pool = pool
+  preload_sample_image: ->
+    post_id = UrlHash.get('post-id')
+    if @preloading_sample_for_post_id == post_id
+      return
+    @preloading_sample_for_post_id = post_id
+    if @sample_preload_container
+      @sample_preload_container.destroy()
+      @sample_preload_container = null
+    if !post_id?
+      return
+
+    ### If this returns null, the browser doesn't support this. ###
+
+    cached_sample_urls = Post.get_cached_sample_urls()
+    if !cached_sample_urls?
+      return
+    if !(String(post_id) of cached_sample_urls)
+      return
+    sample_url = cached_sample_urls[String(post_id)]
+
+    ### If we have an existing preload_container, just add to it and allow any other
+    # preloads to continue.
+    ###
+
+    debug 'Advance preloading sample image for post ' + post_id
+    @sample_preload_container = new PreloadContainer
+    @sample_preload_container.preload sample_url
+    return
+
+  server_load_pool: ->
+    if !@result.pool_id?
+      return
+    if !@result.disable_cache
+      pool = @cached_pools.get(@result.pool_id)
+      if pool
+        @result.pool = pool
+        @request_finished()
+        return
+    new (Ajax.Request)('/pool/show.json',
+      parameters: id: @result.pool_id
+      method: 'get'
+      onCreate: ((resp) ->
+        @current_ajax_requests.push resp.request
+        return
+      ).bind(this)
+      onComplete: ((resp) ->
+        @current_ajax_requests = @current_ajax_requests.without(resp.request)
+        @request_finished()
+        return
+      ).bind(this)
+      onSuccess: ((resp) ->
+        if @current_ajax_requests.indexOf(resp.request) == -1
+          return
+        @result.pool = resp.responseJSON
+        @cached_pools.set @result.pool_id, @result.pool
+        return
+      ).bind(this))
+    return
+
+  server_load_posts: ->
+    tags = @result.tags
+    # Put holds:false at the beginning, so the search can override it.  Put limit: at
+    # the end, so it can't.
+    search = 'holds:false ' + tags + ' limit:' + @result.post_limit
+    if !@result.disable_cache
+      results = @cached_posts.get(search)
+      if results
+        @result.posts = results
+
+        ### Don't Post.register the results when serving out of cache.  They're already
+        # registered, and the data in the post registry may be more current than the
+        # cached search results.
+        ###
+
+        @request_finished()
+        return
+    new (Ajax.Request)('/post.json',
+      parameters:
+        tags: search
+        api_version: 2
+        filter: 1
+        include_tags: 1
+        include_votes: 1
+        include_pools: 1
+      method: 'get'
+      onCreate: ((resp) ->
+        @current_ajax_requests.push resp.request
+        return
+      ).bind(this)
+      onComplete: ((resp) ->
+        @current_ajax_requests = @current_ajax_requests.without(resp.request)
+        @request_finished()
+        return
+      ).bind(this)
+      onSuccess: ((resp) ->
+        if @current_ajax_requests.indexOf(resp.request) == -1
+          return
+        resp = resp.responseJSON
+        @result.posts = resp.posts
+        Post.register_resp resp
+        @cached_posts.set search, @result.posts
+        return
+      ).bind(this)
+      onFailure: ((resp) ->
+        error = 'error ' + resp.status
+        if resp.responseJSON
+          error = resp.responseJSON.reason
+        notice 'Error loading posts: ' + error
+        @result.error = true
+        return
+      ).bind(this))
+    return
+
+  request_finished: ->
+    if @current_ajax_requests.length
+      return
+
+    ### Event handlers for the events we fire below might make requests back to us.  Save and
+    # clear this.result before firing the events, so that behaves properly.
+    ###
+
+    result = @result
+    @result = null
+
+    ### If server_load_posts hit an error, it already displayed it; stop. ###
+
+    if result.error?
+      return
+
+    ### If we have no search tags (result.tags == null, result.posts == null), then we're just
+    # displaying a post with no search, eg. "/post/browse#12345".  We'll still fire off the
+    # same code path to make the post display in the view.
+    ###
+
+    new_post_ids = []
+    if result.posts?
+      i = 0
+      while i < result.posts.length
+        new_post_ids.push result.posts[i].id
+        ++i
+    document.fire 'viewer:displayed-pool-changed', pool: result.pool
+    document.fire 'viewer:searched-tags-changed', tags: result.tags
+
+    ### Tell the thumbnail viewer whether it should allow scrolling over the left side. ###
+
+    can_be_extended_further = true
+
+    ### If we're reading from a pool, we requested a large block already. ###
+
+    if result.pool
+      can_be_extended_further = false
+
+    ### If we're already extending, don't extend further. ###
+
+    if result.load_options.extending
+      can_be_extended_further = false
+
+    ### If we received fewer results than we requested we're at the end of the results,
+    # so don't waste time requesting more.
+    ###
+
+    if new_post_ids.length < result.post_limit
+      debug 'Received posts fewer than requested (' + new_post_ids.length + ' < ' + result.post_limit + '), clamping'
+      can_be_extended_further = false
+
+    ### Now that we have the result, update the URL hash.  Firing loaded-posts may change
+    # the displayed post, causing the post ID in the URL hash to change, so use set_deferred
+    # to help ensure these happen atomically.
+    ###
+
+    UrlHash.set_deferred tags: result.tags
+    document.fire 'viewer:loaded-posts',
+      tags: result.tags
+      post_ids: new_post_ids
+      pool: result.pool
+      extending: result.load_options.extending
+      can_be_extended_further: can_be_extended_further
+      load_options: result.load_options
+    return
+
+  ### If extending is true, load a larger set of posts. ###
+
+  load: (load_options) ->
+    if !load_options
+      load_options = {}
+    disable_cache = load_options.disable_cache
+    extending = load_options.extending
+    tags = load_options.tags
+    if !tags?
+      tags = UrlHash.get('tags')
+
+    ### If neither a search nor a post-id is specified, set a default search. ###
+
+    if !extending and (!tags?) and (!UrlHash.get('post-id')?)
+      UrlHash.set tags: ''
+
+      ### We'll receive another hashchange message for setting "tags".  Don't load now or we'll
+      # end up loading twice.
+      ###
+
+      return
+    debug 'PostLoader.load(' + extending + ', ' + disable_cache + ')'
+    @preload_sample_image()
+    @loaded_extended_results = extending
+
+    ### Discard any running AJAX requests. ###
+
+    @current_ajax_requests = []
+    @result = {}
+    @result.load_options = load_options
+    @result.tags = tags
+    @result.disable_cache = disable_cache
+    if !@result.tags?
+
+      ### If no search is specified, don't run one; return empty results. ###
+
       @request_finished()
       return
-  new (Ajax.Request)('/pool/show.json',
-    parameters: id: @result.pool_id
-    method: 'get'
-    onCreate: ((resp) ->
-      @current_ajax_requests.push resp.request
-      return
-    ).bind(this)
-    onComplete: ((resp) ->
-      @current_ajax_requests = @current_ajax_requests.without(resp.request)
-      @request_finished()
-      return
-    ).bind(this)
-    onSuccess: ((resp) ->
-      if @current_ajax_requests.indexOf(resp.request) == -1
-        return
-      @result.pool = resp.responseJSON
-      @cached_pools.set @result.pool_id, @result.pool
-      return
-    ).bind(this))
-  return
-
-PostLoader::server_load_posts = ->
-  tags = @result.tags
-  # Put holds:false at the beginning, so the search can override it.  Put limit: at
-  # the end, so it can't.
-  search = 'holds:false ' + tags + ' limit:' + @result.post_limit
-  if !@result.disable_cache
-    results = @cached_posts.get(search)
-    if results
-      @result.posts = results
-
-      ### Don't Post.register the results when serving out of cache.  They're already
-      # registered, and the data in the post registry may be more current than the
-      # cached search results.
-      ###
-
-      @request_finished()
-      return
-  new (Ajax.Request)('/post.json',
-    parameters:
-      tags: search
-      api_version: 2
-      filter: 1
-      include_tags: 1
-      include_votes: 1
-      include_pools: 1
-    method: 'get'
-    onCreate: ((resp) ->
-      @current_ajax_requests.push resp.request
-      return
-    ).bind(this)
-    onComplete: ((resp) ->
-      @current_ajax_requests = @current_ajax_requests.without(resp.request)
-      @request_finished()
-      return
-    ).bind(this)
-    onSuccess: ((resp) ->
-      if @current_ajax_requests.indexOf(resp.request) == -1
-        return
-      resp = resp.responseJSON
-      @result.posts = resp.posts
-      Post.register_resp resp
-      @cached_posts.set search, @result.posts
-      return
-    ).bind(this)
-    onFailure: ((resp) ->
-      error = 'error ' + resp.status
-      if resp.responseJSON
-        error = resp.responseJSON.reason
-      notice 'Error loading posts: ' + error
-      @result.error = true
-      return
-    ).bind(this))
-  return
-
-PostLoader::request_finished = ->
-  if @current_ajax_requests.length
-    return
-
-  ### Event handlers for the events we fire below might make requests back to us.  Save and
-  # clear this.result before firing the events, so that behaves properly.
-  ###
-
-  result = @result
-  @result = null
-
-  ### If server_load_posts hit an error, it already displayed it; stop. ###
-
-  if result.error?
-    return
-
-  ### If we have no search tags (result.tags == null, result.posts == null), then we're just
-  # displaying a post with no search, eg. "/post/browse#12345".  We'll still fire off the
-  # same code path to make the post display in the view.
-  ###
-
-  new_post_ids = []
-  if result.posts?
-    i = 0
-    while i < result.posts.length
-      new_post_ids.push result.posts[i].id
-      ++i
-  document.fire 'viewer:displayed-pool-changed', pool: result.pool
-  document.fire 'viewer:searched-tags-changed', tags: result.tags
-
-  ### Tell the thumbnail viewer whether it should allow scrolling over the left side. ###
-
-  can_be_extended_further = true
-
-  ### If we're reading from a pool, we requested a large block already. ###
-
-  if result.pool
-    can_be_extended_further = false
-
-  ### If we're already extending, don't extend further. ###
-
-  if result.load_options.extending
-    can_be_extended_further = false
-
-  ### If we received fewer results than we requested we're at the end of the results,
-  # so don't waste time requesting more.
-  ###
-
-  if new_post_ids.length < result.post_limit
-    debug 'Received posts fewer than requested (' + new_post_ids.length + ' < ' + result.post_limit + '), clamping'
-    can_be_extended_further = false
-
-  ### Now that we have the result, update the URL hash.  Firing loaded-posts may change
-  # the displayed post, causing the post ID in the URL hash to change, so use set_deferred
-  # to help ensure these happen atomically.
-  ###
-
-  UrlHash.set_deferred tags: result.tags
-  document.fire 'viewer:loaded-posts',
-    tags: result.tags
-    post_ids: new_post_ids
-    pool: result.pool
-    extending: result.load_options.extending
-    can_be_extended_further: can_be_extended_further
-    load_options: result.load_options
-  return
-
-### If extending is true, load a larger set of posts. ###
-
-PostLoader::load = (load_options) ->
-  if !load_options
-    load_options = {}
-  disable_cache = load_options.disable_cache
-  extending = load_options.extending
-  tags = load_options.tags
-  if !tags?
+
+    ### See if we have a pool search.  This only checks for pool:id searches, not pool:*name* searches;
+    # we want to know if we're displaying posts only from a single pool.
+    ###
+
+    pool_id = null
+    @result.tags.split(' ').each (tag) ->
+      m = tag.match(/^pool:(\d+)/)
+      if !m
+        return
+      pool_id = parseInt(m[1])
+      return
+
+    ### If we're loading from a pool, load the pool's data. ###
+
+    @result.pool_id = pool_id
+
+    ### Load the posts to display.  If we're loading a pool, load all posts (up to 1000);
+    # otherwise set a limit.
+    ###
+
+    limit = if extending then 1000 else 100
+    if pool_id?
+      limit = 1000
+    @result.post_limit = limit
+
+    ### Make sure that request_finished doesn't consider this request complete until we've
+    # actually started every request.
+    ###
+
+    @current_ajax_requests.push null
+    @server_load_pool()
+    @server_load_posts()
+    @current_ajax_requests = @current_ajax_requests.without(null)
+    @request_finished()
+    return
+
+  hashchange_tags: ->
     tags = UrlHash.get('tags')
-
-  ### If neither a search nor a post-id is specified, set a default search. ###
-
-  if !extending and (!tags?) and (!UrlHash.get('post-id')?)
-    UrlHash.set tags: ''
-
-    ### We'll receive another hashchange message for setting "tags".  Don't load now or we'll
-    # end up loading twice.
-    ###
-
-    return
-  debug 'PostLoader.load(' + extending + ', ' + disable_cache + ')'
-  @preload_sample_image()
-  @loaded_extended_results = extending
-
-  ### Discard any running AJAX requests. ###
-
-  @current_ajax_requests = []
-  @result = {}
-  @result.load_options = load_options
-  @result.tags = tags
-  @result.disable_cache = disable_cache
-  if !@result.tags?
-
-    ### If no search is specified, don't run one; return empty results. ###
-
-    @request_finished()
-    return
-
-  ### See if we have a pool search.  This only checks for pool:id searches, not pool:*name* searches;
-  # we want to know if we're displaying posts only from a single pool.
-  ###
-
-  pool_id = null
-  @result.tags.split(' ').each (tag) ->
-    m = tag.match(/^pool:(\d+)/)
-    if !m
-      return
-    pool_id = parseInt(m[1])
-    return
-
-  ### If we're loading from a pool, load the pool's data. ###
-
-  @result.pool_id = pool_id
-
-  ### Load the posts to display.  If we're loading a pool, load all posts (up to 1000);
-  # otherwise set a limit.
-  ###
-
-  limit = if extending then 1000 else 100
-  if pool_id?
-    limit = 1000
-  @result.post_limit = limit
-
-  ### Make sure that request_finished doesn't consider this request complete until we've
-  # actually started every request.
-  ###
-
-  @current_ajax_requests.push null
-  @server_load_pool()
-  @server_load_posts()
-  @current_ajax_requests = @current_ajax_requests.without(null)
-  @request_finished()
-  return
-
-PostLoader::hashchange_tags = ->
-  tags = UrlHash.get('tags')
-  if tags == @last_seen_tags
-    return
-  @last_seen_tags = tags
-  debug 'changed tags'
-  @load()
-  return
-
-PostLoader::perform_search = (event) ->
-  tags = event.memo.tags
-  @last_seen_tags = tags
-  results_mode = event.memo.results_mode or 'center-on-first'
-  debug 'do search: ' + tags
-  @load
-    tags: tags
-    results_mode: results_mode
-  return
+    if tags == @last_seen_tags
+      return
+    @last_seen_tags = tags
+    debug 'changed tags'
+    @load()
+    return
+
+  perform_search: (event) ->
+    tags = event.memo.tags
+    @last_seen_tags = tags
+    results_mode = event.memo.results_mode or 'center-on-first'
+    debug 'do search: ' + tags
+    @load
+      tags: tags
+      results_mode: results_mode
+    return


require OFXPlayer and CompoundPlayer so that script/console jobs work

--- application/chrome/content/wesabe/download/Job.coffee
+++ application/chrome/content/wesabe/download/Job.coffee
@@ -7,6 +7,8 @@
 Page    = require 'dom/Page'
 Browser = require 'dom/Browser'
 Player  = require 'download/Player'
+OFXPlayer = require 'download/OFXPlayer'
+CompoundPlayer = require 'download/CompoundPlayer'

 class Job
   constructor: (jobid, fid, creds, user_id, options) ->