Synchronously read files when deploying, to prevent EMFILE errors

--- deployment/putFilesToS3.coffee
+++ deployment/putFilesToS3.coffee
@@ -37,46 +37,46 @@
     files_to_deploy.forEach (f) ->
         rel_path = f.replace(build_directory + '/','')

-        fs.readFile f, (err, file_content) ->
-            throw err if err?
-            _ext = rel_path.split('.').pop()
+        # Read the file synchronously. Async results in too many open files.
+        file_content = fs.readFileSync(f)
+        _ext = rel_path.split('.').pop()

-            if _ext is 'js'
-                file_content = minifyJS(file_content.toString())
-            else if _ext is 'css'
-                file_content = minifyCSS(file_content.toString())
+        if _ext is 'js'
+            file_content = minifyJS(file_content.toString())
+        else if _ext is 'css'
+            file_content = minifyCSS(file_content.toString())

-            s3_options =
-                Bucket          : project_config.AWS_BUCKET
-                Key             : rel_path
-                ACL             : 'public-read'
-                ContentType     : mime.lookup(rel_path)
-                StorageClass    : 'REDUCED_REDUNDANCY'
+        s3_options =
+            Bucket          : project_config.AWS_BUCKET
+            Key             : rel_path
+            ACL             : 'public-read'
+            ContentType     : mime.lookup(rel_path)
+            StorageClass    : 'REDUCED_REDUNDANCY'

-            _upload = (body) ->
-                gzipped_total_uploaded += body.length if s3_options.ContentEncoding is 'gzip'
-                total_uploaded += body.length
-                s3_options.Body = body
-                _file_size = SDKError.colors.grey("(#{ body.length } bytes, #{ s3_options.ContentType })")
-                SDKError.log("Uploading #{ SDKError.colors.cyan(rel_path) }... #{ _file_size }")
-                # Put the compressed page HTML at the target S3 key.
-                s3.putObject s3_options, (err, data) ->
-                    to_upload -= 1
-                    if err?
-                        throw new SDKError('deploy.s3', err)
-                    else
-                        SDKError.log("Saved #{ SDKError.colors.green(rel_path) }.")
+        _upload = (body) ->
+            gzipped_total_uploaded += body.length if s3_options.ContentEncoding is 'gzip'
+            total_uploaded += body.length
+            s3_options.Body = body
+            _file_size = SDKError.colors.grey("(#{ body.length } bytes, #{ s3_options.ContentType })")
+            SDKError.log("Uploading #{ SDKError.colors.cyan(rel_path) }... #{ _file_size }")
+            # Put the compressed page HTML at the target S3 key.
+            s3.putObject s3_options, (err, data) ->
+                to_upload -= 1
+                if err?
+                    throw new SDKError('deploy.s3', err)
+                else
+                    SDKError.log("Saved #{ SDKError.colors.green(rel_path) }.")

-                    if to_upload is 0
-                        _file_count = SDKError.colors.green("#{ files_to_deploy.length } files, #{ total_uploaded } bytes")
-                        SDKError.log("Uploaded #{ _file_count } to #{ SDKError.colors.cyan(project_config.AWS_BUCKET) }")
-                        SDKError.log(SDKError.colors.grey("(gzip saved #{ Math.floor((gzipped_total_pre_upload - gzipped_total_uploaded) / gzipped_total_pre_upload * 100) }% on compressable files)"))
-                        callback()
+                if to_upload is 0
+                    _file_count = SDKError.colors.green("#{ files_to_deploy.length } files, #{ total_uploaded } bytes")
+                    SDKError.log("Uploaded #{ _file_count } to #{ SDKError.colors.cyan(project_config.AWS_BUCKET) }")
+                    SDKError.log(SDKError.colors.grey("(gzip saved #{ Math.floor((gzipped_total_pre_upload - gzipped_total_uploaded) / gzipped_total_pre_upload * 100) }% on compressable files)"))
+                    callback()

-            if _ext in COMPRESSABLE
-                s3_options.ContentEncoding = 'gzip'
-                gzipped_total_pre_upload += file_content.length
-                gzipCompress file_content, (compressed) ->
-                    _upload(compressed)
-            else
-                _upload(file_content)
+        if _ext in COMPRESSABLE
+            s3_options.ContentEncoding = 'gzip'
+            gzipped_total_pre_upload += file_content.length
+            gzipCompress file_content, (compressed) ->
+                _upload(compressed)
+        else
+            _upload(file_content)


TerminalSession: another use old/new kite condition is added for getting active sessions

--- client/Terminal/AppView.coffee
+++ client/Terminal/AppView.coffee
@@ -67,7 +67,11 @@

         # fetch all active vm sessions via terminal kites
         activeSessions = []
-        {vmController:{terminalKites : kites}} = KD.singletons
+        {vmController, kontrol} = KD.singletons
+        kites =
+          if KD.useNewKites
+          then kontrol.kites.terminal
+          else vmController.terminalKites
         queue = aliases.map (alias)->->
           kites[alias].webtermGetSessions().then (sessions) =>
             activeSessions = activeSessions.concat sessions


add tests for mailbox fetch params & fix tests for box renaming & box deletion

--- tests/03_mailbox_operations.coffee
+++ tests/03_mailbox_operations.coffee
@@ -1,19 +1,72 @@
 should = require('should')
+_ = require 'lodash'
+{MSGBYPAGE} = require '../server/utils/constants'
+
+describe 'Mailbox fetching', ->
+
+    inboxCount = 0
+    readCount = 0
+    flaggedCount = 0
+
+    testNextLinks = (first, iterator, callback) ->
+        [iterator, callback] = [null, iterator] unless callback
+        totalFound = 0
+        count = null
+        step = (link, callback) ->
+            client.get link, (err, res, body) ->
+                return callback err if err
+                body.messages.length.should.be.lessThan MSGBYPAGE + 1
+                count ?= body.count
+                totalFound += body.messages.length
+                iterator?(body.messages)
+                if body.links.next then step body.links.next, callback
+                else callback null
+
+        step first, ->
+            if totalFound is count then callback null, totalFound
+            else callback new Error 'total & count doesnt match'
+
+
+    it "When I follow the next links", (done) ->
+        testNextLinks "/mailbox/#{store.inboxID}",
+            (messages) ->
+                readCount += messages
+                            .filter (m) -> '\\Seen' in m.flags
+                            .length
+
+                flaggedCount += messages
+                            .filter (m) -> '\\Flagged' in m.flags
+                            .length
+
+            (err, total) ->
+                inboxCount = total
+                done err
+
+    it "When I get a mailbox (filter by flag)", (done) ->
+        testNextLinks "/mailbox/#{store.inboxID}?flag=seen", (err, total) ->
+                total.should.equal readCount
+                done()
+
+
+    it "When I get a mailbox (filter by not flag)", (done) ->
+        testNextLinks "/mailbox/#{store.inboxID}?flag=unseen", (err, total) ->
+                total.should.equal inboxCount - readCount
+                done()
+
+    it "When I get a mailbox (sorted by subject)", (done) ->
+        client.get "/mailbox/#{store.inboxID}?sort=%2Bsubject", (err, res, body) ->
+            body.should.have.property 'count', inboxCount
+            last = body.messages[0]
+            for i in [1..body.messages.length - 1] by 1
+                current = body.messages[i]
+                last.normSubject.should.be.lessThan current.normSubject
+                last = current
+
+            testNextLinks "/mailbox/#{store.inboxID}?sort=%2Bsubject", done
+
+

 describe 'Mailbox operations', ->
-
-    it "When I get a mailbox", (done) ->
-        client.get "/mailbox/#{store.inboxID}", (err, res, body) =>
-            body.should.have.property 'count'
-            body.messages.should.have.lengthOf 7
-            body.messages[0].subject
-                .should.equal 'Re: First message of conversation'
-            body.messages[1].subject
-                .should.equal 'Re: This is the first Message for a conversation'
-            body.messages[1].conversationID
-                .should.equal body.messages[1].conversationID
-            done()
-

     it "When I add a draft mailbox", (done) ->
         box =
@@ -36,13 +89,15 @@
             accountID: store.accountID
             label: 'My Drafts'

-
         client.put "/mailbox/#{store.draftBoxID}", box, (err, res, body) =>
             res.statusCode.should.equal 200
             body.id.should.equal store.accountID
             body.should.have.property('mailboxes').with.lengthOf(5)
-            for box of body.mailboxes when box.id is store.draftBoxID
-                box.label.should.equal 'My Drafts'
+            draftBox = null
+            for box in body.mailboxes when box.id is store.draftBoxID
+                draftBox = box
+            should.exist draftBox
+            draftBox.label.should.equal 'My Drafts'
             store.accountState = body
             done()

@@ -60,9 +115,14 @@

         box =
             accountID: store.accountID
-            favorite: false
+            favorite: true
             mailboxID: store.draftBoxID

-        done() #@TODO : finish this test
+        client.put "/mailbox/#{store.draftBoxID}", box, (err, res, body) =>
+            res.statusCode.should.equal 200
+            body.id.should.equal store.accountID
+            body.should.have.property('favorites').with.lengthOf 5
+            body.favorites.should.containEql store.draftBoxID
+            done()


Add more view keypath operation tests

Add more view keypath operation tests

--- tests/batman/view/view_test.coffee
+++ tests/batman/view/view_test.coffee
@@ -173,7 +173,7 @@

 oldApp = Batman.currentApp

-QUnit.module 'Batman.View lookupKeypath',
+QUnit.module 'Batman.View keypath operations',
   setup: ->
     @controller = new Batman.Controller
     @layout = new Batman.View(controller: @controller)
@@ -186,7 +186,7 @@
   teardown: ->
     Batman.currentApp = oldApp

-test 'should work in the basic case', ->
+test 'lookupKeypath should work in the basic case', ->
   @app.set('test', 'appvalue')
   equal @view.lookupKeypath('test'), 'appvalue'

@@ -199,21 +199,7 @@
   @view.unset('test')
   equal @view.lookupKeypath('test'), 'ctrlvalue'

-test 'targetForKeypath should return undefined if the keypath misses', ->
-  @app.set('test', true)
-  equal @backingView.targetForKeypath('test'), @app
-
-  @app.unset('test')
-  equal @backingView.targetForKeypath('test'), undefined
-
-test 'targetForKeypath with forceTarget should return the closest non-backing view if the keypath misses', ->
-  @app.set('test', true)
-  equal @backingView.targetForKeypath('test', true), @app
-
-  @app.unset('test')
-  equal @backingView.targetForKeypath('test', true), @view
-
-test 'should work for extremely deep keypaths', ->
+test 'lookupKeypath should work for extremely deep keypaths', ->
   @view.set('test', true)
   view = @view
   for i in [1..8]
@@ -222,3 +208,54 @@
   equal view.get('superview.superview.superview.superview.superview.superview.superview.superview.test'), true
   equal view.lookupKeypath('test'), true

+test 'lookupKeypath matches through the base keypath', ->
+  @controller.set('user', Batman(name: 'controller'))
+
+  @view.set('user', user = Batman())
+  equal @view.lookupKeypath('user'), user
+  equal @view.lookupKeypath('user.name'), undefined
+
+  @view.set('user', user = Batman(name: 'view'))
+  equal @view.lookupKeypath('user'), user
+  equal @view.lookupKeypath('user.name'), 'view'
+
+test 'targetForKeypath matches through the full keypath only', ->
+  @controller.set('user', Batman(name: 'controller'))
+
+  @view.set('user', Batman())
+  equal @view.targetForKeypath('user.name'), @controller
+
+  @view.set('user', Batman(name: 'view'))
+  equal @view.targetForKeypath('user.name'), @view
+
+test 'targetForKeypath should return undefined if the keypath misses', ->
+  @app.set('test', true)
+  equal @backingView.targetForKeypath('test'), @app
+
+  @app.unset('test')
+  equal @backingView.targetForKeypath('test'), undefined
+
+test 'targetForKeypath with forceTarget should return the closest non-backing view if the keypath misses', ->
+  @app.set('test', true)
+  equal @backingView.targetForKeypath('test', true), @app
+
+  @app.unset('test')
+  equal @backingView.targetForKeypath('test', true), @view
+
+test 'setKeypath should work in the basic case', ->
+  @app.set('test', null)
+  @view.set('test', null)
+
+  @view.setKeypath('test', 'foo')
+  equal @view.get('test'), 'foo'
+  equal @app.get('test'), null
+
+  @view.unset('test')
+  @view.setKeypath('test', 'foo')
+  equal @view.get('test'), undefined
+  equal @app.get('test'), 'foo'
+
+test 'setKeypath should set on the nearest non-backing view if the keypath misses', ->
+  @backingView.setKeypath('test', 'foo')
+  equal @view.get('test'), 'foo'
+


Remove console.log

Remove console.log

--- source/scripts/controllers/local.coffee
+++ source/scripts/controllers/local.coffee
@@ -8,7 +8,6 @@
     return

   set: (items) =>
-    console.log items
     for key, value of items
       localStorage[key] = JSON.stringify value
     return


Don't cache favicon

Don't cache favicon

--- client/worker/endpoints.coffee
+++ client/worker/endpoints.coffee
@@ -3,7 +3,6 @@
     [
       "/dashboard",
       "/manifest.json",
-      "/favicon.ico",
       "/api/v1/translations?lang=en",
       "/fonts/mdi-2.2.43/materialdesignicons-webfont.woff2",
       "/theme/default_group_logo.png",


Use global Promise if it's available

Use global Promise if it's available

--- src/fs-plus.coffee
+++ src/fs-plus.coffee
@@ -4,7 +4,7 @@

 _ = require 'underscore-plus'
 async = require 'async'
-{Promise} = require 'es6-promise'
+Promise = global.Promise ? require('es6-promise').Promise
 mkdirp = require 'mkdirp'
 rimraf = require 'rimraf'


Emit connect event with error (if connecting) or emit errorMessage event on socket error

When a network issue prevents connection to the database, there is no
callback made to the connection callback nor to the errorMessage
callback. Only the connection.end event is fired. This leads to silent
failures.

--- src/connection.coffee
+++ src/connection.coffee
@@ -357,6 +357,10 @@
     message = "connection to #{@config.server}:#{@config.options.port} - failed #{error}"

     @debug.log(message)
+    if @state == @STATE.CONNECTING
+      @emit('connect', message)
+    else
+      @emit('errorMessage', message)
     @dispatchEvent('socketError', error)

   socketConnect: =>


gcloud: attaching and detaching disks

gcloud: attaching and detaching disks

--- salvus/smc_gcloud.coffee
+++ salvus/smc_gcloud.coffee
@@ -16,6 +16,7 @@
 - [ ] change the machine type of a vm
 - [ ] switch a vm between being pre-empt or not
 - [ ] increase the size of a disk image that is attached to a VM
+- [ ] change the name of a disk

 Rules we care about are:

@@ -57,6 +58,7 @@
     else
         operation.onComplete onCompleteOpts, (err, metadata) ->
             done()
+            #winston.debug("onComplete #{misc.to_json(err)}, #{misc.to_json(metadata)}")
             if err
                 cb?(err)
             else if metadata.error
@@ -101,6 +103,60 @@
         @describe
             cb : (err, x) =>
                 opts.cb(err, x?.status)
+
+    attach_disk: (opts) =>
+        opts = defaults opts,
+            disk : required
+            cb   : required
+        if not opts.disk instanceof Disk
+            opts.cb("disk must be an instance of Disk")
+            return
+        dbg = @dbg("attach_disk")
+        dbg("starting...")
+        @_vm.attachDisk opts.disk._disk, (err, operation, apiResponse) =>
+            handle_operation(err, operation, (->dbg("done")), opts.cb)
+
+    detach_disk: (opts) =>
+        opts = defaults opts,
+            disk : required
+            cb   : required
+        if not opts.disk instanceof Disk
+            opts.cb("disk must be an instance of Disk")
+            return
+        dbg = @dbg("detach_disk")
+        dbg("starting...")
+        vm_data = disk_data = undefined
+        async.series([
+            (cb) =>
+                dbg("getting disk and vm metadata in parallel")
+                async.parallel([
+                    (cb) =>
+                        @get_metadata
+                            cb : (err, x) =>
+                                vm_data = x; cb(err)
+                    (cb) =>
+                        opts.disk.get_metadata
+                            cb : (err, x) =>
+                                disk_data = x; cb(err)
+                ], cb)
+            (cb) =>
+                deviceName = undefined
+                for x in vm_data.disks
+                    if x.source == disk_data.selfLink
+                        deviceName = x.deviceName
+                        break
+                dbg("determining deviceName='#{deviceName}'")
+                if not deviceName
+                    dbg("already done -- disk not connected to this machine")
+                    cb()
+                    return
+                # weird hack around what might be a bug in GCE code (NOT SURE YET)
+                # It's strange we have to make the disk from the deviceName rather than
+                # the actual disk name.  It seems like the node.js api authors got confused.
+                disk = @gcloud._gce.zone(@zone).disk(deviceName)
+                @_vm.detachDisk disk, (err, operation, apiResponse) =>
+                    handle_operation(err, operation, (->dbg("done")), cb)
+        ], opts.cb)

 class Disk
     constructor: (@gcloud, @name, @zone=DEFAULT_ZONE) ->
@@ -162,6 +218,49 @@
         dbg("starting")
         @_disk.delete (err, operation, apiResponse) =>
             handle_operation(err, operation, (->dbg('done')), opts.cb)
+
+    attach_to: (opts) =>
+        opts = defaults opts,
+            vm : required
+            cb : required
+        if not opts.vm instanceof VM
+            opts.cb("vm must be an instance of VM")
+            return
+        opts.vm.attach_disk
+            disk : @
+            cb   : opts.cb
+
+    detach: (opts) =>
+        opts = defaults opts,
+            vm : undefined   # if not given, detach from all users of this disk
+            cb : required
+        dbg = @dbg("detach")
+        vms = undefined
+        async.series([
+            (cb) =>
+                if opts.vm?
+                    vms = [opts.vm]
+                    cb()
+                else
+                    dbg("determine vm that disk is attached to")
+                    @get_metadata
+                        cb : (err, data) =>
+                            if err
+                                cb(err)
+                            else
+                                # all the users must be in the same zone as this disk
+                                vms = (@gcloud.vm(name:misc.path_split(u).tail, zone:@zone) for u in data.users)
+                                cb()
+            (cb) =>
+                dbg('actually detach disk from that vm')
+                f = (vm, cb) =>
+                    vm.detach_disk
+                        disk : @
+                        cb   : cb
+                async.map(vms, f, cb)
+
+            ], opts.cb)
+

 class Snapshot
     constructor: (@gcloud, @name) ->


Fixed the user heartbeat

Fixed the user heartbeat

--- ext/user_heartbeat.coffee
+++ ext/user_heartbeat.coffee
@@ -2,71 +2,75 @@

   message.ext ||= {}

-  if message.channel.match(/meta\/subscribe/ig)
-    token = message.ext.auth_token
-    client_id = message.clientId
+  userId = message.ext.user_id
+  clientId = message.clientId

-    if message.subscription.match(/users\/(.*)\/private/ig)
-      refreshPresence(token,client_id) if token
+  if clientId && userId && message.channel.match(/meta\/connect/ig)
+    console.log "User ID: #{userId}"
+    startHeartbeat(userId,clientId)

   callback(message)

 exports.outgoing = (message, callback) ->
   callback(message)

-refreshPresence = (token,client_id) ->
+startHeartbeat = (userId,clientId) ->
+  setInterval ->
+    refreshPresence(userId,clientId,this)
+  , 30000
+
+refreshPresence = (userId,clientId,hearbeatInterval) ->
   mongodb.collection 'users', (err, collection) ->
-    if err
-      console.log err
-    collection.findOne { auth_token: token }, (err,user) ->
-      if err
-        console.log err
-      if user != null
-        setPresenceKeys(user,client_id,Date.now())
+    console.log err if err
+    collection.findOne { _id: userId }, (err,user) ->
+      console.log err if err
+      checkStatusAndBroadcast(user,clientId,hearbeatInterval) if user != null

-        fayengine.clientExists client_id, (connected,score) ->
-          if connected
-            setTimeout ->
-              status = user.preferred_status
-              msg = { status: status }
-              broadcastOnClouds(user,msg,user.cloud_ids)
-              setTimeout ->
-                refreshPresence(token,client_id)
-              , 24000
-            , 6000
-          else
-            last_seen = rediscli.get "cloudsdale/users/#{user._id}"
-            last_seen = 0 unless last_seen
-            if Date.now() > last_seen
-              deletePresenceKeys(user,client_id)
-              msg = { status: "offline" }
-              broadcastOnClouds(user,msg,user.cloud_ids)
+checkStatusAndBroadcast = (user,clientId,hearbeatInterval) ->
+  setUserHeartbeat(user,Date.now())

-setPresenceKeys = (user,client_id,time) ->
-  user_id   = user._id
-  cloud_ids = user.cloud_ids
+  fayengine.clientExists clientId, (connected,score) ->
+    if connected
+      status = user.preferred_status
+      broadcastStatus(user,status) unless status == "offline"
+    else
+      rediscli.get "cloudsdale/users/#{user._id}", (err, lastSeen) ->
+        lastSeen ||= 0

-  rediscli.set "cloudsdale/users/#{user_id}", time
+        if Date.now() > lastSeen
+          clearUserHeartbeat(user,clientId)
+          broadcastStatus(user,"offline")

-  if cloud_ids
-    for cloud_id in cloud_ids
-      do (cloud_id) ->
-        rediscli.hset "cloudsdale/clouds/#{cloud_id}/users", user_id, time
+      clearInterval(hearbeatInterval)

-deletePresenceKeys = (user,client_id) ->
-  user_id   = user._id
-  cloud_ids = user.cloud_ids
+setUserHeartbeat = (user,time) ->
+  userId   = user._id
+  cloudIds = user.cloud_ids

-  rediscli.del "cloudsdale/users/#{user_id}"
+  rediscli.set "cloudsdale/users/#{userId}", time
+  rediscli.expire "cloudsdale/users/#{userId}", redisExpire

-  if cloud_ids
-    for cloud_id in cloud_ids
-      do (cloud_id) ->
-        rediscli.hdel "cloudsdale/clouds/#{cloud_id}/users", user_id
+  if cloudIds
+    for cloudId in cloudIds
+      do (cloudId) -> rediscli.hset "cloudsdale/clouds/#{cloudId}/users", userId, time

-broadcastOnClouds = (user,msg,cloud_ids) ->
-  if cloud_ids
-    for cloud_id in cloud_ids
-      do (cloud_id) ->
-        fayeCli.publish "/clouds/#{cloud_id}/users/#{user._id}", msg
+clearUserHeartbeat = (user) ->
+  userId   = user._id
+  cloudIds = user.cloud_ids

+  rediscli.del "cloudsdale/users/#{userId}"
+
+  if cloudIds
+    for cloudId in cloudIds
+      do (cloudId) -> rediscli.hdel "cloudsdale/clouds/#{cloudId}/users", userId
+
+broadcastStatus = (user,status) ->
+
+  userId   = user._id
+  cloudIds = user.cloud_ids
+  status   = status || "offline"
+
+  if cloudIds
+    for cloudId in cloudIds
+      do (cloudId) -> fayeCli.publish "/clouds/#{cloudId}/users/#{userId}", { status: status }
+


client/style: require changeteammodal.styl

--- client/app/lib/styl/require-styles.coffee
+++ client/app/lib/styl/require-styles.coffee
@@ -35,6 +35,8 @@
 require './terminal/terminal.styl'
 require './terminal/webterm.styl'

+require './changeteammodal.styl'
+
 # require 'about/styl'
 # require 'account/styl'
 require 'ace/styl'


:beer: skip item area update on undo/redo

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -236,7 +236,7 @@
   renderItems: (items) ->
     texts = items.map (item) => @provider.viewForItem(item)
     itemArea = new Range(@itemAreaStart, @editor.getEofBufferPosition())
-    range = @editor.setTextInBufferRange(itemArea, texts.join("\n"))
+    range = @editor.setTextInBufferRange(itemArea, texts.join("\n"), undo: 'skip')
     @editorLastRow = range.end.row

   ensureNarrowEditorIsValidState: ->


Bug fix: should now be able to type 'z'

Bug fix: should now be able to type 'z'

--- src/controller.coffee
+++ src/controller.coffee
@@ -413,7 +413,7 @@
             # put the cursor into the input that changed.
             when 'socketTextChange'
               socketStart = @tree.getTokenAtLocation operation.location
-              socketStart.socket.content().remove()
+              socketStart.socket.content()?.remove()
               socketStart.insert new model.TextToken operation.before

               setTextInputFocusRaw socketStart.socket
@@ -966,6 +966,9 @@
       @hiddenInput.addEventListener 'blur', (event) =>
         # If we have actually blurred (as opposed to simply unfocused the browser window)
         if event.target isnt document.activeElement then setTextInputFocus null
+
+        # Focus the editor to capture keypresses
+        @el.focus()

       # Bind keyboard shortcut events to the document

@@ -1011,12 +1014,13 @@

           when 90 then if ctrlKeyPressed
             @undo()
+            event.preventDefault()

         # If we manipulated the root tree, redraw.
         if event.keyCode in [13, 38, 40, 8] then @redraw()

         # If we caught the keypress, prevent default.
-        if event.keyCode in [13, 38, 40, 8, 37, 90] then event.preventDefault()
+        if event.keyCode in [13, 38, 40, 8, 37] then event.preventDefault()

       @el.addEventListener 'keyup', (event) =>
         switch event.keyCode
@@ -1581,8 +1585,6 @@

           try
             if @focus.handwritten
-              console.log @focus.start.prev.block.stringify()
-
               # If we are in a handwritten block, we actually want to reparse
               # the entire block we're in
               newParse = coffee.parse(@focus.start.prev.block.stringify()).start.next
@@ -1590,8 +1592,6 @@
               # If what has been parsed ends up creating a new block,
               # subsitute this new block for the old (unstable) text
               if newParse.type is 'blockStart'
-                console.log newParse
-
                 # Log in the undo stack the operation
                 # we're about to do
                 @addMicroUndoOperation


DigitalWrite: improve first value write

DigitalWrite: improve first value write

--- components/DigitalWrite.coffee
+++ components/DigitalWrite.coffee
@@ -9,6 +9,7 @@
       pin: new noflo.Port 'number'
       in: new noflo.Port 'boolean'

+    @value = null
     @inPorts.pin.on 'data', (pinNumber) =>
       @stopGpio()
       @pinNumber = pinNumber
@@ -19,7 +20,7 @@
   setValue: (value) ->
     @value = value
     return unless @gpio
-    @gpio.set(if value then 1 else 0)
+    @gpio.set(if @value then 1 else 0)

   stopGpio: () ->
     return unless @gpio
@@ -30,7 +31,9 @@
   startGpio: () ->
     return unless @pinNumber
     @gpio = gpio.export(@pinNumber, { direction: 'out' })
-    @gpio.set(if @value then 1 else 0) if @value
+    @gpio.on 'directionChange', () =>
+      if @value != null
+        @gpio.set(if @value then 1 else 0)

   shutdown: () ->
     @stopGpio()


fix: list-based analyzer works

fix: list-based analyzer works

--- scripts-src/cells.coffee
+++ scripts-src/cells.coffee
@@ -37,7 +37,6 @@
       y0 = Math.min(y0, y)
       y1 = Math.max(y1, y)
     [x0, y0, x1, y1]
-

   transform: (lst, tfm, need_normalize = true) ->
     #transform cell block: rotate or flip
@@ -52,7 +51,7 @@
       @normalize lst1
     else
       lst1
-
+
   #Normalize list: sort cells and offset them to the origin
   normalize: (lst1) -> @sortXY @normalizeXY lst1

@@ -79,6 +78,7 @@
   extent: (lst) ->
     [_,_,x1, y1] = @bounds lst
     [x1, y1]
+
   #Find index of the first cell with given coordinates
   find: (lst, [x,y]) ->
     for [xi, yi], i in lst
@@ -170,7 +170,9 @@
     throw new Error "Impossible to rotate vector (#{dx},#{dy}) to the positive direction"

-  analyze: (pattern, rule, max_iters = 2048, stop_on_border_hit=true) ->
+
+
+  analyze1: (pattern, rule, max_iters = 2048, stop_on_border_hit=true) ->
     throw new Error ("Pattern undefined")  unless pattern
     throw new Error ("Rule undefined")  unless rule
     #sort cells by Y, then by X
@@ -191,7 +193,7 @@

     bestPatternSearch = new Maximizer @energy
     bestPatternSearch.put pattern
-
+
     #start search
     cycle_found = false
     for iter in [1 .. max_iters] by 1
@@ -225,7 +227,68 @@
         result.period = iter
      result.cells = cells_best
      return result
-
+
+  analyze: (pattern, rule, max_iters = 2048, stop_on_border_hit=true) ->
+    throw new Error ("Pattern undefined")  unless pattern
+    throw new Error ("Rule undefined")  unless rule
+    snap_below = (x,generation) ->
+      x - mod(x + generation, 2)
+
+    offsetToOrigin = (pattern, generation) ->
+      [x0,y0] = Cells.bounds pattern
+      x0 = snap_below x0, generation
+      y0 = snap_below y0, generation
+      Cells.offset pattern, -x0, -y0
+      return [pattern, x0, y0]
+
+    vacuum_period = Rules.vacuum_period rule
+    unless vacuum_period is 1
+      throw new Error "Empty field is not periodic for this rule. Analysis impossible"
+
+    pattern = @normalize pattern
+
+    #Shift pattern to the origin
+    [pattern] = offsetToOrigin pattern, 0
+
+    bestPatternSearch = new Maximizer @energy
+    bestPatternSearch.put pattern
+
+    #start search
+    cycle_found = false
+    curPattern = pattern
+    dx = 0
+    dy = 0
+    #console.log "####Analyze pattern #{ Cells.to_rle curPattern }"
+    for iter in [1 .. max_iters] by 1
+      #TODO: evaluate cell list, always assuming initial, 0 phase
+      curPattern = evaluateCellList rule, curPattern, 0
+      @sortXY curPattern
+      #console.log "#### Iter #{ iter }\t:  #{ @to_rle @normalizeXY curPattern[..] }"
+      #After evaluation, pattern is in phase 1. remove offset and transform back to phase 0
+      [curPattern, x0, y0] = offsetToOrigin curPattern, 1
+      dx += x0
+      dy += y0
+
+      if iter % vacuum_period isnt 0
+        continue #Skip states with nonzero vacuum
+
+      if @areEqual pattern, curPattern
+        cycle_found = true
+        break
+      bestPatternSearch.put curPattern
+
+     #Return results
+    result = {
+      analyzed_generations: max_iters
+    }
+    cells_best = bestPatternSearch.getArg()
+    if cycle_found
+       [cells_best, result.dx, result.dy] =
+         @canonicalize_spaceship cells_best, rule, dx, dy
+       result.period = iter
+    result.cells = cells_best
+    return result
+
   #Given a spaceship and its direction,
   # Rotate it, if rule is invariant in relation to rotation
   canonicalize_spaceship: (pattern, rule, dx, dy) ->


:memo: Tweak content name commment

:memo: Tweak content name commment

--- src/grammar.coffee
+++ src/grammar.coffee
@@ -224,8 +224,9 @@
       scopes.push(scopeName) if scopeName
       scopes.push(contentScopeName) if contentScopeName

-    # Pop last content name scope if an end pattern is matched since only
-    # text between the begin/end patterns should have the content name scope
+    # Pop the last content name scope if the end pattern at the top of the stack
+    # was matched since only text between the begin/end patterns should have the
+    # content name scope
     if endPatternMatch and rule?.contentScopeName and rule is stack[stack.length - 1]
       scopes.pop()


set inverse on options

set inverse on options

--- client/lanes/models/AssociationMap.coffee
+++ client/lanes/models/AssociationMap.coffee
@@ -46,6 +46,7 @@
         definition = @definitions[name]
         options = { parent: model }
         if definition.inverse
+            options.inverse = { name: definition.inverse, value: model }
             options[ definition.inverse ] = model
         if definition.options
             _.extend(options, Lanes.u.resultsFor(model, definition.options))
@@ -82,6 +83,8 @@
             options.model = target_class
             options.association_name = name
             klass = options.collectionClass or Lanes.Models.AssociationCollection
+            if _.isString(klass)
+                klass = Lanes.u.findObject(klass, 'Models', association.klass::FILE)
             new klass(options.models || [], options)

     # returns a collection for the given association.


Model is set by spread operation on props

--- client/skr/components/CustomerFinder.cjsx
+++ client/skr/components/CustomerFinder.cjsx
@@ -39,13 +39,11 @@
         if props.selectField
             <LC.SelectField sm=2
                 labelField="code"
-                model={@customer}
                 setSelection={@selectSetCustomer}
                 getSelection={@selectGetSelection}
                 {...props} />
         else
             <LC.RecordFinder ref="finder" sm=3 autoFocus
                 commands={@state.commands}
-                model={@props.customer}
                 query={@query}
                 {...props} />


filter warnings with environment variable COFFEE_WARNING_FILTER

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -157,6 +157,9 @@
 exports.currentFilename = () -> translating_filename

 exports.sendSyntaxWarning = (message, location) ->
+  filter = process.env.COFFEE_WARNING_FILTER
+  return if filter? and filter != "" and message != filter
+
   return if exports.noWarn
   error = new SyntaxError message
   if location


fixed JS error where users where no longer able to crop banners

--- app/assets/javascripts/banners.js.coffee
+++ app/assets/javascripts/banners.js.coffee
@@ -1,6 +1,3 @@
-jQuery ->
-  new CarrierWaveCropper()
-
 class CarrierWaveCropper
   constructor: ->
     $('#banner_image_cropbox').Jcrop
@@ -25,3 +22,5 @@
       height: Math.round(100/coords.h * $('#banner_image_cropbox').height()) + 'px'
       marginLeft: '-' + Math.round(100/coords.w * coords.x) + 'px'
       marginTop: '-' + Math.round(100/coords.h * coords.y) + 'px'
+
+new CarrierWaveCropper()


Prevent loading first 20 commits twice

Prevent loading first 20 commits twice

Signed-off-by: Dmitriy Zaporozhets <be23d75b156792e5acab51b196a2deb155d35d6a@gmail.com>

--- app/assets/javascripts/commits.js.coffee
+++ app/assets/javascripts/commits.js.coffee
@@ -8,7 +8,7 @@
         e.stopPropagation()
         return false

-    Pager.init limit, true
+    Pager.init limit, false

     @content = $("#commits-list")
     @searchField = $("#commits-search")
@@ -24,14 +24,14 @@
     form = $(".commits-search-form")
     search = @searchField.val()
     commitsUrl = form.attr("action") + '?' + form.serialize()
-    @setOpacitiy("0.5")
+    @setOpacitiy(0.5)

     $.ajax
       type: "GET"
       url: form.attr("action")
       data: form.serialize()
       complete: =>
-        @setOpacitiy("1.0")
+        @setOpacitiy(1.0)
       success: (data) =>
         @content.html(data.html)
         # Change url so if user reload a page - search results are saved


Fix danmaku duplicate polling timer

Fix danmaku duplicate polling timer

--- app/assets/javascripts/danmaku.js.coffee
+++ app/assets/javascripts/danmaku.js.coffee
@@ -1,7 +1,6 @@
 class @Danmaku
   constructor: ->
     self = this
-    self.tid = 0
     self.last_id = 0
     self.count = 0
     self.size = null
@@ -12,12 +11,6 @@

   connect: =>
     self = this
-    if self.tid
-      clearTimeout(self.tid)
-      self.tid = false
-    self.tid = setTimeout(->
-      self.connect()
-    , 120000)
     DEBUG('Danmaku: Hooking')
     self.ajax = $.ajax
       url: "//#{self.server}/poll"
@@ -40,8 +33,7 @@
             self.create(item.text, item, delay)
             self.last_id = item.id
           break
-        self.reconnect()
-      error: ->
+      complete: ->
         self.reconnect()

   reconnect: =>


click event

click event

--- app/assets/javascripts/letters.js.coffee
+++ app/assets/javascripts/letters.js.coffee
@@ -1,3 +1,2 @@
-# Place all the behaviors and hooks related to the matching controller here.
-# All this logic will automatically be available in application.js.
-# You can use CoffeeScript in this file: http://coffeescript.org/
+$('.open-file').click ->
+	# upload file and load it


Disable application menu for initial screen

--- src/initial.coffee
+++ src/initial.coffee
@@ -4,6 +4,7 @@
 fs = require 'fs'
 pathExists = require 'path-exists'
 path = require 'path'
+menu = e.Menu
 BrowserWindow = e.BrowserWindow
 ipcMain = (require 'electron').ipcMain
 KindredAPI = require 'kindred-api'
@@ -20,6 +21,7 @@
   resizable: false
   title: "Setup LP-Tracker"
 }
+menu.setApplicationMenu null
 mainWindow.webContents.on 'will-navigate', (event) ->
   event.preventDefault()
 mainWindow.loadURL "file://#{__dirname}/../views/initial.html"


space

space

--- app/frontend/scripts/templates.js.coffee
+++ app/frontend/scripts/templates.js.coffee
@@ -18,6 +18,7 @@
     by {{{artist_tag}}}
     from {{album}}
     {{{art_tag}}}
+
     {{>star}}
     {{>queuing}}
     {{>downloading}}


Changed default ordering of Scheduled Tasks

--- SingularityUI/app/views/tasks.coffee
+++ SingularityUI/app/views/tasks.coffee
@@ -69,7 +69,7 @@
             tasks = _.sortBy tasks, @sortAttribute
             if not @sortAscending
                 tasks = tasks.reverse()
-        else
+        else unless @tasksFilter is 'scheduled'
             tasks.reverse()

         @currentTasks = tasks


Fix off-by-1 error in quote of the day

Fix off-by-1 error in quote of the day

--- scripts/nostalgia.coffee
+++ scripts/nostalgia.coffee
@@ -406,9 +406,9 @@
     schedule.scheduleJob '0 0 9 * * *', () ->
         qotd = 'Here is your Quote Of The Day™!\n\n'
         names = Object.keys(memories)
-        randomName = names[Math.round(Math.random() * names.length)]
+        randomName = names[Math.round(Math.random() * (names.length - 1))]
         quotes = memories[randomName]
-        randomQuote = quotes[Math.round(Math.random() * quotes.length)]
+        randomQuote = quotes[Math.round(Math.random() * (quotes.length - 1))]
         qotd += "\"#{randomQuote}\" - #{randomName}"
         robot.send room: process.env.GENERAL_ROOM_ID, qotd


[TEST] Added various tests for template.render()

--- test/liquid.coffee
+++ test/liquid.coffee
@@ -51,3 +51,18 @@
     it "in the middle of a line", ->
       expect(@engine.parse("{{ okay }}\n\n   {% illegal %}")).to.be.rejectedWith Liquid.SyntaxError,
         "Unknown tag 'illegal'\n    at {% illegal %} (undefined:3:4)"
+
+  context "template", ->
+    context ".render()", ->
+      it "fails unless parsed", ->
+        template = new Liquid.Template()
+        expect(template.render()).to.be.rejectedWith Error, /No document root/
+
+      it "fails with illegal context", ->
+        expect(@engine.parse("foo")).to.be.fulfilled.then (template) ->
+          expect(template.render(1)).to.be.rejectedWith Error, /Expected Object or Liquid::Context as parameter/
+
+      it "takes a context and options", ->
+        expect(@engine.parse("foo")).to.be.fulfilled.then (template) ->
+          ctx = new Liquid.Context
+          expect(template.render(ctx, { registers: { x: 3 }, filters: {} })).to.be.fulfilled


Replace mccme to msk in level_about

Replace mccme to msk in level_about

--- server/materials/data/level_about.coffee
+++ server/materials/data/level_about.coffee
@@ -74,7 +74,7 @@
         Если вы хотите начать заниматься в этом курсе, то сделайте следующее:

Во-первых, можно просто придти на специальное очное занятие для начинающих (см. информацию об очных занятиях в разделе "О курсе"). Но можно начать (и продолжать) заниматься и дома, не дожидаясь очного занятия, можно и вообще на очные занятия не ходить. Все инструкции ниже идут как раз для случая, если вы хотите начать заниматься из дома.

-
Зарегистрируйтесь на сайте (вас попросят еще зарегистрироваться на informatics.mccme.ru). При регистрации укажите свое настоящее имя и правильный населенный пункт, чтобы я вас мог отличить среди других пользователей сайта.

+
Зарегистрируйтесь на сайте (вас попросят еще зарегистрироваться на informatics.msk.ru). При регистрации укажите свое настоящее имя и правильный населенный пункт, чтобы я вас мог отличить среди других пользователей сайта.

Напишите мне любым из указанных в разделе "О курсе" способов. В письме укажите, как вас зовут, школу, класс, город. Кроме того, напишите вкратце, какой ваш опыт в программировании, или вы будете заниматься "с нуля"; если вы участвовали в каких-либо олимпиадах по математике или программированию, укажите это. (Это не значит, что здесь будет какой-то отбор, я готов принять в курс всех желающих. Но я просто хочу заранее знать, какой у вас уже есть опыт.)

Прочитайте, или хотя бы просмотрите, все тексты разделе "О курсе".

Далее:
@@ -88,7 +88,7 @@

Для студентов и старше

-
Зарегистрируйтесь на сайте (для этого вас попросят еще зарегистрироваться на сайте informatics.mccme.ru). При регистрации укажите свое настоящее имя и правильный населенный пункт, чтобы я вас мог отличить среди других пользователей сайта.

+
Зарегистрируйтесь на сайте (для этого вас попросят еще зарегистрироваться на сайте informatics.msk.ru). При регистрации укажите свое настоящее имя и правильный населенный пункт, чтобы я вас мог отличить среди других пользователей сайта.

Напишите мне любым из указанных в разделе "О курсе" способов. В письме укажите, как вас зовут, где вы учитесь/работаете. Кроме того, напишите вкратце, какой ваш опыт в программировании, или вы будете заниматься "с нуля".

Прочитайте, или хотя бы просмотрите, все тексты в разделе "О курсе".

Дождитесь ответа и дальнейших инструкций от меня.
@@ -159,7 +159,7 @@

Для всех остальных

-
Занятия для всех остальных (не-нижегородских школьников, а также не-школьников) платные. Точнее, собственно все материалы в курсе бесплатные. Большая часть из них есть на сайте informatics.mccme.ru в соответствующем разделе, вы можете бесплатно заниматься там. Я беру деньги за то, что я добавлю вас в сводные таблицы, буду просматривать и комментировать ваши решения, отвечать на ваши вопросы (заочно) и т.д., а также за доступ к разделам «Хорошие решения» и «Найди ошибку».

+
Занятия для всех остальных (не-нижегородских школьников, а также не-школьников) платные. Точнее, собственно все материалы в курсе бесплатные. Большая часть из них есть на сайте informatics.msk.ru в соответствующем разделе, вы можете бесплатно заниматься там. Я беру деньги за то, что я добавлю вас в сводные таблицы, буду просматривать и комментировать ваши решения, отвечать на ваши вопросы (заочно) и т.д., а также за доступ к разделам «Хорошие решения» и «Найди ошибку».

Стоимость занятий определяется следующим образом:

@@ -517,7 +517,7 @@

Что надо сделать, чтобы начать заниматься?

-
Зарегистрируйтесь на сайте (для этого вас попросят еще зарегистрироваться на сайте informatics.mccme.ru). При регистрации укажите свое настоящее имя и правильный населенный пункт, чтобы я вас мог отличить среди других пользователей сайта.

+
Зарегистрируйтесь на сайте (для этого вас попросят еще зарегистрироваться на сайте informatics.msk.ru). При регистрации укажите свое настоящее имя и правильный населенный пункт, чтобы я вас мог отличить среди других пользователей сайта.

Напишите мне любым из указанных в разделе "О курсе" способов. В письме укажите, как вас зовут, где вы учитесь/работаете. Кроме того, напишите вкратце, какой ваш опыт в программировании, или вы будете заниматься "с нуля".

Прочитайте, или хотя бы просмотрите, все тексты в разделе "О курсе".

Дождитесь ответа и дальнейших инструкций от меня.
@@ -541,8 +541,8 @@

Вы всегда можете связаться со мной, контактные данные указаны в разделе "О курсе".

-
А что за сайт informatics.mccme.ru?

-
Informatics.mccme.ru — это сайт дистанционной подготовки по информатике, организованный Московским центром непрерывного математического образования (МЦНМО) и Московским институтом открытого образования (МИОО); он очень удобен для организации курсов подобных этому, и, как вы можете видеть на главной странице сайта, реально широко используется, но в первую очередь для организации очных занятий. Я не имею непосредственного отношения к этому сайту (хотя и знаком с людьми, которые его создали и поддерживают).

+
А что за сайт informatics.msk.ru?

+
Informatics.msk.ru — это сайт дистанционной подготовки по информатике, организованный Московским центром непрерывного математического образования (МЦНМО) и Московским институтом открытого образования (МИОО); он очень удобен для организации курсов подобных этому, и, как вы можете видеть на главной странице сайта, реально широко используется, но в первую очередь для организации очных занятий. Я не имею непосредственного отношения к этому сайту (хотя и знаком с людьми, которые его создали и поддерживают).

Мой курс строится на базе материалов с informatics и использует его в качестве тестирующей системы (а раньше вообще курс существовал только на informatics). Местами еще могли остаться ссылки на информатикс, если обнаруживаете такую, пишите мне.

     """)
@@ -573,7 +573,7 @@

Если вы не уверены, какая лицензия распространяется на конкретный материал, спросите меня.

Условия задач

-
Условия задач взяты с сайта informatics.mccme.ru, где они в большинстве случаев собраны с различных олимпиад. Условия лицензии на них не ясны.

+
Условия задач взяты с сайта informatics.msk.ru, где они в большинстве случаев собраны с различных олимпиад. Условия лицензии на них не ясны.

     """)

@@ -757,8 +757,8 @@

Вы всегда можете связаться со мной, контактные данные указаны в разделе "О курсе".

-
А что за сайт informatics.mccme.ru?

-
Informatics.mccme.ru — это сайт дистанционной подготовки по информатике, организованный Московским центром непрерывного математического образования (МЦНМО) и Московским институтом открытого образования (МИОО); он очень удобен для организации курсов подобных этому, и, как вы можете видеть на главной странице сайта, реально широко используется, но в первую очередь для организации очных занятий. Я не имею непосредственного отношения к этому сайту (хотя и знаком с людьми, которые его создали и поддерживают).

+
А что за сайт informatics.msk.ru?

+
Informatics.msk.ru — это сайт дистанционной подготовки по информатике, организованный Московским центром непрерывного математического образования (МЦНМО) и Московским институтом открытого образования (МИОО); он очень удобен для организации курсов подобных этому, и, как вы можете видеть на главной странице сайта, реально широко используется, но в первую очередь для организации очных занятий. Я не имею непосредственного отношения к этому сайту (хотя и знаком с людьми, которые его создали и поддерживают).

Этот курс строится на базе материалов с informatics и использует его в качестве тестирующей системы (а раньше вообще курс существовал только на informatics). Местами еще могли остаться ссылки на информатикс, если обнаруживаете такую, пишите мне.

     """)
@@ -777,7 +777,7 @@

Материалы

-
Если вам нужен не функционал алгопрога, а только материалы — теоретические материалы, видеолекции, задачи и т.д. — то они доступны в том или ином виде свободно. Теоретические материалы, автором которых являюсь я, как правило, распространяются под той или иной свободной лицензией, вы можете их использовать. Она обычно указана в самом материале, если там лицензия не указана, то спросите меня. Материалы, автором которых являюсь не я, взяты с других сайтов, вы обычно тоже можете их использовать. Почти все задачи взяты с общедоступных сайтов informatics.mccme.ru и codeforces.com, на обоих этих сайтах вы можете формировать свои курсы из доступных там задач и т.д. (в частности, информатикс специально сделан для проведения различных курсов). Основное (хотя уже и подустаревшее) содержимое алгопрога есть на информатиксе: informatics.msk.ru/course/view.php?id=1135 — собственно, весь алгопрог вырос из этого моего курса на информатиксе.

+
Если вам нужен не функционал алгопрога, а только материалы — теоретические материалы, видеолекции, задачи и т.д. — то они доступны в том или ином виде свободно. Теоретические материалы, автором которых являюсь я, как правило, распространяются под той или иной свободной лицензией, вы можете их использовать. Она обычно указана в самом материале, если там лицензия не указана, то спросите меня. Материалы, автором которых являюсь не я, взяты с других сайтов, вы обычно тоже можете их использовать. Почти все задачи взяты с общедоступных сайтов informatics.msk.ru и codeforces.com, на обоих этих сайтах вы можете формировать свои курсы из доступных там задач и т.д. (в частности, информатикс специально сделан для проведения различных курсов). Основное (хотя уже и подустаревшее) содержимое алгопрога есть на информатиксе: informatics.msk.ru/course/view.php?id=1135 — собственно, весь алгопрог вырос из этого моего курса на информатиксе.

Движок


Update tests.

Update tests.

--- test/mvstar.coffee
+++ test/mvstar.coffee
@@ -5,3 +5,27 @@
   it 'should expose Model and View constructors', ->
     mvstar.View.should.exist
     mvstar.Model.should.exist
+
+describe 'mvstar#Model', ->
+  User = null
+  user = null
+
+  before ->
+    User = mvstar.Model
+      defaults:
+        age: 0
+      validators:
+        name: (name) -> name?
+
+  it 'should set defaults for new instances', ->
+    user = User name: 'Sam'
+    user.age.should.eq 0
+
+  it 'should validate instances', ->
+    user = User()
+    user.validate().should.eq false
+    user = User name: 'Sam'
+    user.validate().should.eq true
+
+# describe 'mvstar#View', ->
+#   it 'should render elements', ->


Use request to get plugin data

Use request to get plugin data

--- views/services/plugin-manager.cjsx
+++ views/services/plugin-manager.cjsx
@@ -9,6 +9,8 @@
 __n = i18n.setting.__n.bind(i18n.setting)
 {remote} = require 'electron'
 windowManager = remote.require './lib/window'
+request = Promise.promisifyAll require 'request'
+requestAsync = Promise.promisify request, multiArgs: true

 # we need only glob here
 globAsync = Promise.promisify require 'glob'
@@ -219,10 +221,12 @@
     tasks = plugins.map async (plugin, index) =>
       if !plugin.needRollback
         try
-          rawData = yield Promise.promisify(npm.commands.view)([plugin.packageName, 'dist-tags', 'poiPlugin'])
-          for key of rawData
-            data = rawData[key]
-          distTag = data['dist-tags']
+          data = JSON.parse (yield requestAsync "#{@config_.mirror.server}#{plugin.packageName}/latest")[1]
+          distTag =
+            latest: data.version
+          if @config_.betaCheck
+            _.extend distTag,
+              beta: JSON.parse((yield requestAsync "#{@config_.mirror.server}#{plugin.packageName}/beta")[1]).version
           latest = "#{plugin.version}"
           notCompatible = false
           apiVer = data.poiPlugin?.apiVer || plugin.apiVer
@@ -242,6 +246,8 @@
             @plugins_[index]?.isOutdated = true
             @plugins_[index]?.lastestVersion = latest
             if plugin.isRead then outdatedList.push plugin.name
+        catch e
+          console.error e
     yield Promise.all(tasks)
     if isNotif && outdatedList.length > 0
       content = "#{outdatedList.join(' ')} #{__ "have newer version. Please update your plugins."}"


restored propper "ignoring" of setting virtual properties with no setter

--- src/art/engine/core/element_base.coffee
+++ src/art/engine/core/element_base.coffee
@@ -244,23 +244,12 @@
           log externalName:externalName, options:options
           throw new Error "use getterNew: (pending) -> "

+        # if no setter, setting a virtual property is simply ignored
+        _setter = options.setter || ->
         _getterNew = options.getterNew
-        _setter = options.setter
-
-        # if no setter, setting a virtual property is simply ignored
-        setter ||= ->
-
-        getter        = (pending) ->
-          # log
-          #   virtual_getter: externalName
-          #   pending: pending
-          #   self: @inspectedName
-            _getterNew.call @, pending
-        pendingGetter = ->
-          # log
-          #   virtual_pendingGetter: externalName
-          #   self: @inspectedName
-            _getterNew.call @, true
+
+        getter        = _getterNew
+        pendingGetter = -> _getterNew.call @, true
         setter        = (rawValue) -> _setter.call @, rawValue, preprocessor

       else
@@ -423,7 +412,7 @@
       for i in [0...virtualCount]
         mp = virtualPropertySecondPassMetaProperties[i]
         v = virtualPropertySecondPassValues[i]
-        @[mp.setterName]? v
+        @[mp.setterName] v

       @_elementChanged true, true, true
       null


Only add modifiers if set

Only add modifiers if set

--- resources/assets/coffee/osu_common.coffee
+++ resources/assets/coffee/osu_common.coffee
@@ -31,7 +31,10 @@

   classWithModifiers: (className, modifiers) ->
     ret = className
-    ret += " #{className}--#{modifier}" for modifier in modifiers
+
+    if modifiers?
+      ret += " #{className}--#{modifier}" for modifier in modifiers
+
     ret


detaching the element which allows the change event to take place

--- jsapp/xlform_model_view/view.row.coffee
+++ jsapp/xlform_model_view/view.row.coffee
@@ -89,12 +89,7 @@
         show = !@_settingsExpanded

       if show and !@_settingsExpanded
-        if @already_rendered_expanded
-          @$('.card__settings').show()
-        else
-          @_expandedRender()
-          @already_rendered_expanded = true
-
+        @_expandedRender()
         @$card.addClass('card--expanded-settings')
         @hideMultioptions?()
         @_settingsExpanded = true
@@ -105,7 +100,7 @@
       ``

     _cleanupExpandedRender: ->
-      @$('.card__settings').hide()
+      @$('.card__settings').detach()

     clone: (event) =>
       @model.getSurvey().insert_row @model, @model._parent.models.indexOf(@model) + 1


Remove Shift-Space autocomplete trigger, far to easy to misfire

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
@@ -170,7 +170,7 @@
 						$(editor.completer.popup?.container).css({'font-size': @$scope.fontSize + 'px'})
 						if editor.completer?.completions?.filtered?.length == 0
 							editor.completer.detach()
-					bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space|Shift-Space"
+					bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
 				}

 			Util.retrievePrecedingIdentifier = (text, pos, regex) ->


fix silly paren issue for index var

fix silly paren issue for index var

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
@@ -162,7 +162,8 @@
 			cursorPosition = @editor.getCursorPosition()
 			end = change.end
 			{lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end)
-			if (i = lineUpToCursor.indexOf('%') > -1 and lineUpToCursor[i-1] != '\\')
+			if ((i = lineUpToCursor.indexOf('%')) > -1 and lineUpToCursor[i-1] != '\\')
+				console.log lineUpToCursor, i
 				return
 			lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\"
 			lastTwoChars = lineUpToCursor.slice(-2)


Always hide the spinner when we close

Always hide the spinner when we close

--- app/assets/javascripts/overlay.js.coffee
+++ app/assets/javascripts/overlay.js.coffee
@@ -23,7 +23,6 @@
     $('body').on 'keyup', (event) ->
       return if window.isFormElement(event.target)
       return unless event.which == ESC_KEY_CODE
-      self.hideSpinner()
       self.close()

     @dom.el.on 'click', @dom.closeButtonSelector, (event) ->
@@ -107,6 +106,7 @@

   close: ->
     @closed = true
+    @hideSpinner()
     @resetUrl()
     @clear()
     @dom.el.hide()


default n threshold

default n threshold

--- app/assets/javascripts/zvg/column.coffee
+++ app/assets/javascripts/zvg/column.coffee
@@ -167,7 +167,7 @@
     @series_2.attr('label', (d) -> d.key).transition().duration(500)
       .attr('transform', (d,i) => "translate(#{@column_band(i)}, 0)")
       .attr('opacity', (d) =>
-        if @series_2_label_sum(d) < @_n_threshold then 0.3 else 1.0
+        if @series_2_label_sum(d) < (@_n_threshold or 0) then 0.3 else 1.0
       )
     @series_2.exit()
       .transition().duration(500)


remove state.optionsKeys from dropdown task

--- app/classifier/tasks/dropdown/index.cjsx
+++ app/classifier/tasks/dropdown/index.cjsx
@@ -82,7 +82,6 @@
         not select.required or (annotation.value[i]?.value? and annotation.value[i]?.value isnt "")

   getInitialState: ->
-    optionsKeys: {}
     selectedOptions: []

   componentWillMount: ->
@@ -91,10 +90,7 @@

   componentDidMount: ->
     annotationValues = @props.annotation.value
-    if annotationValues.length
-      for answer, i in annotationValues
-        @handleOptionsKeys(i, answer.value)
-    else
+    unless annotationValues.length
       @props.annotation.value = @props.task.selects.map -> {value: null, option: false}

     if @props.autoFocus is true
@@ -114,12 +110,8 @@
     selectedOptions = []
     optionsKeys = []
     @props.annotation.value.map (annotation, i) =>
-      console.log 'ANNOTATION', i, annotation
       if annotation.option
-        optionsKey = if optionsKeys.length then optionsKeys.join(';') else '*'
-        console.log 'MENU', @getOptions(i, optionsKey)
-        [selected] = @getOptions(i, optionsKey).filter (option) -> option.value is annotation.value
-        console.log 'OPTION', selected
+        [selected] = @getOptions(i, optionsKeys).filter (option) -> option.value is annotation.value
         optionsKeys.push selected.value if selected?.value
         selectedOptions[i] = selected
       else
@@ -129,34 +121,22 @@
   handleFocus: ->
     @menus[0].focus()

-  handleOptionsKeys: (i, value) ->
-    {id, condition} = @props.task.selects[i]
-    optionsKeys = @state.optionsKeys
-
-    if i is 0 or i is '0'
-      optionsKeys[id] = value
-    else if value is null
-      optionsKeys[id] = null
-    else
-      optionsKeys[id] = "#{optionsKeys[condition]};#{value}"
-    @setState optionsKeys: optionsKeys
-
-  getOptions: (i, optionsKey) ->
+  getOptions: (i, optionsKeys) ->
+    optionsKey = if optionsKeys.length then optionsKeys.join(';') else '*'
     select = @props.task.selects[i]
-    optionsKey ?= if not select.condition? then '*' else @state.optionsKeys[select.condition]
     options = select.options[optionsKey]
     if options then options else []

   getDisabledAttribute: (i) ->
     {selects} = @props.task
     select = selects[i]
-    condition = selects.filter (filterSelect) => filterSelect.id is select.condition
-    conditionIndex = selects.indexOf(condition[0])
-    optionsKeys = @state.optionsKeys
+    optionsKeys = (o.value for o in @state.selectedOptions.slice(0,i))
+    [condition] = selects.filter (filterSelect) => filterSelect.id is select.condition
+    conditionIndex = selects.indexOf(condition)

     if select.condition? and not @props.annotation.value[conditionIndex]
       return true
-    if select.condition? and select.allowCreate is false and not select.options[optionsKeys[select.condition]]?.length
+    if select.condition? and select.allowCreate is false and not @getOptions(i, optionsKeys).length
       return true
     false

@@ -175,9 +155,8 @@
     <GenericTask question={@props.task.instruction} help={@props.task.help} required={@props.task.required}>
       <div>
         {selects.map (select, i) =>
-          optionsKey = if optionsKeys.length then optionsKeys.join(';') else '*'
           options = []
-          options.push option for option in @getOptions(i, optionsKey)
+          options.push option for option in @getOptions(i, optionsKeys)
           disabled = @getDisabledAttribute(i)
           selectedOption = if @state.selectedOptions[i]?.value then @state.selectedOptions[i] else null
           optionsKeys.push selectedOption.value if selectedOption?.value
@@ -217,14 +196,13 @@
     selectedOptions[i] = option
     value = @props.annotation.value

-    options = @getOptions(i)
+    optionsKeys = (o.value for o in selectedOptions.slice(0,i))
+    options = @getOptions(i, optionsKeys)
     newIndex = options?.indexOf(option)
     if newIndex is -1 or newIndex is undefined
       value[i] = {value: option.value, option: false}
-      @handleOptionsKeys(i, null)
     else
       value[i] = {value: option.value, option: true}
-      @handleOptionsKeys(i, option.value)

     @clearRelated(i)


Remove i18n mock from wisiwyg directive.

--- app/coffee/modules/common/wisiwyg.coffee
+++ app/coffee/modules/common/wisiwyg.coffee
@@ -28,32 +28,7 @@
 #############################################################################
 ## WYSIWYG markitup editor directive
 #############################################################################
-
-# TODO: fix when i18n is implemented
-$i18next = {
-    t: (key) ->
-        keywords = {
-            "markdown-editor.heading-1": "First Level Heading",
-            "markdown-editor.heading-2": "Second Level Heading",
-            "markdown-editor.heading-3": "Third Level Heading",
-            "markdown-editor.bold": "Bold",
-            "markdown-editor.italic": "Italic",
-            "markdown-editor.strike": "Strike",
-            "markdown-editor.bulleted-list": "Bulleted List",
-            "markdown-editor.numeric-list": "Numeric List",
-            "markdown-editor.picture": "Picture",
-            "markdown-editor.link": "Link",
-            "markdown-editor.quotes": "Quotes",
-            "markdown-editor.code-block": "Code Block / Code",
-            "markdown-editor.preview": "Preview",
-            "markdown-editor.help": "Help",
-            "markdown-editor.placeholder": "Your title here...",
-            "markdown-editor.link-placeholder": "Your text to link here..."
-        }
-        return keywords[key] or key
-}
-
-tgMarkitupDirective = ($rootscope, $rs) ->
+tgMarkitupDirective = ($rootscope, $rs, $tr) ->
     previewTemplate = _.template("""

@@ -175,40 +150,40 @@

             markupSet: [
                 {
-                    name: $i18next.t('markdown-editor.heading-1')
+                    name: $tr.t("markdown-editor.heading-1")
                     key: "1"
-                    placeHolder: $i18next.t('markdown-editor.placeholder')
-                    closeWith: (markItUp) -> markdownTitle(markItUp, '=')
-                },
-                {
-                    name: $i18next.t('markdown-editor.heading-2')
+                    placeHolder: $tr.t("markdown-editor.placeholder")
+                    closeWith: (markItUp) -> markdownTitle(markItUp, "=")
+                },
+                {
+                    name: $tr.t("markdown-editor.heading-2")
                     key: "2"
-                    placeHolder: $i18next.t('markdown-editor.placeholder')
-                    closeWith: (markItUp) -> markdownTitle(markItUp, '-')
-                },
-                {
-                    name: $i18next.t('markdown-editor.heading-3')
+                    placeHolder: $tr.t("markdown-editor.placeholder")
+                    closeWith: (markItUp) -> markdownTitle(markItUp, "-")
+                },
+                {
+                    name: $tr.t("markdown-editor.heading-3")
                     key: "3"
-                    openWith: '### '
-                    placeHolder: $i18next.t('markdown-editor.placeholder')
-                },
-                {
-                    separator: '---------------'
-                },
-                {
-                    name: $i18next.t('markdown-editor.bold')
+                    openWith: "### "
+                    placeHolder: $tr.t("markdown-editor.placeholder")
+                },
+                {
+                    separator: '---------------'
+                },
+                {
+                    name: $tr.t("markdown-editor.bold")
                     key: "B"
                     openWith: '**'
                     closeWith: '**'
                 },
                 {
-                    name: $i18next.t('markdown-editor.italic')
+                    name: $tr.t("markdown-editor.italic")
                     key: "I"
                     openWith: '_'
                     closeWith: '_'
                 },
                 {
-                    name: $i18next.t('markdown-editor.strike')
+                    name: $tr.t("markdown-editor.strike")
                     key: "S"
                     openWith: '~~'
                     closeWith: '~~'
@@ -217,45 +192,45 @@
                     separator: '---------------'
                 },
                 {
-                    name: $i18next.t('markdown-editor.bulleted-list')
-                    openWith: '- '
-                },
-                {
-                    name: $i18next.t('markdown-editor.numeric-list')
-                    openWith: (markItUp) -> markItUp.line+'. '
-                },
-                {
-                    separator: '---------------'
-                },
-                {
-                    name: $i18next.t('markdown-editor.picture')
+                    name: $tr.t("markdown-editor.bulleted-list")
+                    openWith: "- "
+                },
+                {
+                    name: $tr.t("markdown-editor.numeric-list")
+                    openWith: (markItUp) -> markItUp.line+". "
+                },
+                {
+                    separator: '---------------'
+                },
+                {
+                    name: $tr.t("markdown-editor.picture")
                     key: "P"
                     replaceWith: '![[![Alternative text]!]]([![Url:!:http://]!] "[![Title]!]")'
                 },
                 {
-                    name: $i18next.t('markdown-editor.link')
+                    name: $tr.t("markdown-editor.link")
                     key: "L"
                     openWith: '['
                     closeWith: ']([![Url:!:http://]!] "[![Title]!]")'
-                    placeHolder: $i18next.t('markdown-editor.link-placeholder')
-                },
-                {
-                    separator: '---------------'
-                },
-                {
-                    name: $i18next.t('markdown-editor.quotes')
-                    openWith: '> '
-                },
-                {
-                    name: $i18next.t('markdown-editor.code-block')
-                    openWith: '```\n'
-                    closeWith: '\n```'
-                },
-                {
-                    separator: '---------------'
-                },
-                {
-                    name: $i18next.t('markdown-editor.preview')
+                    placeHolder: $tr.t("markdown-editor.link-placeholder")
+                },
+                {
+                    separator: '---------------'
+                },
+                {
+                    name: $tr.t("markdown-editor.quotes")
+                    openWith: "> "
+                },
+                {
+                    name: $tr.t("markdown-editor.code-block")
+                    openWith: "```\n"
+                    closeWith: "\n```"
+                },
+                {
+                    separator: '---------------'
+                },
+                {
+                    name: $tr.t("markdown-editor.preview")
                     call: preview
                     className: "preview-icon"
                 },
@@ -263,7 +238,7 @@
                 #     separator: '---------------'
                 # },
                 # {
-                #     name: $i18next.t('markdown-editor.help')
+                #     name: $tr.t("markdown-editor.help")
                 #     call: openHelp
                 #     className: "help"
                 # }
@@ -291,4 +266,4 @@

     return {link:link, require:"ngModel"}

-module.directive("tgMarkitup", ["$rootScope", "$tgResources", tgMarkitupDirective])
+module.directive("tgMarkitup", ["$rootScope", "$tgResources", "$tgI18n", tgMarkitupDirective])


Server static content if NODE_ENV isn't staging or production.

--- server/code/index.coffee
+++ server/code/index.coffee
@@ -195,7 +195,7 @@

   # Add Connect Assets
   app.use assets({src: 'client'})
-  if not process.env.NODE_ENV
+  if not /staging|production/.test process.env.NODE_ENV
     # Set the public folder as static assets
     app.use express.static(process.cwd() + '/shared')
   if process.env.NODETIME_KEY


Fixed issue where $scope.options was not set when accessing it

--- src/core/javascripts/controllers/service_list.js.coffee
+++ src/core/javascripts/controllers/service_list.js.coffee
@@ -5,36 +5,34 @@
   replace: true
   scope : true
   controller : 'ServiceList'
-  link : (scope, element, attrs) ->

-    scope.options = scope.$eval(attrs.bbServices) or {}
-
-    if attrs.bbItem
-      scope.booking_item = scope.$eval( attrs.bbItem )
-    if attrs.bbShowAll or scope.options.show_all
-      scope.show_all = true
-    if scope.options.allow_single_pick
-      scope.allowSinglePick = true
-    return
-
-
-angular.module('BB.Controllers').controller 'ServiceList',($scope,  $rootScope, $q, $attrs, $modal, $sce, ItemService, FormDataStoreService, ValidatorService, PageControllerService, halClient, AlertService, ErrorService, $filter, CategoryService) ->
+angular.module('BB.Controllers').controller 'ServiceList',($scope, $rootScope, $q, $attrs, $modal, $sce, ItemService, FormDataStoreService, ValidatorService, PageControllerService, halClient, AlertService, ErrorService, $filter, CategoryService) ->

   $scope.controller = "public.controllers.ServiceList"
+
   FormDataStoreService.init 'ServiceList', $scope, [
     'service'
   ]
+
   $scope.notLoaded $scope
+
   angular.extend(this, new PageControllerService($scope, $q))

   $scope.validator = ValidatorService

   $scope.filters = {price: {}, name: null}

+  $scope.options = $scope.$eval($attrs.bbServices) or {}
+
+  $scope.booking_item = $scope.$eval($attrs.bbItem) if $attrs.bbItem
+  $scope.show_all = true if $attrs.bbShowAll or $scope.options.show_all
+  $scope.allowSinglePick = true if $scope.options.allow_single_pick
+
+
   $rootScope.connection_started.then () =>
     if $scope.bb.company
       $scope.init($scope.bb.company)
-  , (err) ->  $scope.setLoadedAndShowError($scope, err, 'Sorry, something went wrong')
+  , (err) -> $scope.setLoadedAndShowError($scope, err, 'Sorry, something went wrong')

   $scope.init = (comp) ->
@@ -69,6 +67,8 @@
       if !$scope.options.show_event_groups
         items = items.filter (x) -> !x.is_event_group

+      # if there's only one service and single pick hasn't been enabled,
+      # automatically select the service.
       if (items.length is 1 && !$scope.allowSinglePick)
         if !$scope.selectItem(items[0], $scope.nextRoute )
           setServiceItem items


standardise "created/updated" google analytics events from postTools API

--- server/code/index.coffee
+++ server/code/index.coffee
@@ -481,10 +481,10 @@
                 code = if isNew then 201 else 200
                 if isNew
                   code = 201
-                  action = 'created'
+                  action = 'Created'
                 else
                   code = 200
-                  action = 'updated'
+                  action = 'Updated'
                 na.trackEvent 'tools', action, body.name
                 return resp.send code, tool


Code refactoring

Code refactoring

--- client/ide/lib/views/tabview/ideview.coffee
+++ client/ide/lib/views/tabview/ideview.coffee
@@ -615,13 +615,12 @@
     frontApp.forEachSubViewInIDEViews_ 'terminal', (pane) =>
       activeSessions.push pane.remote.session  if pane.remote?

-    # Collect inactive sessions
-    for session in sessions
-      isActive = activeSessions.indexOf session
-      inActiveSessions.push session  if isActive is -1
-
     sessions.forEach (session, i) =>
       isActive = session in activeSessions
+
+      # Collect inactive sessions
+      inActiveSessions.push session  unless isActive
+
       terminalSessions["Session (#{session[0..5]}) &nbsp"] =
         disabled          : isActive
         separator         : sessions.length is i


change icon to file:// res

change icon to file:// res

--- browser/src/coffee/search.coffee
+++ browser/src/coffee/search.coffee
@@ -1,5 +1,8 @@
 remote = require 'remote'
 _ = require 'lodash'
+
+docSetDir = remote.require('./args').docsetdir.replace(/\\/g, '/')
+console.log docSetDir

 # register global shortcut
 if S.cfg.shortCut
@@ -89,7 +92,7 @@
                     str += "#{i.docset}: #{i.data}\n"
                 alert(str)
         getBg: (name)->
-            "url('http://localhost:#{S.docPort}/" +
+            "url('#{docSetDir}/" +
                 encodeURIComponent(name) +
                 "/icon.png')"
 $('document').ready ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/sprite.coffee
+++ test/sprite.coffee
@@ -4,5 +4,8 @@
   ok Sprite.EMPTY
   ok Sprite.NONE

+test "callback and cache", ->
+
+
 module()


fixed deleting facet on empty select2 selection close #49

--- _src/js/views/facets/subselect.coffee
+++ _src/js/views/facets/subselect.coffee
@@ -149,7 +149,7 @@

 	_convertValue: ( data )=>
 		_data = {}
-		if @convertValueToInt
+		if @convertValueToInt
 			_data.value = parseFloat( data.id )
 		else
 			_data.value = data.id
@@ -192,7 +192,9 @@
 		evnt.stopPropagation() if evnt?.stopPropagation
 		_vals = @getValue()
 		if not _vals?.length
+			# Issue#49 if nothing was selected close the select-view and remove the whole facet
 			@close()
+			@sub.del()
 			return
 		@_select( _vals )


controlsViewSpec: separate events testing

use a describe block for elapsedTimeControlView events

--- coffee/test/views/controls_view.spec.coffee
+++ coffee/test/views/controls_view.spec.coffee
@@ -216,19 +216,23 @@
     view.render()
     expect(view.$('.total').text()).toBe '00:20'

-  it 'should trigger event `ElapsedTimeControl:Click` on click', ->
-    view = new ElapsedTimeControlView
-    spy = new EventSpy view, 'ElapsedTimeControl:Click'
-    view.render()
-    view.$el.trigger 'click'
-    expect(spy.triggered).toBe true
-
   it 'should refresh values if model changes', ->
     view = new ElapsedTimeControlView
     spyOn view, 'refreshValues'
     view.model.set 'total', 10
     expect(view.refreshValues).toHaveBeenCalled()

+
+  describe 'ElapsedTimeControlView: events', ->
+
+    it 'should trigger event `ElapsedTimeControl:Click` on click', ->
+      view = new ElapsedTimeControlView
+      spy = new EventSpy view, 'ElapsedTimeControl:Click'
+      view.render()
+      view.$el.trigger 'click'
+      expect(spy.triggered).toBe true
+
+
   it 'should look good', ->
     # setup DOM
     acorn.util.appendCss()


Update mainIndexCtrl.js.coffee

Update mainIndexCtrl.js.coffee

--- web/app/assets/javascripts/controllers/main/mainIndexCtrl.js.coffee
+++ web/app/assets/javascripts/controllers/main/mainIndexCtrl.js.coffee
@@ -9,4 +9,5 @@

   _.each($scope.votes, (item) ->
     item.color = if item.prediction > 50 then 'green' else 'red'
-  )+  )
+


JCredential: schema: generate default each time

--- workers/social/lib/social/models/computeproviders/credential.coffee
+++ workers/social/lib/social/models/computeproviders/credential.coffee
@@ -126,7 +126,7 @@
             ACCESSLEVEL.PRIVATE
           ]
         ]
-        default       : 'private'
+        default       : -> ACCESSLEVEL.PRIVATE

     relationships     :


wait for settings to load before showing follow button

--- js/app.coffee
+++ js/app.coffee
@@ -94,7 +94,10 @@

     # user is looking at the project's summary, but hasn't followed it
     @show_follow_project_button = @komp =>
-      @project() && !@project().followed() && @project().project_name() is @current_page().project_name
+      @project() &&
+       @project().loaded_settings() &&
+        !@project().followed() &&
+         @project().project_name() is @current_page().project_name

     if window.renderContext.current_user


Fix a type error.

Fix a type error.

--- atom/browser/api/lib/web-contents.coffee
+++ atom/browser/api/lib/web-contents.coffee
@@ -96,7 +96,7 @@
       printingSetting.marginsType = options.marginsType
     if options.printSelectionOnly
       printingSetting.shouldPrintSelectionOnly = options.printSelectionOnly
-    if options.printCSSBackgrounds
+    if options.printBackgrounds
       printingSetting.shouldPrintBackgrounds = options.printBackgrounds

     webContents._printToPDF printingSetting, callback


Pass regex as options to Grammar::createPattern

--- src/injections.coffee
+++ src/injections.coffee
@@ -13,7 +13,7 @@
       patterns = []
       anchored = false
       for regex in values.patterns
-        pattern = @grammar.createPattern({regex})
+        pattern = @grammar.createPattern(regex)
         anchored = true if pattern.anchored
         patterns.push(pattern.getIncludedPatterns(grammar, patterns)...)
       @injections.push


Fix the global task search in UI to be able to perfrom a search on no query params

--- SingularityUI/app/components/taskSearch/DisplayResults.cjsx
+++ SingularityUI/app/components/taskSearch/DisplayResults.cjsx
@@ -10,8 +10,8 @@

 DisplayResults = React.createClass

-    # Used to detect if any props have changed
-    didPropsChange: (nextProps) ->
+    # Used to detect if any query params have changed
+    didQueryParamsChange: (nextProps) ->
         return true unless nextProps.requestId == @props.requestId
         return true unless nextProps.global == @props.global
         return true unless nextProps.deployId == @props.deployId
@@ -63,7 +63,8 @@
         @fetchCollection()

     componentWillReceiveProps: (nextProps) ->
-        if @didPropsChange nextProps
+        # Note that if adding another query param you MUST update @didQueryParamsChange
+        if @didQueryParamsChange(nextProps) or (@props.holdOffOnSearching and not nextProps.holdOffOnSearching)
             @willFetch = true
             @setState
                 loading: true


reorganised g-code grammar for cnc pilot

--- grammars/gcode_pilot.cson
+++ grammars/gcode_pilot.cson
@@ -1,12 +1,14 @@
 scopeName : 'source.g-code.cncpilot';
-name: 'G-Code - CNCpilot'
+name: 'G-Code - CNCpilot';
 fileTypes: [
-  'ncs', # CNCpilot or ManualPlus
+  'ncs',
+  'nc',
 ]

-foldingStartMarker: 'UNIT'
-foldingStopMarker: 'END_OF_UNIT'
-limitLineLength: false
+foldingStartMarker: 'UNIT';
+foldingStopMarker: 'END_OF_UNIT';
+firstLineMatch: '"TURN_V1\\.0"$';
+limitLineLength: true;

 patterns: [
     {
@@ -15,19 +17,21 @@
       name: 'comment.line.g-code'
     },
     {
-      comment: 'comment until the end of the line'
-      match: ';(.*)$'
-      name: 'comment.line.g-code'
+      comment: 'unit block'
+      name: 'keyword.control.g-code'
+      contentName: 'keyword.operator.g-code';
+      begin: '\\[<unit'
+      end: '/>\\]'
     },
     {
-      comment: 'comment'
-      match: '(\\(.*\\))$'
-      name: 'comment.line.g-code'
+      comment: 'comment block in square brackets'
+      match: '\\[.*?\\]'
+      name: 'comment.block.g-code'
     },
     {
-        comment: 'commented out line'
-        match: '^(/N\\d+)\\s+(.*)$'
-        name: 'comment.block.g-code'
+      comment: 'string in double quouts'
+      match: '"[\\w\\s#]*"'
+      name: 'variable.string.g-code'
     },
     {
         comment: 'program meta info'
@@ -35,13 +39,8 @@
         name: 'comment.block.g-code'
     },
     {
-        comment: 'line termination'
-        match: '\\*$'
-        name: 'comment.line.g-code'
-    },
-    {
         comment: 'program name'
-        match: '(%)([\\w\\d_]+(?:(?:\\.nc)|(?:\\.NCS))?)(\\s+\\".*\\")?'
+        match: '(%)([\\w\\d_]+\\.[ncsNCS]+)(\\s+".*")?'
         captures:
             1:
                 name: 'keyword.control.g-code'
@@ -51,20 +50,7 @@
                 name: 'constant.string.g-code'
     },
     {
-        comment: 'tool call'
-        match: '\\b(T)(?:(=)(\\".*\\"))|(\\d+(?:\\.\\d+)?)\\b'
-        captures:
-            1:
-                name: 'keyword.control.g-code'
-            2:
-                name: 'keyword.control.g-code'
-            3:
-                name: 'constant.string.g-code'
-            4:
-                name: 'constant.numeric.g-code'
-    },
-    {
-      comment: 'Ingeger Values: g-command'
+      comment: 'G-Command'
       match: '\\b(G)(\\d+)\\b'
       captures:
         1:
@@ -73,34 +59,25 @@
           name: 'constant.numeric.g-code'
     },
     {
-      comment: 'Ingeger Values: machin function, spindle speed, radius, l, p'
-      match: '\\b([GMSRLPQHVDWC])(\\d+)\\b'
+      comment: 'Machine Function'
+      match: '\\b(M)(\\d+)\\b'
       captures:
         1:
-          name: 'keyword.operator.g-code'
+          name: 'keyword.control.g-code'
         2:
           name: 'constant.numeric.g-code'
     },
     {
-      comment: 'Float Values: feed rate'
-      match: '\\b([FB])(\\d+(?:\\.\\d+)?)\\b'
+      comment: 'Tool call'
+      match: '\\b(T)(\\d+)\\b'
       captures:
         1:
-          name: 'keyword.operator.g-code'
+          name: 'keyword.control.g-code'
         2:
           name: 'constant.numeric.g-code'
     },
     {
-      comment: 'Signed Float Values:'
-      match: '\\b([P])([\\+-]?\\d+(?:\\.\\d+)?)\\b'
-      captures:
-        1:
-          name: 'keyword.operator.g-code'
-        2:
-          name: 'constant.numeric.g-code'
-    },
-    {
-      comment: 'cartesian coordinate'
+      comment: 'axis values'
       match: '\\b([XYZ])([\\+-]?\\d+(?:\\.\\d+)?)\\b'
       captures:
         1:
@@ -109,8 +86,8 @@
           name: 'constant.numeric.g-code'
     },
     {
-      comment: 'circle center offset'
-      match: '\\b([IJK])([\\+-]?\\d+(?:\\.\\d+)?)\\b'
+      comment: 'Spindle speed'
+      match: '\\b(S)(\\d+)\\b'
       captures:
         1:
           name: 'keyword.operator.g-code'
@@ -118,38 +95,63 @@
           name: 'constant.numeric.g-code'
     },
     {
-      comment: 'Keywords: groups'
-      match: '\\b((?:UNIT)|(?:END_OF_UNIT))\\b'
+      comment: 'feed rate'
+      match: '\\b(F)(\\d+(?:\\.\\d+)?)\\b'
+      captures:
+        1:
+          name: 'keyword.operator.g-code'
+        2:
+          name: 'constant.numeric.g-code'
+    },
+    {
+      comment: 'Keyword: Unit start'
+      match: '\\b(UNIT ID)'
+      name: 'keyword.control.g-code'
+    },
+    {
+      comment: 'Keyword: Unit end'
+      match: '\\b(END_OF_UNIT) (S)(\\d+)'
       captures:
         1:
           name: 'keyword.control.g-code'
+        2:
+          name: 'keyword.operator.g-code'
+        3:
+          name: 'constant.numeric.g-code'
     },
     {
-      comment: 'Keywords: CNCpilot od ManualPlus German'
+      comment: 'Keywords: Groups German'
       match: '\\b((?:PROGRAMMKOPF)|(?:REVOLVER)|(?:ROHTEIL)|(?:FERTIGTEIL)|(?:BEARBEITUNG))\\b'
-      captures:
-        1:
-          name: 'keyword.control.g-code'
+      name: 'keyword.control.g-code'
+    },
+    {
+      comment: 'Keywords: Groups English'
+      match: '\\b((?:CONST)|(?:VAR)|(?:MACHINING))\\b'
+      name: 'keyword.control.g-code'
     },
     {
       comment: 'Keywords: GLOBAL'
       match: '\\b((?:ENDE)|(?:RETURN))\\b'
-      captures:
-        1:
-          name: 'keyword.control.g-code'
+      name: 'keyword.control.g-code'
     },
     {
       comment: 'Keywords: Flow control - Part1'
-      match: '\\b((?:IF)|(?:THEN)|(?:ELSE)|(?:ENDIF)|(?:WHILE)|(?:ENDWHILE)|(?:FOR)|(?:TO)|(?:bENDFOR))\\b'
-      captures:
-        1:
-          name: 'keyword.control.g-code'
+      match: '\\b((?:IF)|(?:OR)|(?:AND)|(?:THEN)|(?:ELSE)|(?:ENDIF))\\b'
+      name: 'keyword.control.g-code'
     },
     {
-      comment: 'Keywords: Flow control - Part2'
+      comment: 'Keywords: Flow control - Loop'
+      match: '\\b((?:WHILE)|(?:ENDWHILE)|(?:FOR)|(?:TO)|(?:bENDFOR))\\b'
+      name: 'keyword.control.g-code'
+    },
+    {
+      comment: 'Keywords: Flow control - Switch'
       match: '\\b((?:SWITCH)|(?:CASE)|(?:ENDSWITCH)|(?:BREAK)|(?:DEFAULT))\\b'
-      captures:
-        1:
-          name: 'keyword.control.g-code'
+      name: 'keyword.control.g-code'
+    },
+    {
+      comment: 'Keywords: Functions'
+      match: '\\b((?:INPUT)|(?:PRINT)|(?:PARA)|(?:DEF)|(?:NDEF)|(?:INT))\\b'
+      name: 'keyword.control.g-code'
     },
 ]


Add compress progresses

Add compress progresses

--- gulp/tasks/compile.coffee
+++ gulp/tasks/compile.coffee
@@ -38,11 +38,15 @@
       babelify
     ]
   })
-  .bundle()
-  .on("error", (err) ->
-    console.log(err.message)
-    console.log(err.stack)
-  )
-  .pipe(source(config.dist.vue))
-  .pipe(buffer())
-  .pipe(gulp.dest(config.dist.js));
+    .bundle()
+    .on("error", (err) ->
+      console.log(err.message)
+      console.log(err.stack)
+    )
+    .pipe($.plumber())
+    .pipe($.sourcemaps.init())
+    .pipe(source(config.dist.vue))
+    .pipe(buffer())
+    .pipe($.uglify())
+    .pipe($.sourcemaps.write(config.map))
+    .pipe(gulp.dest(config.dist.js));


delete users without any permissions

delete users without any permissions

--- both/methods/user/userPermissions.coffee
+++ both/methods/user/userPermissions.coffee
@@ -130,4 +130,6 @@

 			if project? && project.tags
 				for tag in project.tags
-					Meteor.call 'changeTagRole', tag._id, user._id, 'none'
+					Meteor.call 'changeTagRole', tag._id, user._id, 'none', ->
+						if Roles.getRolesForUser(user._id).length == 0
+							Meteor.users.remove user._id


! fix zoom direction

! fix zoom direction

--- lib/controllers/scale_controller.coffee
+++ lib/controllers/scale_controller.coffee
@@ -19,10 +19,10 @@
     @map.zoom(@scale);

   zoomInAnim: () =>
-    @animZoom(0.01, 10);
+    @animZoom(-0.01, 10);

   zoomOutAnim: () =>
-    @animZoom(-0.01, 10);
+    @animZoom(0.01, 10);

   animZoom: (step, counter) =>
     if counter > 0


syntax tweaks

syntax tweaks

--- maskew.coffee
+++ maskew.coffee
@@ -91,7 +91,7 @@
   skew: (angle) =>
     angle  ?= @_dragAngle or 0
     angle   = 0 if angle < 0
-    sine    = sin (rads = rad angle)
+    sine    = sin rads = rad angle
     cosine  = cos rads
     tlX     = @_height * sine
     tlY     = @_height * cosine
@@ -183,18 +183,18 @@

     if typeof angle is 'object'
       options = angle
-      angle = 0
+      angle   = 0
     else if typeof angle is 'string'

       for el in @
-        return @ unless (instance = $.data el, 'maskew')
+        return @ unless instance = $.data el, 'maskew'
         return @ unless typeof instance[angle] is 'function'
         instance[angle].call instance, options

       return @

     for el in @
-      return instance if (instance = $.data el, 'maskew')
+      return instance if instance = $.data el, 'maskew'
       $.data el, 'maskew', new Maskew el, angle, options

     @


enabled module export

enabled module export

--- maskew.coffee
+++ maskew.coffee
@@ -2,7 +2,7 @@
 # Skew the shapes of DOM elements
 # Dan Motzenbecker
 # http://oxism.com
-# 0.1.6
+# 0.1.8
 # MIT License

@@ -37,7 +37,7 @@
     break

-class window.Maskew
+class Maskew

   constructor: (@_el, @angle, @_options = {}) ->
     return unless hasSupport
@@ -170,12 +170,12 @@
   _onTouchLeave: => @_onTouchEnd()

-  @VERSION: '0.1.6'
+  @VERSION: '0.1.8'

   @isSupported: hasSupport

-if window.jQuery? or window.$?.data?
+if window and (window.jQuery? or window.$?.data?)

   $::maskew = (angle, options) ->

@@ -199,3 +199,10 @@

     @

+
+if module?.exports
+  module.exports = Maskew
+else if define?.amd
+  define -> Maskew
+else
+  window.Maskew = Maskew


Improve overseer item search.

Improve overseer item search.

- Select first search term field contents on result if it was focused.
- Convert barcode part search always to uppercase.

--- kirppu/static_src/js/overseer/item_find_mode.coffee
+++ kirppu/static_src/js/overseer/item_find_mode.coffee
@@ -6,6 +6,10 @@
     @itemList = new ItemFindList()
     @searchForm = new ItemSearchForm(@doSearch)
     @search = null
+
+    @_focusInSearch = false
+    @searchForm.searchInput.on("focus", () => @_focusInSearch = true)
+    @searchForm.searchInput.on("blur", () => @_focusInSearch = false)

   enter: ->
     super
@@ -20,7 +24,7 @@
   doSearch: (query, code, vendor, min_price, max_price, type, state, show_hidden) =>
     @search =
       query: query
-      code: code
+      code: code.toUpperCase()
       vendor: vendor
       min_price: min_price
       max_price: max_price
@@ -41,6 +45,8 @@
       )(item_, index_)
     if items.length == 0
       @itemList.no_results()
+    if @_focusInSearch
+      @searchForm.searchInput.select()
     return

   onItemClick: (item) =>


Small fix

Small fix

--- ngtuvok/src/app/blog/blogpost.controller.coffee
+++ ngtuvok/src/app/blog/blogpost.controller.coffee
@@ -15,7 +15,7 @@
     )

     $scope.nextPost = ->
-      $location.path('/blog/' + $scope.nextpost._id);
+      $location.path('/blog/' + $scope.nextpost._id)
       return

     $scope.prevpost = BlogService.getPrevPost($routeParams.id)
@@ -25,7 +25,7 @@
     )

     $scope.prevPost = ->
-      $location.path('/blog/' + $scope.prevpost._id);
+      $location.path('/blog/' + $scope.prevpost._id)
       return

     return


Search for routes in chunks (includes workarounds for MAX_WAYPOINTS_EXCEEDED and OVER_QUERY_LIMIT errors)

--- app/assets/javascripts/angular/controllers/walking_guide_ctrl.coffee
+++ app/assets/javascripts/angular/controllers/walking_guide_ctrl.coffee
@@ -1,5 +1,7 @@
+#= require ../../variables
+
 angular.module('Relics').controller 'WalkingGuideCtrl',
-  ($scope, Relic, WalkingGuide) ->
+  ($scope, $timeout, Relic, WalkingGuide) ->
     $scope.query = ''
     $scope.widget =
       relics: []
@@ -10,7 +12,11 @@
     $scope.totalPages = -1
     $scope.loading = false
     directionsService  = new google.maps.DirectionsService()
-    directionsRenderer = new google.maps.DirectionsRenderer()
+    directionsRenderers = []
+    directionsData = []
+    latLngBounds = {}
+    findRoutePromises = []
+    relicsChunks = []
     center =
       latitude: 52.4118436
       longitude: 19.0984013
@@ -57,8 +63,16 @@
       $scope.currentPage = Math.min($scope.totalPages - 1, $scope.currentPage + 1)
       $scope.loadRelics()

+    createMarker = (relic) ->
+      new google.maps.Marker
+        map: $scope.map.instance
+        position: relicLatLng(relic)
+        icon: gmap_marker # From variables.js
+
     $scope.selectRelic = (relic) ->
-      $scope.widget.relics.push(angular.copy(relic))
+      _relic = angular.copy(relic)
+      _relic.marker = createMarker(_relic)
+      $scope.widget.relics.push(_relic)
       $scope.drawRoute()

     $scope.filteredSuggestions = ->
@@ -71,6 +85,7 @@

     $scope.removeRelic = (relic) ->
       index = $scope.widget.relics.indexOf(relic)
+      $scope.widget.relics[index].marker.setMap(null)
       $scope.widget.relics.splice(index, 1)
       $scope.drawRoute()

@@ -89,29 +104,91 @@
       $scope.totalPages = -1

     $scope.clearRoute = ->
-      directionsRenderer.setMap(null)
+      for promise in findRoutePromises
+        $timeout.cancel(promise)
+
+      for renderer in directionsRenderers
+        renderer.setMap(null)
+
+      findRoutePromises   = []
+      directionsRenderers = []
+      directionsData      = []

     relicLatLng = (relic) ->
       new google.maps.LatLng(relic.latitude, relic.longitude)

-    $scope.drawRoute = ->
-      $scope.clearRoute()
-
-      if !$scope.widget.relics.length
-        return $scope.resetMap()
-
-      request =
-        origin: relicLatLng($scope.widget.relics.first())
-        destination: relicLatLng($scope.widget.relics.last())
-        waypoints: $scope.widget.relics.slice(1, -1).map (relic) -> location: relicLatLng(relic)
-        travelMode: google.maps.TravelMode.WALKING
+    requestParams = (relics) ->
+      origin: relicLatLng(relics.first())
+      destination: relicLatLng(relics.last())
+      waypoints: relics.slice(1, -1).map (relic) -> location: relicLatLng(relic)
+      travelMode: google.maps.TravelMode.WALKING
+
+    relicsIntoChunks = ->
+      relics = $scope.widget.relics
+      return [] if !relics.length
+      chunks = []
+      for i in [0...relics.length] by 9
+        chunk = relics.slice(i, i + 10)
+        chunks.push(chunk) if chunk.length > 1
+      chunks
+
+    renderDirections = (callback) ->
+      latLngBounds = new google.maps.LatLngBounds()
+
+      for index in [0...directionsData.length]
+        data = directionsData[index]
+        directionsRenderer = new google.maps.DirectionsRenderer
+          suppressMarkers: true
+          preserveViewport: true
+        directionsRenderer.setDirections(data)
+        directionsRenderer.setMap($scope.map.instance)
+        directionsRenderers.push(directionsRenderer)
+
+        for route in data.routes
+          latLngBounds.union(route.bounds)
+
+        if directionsData.length == index + 1
+          $scope.map.instance.fitBounds(latLngBounds, true)
+          callback()
+
+    findRoute = (index = 0) ->
+      relics  = relicsChunks[index]
+      request = requestParams(relics)
+      last    = index + 1 == relicsChunks.length

       directionsService.route request, (result, status) ->
         if status == google.maps.DirectionsStatus.OK
-          directionsRenderer.setDirections(result)
-          directionsRenderer.setMap($scope.map.instance)
+          directionsData.push(result)
+          if last
+            # Render directions if this is the last request
+            renderDirections ->
+              $scope.loading = false
+          else
+            # Delay every two requests to prevent OVER_QUERY_LIMIT
+            delay = if index % 2
+              1000
+            else
+              50
+            promise = $timeout ->
+              findRoute(index + 1)
+            , delay
+            findRoutePromises.push(promise)
         else
-          # TODO
+          console.error status # TODO: handle error
+
+    $scope.drawRoute = ->
+      # Split relics in to chunks to prevent MAX_WAYPOINTS_EXCEEDED error
+      relicsChunks = relicsIntoChunks()
+      $scope.loading = true
+      $scope.clearRoute()
+
+      if $scope.widget.relics.length < 2
+        return $scope.resetMap()
+
+      promise = $timeout ->
+        findRoute()
+      , 1000
+      findRoutePromises.push(promise)

     $scope.sortableOptions =
       update: (e, ui) ->
@@ -122,6 +199,7 @@
       success = (response) ->
         $scope.loading = false
         $scope.widget = angular.copy(response.data)
+        $scope.drawRoute()

       error = (response) ->
         $scope.loading = false


Nodejs: metrics: move sending metrics logic to expressify.coffee

--- node_modules_koding/bongo/lib/expressify.coffee
+++ node_modules_koding/bongo/lib/expressify.coffee
@@ -2,6 +2,17 @@
   { dash } = require 'bongo'

   bongo = this
+
+  sendMetrics = (start, message) ->
+
+    parsedMessage =
+      try JSON.parse message
+      catch e then message
+
+    duration   = new Date() - start
+    { method } = parsedMessage
+    bongo.metrics.sendMethodMetrics { method, duration }
+

   process = (req, client, callback) ->

@@ -12,7 +23,13 @@
     responses = new Array queue.length

     workQueue = queue.map (message, i) -> ->
+
+      start = new Date()
       bongo.handleRequest channelName, { message, client }, (secretName, callbackId, args) ->
+
+        if bongo.metrics
+          sendMetrics start, message
+
         bongo.scrubResponse callbackId, args, (message) ->
           responses[i] = message
           workQueue.fin()


fixed my typo

fixed my typo

--- lib/assets/javascripts/firehose/long_poll.js.coffee
+++ lib/assets/javascripts/firehose/long_poll.js.coffee
@@ -13,7 +13,7 @@
   constructor: (args) ->
     super args
     # Configrations specifically for web sockets
-    @config.LongPoll         ||= {}
+    @config.longPoll         ||= {}
     # Protocol schema we should use for talking to WS server.
     @config.longPoll.url     ||= "http:#{@config.uri}"
     # How many ms should we wait before timing out the AJAX connection?


Remove unused function.

Remove unused function.

--- application/chrome/content/wesabe.coffee
+++ application/chrome/content/wesabe.coffee
@@ -215,13 +215,6 @@

     return true

-  getMyURI: ->
-    return wesabe._myURI if wesabe._myURI
-
-    for script in document.getElementsByTagName('script')
-      if script.getAttribute('src').match(/wesabe.coffee/)
-        return wesabe._myURI = script.getAttribute('src')
-
 walk = (module, callback) ->
   base = wesabe


fix sticky

fix sticky

--- apps/artist/client/views/overview.coffee
+++ apps/artist/client/views/overview.coffee
@@ -42,7 +42,11 @@
       limit: 170,
       label: 'Read More',
       heightBreakOffset: 20
-      onExpand: => @sticky.rebuild()
+      onExpand: =>
+        if @useNewArtworkFilter
+          @filterView.sticky.rebuild()
+        else
+          @sticky.rebuild()
     _.defer =>
       @$('.artist-blurb').addClass('is-fade-in')
       @$('.artist-exhibition-highlights').addClass 'is-fade-in'
@@ -92,7 +96,7 @@
     # Main section

     if @useNewArtworkFilter
-      { @sticky } = @filterView = (new ArtworkFilterView
+      @filterView = (new ArtworkFilterView
         el: @$('#artwork-section')
         artistID: @model.get('id')
         topOffset: $('.artist-sticky-header-container').height()


Bug修复

Bug修复

--- lib/project/select-template-view.coffee
+++ lib/project/select-template-view.coffee
@@ -36,7 +36,7 @@
   nextStep:(box) ->
     nextStepView = new infoView()
     box.setPrevStep @
-    box.mergeOptions {subview:nextStepView, tmpType: @createType}
+    box.mergeOptions {subview:nextStepView, tmpType: @createType, newType:'template'}
     box.nextStep()

   renderThumbList: () ->


Use referenceTypeId when it is available

--- src/coffee/product-type-generator.coffee
+++ src/coffee/product-type-generator.coffee
@@ -107,7 +107,9 @@
           label: @_i18n row, "#{ATTRIBUTE_TYPES.enum}Label"
         ]
       when ATTRIBUTE_TYPES.reference
-        if row['type']
+        if row['type.referenceTypeId']
+          type['referenceTypeId'] = row['type.referenceTypeId']
+        else if row['type']
           type['referenceTypeId'] = @_type(@_typeOrElementType(rawTypeName))
       when ATTRIBUTE_TYPES.set
         # TODO: ensure set is correctly build


remove space after equals of o.unwrapWidth

--- src/coffee/responsive.breadcrumbs.coffee
+++ src/coffee/responsive.breadcrumbs.coffee
@@ -103,7 +103,7 @@
             o.cloneEl       = o.el.clone()            # Reference to the old untouched element
             o.browserWindow = $ window
             o.windowWidth   = o.browserWindow.width()
-            o.unwrapWidth   =  o.getChildrenWidth()   # unwrapped width for the breadcrumbs
+            o.unwrapWidth   = o.getChildrenWidth()   # unwrapped width for the breadcrumbs
             o.documentBody  = $ document.body

             o.opts.onLoad(_this) if $.isFunction(o.opts.onLoad)