[grunt] slightly more sane code style for Nunjucks global `getPage()`

--- tasks/templates.coffee
+++ tasks/templates.coffee
@@ -205,11 +205,13 @@
            * @return {object} Contains all page's properties, including it's sub pages
           ###
           env.addGlobal 'getPage', (path, pages = localizedData.site.pages) ->
-            _result = _.get(pages, path)
-            if _result
-              Object.defineProperty _result, 'props', enumerable: false
-              return _result
-            else grunt.log.error('[getPage] can\'t find requested `' + path + '` inside specified object', '[' + this.ctx.page.href + ']')
+            result = _.get(pages, path)
+
+            if result
+              Object.defineProperty result, 'props', enumerable: false
+              return result
+            else
+              grunt.log.error('[getPage] can\'t find requested `' + path + '` inside specified object', '[' + this.ctx.page.href + ']')

           ###*
            * Explodes string into array breadcrumb. See `crumble` helper for details


Print errors that happened before task-bootstrap

--- src/task.coffee
+++ src/task.coffee
@@ -83,7 +83,7 @@
     taskPath = taskPath.replace(/\\/g, "\\\\")

     env = _.extend({}, process.env, {taskPath, userAgent: navigator.userAgent})
-    @childProcess = fork '--eval', [bootstrap], {env}
+    @childProcess = fork '--eval', [bootstrap], {env, silent: true}

     @on "task:log", -> console.log(arguments...)
     @on "task:warn", -> console.warn(arguments...)
@@ -100,6 +100,11 @@
     @childProcess.removeAllListeners()
     @childProcess.on 'message', ({event, args}) =>
       @emit(event, args...) if @childProcess?
+    # Catch the errors that happened before task-bootstrap.
+    @childProcess.stdout.on 'data', (data) ->
+      console.log data.toString()
+    @childProcess.stderr.on 'data', (data) ->
+      console.error data.toString()

   # Public: Starts the task.
   #


Reconnect

Reconnect

--- src/quip.coffee
+++ src/quip.coffee
@@ -64,11 +64,7 @@
         if not @connected then return
         if Date.now() - @lastBeatSeen > 30000
           @robot.logger.error "Heartbeat too old at %ds", (Date.now() - @lastBeatSeen) / 1000
-          @ws.close()
-          @connected = false
-          setTimeout =>
-            @connect()
-          , 5000
+          @reconnect()
         @ws.send JSON.stringify({"type": "heartbeat"})
         @robot.logger.info "Sent heartbeat"
       , 5000
@@ -77,22 +73,25 @@
       @websocketMessage JSON.parse(data)
     @ws.on "error", (error) =>
       @robot.logger.error error
-      @ws.close()
-      @connected = false
-      setTimeout =>
-        @connect()
-      , 5000
-      @connect()
+      @reconnect()
     @ws.on "close", =>
       @robot.logger.info "Closed"
-      @connected = false
+      @reconnect()
     @ws.on "ping", (data, flags) =>
       @ws.pong
+
+  reconnect: ->
+    @ws.close()
+    @connected = false
+    setTimeout =>
+      @connect()
+    , 5000

   websocketMessage: (packet) ->
     switch packet.type
       when "heartbeat"
         @robot.logger.info "Got heartbeat"
+        @lastBeatSeen = Date.now()
       when "message"
         user = @robot.brain.userForId packet.user.id, name: packet.user.name, room: packet.thread.id
         message = new TextMessage user, packet.message.text, packet.message.id


Field change

Field change

--- src/quip.coffee
+++ src/quip.coffee
@@ -128,7 +128,7 @@
     @lastMessageSeen = Date.now()
     switch packet.type
       when "error"
-        @robot.logger.error packet.message
+        @robot.logger.error packet.debug
       when "message"
         if -1 != @selfMention.indexOf packet.user.id
           return


remove expectation on filters, this is too much detail

--- jax-core/spec/javascripts/jax/texture_spec.js.coffee
+++ jax-core/spec/javascripts/jax/texture_spec.js.coffee
@@ -38,8 +38,6 @@

   describe 'by default', ->
     it 'should set defaults to prefer power-of-two texture sizes', ->
-      expect(@tex.get 'min_filter').toEqual GL_NEAREST
-      expect(@tex.get 'mag_filter').toEqual GL_NEAREST
       expect(@tex.get 'wrap_s').toEqual GL_REPEAT
       expect(@tex.get 'wrap_t').toEqual GL_REPEAT
       expect(@tex.get 'generate_mipmap').toEqual true


Ignore SHY in clerk login code.

Ignore SHY in clerk login code.

Mainly issue when copypasting from people management.

--- kirppu/static_src/js/checkout/clerkloginmode.coffee
+++ kirppu/static_src/js/checkout/clerkloginmode.coffee
@@ -15,6 +15,7 @@
   actions: -> [[
     "",
     (code) =>
+      code = code.replace("\u00AD", "")
       Api.clerk_login(
         code: code
         counter: @cfg.settings.counterCode


tests for * and #

tests for * and #

--- test/ace/search.coffee
+++ test/ace/search.coffee
@@ -11,3 +11,23 @@
   deepEqual @adaptor.position(), [0, 31]

   window.prompt = realPrompt
+
+test '*', ->
+  @adaptor.moveTo 0, 14
+  @press '*'
+  deepEqual @adaptor.position(), [1, 28]
+
+  @press 'n'
+  deepEqual @adaptor.position(), [6, 10]
+  @press 'N'
+  deepEqual @adaptor.position(), [1, 28]
+
+test '#', ->
+  @adaptor.moveTo 1, 4
+  @press '#'
+  deepEqual @adaptor.position(), [8, 4]
+
+  @press 'n'
+  deepEqual @adaptor.position(), [2, 4]
+  @press 'N'
+  deepEqual @adaptor.position(), [8, 4]


updating tests

updating tests

--- test/giphy.spec.coffee
+++ test/giphy.spec.coffee
@@ -4,8 +4,6 @@

 should = chai.should()
 chai.use require 'sinon-chai'
-
-exampleImageUri = 'http://giphy.com/example.gif'

 # this allows us to instrument the internal Giphy instance
 global.EXPOSE_INSTANCE = true
@@ -32,6 +30,10 @@
     }

     @giphy = giphy @robot
+    sinon.stub @giphy.api, '_request', (options, callback) -> callback null, null
+
+  afterEach ->
+    @giphy.api._request.restore()

   describe 'test instrumentation', ->
     it 'has a valid class instance', ->
@@ -325,12 +327,102 @@
         state.options.should.eql { test1: 1, test3: 'test3' }

     describe '.getSearchUri', ->
+      it 'searches using args', ->
+        state = { args: 'testing' }
+        sinon.stub @giphy.api, 'search'
+        @giphy.getSearchUri state
+        @giphy.api.search.should.have.been.called.once
+        @giphy.api.search.should.have.been.calledWith { q: 'testing' }, sinon.match.func
+        @giphy.api.search.restore()
+
+      it 'searches using args and options', ->
+        state = { args: 'testing', options: { limit: 10 } }
+        sinon.stub @giphy.api, 'search'
+        @giphy.getSearchUri state
+        @giphy.api.search.should.have.been.called.once
+        @giphy.api.search.should.have.been.calledWith { q: 'testing', limit: 10 }, sinon.match.func
+        @giphy.api.search.restore()
+
+      it 'handles the callback response', ->
+        state = { msg: 'msg', args: 'testing' }
+        sinon.stub @giphy.api, 'search', (options, callback) -> callback null, 'response'
+        sinon.stub @giphy, 'sendResponse'
+        @giphy.getSearchUri state
+        @giphy.sendResponse.should.have.been.called.once
+        @giphy.sendResponse.should.have.been.calledWith state
+        @giphy.api.search.restore()
+        @giphy.sendResponse.restore()
+        should.exist state.uri
+
+      it 'handles errors in the callback', ->
+        state = { msg: 'msg', args: 'testing' }
+        sinon.stub @giphy.api, 'search', (options, callback) -> callback 'error'
+        sinon.stub @giphy, 'error'
+        @giphy.getSearchUri state
+        @giphy.error.should.have.been.called.once
+        @giphy.error.should.have.been.calledWith 'msg', 'error'
+        @giphy.api.search.restore()
+        @giphy.error.restore()
+
     describe '.getIdUri', ->
     describe '.getTranslateUri', ->
     describe '.getRandomUri', ->
     describe '.getTrendingUri', ->
     describe '.getHelp', ->
     describe '.getUri', ->
+      it 'handles a null endpoint', ->
+        sinon.stub @giphy, 'error'
+        @giphy.getUri {}
+        @giphy.error.should.have.been.called.once
+        @giphy.error.restore()
+
+      it 'handles a search endpoint', ->
+        state = { endpoint: @giphy.constructor.SearchEndpointName }
+        sinon.stub @giphy, 'getSearchUri'
+        @giphy.getUri state
+        @giphy.getSearchUri.should.have.been.called.once
+        @giphy.getSearchUri.should.have.been.calledWith state
+        @giphy.getSearchUri.restore()
+
+      it 'handles an id endpoint', ->
+        state = { endpoint: @giphy.constructor.IdEndpointName }
+        sinon.stub @giphy, 'getIdUri'
+        @giphy.getUri state
+        @giphy.getIdUri.should.have.been.called.once
+        @giphy.getIdUri.should.have.been.calledWith state
+        @giphy.getIdUri.restore()
+
+      it 'handles a translate endpoint', ->
+        state = { endpoint: @giphy.constructor.TranslateEndpointName }
+        sinon.stub @giphy, 'getTranslateUri'
+        @giphy.getUri state
+        @giphy.getTranslateUri.should.have.been.called.once
+        @giphy.getTranslateUri.should.have.been.calledWith state
+        @giphy.getTranslateUri.restore()
+
+      it 'handles a random endpoint', ->
+        state = { endpoint: @giphy.constructor.RandomEndpointName }
+        sinon.stub @giphy, 'getRandomUri'
+        @giphy.getUri state
+        @giphy.getRandomUri.should.have.been.called.once
+        @giphy.getRandomUri.should.have.been.calledWith state
+        @giphy.getRandomUri.restore()
+
+      it 'handles a trending endpoint', ->
+        state = { endpoint: @giphy.constructor.TrendingEndpointName }
+        sinon.stub @giphy, 'getTrendingUri'
+        @giphy.getUri state
+        @giphy.getTrendingUri.should.have.been.called.once
+        @giphy.getTrendingUri.should.have.been.calledWith state
+        @giphy.getTrendingUri.restore()
+
+      it 'handles help', ->
+        state = { endpoint: @giphy.constructor.HelpName }
+        sinon.stub @giphy, 'getHelp'
+        @giphy.getUri state
+        @giphy.getHelp.should.have.been.called.once
+        @giphy.getHelp.should.have.been.calledWith state
+        @giphy.getHelp.restore()

     describe '.sendResponse', ->
       beforeEach ->
@@ -402,10 +494,3 @@
         @giphy.respond { match: [ null, null ] }
         @giphy.getUri.should.not.have.been.called
         @giphy.error.should.have.callCount 5
-
-  describe 'api', ->
-    beforeEach ->
-      sinon.stub @giphy.api, '_request', (options, callback) -> callback null, exampleImageUri
-
-    afterEach ->
-      @giphy.api._request.restore()


use opts object instead of non-rememberable arguments

--- src/slug.coffee
+++ src/slug.coffee
@@ -73,11 +73,14 @@
 }

-module.exports = slug = (string, replacement = '-', forceLowercase = false) ->
-    if typeof replacement is 'boolean'
-        replacement = '-'
-        forceLowercase = replacement
+module.exports = slug = (string, opts = {}) ->
+    if 'string' is typeof opts
+        opts = replacement:opts
+    else if 'boolean' is typeof opts
+        opts = lowerCase:opts
+    opts.replacement ?= '-'
     result = ""
+    string = string.toLowerCase() if opts.lowerCase
     for char, i in string
         code = string.charCodeAt(i)
         if char_map[char]
@@ -89,10 +92,8 @@
             char = char.replace(word, '') for word in removelist
             char = char.replace(/^\s+|\s+$/g, '')
         char = char.replace(/[^\w\s$\*\_\+~\.\(\)\!\-:@]/g, '') # allowed
-        if forceLowercase
-            char = char.toLowerCase()
         result += char
     result = result.replace(/^\s+|\s+$/g, '') # trim leading/trailing spaces
-    result = result.replace(/[-\s]+/g, replacement) # convert spaces
-    result.replace("#{replacement}$", '') # remove trailing separator
+    result = result.replace(/[-\s]+/g, opts.replacement) # convert spaces
+    result.replace("#{opts.replacement}$", '') # remove trailing separator


Increasing timeout.

Increasing timeout.

--- tests.coffee
+++ tests.coffee
@@ -1,4 +1,4 @@
-WAIT_FOR_DATABASE_TIMEOUT = 1500 # ms
+WAIT_FOR_DATABASE_TIMEOUT = 2000 # ms

 # The order of documents here tests delayed definitions


Added a failing test case.

Added a failing test case.

--- tests.coffee
+++ tests.coffee
@@ -26,21 +26,35 @@
   Meteor.publish null, ->
     Users.find()

-  Meteor.publish 'users-posts', ->
+  Meteor.publish 'posts', (ids) ->
+     Posts.find
+      _id:
+        $in: ids
+
+  Meteor.publish 'users-posts', (userId) ->
     @related (user) ->
-      Posts.find
+      Posts.find(
         _id:
           $in: user?.posts or []
+      ).observeChanges
+        added: (id, fields) =>
+          fields.dummyField = true
+          @added 'Posts_meteor_related_tests', id, fields
+        changed: (id, fields) =>
+          @changed 'Posts_meteor_related_tests', id, fields
+        removed: (id) =>
+          @removed 'Posts_meteor_related_tests', id
+
+      @ready()
     ,
-      Users.find {},
+      Users.find userId,
         fields:
           posts: 1

-  Meteor.publish 'users-posts-count', ->
+  Meteor.publish 'users-posts-count', (userId, countId) ->
     @related (user) ->
       count = 0
       initializing = true
-      countId = Random.id()

       Posts.find(
         _id:
@@ -60,7 +74,7 @@

       @ready()
     ,
-      Users.find {},
+      Users.find userId,
         fields:
           posts: 1

@@ -69,12 +83,15 @@

   testAsyncMulti 'related - basic', [
     (test, expect) ->
-      Meteor.subscribe 'users-posts', expect()
-      Meteor.subscribe 'users-posts-count', expect()
-  ,
-    (test, expect) ->
-      test.equal Posts.find().fetch(), []
-      test.equal Counts.findOne()?.count, 0
+      @userId = Random.id()
+      @countId = Random.id()
+
+      Meteor.subscribe 'users-posts', @userId, expect()
+      Meteor.subscribe 'users-posts-count', @userId, @countId, expect()
+  ,
+    (test, expect) ->
+      test.equal Posts.find().fetch(), []
+      test.equal Counts.findOne(@countId)?.count, 0

       @posts = []

@@ -86,19 +103,22 @@
   ,
     (test, expect) ->
       test.equal Posts.find().fetch(), []
-      test.equal Counts.findOne()?.count, 0
+      test.equal Counts.findOne(@countId)?.count, 0

       Users.insert
+        _id: @userId
         posts: @posts
       ,
         expect (error, userId) =>
           test.isFalse error, error?.toString?() or error
           test.isTrue userId
-          @userId = userId
-  ,
-    (test, expect) ->
-      testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), @posts
-      test.equal Counts.findOne()?.count, @posts.length
+          test.equal userId, @userId
+  ,
+    (test, expect) ->
+      Posts.find().forEach (post) ->
+        test.isTrue post.dummyField, true
+      testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), @posts
+      test.equal Counts.findOne(@countId)?.count, @posts.length

       @shortPosts = @posts[0...5]

@@ -110,8 +130,10 @@
           test.equal count, 1
   ,
     (test, expect) ->
+      Posts.find().forEach (post) ->
+        test.isTrue post.dummyField, true
       testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), @shortPosts
-      test.equal Counts.findOne()?.count, @shortPosts.length
+      test.equal Counts.findOne(@countId)?.count, @shortPosts.length

       Users.update @userId,
         posts: []
@@ -122,7 +144,7 @@
   ,
     (test, expect) ->
       testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), []
-      test.equal Counts.findOne()?.count, 0
+      test.equal Counts.findOne(@countId)?.count, 0

       Users.update @userId,
         posts: @posts
@@ -132,16 +154,20 @@
           test.equal count, 1
   ,
     (test, expect) ->
-      testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), @posts
-      test.equal Counts.findOne()?.count, @posts.length
+      Posts.find().forEach (post) ->
+        test.isTrue post.dummyField, true
+      testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), @posts
+      test.equal Counts.findOne(@countId)?.count, @posts.length

       Posts.remove @posts[0], expect (error, count) =>
           test.isFalse error, error?.toString?() or error
           test.equal count, 1
   ,
     (test, expect) ->
+      Posts.find().forEach (post) ->
+        test.isTrue post.dummyField, true
       testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), @posts[1..]
-      test.equal Counts.findOne()?.count, @posts.length - 1
+      test.equal Counts.findOne(@countId)?.count, @posts.length - 1

       Users.remove @userId,
         expect (error) =>
@@ -149,5 +175,56 @@
   ,
     (test, expect) ->
       testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), []
-      test.equal Counts.findOne()?.count, 0
+      test.equal Counts.findOne(@countId)?.count, 0
   ]
+
+  testAsyncMulti 'related - unsubscribing', [
+    (test, expect) ->
+      @userId = Random.id()
+      @countId = Random.id()
+
+      @usersPostsSubscribe = Meteor.subscribe 'users-posts', @userId, expect()
+      @usersPostsCount = Meteor.subscribe 'users-posts-count', @userId, @countId, expect()
+  ,
+    (test, expect) ->
+      test.equal Posts.find().fetch(), []
+      test.equal Counts.findOne(@countId)?.count, 0
+
+      @posts = []
+
+      for i in [0...10]
+        Posts.insert {}, expect (error, id) =>
+          test.isFalse error, error?.toString?() or error
+          test.isTrue id
+          @posts.push id
+  ,
+    (test, expect) ->
+      test.equal Posts.find().fetch(), []
+      test.equal Counts.findOne(@countId)?.count, 0
+
+      Users.insert
+        _id: @userId
+        posts: @posts
+      ,
+        expect (error, userId) =>
+          test.isFalse error, error?.toString?() or error
+          test.isTrue userId
+          test.equal userId, @userId
+  ,
+    (test, expect) ->
+      Posts.find().forEach (post) ->
+        test.isTrue post.dummyField, true
+      testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), @posts
+      test.equal Counts.findOne(@countId)?.count, @posts.length
+
+      Meteor.subscribe 'posts', @posts, expect()
+      @usersPostsSubscribe.stop()
+  ,
+    (test, expect) ->
+      # After unsubscribing from the related-based publish which added dummyField,
+      # dummyField should be removed from documents available on the client side
+      Posts.find().forEach (post) ->
+        test.isUndefined post.dummyField
+      testSetEqual test, _.pluck(Posts.find().fetch(), '_id'), @posts
+
+  ]


More tests for abstract documents.

More tests for abstract documents.

--- tests.coffee
+++ tests.coffee
@@ -1413,6 +1413,19 @@
   # Should not try to define invalid document again
   Document.defineAll()

+Tinytest.add 'meteor-peerdb - abstract with parent', (test) ->
+  test.throws ->
+    class BadPost4 extends Post
+      @Meta
+        abstract: true
+  , /Abstract document with a parent/
+
+  # Invalid document should not be added to the list
+  testDocumentList test, ALL
+
+  # Should not try to define invalid document again
+  Document.defineAll()
+
 testAsyncMulti 'meteor-peerdb - circular changes', [
   (test, expect) ->
     Log._intercept 3 if Meteor.isServer # Three to see if we catch more than expected
@@ -2146,9 +2159,9 @@

 testAsyncMulti 'meteor-peerdb - delayed defintion', [
   (test, expect) ->
-    class BadPost4 extends Document
+    class BadPost5 extends Document
       @Meta
-        name: 'BadPost4'
+        name: 'BadPost5'
         fields: =>
           author: @ReferenceField undefined, ['username']

@@ -2165,11 +2178,11 @@

     # Let's find it
     for i in intercepted
-      break if i.indexOf('BadPost4') isnt -1
+      break if i.indexOf('BadPost5') isnt -1
     test.isTrue _.isString(i), i
     intercepted = EJSON.parse i

-    test.equal intercepted.message.lastIndexOf("Not all delayed document definitions were successfully retried:\nBadPost4 from"), 0, intercepted.message
+    test.equal intercepted.message.lastIndexOf("Not all delayed document definitions were successfully retried:\nBadPost5 from"), 0, intercepted.message
     test.equal intercepted.level, 'error'

     testDocumentList test, ALL


:penguin: Add keybindings for select-to-top/bottom

ctrl-shift-home/end are now mapped

Closes #2251

--- keymaps/linux.cson
+++ keymaps/linux.cson
@@ -69,6 +69,8 @@
   'ctrl-delete': 'editor:delete-to-end-of-word'
   'ctrl-home': 'core:move-to-top'
   'ctrl-end': 'core:move-to-bottom'
+  'cmd-shift-home': 'core:select-to-top'
+  'cmd-shift-end': 'core:select-to-bottom'

   # Sublime Parity
   'ctrl-a': 'core:select-all'


add new key bindings

add new key bindings

--- keymaps/term2.cson
+++ keymaps/term2.cson
@@ -12,3 +12,8 @@
   'ctrl-alt-left': 'term2:open-split-left'
   'ctrl-alt-up': 'term2:open-split-up'
   'ctrl-alt-down': 'term2:open-split-down'
+
+  'cmd-k t right': 'term2:open-split-right'
+  'cmd-k t left': 'term2:open-split-left'
+  'cmd-k t up': 'term2:open-split-up'
+  'cmd-k t down': 'term2:open-split-down'


Don't use Emitter::off in Task

Don't use Emitter::off in Task

Signed-off-by: Nathan Sobo <2e8aa918660411855c6d44d5bb2da677aa033255@github.com>

--- src/task.coffee
+++ src/task.coffee
@@ -111,7 +111,7 @@
   handleEvents: ->
     @childProcess.removeAllListeners()
     @childProcess.on 'message', ({event, args}) =>
-      @emit(event, args...)
+      @emit(event, args...) if @childProcess?

   # Public: Starts the task.
   #
@@ -154,5 +154,4 @@
     @childProcess.kill()
     @childProcess = null

-    @off()
     undefined


Don't try to report that a request was sent when it wasn't

--- src/teak.coffee
+++ src/teak.coffee
@@ -229,10 +229,11 @@
     if carrotResponse.code == 200
         postMethod(carrotResponse.fb_data,
           (fbResponse) =>
-            @ajaxPost("#{@scheme}://posts.gocarrot.com/requests/#{carrotResponse.request_id}/ids", {platform_id: fbResponse.request})
-            if fbResponse && fbResponse.to
-              for receivingUser in fbResponse.to
-                @ajaxPost("#{@scheme}://parsnip.gocarrot.com/request_send", {platform_id: carrotResponse.request_id, posting_user_id: @udid, user_id: receivingUser})
+            if(fbResponse && fbResponse.request)
+              @ajaxPost("#{@scheme}://posts.gocarrot.com/requests/#{carrotResponse.request_id}/ids", {platform_id: fbResponse.request})
+              if fbResponse && fbResponse.to
+                for receivingUser in fbResponse.to
+                  @ajaxPost("#{@scheme}://parsnip.gocarrot.com/request_send", {platform_id: carrotResponse.request_id, posting_user_id: @udid, user_id: receivingUser})

             @uiCallbackHandler(callback, carrotResponse, fbResponse)
         )


simplify url report response

simplify url report response

--- src/tell.coffee
+++ src/tell.coffee
@@ -42,6 +42,6 @@
         unclean.push "`#{scanner}` (#{obj.result})" if obj.detected
       unclean.push "none" unless unclean.length > 0

-      return robot.send {room: msg.envelope.user.name}, "Virus total: `#{res.resource.replace('http:\/\/','')}` rated clean by #{clean.join ', '}.  Rated unclean by #{unclean.join ', '}."
+      return robot.send {room: msg.envelope.user.name}, "Virus total: `#{res.resource.replace('http:\/\/','')}` rated clean by #{clean.length}.  Rated unclean by #{unclean.length}.  More information at #{res.permalink}"

     return


Refs #68; Real-time plots now respect changes to tickFormats option.

--- src/time.coffee
+++ src/time.coffee
@@ -35,6 +35,11 @@
     'option:ticks.right': 'ticksChanged'
     'option:ticks.bottom': 'ticksChanged'
     'option:ticks.left': 'ticksChanged'
+    'option:tickFormats': 'tickFormatsChanged'
+    'option:tickFormats.top': 'tickFormatsChanged'
+    'option:tickFormats.right': 'tickFormatsChanged'
+    'option:tickFormats.bottom': 'tickFormatsChanged'
+    'option:tickFormats.left': 'tickFormatsChanged'

   # Creates a new real-time plot.
   #
@@ -505,6 +510,12 @@
     @_transitionRangeAxes()
     @draw(@animation.frame * @animation.delta())

+  # Updates tick formats in response to an option.tickFormats.* event.
+  tickFormatsChanged: ->
+    @_resetInitialTimeTicks()
+    @_transitionRangeAxes()
+    @draw(@animation.frame * @animation.delta())
+

 # Base class for all "stacked" plot types (e.g. bar charts, area charts, etc.)
 # @abstract It does not perform rendering but instead formats the data


how to access objects in coffee?

how to access objects in coffee?

i need coffee

--- app/assets/javascripts/components/articles.js.coffee
+++ app/assets/javascripts/components/articles.js.coffee
@@ -8,7 +8,7 @@
 		articles.push article
 		@setState articles: articles
 	doOrderFilta: (orderFilta) ->
-		console.log orderFilta
+		console.log orderFilta.order
 	render: ->
 		React.DOM.div
 			className: 'articles'


fixed scroll height for master-overlay-parent

--- app/assets/javascripts/modules/master_overlay.coffee
+++ app/assets/javascripts/modules/master_overlay.coffee
@@ -73,6 +73,9 @@
       secondary_content = @root.find('.master-overlay-secondary-content').find(".master-overlay-content")
       secondary_content.height(secondary_content.parents('.main-content').height() - $(secondary_content).offset().top - 90) if secondary_content.offset()

+      parent_content = @root.find('.master-overlay-parent').find(".master-overlay-content")
+      parent_content.height(parent_content.parents('.main-content').height() - $(parent_content).offset().top - 40) if parent_content && parent_content.offset()
+
   $document = $(document)

   # Hide the project list after the back animation completes


added mechanism of nml file uploading to modal #65

--- app/assets/javascripts/oxalis/view/mergemodal.coffee
+++ app/assets/javascripts/oxalis/view/mergemodal.coffee
@@ -11,6 +11,7 @@
 admin/models/tasktype/task_type_collection : TaskTypeCollection
 admin/models/project/project_collection : ProjectCollection
 admin/models/project/project_model : ProjectModel
+routes : MyJSRoutesModule
 ###

 class MergeModalView extends Backbone.Marionette.LayoutView
@@ -54,7 +55,19 @@
             NML

-                NML
+                <form action="<%= jsRoutes.controllers.admin.NMLIO.upload().url %>"
+                    method="POST"
+                    enctype="multipart/form-data"
+                    id="upload-and-explore-form"
+                    class="form-inline inline-block">
+
+                    <input type="file" name="nmlFile" multiple>
+
+
+                      Upload NML
+                    </input>
+
+                </form>

                 <button class="btn btn-primary" id="nml-merge">Merge</button>
@@ -86,6 +99,7 @@
     "click #task-type-merge"   : "mergeTaskType"
     "click #project-merge"     : "mergeProject"
     "click #nml-merge"         : "mergeNml"
+    "submit @ui.uploadAndExploreForm" : "uploadFiles"
     "click #explorativs-merge" : "mergeExplorativs"

   ui :
@@ -93,7 +107,7 @@
     "tasktype"    : ".task-type"
     "project"     : ".project"
     "explorativs" : ".explorativs"
-
+    "uploadAndExploreForm" : "#upload-and-explore-form"

   initialize : (options) ->


show alert when there is an error uploading/deleting froala image

--- app/assets/javascripts/rails_admin/ra.widgets.coffee
+++ app/assets/javascripts/rails_admin/ra.widgets.coffee
@@ -239,6 +239,7 @@
         $(@).editable(config_options)
         if uploadEnabled
           $(@).on 'editable.imageError', (e, editor, error) ->
+            alert("error uploading image: " + error.message);
             # Custom error message returned from the server.
             if error.code == 0

@@ -279,6 +280,7 @@
             # handle success
           ).on 'editable.imageDeleteError', (e, editor, error) ->
             # handle error
+            alert("error deleting image: " + error.message);

     array = content.find('[data-richtext=froala-wysiwyg]').not('.froala-wysiwyged')
     if array.length


Re-add deprecated browser.getURI method, for now.

--- application/chrome/content/wesabe/dom/Browser.coffee
+++ application/chrome/content/wesabe/dom/Browser.coffee
@@ -88,7 +88,11 @@
 # with the transition, this section adds back wesabe.dom.browser.go and
 # friends but yells at you that you're using a deprecated method.
 #
-deprecated = {}
+deprecated =
+  getURI: (browser) ->
+    logger.deprecated "wesabe.dom.browser.getURI(browser)", "browser.url"
+    browser.url
+
 for name in ['go', 'joinURI', 'atURI']
   do (name) ->
     deprecated[name] = (browser, args...) ->


Watch all coffee files for running test

Watch all coffee files for running test

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -25,5 +25,4 @@
 		.pipe(coffeelint.reporter())

 gulp.task 'watch', [ 'test', 'lint' ], ->
-	gulp.watch(OPTIONS.files.tests, [ 'test' ])
-	gulp.watch(OPTIONS.files.coffee, [ 'lint' ])
+	gulp.watch(OPTIONS.files.coffee, [ 'test', 'lint' ])


Add command to build stack-ide

Add command to build stack-ide

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -25,6 +25,10 @@
   'cabal build'
 ])

+gulp.task 'build-stack-ide', shell.task([
+  'stack install stack-ide stack-ide-api ../ide-backend/ide-backend ../ide-backend/ide-backend-server ../ide-backend/ide-backend-common'
+], {cwd: 'stack-ide'})
+
 hspecLoc = 'dist/build/hspec/hspec'

 gulp.task 'test', ['build-haskell'], haskellCmds([


remove gulp-protractor. using protractor directly.

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -30,7 +30,6 @@
 path                  = require 'path'
 plato                 = require 'gulp-plato'
 pkg                   = require './package.json'
-protractor            = require 'gulp-protractor'
 proxy                 = require 'proxy-middleware'
 q                     = require 'q'
 rev                   = require 'gulp-rev'
@@ -521,41 +520,6 @@
 	protractorDir = path.resolve(path.join(path.dirname(pkgPath), '..', 'bin'))
 	path.join protractorDir, '/' + binaryName + winExt

-# Execute E2E tests
-gulp.task 'e2e', ->
-	e2eConfigFile       = path.join './', TEMP_DIRECTORY, 'e2e-config.coffee'
-	phantomjsBinaryPath = windowsify './node_modules/.bin/phantomjs.cmd', './node_modules/phantomjs/bin/phantomjs'
-	sources             = '**/*.spec.{coffee,js}'
-
-	# create temporary e2e-config file to avoid an additional config file
-	# currently gulp-protractor requires one the existence of an e2e-config file
-	do (e2eConfigFile) ->
-		doesExist = fs.existsSync TEMP_DIRECTORY
-
-		if !doesExist
-			throw new Error 'The app must be currently running (gulp).'
-
-		contents = 'exports.config = {}'
-
-		fs.writeFileSync e2eConfigFile, contents
-
-	options =
-		protractor:
-			configFile: e2eConfigFile
-			args: [
-				'--baseUrl', appUrl
-				'--browser', 'phantomjs'
-				'--capabilities.phantomjs.binary.path', phantomjsBinaryPath
-			]
-
-	gulp
-		.src sources, {cwd: E2E_DIRECTORY, read: false, nodir: true}
-		.on 'error', onError
-
-		.pipe protractor.protractor options.protractor
-		.on 'error', onError
-
-
 # Update E2E driver
 gulp.task 'e2e-driver-update', (done) ->
 	childProcess.spawn(getProtractorBinary('webdriver-manager'), [ 'update' ], stdio: 'inherit').once 'close', done


Fix Wercker weirdness

Fix Wercker weirdness

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -90,29 +90,38 @@

 gulp.task "version", (callback) ->

-	async.series [
-		(cb) -> command("git rev-parse --abbrev-ref HEAD", cb) # branch
-		(cb) -> command("git describe --always --dirty", cb) # hash
-		(cb) -> command("git rev-list --count HEAD", cb) # build
-	], (err, results) ->
+	# This is needed for a complete build number
+	command "git fetch --unshallow", (err) ->

-		info =
-			branch: results[0]
-			hash: results[1]
-			build: results[2]
-			date: Math.floor(Date.now() / 1000)
+		async.series [
+			(cb) -> command("git rev-parse --abbrev-ref HEAD", cb) # branch
+			(cb) -> command("git describe --always --dirty", cb) # hash
+			(cb) -> command("git rev-list --count HEAD", cb) # build
+		], (err, results) ->

-		gutil.log "Building ", gutil.colors.green("#{info.branch}/#{info.hash} @#{info.build}")
-
-		task = gulp.src("framer/Version.coffee.template")
-			.pipe(template(info))
-			.pipe(rename({
-				basename: "Version",
-				extname: ".coffee"
-			}))
-			.pipe(gulp.dest("build"))
+			info =
+				branch: results[0]
+				hash: results[1]
+				build: results[2]
+				date: Math.floor(Date.now() / 1000)

-		callback(null, task)
+			# If we are on the wercker platform, we need to get the branch
+			# name from the env variables and remove the dirty vrom version.
+			if process.env.WERCKER_GIT_BRANCH
+				info.branch = process.env.WERCKER_GIT_BRANCH
+				info.hash = info.hash.replace("-dirty", "")
+
+			gutil.log "Building ", gutil.colors.green("#{info.branch}/#{info.hash} @#{info.build}")
+
+			task = gulp.src("framer/Version.coffee.template")
+				.pipe(template(info))
+				.pipe(rename({
+					basename: "Version",
+					extname: ".coffee"
+				}))
+				.pipe(gulp.dest("build"))
+
+			callback(null, task)

 gulp.task "build:coverage", ->


Bump version to 0.15.1

Bump version to 0.15.1

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -20,7 +20,7 @@
     nw = new NwBuilder(
       files: './src/**'
       platforms: [platform]
-      version: '0.14.5-sdk'
+      version: '0.15.1'
       winIco: if process.argv.indexOf('--noicon') > 0 then undefined else './assets-windows/icon.ico'
       macIcns: './assets-osx/icon.icns'
       macZip: true


Update: gulpfile.coffee: index

Update: gulpfile.coffee: index

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -4,10 +4,10 @@
 $ = do require 'gulp-load-plugins'

 gulp.task 'coffee', ->
-	gulp.src './src/**/*.coffee'
-	.pipe $.coffeelint './coffeelint.json'
+    gulp.src './src/**/*.coffee'
+    .pipe $.coffeelint './coffeelint.json'
     .pipe $.coffeelint.reporter()
-	.pipe $.coffee({ bare: true }).on('error', $.util.log)
-  	.pipe gulp.dest './lib/'
+    .pipe $.coffee({ bare: true }).on('error', $.util.log)
+    .pipe gulp.dest './lib/'

 gulp.task 'default', ['coffee']


Upgrade gulp tasks

Upgrade gulp tasks

- filter slides folder on populate task
- filter out slides folder on copy dependencies task

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -11,7 +11,7 @@
   del(['./dist/**/*'])

 gulp.task 'populate-slides', ['clean'], ->
-  gulp.src mainBowerFiles(group: 'slides'), base: './bower_components'
+  gulp.src mainBowerFiles(/slides-/, group: 'slides'), base: './bower_components'
     .pipe gulp.dest('./dist')

 gulp.task 'copy-index-files', ['clean'], ->
@@ -29,14 +29,8 @@

 gulp.task 'copy-dependencies', ['clean'], ->
   gulp.src [
-    'bower_components/bootstrap/dist/**/*.{css,js}'
-    'bower_components/bootstrap/dist/fonts/**/*'
-    'bower_components/font-awesome/**/*.{css,js}'
-    'bower_components/font-awesome/fonts/**/*'
-    'bower_components/headjs/**/*.js'
-    'bower_components/jquery/dist/**/*.js'
-    'bower_components/reveal.js/**/*.{css,js}'
-    'bower_components/reveal.js/lib/font/**/*'
+    'bower_components/**'
+    '!bower_components/slides-*/**/*'
   ], base: './'
     .pipe gulp.dest('./dist')


change gulp-coffee options

change gulp-coffee options

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -17,14 +17,14 @@

 gulp.task 'build', ->
   gulp.src './src/**/*.coffee'
-  .pipe coffee()
+  .pipe coffee(bare: true)
   .pipe uglify()
   .pipe gulp.dest './lib/'

 gulp.task 'build-dev', ->
   gulp.src './src/**/*.coffee'
   .pipe sourcemaps.init()
-  .pipe ignoreError coffee()
+  .pipe ignoreError coffee(bare: true)
   .pipe sourcemaps.write()
   .pipe gulp.dest './.tmp/src/'

@@ -71,4 +71,3 @@
       'test-dev'
       ->
     ]
-


parse coffee globally for generate browser version

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -35,7 +35,7 @@
 gulp.task 'browserify', ->
   browserify
       extensions: ['.coffee', '.js']
-  .transform coffeeify
+  .transform coffeeify, global: true
   .require(src.main, { expose: module.shortcut })
   .ignore('coffee-script')
   .bundle()


Work around gulp not handling symlinks

Work around gulp not handling symlinks

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -102,10 +102,10 @@
       frameworks.forEach fixFramework
   gulp.task "build:#{id}", ["clean:build", "fix:#{id}"], ->
     if platform is 'darwin'
-      gulp.src "./dist/#{id}/**/*", base: "./dist/#{id}"
-      .pipe tar "Relief-Valve-v#{packageInfo.version}-#{id}.tar"
-      .pipe gzip()
-      .pipe gulp.dest "./build/"
+      child.execSync "mkdir -p ../../build", cwd: "./dist/#{id}"
+      child.execSync "tar czf
+        ../../build/Relief-Valve-v#{packageInfo.version}-#{id}.tar.gz
+        *", cwd: "./dist/#{id}"
     else
       gulp.src "./dist/#{id}/**/*", base: "./dist/#{id}"
       .pipe zip "Relief-Valve-v#{packageInfo.version}-#{id}.zip"


Bump rounding to 100, hide note for segments

--- app/assets/javascripts/mix.js.coffee
+++ app/assets/javascripts/mix.js.coffee
@@ -9,9 +9,13 @@
 editAnnotation = (region) ->
   form = document.forms.edit
   form.classList.remove 'hide'
-  form.elements.start.value = round10(region.start)
-  form.elements.end.value = round10(region.end)
+  form.elements.start.value = round100(region.start)
+  form.elements.end.value = round100(region.end)
   form.elements.note.value = region.data.note or ''
+  if region.data.type == 'segment'
+    form.elements.note.setAttribute('disabled', 'disabled')
+  else
+    form.elements.note.removeAttribute('disabled')
   form.elements.type.value = region.data.type or ''

   form.onsubmit = (e) ->
@@ -39,8 +43,8 @@
   time = wavesurfer.getCurrentTime()
   # Assume that time is always within a region, since we initialize a region over full audio at start
   splitCandidate = getRegions(time)[0]
-  wavesurfer.addRegion { start: splitCandidate.start, end: round10(time), drag: false, resize: false, data: { type: 'segment' } }
-  wavesurfer.addRegion { start: round10(time), end: splitCandidate.end, drag: false, resize: false, data: { type: 'segment' } }
+  wavesurfer.addRegion { start: splitCandidate.start, end: round100(time), drag: false, resize: false, data: { type: 'segment' } }
+  wavesurfer.addRegion { start: round100(time), end: splitCandidate.end, drag: false, resize: false, data: { type: 'segment' } }
   wavesurfer.regions.list[splitCandidate.region_id].remove()
   saveRegions()

@@ -55,13 +59,14 @@
   form = document.forms.edit
   regionId = form.dataset.region
   deleteCandidate = wavesurfer.regions.list[regionId]
-  if deleteCandidate.start == 0
-    # set second's start as 0
-    wavesurfer.regions.list[regionsArray()[1].region_id].update(start: 0)
-  else
-    # set prev's end as this end
-    replacer = (region for region_id, region of wavesurfer.regions.list when round10(region.end) == round10(deleteCandidate.start))[0]
-    wavesurfer.regions.list[replacer.id].update(end: deleteCandidate.end)
+  if deleteCandidate.data.type == 'segment'
+    if deleteCandidate.start == 0
+      # set second's start as 0
+      wavesurfer.regions.list[regionsArray()[1].region_id].update(start: 0)
+    else
+      # set prev's end as this end
+      replacer = (region for region_id, region of wavesurfer.regions.list when round100(region.end) == round10(deleteCandidate.start))[0]
+      wavesurfer.regions.list[replacer.id].update(end: deleteCandidate.end)
   if regionId
     wavesurfer.regions.list[regionId].remove()
     form.reset()
@@ -69,7 +74,7 @@

 setMarker = ->
   time = wavesurfer.getCurrentTime()
-  wavesurfer.addRegion(start: time, drag: true, resize: false, data: { type: 'segment_item' }, color: 'rgba(0,0,0,0.5)')
+  wavesurfer.addRegion(start: round100(time), drag: true, resize: false, data: { type: 'segment_item' }, color: 'rgba(0,0,0,0.5)')
   saveRegions()

 window.regionsArray = ->
@@ -77,8 +82,8 @@
     region = wavesurfer.regions.list[id]
     {
       region_id: id
-      start: region.start
-      end: region.end
+      start: round100(region.start)
+      end: round100(region.end)
       data: region.data
       texts: []
       drag: (if region.data.type == 'segment_item' then true else false)
@@ -89,8 +94,8 @@
   mapped.sort (a,b)->
     a.start - b.start

-round10 = (input) ->
-  Math.round(input * 10)/10
+round100 = (input) ->
+  Math.round(input * 100)/100

 loadRegions = (regions) ->
   regions.forEach (region) ->
@@ -102,7 +107,8 @@
 showNote = (region) ->
   if !showNote.el
     showNote.el = $('#subtitle')
-  showNote.el.html region.data.note or '–'
+  if region.data.type == 'segment_item'
+    showNote.el.html region.data.note or '–'
   return

 window.wavesurfer = Object.create(WaveSurfer)
@@ -127,7 +133,7 @@
     if localStorage.regions
       loadRegions JSON.parse(localStorage.regions)
     else
-      wavesurfer.addRegion(start: 0, end: round10(wavesurfer.getDuration()), drag: false, resize: false, data: { type: 'segment' })
+      wavesurfer.addRegion(start: 0, end: round100(wavesurfer.getDuration()), drag: false, resize: false, data: { type: 'segment' })
       saveRegions()
     return


merging

merging

--- app/assets/javascripts/webrtc.coffee
+++ app/assets/javascripts/webrtc.coffee
@@ -60,7 +60,7 @@
     @pc_config = {"iceServers":
           [
               {url:'stun:stun.l.google.com:19302'},
-             # {url:'stun:137.135.63.9'},
+              {url:'stun:waturn.cloudapp.net'},
               {
                   url: 'turn:waturn.cloudapp.net:3478?transport=tcp',
                   credential: 'walkaboutpass',
@@ -227,13 +227,13 @@
     console.log('config',config)
     if (config.sendOffer)
       console.log("Creating RTC offer to " + peer_id)
-      offerConstraints = {
-        "optional": [
-          { "OfferToReceiveAudio": true },
-          { "OfferToReceiveVideo": true },
-        ]
-      }
-      constraints = @mergeConstraints(offerConstraints, sdpConstraints)
+      #offerConstraints = {
+      #  "optional": [
+      #    { "OfferToReceiveAudio": true },
+      #    { "OfferToReceiveVideo": true },
+      #  ]
+      #}
+      #constraints = @mergeConstraints(offerConstraints, sdpConstraints)

       peer_connection.createOffer((local_description) =>
         console.log("Local offer description is: ", local_description)
@@ -253,7 +253,8 @@
         )
       , (error) ->
         console.log("Error sending offer: "+error)
-      ,constraints)
+      ,sdpConstraints)
+      #,constraints)
     peer_connection

   mergeConstraints: (cons1, cons2) ->


categoties select fix

categoties select fix

--- app/assets/javascripts/wizard.coffee
+++ app/assets/javascripts/wizard.coffee
@@ -484,7 +484,7 @@
       step.view() if step.hasClass('step-edit')

     tags = step.find('input[type="checkbox"]:checked').toArray().map (e) ->
-      $(e).text()
+      $(e).parents('label:first').find('span').text()

     if tags.length > 0
       step.find('.tag-names:first').val(tags.join(', '))


Update text annotations after adding tags

--- app/classifier/tasks/text/index.cjsx
+++ app/classifier/tasks/text/index.cjsx
@@ -99,6 +99,7 @@
       textAfter = textAreaValue.substring(selectionEnd, textAreaValue.length)
       value = textBefore + startTag + textInBetween + endTag + textAfter
     @setState { value }
+    @debouncedUpdateAnnotation()

   updateHeight: ->
     @setState textareaHeight: @refs.textInput.scrollHeight + 2


Add name to default metadata

Add name to default metadata

--- src/view.coffee
+++ src/view.coffee
@@ -61,8 +61,8 @@
         callback(error)
       else if response.statusCode is 200
         @getLatestCompatibleVersion body, options, (version) ->
-          {readme, downloads, stargazers_count} = body
-          metadata = body.versions?[version] ? {}
+          {name, readme, downloads, stargazers_count} = body
+          metadata = body.versions?[version] ? {name}
           pack = _.extend({}, metadata, {readme, downloads, stargazers_count})
           callback(null, pack)
       else


- Somewhat improved handling of action parameters; now converts null to actual null and doesn't allow 'hello" or "hello'. - Actions don't get delegated if the view is disabled.

--- src/view.coffee
+++ src/view.coffee
@@ -126,24 +126,25 @@
 		@actions

 	delegateAction: (e, el = false) =>
+		return unless @enabled
+
 		actions = @actions[e.type]

-		if (!actions) then return
+		return unless actions

 		for action in actions
 			continue if el != false && action.el[0] != el
 			parameters = []

 			for parameter in action.parameters
-				string = parameter.replace(/['"]/g, "") # Could be improved 'hello" now works as well.
-				if string is parameter
+				if parameter is 'null'
+					parameters.push(null)
+				else if parameter.charAt(0).match(/['"]/g) and parameter.charAt(0) is parameter.charAt(parameter.length)
+					parameters.push(parameter.substring(1, parameter.length - 2))
+				else
 					parameters.push(@[parameter])
-				else
-					parameters.push(string)
-
-			parameters.push(@)
-			parameters.push(el)
-			parameters.push(e)
+
+			parameters.push(@, el, e)

 			@dispatch(action.function + 'Action', parameters)


Fixed action parameters

Fixed action parameters

--- src/view.coffee
+++ src/view.coffee
@@ -189,8 +189,8 @@
 			for parameter in action.parameters
 				if parameter is 'null'
 					parameters.push(null)
-				else if parameter.charAt(0).match(/['"]/g) and parameter.charAt(0) is parameter.charAt(parameter.length)
-					parameters.push(parameter.substring(1, parameter.length - 2))
+				else if parameter.charAt(0).match(/['"]/g) and parameter.charAt(0) is parameter.charAt(parameter.length-1)
+					parameters.push(parameter.substring(1, parameter.length - 1))
 				else
 					parameters.push(@[parameter])

@@ -410,7 +410,7 @@
 		if i is @views.content.length
 			view.appendTo(@el)
 		else
-			view.el.insertBefore(@listViews[i].el)
+			view.el.insertBefore(@views.content[i].el)
 		@views.content.splice(i, 0, view)

 		view


Prompt for login in /courses redeem code dialog

--- app/views/courses/CoursesView.coffee
+++ app/views/courses/CoursesView.coffee
@@ -1,4 +1,5 @@
 app = require 'core/application'
+AuthModal = require 'views/core/AuthModal'
 CocoCollection = require 'collections/CocoCollection'
 Course = require 'models/Course'
 CourseInstance = require 'models/CourseInstance'
@@ -79,6 +80,7 @@

   onClickEnroll: (e) ->
     $('.continue-dialog').modal('hide')
+    return @openModalView new AuthModal() if me.isAnonymous()
     courseID = $(e.target).data('course-id')
     prepaidCode = ($(".code-input[data-course-id=#{courseID}]").val() ? '').trim()
     @courseEnroll(prepaidCode)


Client: Activity: Added resetChannelParticipantsSelectedIndex action.

--- client/activity/lib/flux/chatinput/actions/user.coffee
+++ client/activity/lib/flux/chatinput/actions/user.coffee
@@ -118,6 +118,16 @@
   { UNSET_CHANNEL_PARTICIPANTS_USERS_QUERY } = actionTypes
   dispatch UNSET_CHANNEL_PARTICIPANTS_USERS_QUERY
   resetChannelParticipantsSelectedIndex()
+
+
+###*
+ * Action to reset users selected index to initial value
+###
+resetChannelParticipantsSelectedIndex = ->
+
+  { RESET_CHANNEL_PARTICIPANTS_SELECTED_INDEX } = actionTypes
+  dispatch RESET_CHANNEL_PARTICIPANTS_SELECTED_INDEX
+
 ###*
  * Action to increment users selected index
 ###
@@ -146,6 +156,7 @@
   setVisibility
   setChannelParticipantsInputQuery
   unsetChannelParticipantsInputQuery
+  resetChannelParticipantsSelectedIndex
   moveToNextChannelParticipantIndex
   moveToPrevChannelParticipantIndex
 }


game field cell will be contain game objects links

--- scripts/coffee/game-field.coffee
+++ scripts/coffee/game-field.coffee
@@ -11,7 +11,7 @@
 			for y in [0...@size.y]
 				x: x
 				y: y
-				status: 0
+				gameObject: null

 	getCellStatus: (cell) ->
 		if @fieldControl[cell.x][cell.y]


delete to concat

delete to concat

--- scripts/coffee/view-field.coffee
+++ scripts/coffee/view-field.coffee
@@ -1,33 +0,0 @@
-# Class for DOM manipulations
-class ViewField
-	constructor: (@size) ->
-		do @initCells
-
-	initCells: ->
-		#generate cells
-		@cells = []
-		for y in [0...@size.y]
-			for x in [0...@size.x]
-				loopCell = new ViewCell({y: y, x: x})
-				do loopCell.deactivate
-				@cells.push loopCell
-
-	getCell: ->
-		#work with one or two arguments
-		return if arguments.length > 2 || arguments.length == 0
-		position = switch arguments.length
-			when 1 then arguments[0]
-			when 2 then { x: arguments[0], y: arguments[1] }
-
-		return if position.x > @size.x || position.y > @size.y
-		@cells[(position.y * @size.x) + position.x]
-
-	setCellStatus: (cell, status) ->
-		cell = @getCell(cell)
-		if status
-			do cell.activate
-		else
-			do cell.deactivate
-
-	clear: ->
-		do cell.deactivate for cell in @cells


catchup script update

catchup script update

--- server/api/scripts/2noddy.coffee
+++ server/api/scripts/2noddy.coffee
@@ -55,7 +55,9 @@
       for u in newusers.data.hits.hits
         rec = u._source
         API.es.call 'POST', '/noddy/users/' + rec._id, rec, undefined, undefined, undefined, undefined, undefined, false
-      #oab_request.each 'createdAt:>1520246760683', (rec) ->
+
+      #newoab = API.es.call 'GET', '/oab/request?q=createdAt:>1520248232218&size=10000', undefined, undefined, undefined, undefined, undefined, undefined, false
+      #for u in newoab.data.hits.hits
       #  rec = _oab_requests_sherpa rec
       #  API.es.call 'POST', '/oab/request/' + rec._id, rec, undefined, undefined, undefined, undefined, undefined, false
       return newusers.data.hits.total


<controller> ActuatorRule - Adding Routes.

--- server/controllers/routes.coffee
+++ server/controllers/routes.coffee
@@ -16,12 +16,13 @@
 module.exports =

     # Fetch on params
-    'sensorId': 	param: sensor.fetch
-    'actuatorId': 	param: actuator.fetch
-    'ruleId': 		param: rule.fetch
-    'measureId':    param: measure.fetch
-    'driverId':     param: driver.fetch
-    'sensorRuleId': param: sensorRule.fetch
+    'sensorId': 	  param: sensor.fetch
+    'actuatorId': 	  param: actuator.fetch
+    'ruleId': 		  param: rule.fetch
+    'measureId':      param: measure.fetch
+    'driverId':       param: driver.fetch
+    'sensorRuleId':   param: sensorRule.fetch
+    'actuatorRuleId': param: actuatorRule.fetch

     'sensors/?':
         get: sensor.all
@@ -62,15 +63,18 @@
         post: rule.createSensorRule

     'rules/:ruleId/sensorRules/:sensorRuleId?':
-        # @todo Get by Rule: get: sensor.getSensorRules
         get: sensorRule.read
         put: sensorRule.update
         delete: rule.deleteSensorRule
-

     'rules/:ruleId/actuatorRules/?':
         # @todo Get by Rule: get: sensor.getActuatorRules
         post: rule.createActuatorRule
+
+    'rules/:ruleId/actuatorRules/:actuatorRuleId?':
+        get: actuatorRule.read
+        put: actuatorRule.update
+        delete: actuatorRule.delete

     'rules/?':
         get: rule.all


Change re-submit timeout

Change re-submit timeout

--- server/cron/submitSubmits.coffee
+++ server/cron/submitSubmits.coffee
@@ -18,7 +18,7 @@
 import logger from '../log'

 makeTimeouts = () ->
-    timeout = 1000
+    timeout = 1000 * 60
     result = []
     while true
         result.push(timeout)


appcache

appcache

--- source/javascripts/pgtune.coffee
+++ source/javascripts/pgtune.coffee
@@ -1,6 +1,8 @@
 class Pgtune
   constructor: (@form, @codeOut, @oldPgkernel) ->
     @form.submit @_generateConfigForm
+    # appcache
+    @_initAppcache()
     # init foundation
     $(document).foundation()
     # constants
@@ -172,6 +174,16 @@
   _notSizeValues: =>
     ['max_connections', 'checkpoint_segments', 'checkpoint_completion_target', 'default_statistics_target', '# WARNING']

+  # appcache
+  _initAppcache: =>
+    return unless Modernizr.applicationcache is true
+    window.applicationCache.addEventListener 'updateready', @_appCacheUpdated, false
+  # new assets available
+  _appCacheUpdated: (e) =>
+    return unless window.applicationCache.status is window.applicationCache.UPDATEREADY
+    window.applicationCache.swapCache()
+    window.location.reload() if confirm('A new version of this app is available. Load it?')
+
 $ ->
   if $('#pgTuneForm').length and $('#postgresConfigOut').length and $('#postgresOldkernelOut').length
     new Pgtune($('#pgTuneForm'), $('#postgresConfigOut'), $('#postgresOldkernelOut'))


Clean up command.coffee.

Clean up command.coffee.

--- lib/command.coffee
+++ lib/command.coffee
@@ -42,11 +42,10 @@
 basename = path.basename
 cmd      = basename(process.argv[1])

-
 parseOptions = ->
-    optionParser  = new optparse.OptionParser SWITCHES, BANNER
-    options       = optionParser.parse process.argv.slice 2
-    sources       = options.arguments
+  optionParser  = new optparse.OptionParser SWITCHES, BANNER
+  options       = optionParser.parse process.argv.slice 2
+  sources       = options.arguments

 writeFile = (dir, currfile, coffee) ->
   outputdir = options.output || '.'
@@ -73,7 +72,7 @@
         if ((f.split '.')[1] == 'js')
           readf = dirPath + '/' + f
           (console.warn "read file %s", readf) if options.verbose
-          contents = fs.readFileSync(readf, encoding);
+          contents = fs.readFileSync(readf, encoding)
           output = js2coffee.build(contents,options)
           writeFile(dirPath, f, output)
       catch e


Update setCursor to move cursor if existing

--- lib/editors.coffee
+++ lib/editors.coffee
@@ -71,8 +71,19 @@

   setCursor: (param) -> @locker.try ()=>
     console.log
-    if param.index?
-      @_cursors.setCursor param.id, param.index, param.author, param.color
+    cursor = @_cursors.cursors[param.id]
+    if cursor? and cursor.color == param.color
+      fun = (index) -> @_cursors.moveCursor param.id, index
+    else
+      if cursor.color != param.color
+        @removeCursor param.id
+
+      fun = (index) ->  @_cursors.setCursor( param.id, index,
+        param.author, param.color)
+
+    if param.index
+      fun param.index
+
   removeCursor: (id) ->
     @_cursors.removeCursor(id)


Added config to customize Firebase Url Resolves #16

--- lib/firepad.coffee
+++ lib/firepad.coffee
@@ -6,6 +6,11 @@
 ShareSetupView = require './sharesetup-view'

 module.exports =
+  config:
+    firebaseUrl:
+      type: 'string'
+      default: 'https://atom-firepad.firebaseio.com'
+
   activate: (state) ->
     @shareview = new ShareView
     @shareSetupView = new ShareSetupView
@@ -23,7 +28,7 @@

   setupShare: ->
     hash = Crypto.createHash('sha256').update(@shareIdentifier).digest('base64')
-    @firebase = new Firebase('https://atom-firepad.firebaseio.com').child(hash)
+    @firebase = new Firebase(atom.config.get('firepad.firebaseUrl')).child(hash)

     editor = atom.workspace.getActiveTextEditor()
     @firebase.once 'value', (snapshot) =>


Fixed Unix directory separator

Fixed Unix directory separator

--- lib/helpers.coffee
+++ lib/helpers.coffee
@@ -17,9 +17,10 @@
     console.log localPath
     diffFiles = []
     fs = require 'fs'
+    path = require 'path'

     for sName, sFile of serverFiles
-      filePath = localPath + "\\" + sName
+      filePath = localPath + path.sep + sName
       if sFile.type is "directory"
         @createFileIfNotExists( {path: filePath}, ->
           clientFiles[sName] = {name: sName, content: {}, type: "directory"}
@@ -39,7 +40,7 @@

     for cName, cFile of clientFiles
       if !(serverFiles[cName]?)
-        filePath = localPath + "\\" + cName
+        filePath = localPath + path.sep + cName
         if cFile.type is "directory"
           @deleteFolderRecursive( cFile, filePath )
         else
@@ -107,7 +108,7 @@
       files = fs.readdirSync curPath
       arr = {}
       for file in files
-        filePath = curPath + "\\" + file
+        filePath = curPath + path.sep + file
         stat = fs.statSync filePath
         if stat.isDirectory()
           arr[file] = {name: file, type: "directory",


added a safely_pop_next to ensure only one pop at a time

--- lib/manager.coffee
+++ lib/manager.coffee
@@ -194,7 +194,7 @@
   listen: ->
     @sub.on 'message', (channel, msg) => @perform msg
     @sub.subscribe @control
-    @pop_next()
+    @safely_pop_next()

   # Pop job message from the work queues and call `@job` to handle it;
   # some time later `@pop_next` will be called again.
@@ -203,6 +203,15 @@
       return @error err if err
       [type, value] = response
       @job type, value
+
+  # Safely Pop job message ensuring only one pop at a time from the work queues and call `@job` to handle it;
+  # some time later `@pop_next` will be called again.
+  safely_pop_next: ->
+    return if @popping
+    @popping = true
+    process.nextTick =>
+      @popping = false
+      @pop_next()

   # Dispatch the control message to one of our handlers.
   perform: (msg) ->
@@ -242,7 +251,7 @@
         @workers[key].run()
       catch e
         @error e
-      @pop_next()
+      @safely_pop_next()

   # We want the given worker to resume running, either because its
   # dependencies have been satisfied, or we want to inject an error


private functions are now _private

private functions are now _private

--- lib/orpheus.coffee
+++ lib/orpheus.coffee
@@ -132,7 +132,7 @@
 							else
 								# new
 								model = new OrpheusAPI(null, this)
-								model.add_map pk, v
+								model._add_map pk, v
 								fn null, model, model.id, true

@@ -162,7 +162,7 @@
 		@validation_errors = []

 		# new or existing id
-		@id = id or @generate_id()
+		@id = id or @_generate_id()

 		# Create functions for working with the relation set
 		# e.g. user(15).books.sadd('dune') will map to sadd
@@ -201,8 +201,8 @@
 						, (err) ->
 							fn err, results

-		# for every field, add all the redis commands
-		# for its corresponding type.
+		# Add all redis commands relevant to the field and its type
+		# Example: model('id').ranking.zadd (zrank, zscore...)
 		for key, value of @model
 			@[key] = {}

@@ -210,10 +210,10 @@
 				do (key, f) =>
 					@[key][f] = (args...) =>
 						# add multi command
-						@_commands.push _.flatten [f, @get_key(key), args]
+						@_commands.push _.flatten [f, @_get_key(key), args]

 						# Extra commands: mapping
-						@extra_commands(key, f, args) if @model[key].options.map
+						@_extra_commands(key, f, args) if @model[key].options.map

 						return @

@@ -252,7 +252,7 @@

 	# Generate a unique ID for model, similiar to MongoDB
 	# http://www.mongodb.org/display/DOCS/Object+IDs
-	generate_id: ->
+	_generate_id: ->
 		time = "#{new Date().getTime()}" # we convert to a str to
 		                                 # avoid 4.3e+79 as id
 		pid = process.pid
@@ -262,7 +262,7 @@
 		"#{host}#{pid}#{time}#{counter}"

-	get_key: (key) ->
+	_get_key: (key) ->
 		# Default: orpheus:pl:15
 		k = "#{@prefix}:#{@q}:#{@id}"

@@ -289,15 +289,15 @@
 			# orpheus:us:15:somelist
 			return "#{k}:#{key}"

-	add_map: (field, key) ->
+	_add_map: (field, key) ->
 		@_commands.push ['hset', "#{@prefix}:#{@pname}:map:#{field}", key, @id]

-	extra_commands: (key, command, args) ->
+	_extra_commands: (key, command, args) ->
 		# Map stuff, e.g.
 		# orpheus:users:map:fb_ids
 		if @model[key].options.map and command is 'hset'
 			pkey = inflector.pluralize key
-			@add_map pkey, args[0]
+			@_add_map pkey, args[0]

 	# deletes the model.
 	delete: (fn) ->
@@ -311,9 +311,9 @@
 			if type is 'str' or type is 'num'
 				unless hdel_flag
 					hdel_flag = true
-					@_commands.push ['del', @get_key()]
+					@_commands.push ['del', @_get_key()]
 			else
-				@_commands.push ['del', @get_key(key)]
+				@_commands.push ['del', @_get_key(key)]

 		@exec fn

@@ -347,28 +347,28 @@

 				when 'list'
 					if not_private()
-						@_commands.push ['lrange', @get_key(key), 0, -1]
+						@_commands.push ['lrange', @_get_key(key), 0, -1]

 				when 'set'
 					if not_private()
-						@_commands.push ['smembers', @get_key(key)]
+						@_commands.push ['smembers', @_get_key(key)]

 				when 'zset'
 					if not_private()
-						@_commands.push ['zrange', @get_key(key), 0, -1, 'withscores']
+						@_commands.push ['zrange', @_get_key(key), 0, -1, 'withscores']

 				when 'hash'
 					if not_private()
-						@_commands.push ['hgetall', @get_key(key)]
+						@_commands.push ['hgetall', @_get_key(key)]

 			schema.push key unless type is 'str' or type is 'num'

 		if hash_parts_are_private
 			if hmget.length
-				@_commands.push ['hmget', @get_key()].concat(hmget)
+				@_commands.push ['hmget', @_get_key()].concat(hmget)
 				schema.push hmget
 		else if hgetall_flag
-			@_commands.push ['hgetall', @get_key()]
+			@_commands.push ['hgetall', @_get_key()]
 			schema.push 'hash'

 		@exec (err, res, id) =>


runtime: Remove dead code

runtime: Remove dead code

--- lib/runtime.coffee
+++ lib/runtime.coffee
@@ -47,16 +47,6 @@
         else
             newList.push conn
     return newList
-
-printReceived = ->
-    args = []
-    i = 0
-
-    while i < arguments.length
-        args.push arguments[i]
-        i++
-    console.log args.join(", ")
-    return

 listComponents = (runtime, connection) ->
     componentLib = runtime.library


Fix repeating root GPN requests bug

Fix repeating root GPN requests bug

--- lib/session.coffee
+++ lib/session.coffee
@@ -581,7 +581,7 @@

   if sessionContext.declarations.length <= sessionContext.virtualParameters.length
     inception = sessionContext.declarations.length
-    revision = sessionContext.revisions[inception] + 1
+    revision = (sessionContext.revisions[inception] or 0) + 1
     sessionContext.deviceData.timestamps.revision = revision
     sessionContext.deviceData.attributes.revision = revision

@@ -1350,7 +1350,7 @@

   timestamp = sessionContext.timestamp + sessionContext.iteration

-  revision = sessionContext.revisions[sessionContext.revisions.length - 1] + 1
+  revision = (sessionContext.revisions[sessionContext.revisions.length - 1] or 0) + 1
   sessionContext.deviceData.timestamps.revision = revision
   sessionContext.deviceData.attributes.revision = revision


Fix issue with declaring values using wildcards

When declaring parameter values using a wildcard path, overlapping
declarations are not applied correctly according to their weight and
execution order. The fix is to unpack first then apply the declarations
to the individual parameters.

--- lib/session.coffee
+++ lib/session.coffee
@@ -476,6 +476,21 @@

   allVirtualParameters = virtualParametersCache.get(sessionContext)

+  mergeAttributeTimestamps = (p, attrs) ->
+    if cur = allDeclareAttributeTimestamps.get(p)
+      cur = Object.assign({}, cur)
+      cur[k] = Math.max(v, cur[k] or 0) for k, v of attrs
+      allDeclareAttributeTimestamps.set(p, cur)
+    else
+      allDeclareAttributeTimestamps.set(p, attrs)
+
+  mergeAttributeValues = (p, attrs) ->
+    if cur = allDeclareAttributeValues.get(p)
+      cur = Object.assign({}, cur, attrs)
+      allDeclareAttributeValues.set(p, cur)
+    else
+      allDeclareAttributeValues.set(p, attrs)
+
   for declaration, i in declarations
     path = common.addPathMeta(declaration[0])
     unpacked = null
@@ -498,47 +513,31 @@
       for ad in aliasDecs
         p = sessionContext.deviceData.paths.add(ad[0])
         allDeclareTimestamps.set(p, Math.max(ad[1] or 1, allDeclareTimestamps.get(p) or 0))
-        attrTrackers = []
+        attrTrackers = null
         if ad[2]
-          cur = allDeclareAttributeTimestamps.get(p)
-          allDeclareAttributeTimestamps.set(p, cur = {}) if not cur
-          for k, v of ad[2]
-            attrTrackers.push(k)
-            cur[k] = Math.max(v, cur[k] or 0)
-
+          attrTrackers = Object.keys(ad[2])
+          mergeAttributeTimestamps(p, ad[2])
         device.track(sessionContext.deviceData, p, 'prerequisite', attrTrackers)

       unpacked = device.unpack(sessionContext.deviceData, path)
       for u in unpacked
         allDeclareTimestamps.set(u, Math.max(declaration[1] or 1, allDeclareTimestamps.get(u) or 0))
         if declaration[2]
-          cur = allDeclareAttributeTimestamps.get(u)
-          allDeclareAttributeTimestamps.set(u, cur = {}) if not cur
-          for k, v of declaration[2]
-            cur[k] = Math.max(v, cur[k] or 0)
-
-        if declaration[4]
-          cur = allDeclareAttributeValues.get(u)
-          allDeclareAttributeValues.set(u, cur = {}) if not cur
-          for k, v of declaration[4]
-            cur[k] = v
+          mergeAttributeTimestamps(u, declaration[2])
     else
       path = sessionContext.deviceData.paths.add(path)
       allDeclareTimestamps.set(path, Math.max(declaration[1] or 1, allDeclareTimestamps.get(path) or 0))
       if declaration[2]
-        cur = allDeclareAttributeTimestamps.get(path)
-        allDeclareAttributeTimestamps.set(path, cur = {}) if not cur
-        for k, v of declaration[2]
-          cur[k] = Math.max(v, cur[k] or 0)
-
-      if declaration[4]
-        if declaration[4]
-          cur = allDeclareAttributeValues.get(path)
-          allDeclareAttributeValues.set(path, cur = {}) if not cur
-          for k, v of declaration[4]
-            cur[k] = v
-
+        mergeAttributeTimestamps(path, declaration[2])
       device.track(sessionContext.deviceData, path, 'prerequisite')
+
+    if declaration[4]
+      if path.alias | path.wildcard
+        unpacked ?= device.unpack(sessionContext.deviceData, path)
+        for u in unpacked
+          mergeAttributeValues(u, declaration[4])
+      else
+        mergeAttributeValues(path, declaration[4])

     if declaration[3]?
       if Array.isArray(declaration[3])
@@ -1113,8 +1112,8 @@
     currentPath = paths[0]
     children = {}
     declareTimestamp = 0
-    declareAttributeTimestamps = {}
-    declareAttributeValues = {}
+    declareAttributeTimestamps = null
+    declareAttributeValues = null

     currentTimestamp = 0
     currentAttributes = null
@@ -1138,12 +1137,14 @@

       if currentPath.wildcard == 0
         if attrs = allDeclareAttributeTimestamps.get(path)
-          for attrName of attrs
-            declareAttributeTimestamps[attrName] = Math.max(attrs[attrName], declareAttributeTimestamps[attrName] or 0)
-
-          if attrs = allDeclareAttributeValues.get(path)
-            for attrName of attrs
-              declareAttributeValues[attrName] = attrs[attrName]
+          if declareAttributeTimestamps
+            delcareAttributeTimestamps = Object.assign({}, declareAttributeTimestamps)
+            declareAttributeTimestamps[k] = Math.max(v, attrs[k] or 0) for k, v of attrs
+          else
+            declareAttributeTimestamps = attrs
+
+        if attrs = allDeclareAttributeValues.get(path)
+          declareAttributeValues = attrs

     if currentAttributes
       leafParam = currentPath
@@ -1156,11 +1157,11 @@
     switch (if currentPath[0] != '*' then currentPath[0] else leafParam[0])
       when 'Reboot'
         if currentPath.length == 1
-          if declareAttributeValues.value?
+          if declareAttributeValues?.value?
             syncState.reboot = +(new Date(declareAttributeValues.value[0]))
       when 'FactoryReset'
         if currentPath.length == 1
-          if declareAttributeValues.value?
+          if declareAttributeValues?.value?
             syncState.factoryReset = +(new Date(declareAttributeValues.value[0]))
       when 'Tags'
         if currentPath.length == 2 and currentPath.wildcard == 0 and declareAttributeValues?.value?
@@ -1168,7 +1169,7 @@
       when 'Events', 'DeviceID' then
         # Do nothing
       when 'Downloads'
-        if currentPath.length == 3 and currentPath.wildcard == 0 and declareAttributeValues.value?
+        if currentPath.length == 3 and currentPath.wildcard == 0 and declareAttributeValues?.value?
           if currentPath[2] == 'Download'
             syncState.downloadsDownload.set(currentPath, declareAttributeValues.value[0])
           else
@@ -1182,14 +1183,12 @@
           if currentPath.wildcard == 0
             if declareAttributeTimestamps
               for attrName of declareAttributeTimestamps
-                if declareAttributeTimestamps[attrName] <= currentAttributes?[attrName]?[0]
-                  delete declareAttributeTimestamps[attrName]
-
-              if Object.keys(declareAttributeTimestamps).length
-                d ?= [currentPath]
-                d[1] = declareAttributeTimestamps
-
-            if Object.keys(declareAttributeValues).length
+                if not (declareAttributeTimestamps[attrName] <= currentAttributes?[attrName]?[0])
+                  d ?= [currentPath]
+                  d[1] ?= {}
+                  d[1][attrName] = declareAttributeTimestamps[attrName]
+
+            if declareAttributeValues
               d ?= [currentPath]
               d[2] = declareAttributeValues

@@ -1223,7 +1222,7 @@
               else
                 syncState.refreshAttributes[attrName].add(currentPath)

-          if declareAttributeValues.value?
+          if declareAttributeValues?.value?
             syncState.spv.set(currentPath, declareAttributeValues.value)

     for child of children


Sometimes empty value is passed to lines, abort in such cases

--- assets/scripts/lib/travis/log.coffee
+++ assets/scripts/lib/travis/log.coffee
@@ -15,6 +15,8 @@
       @addFold fold

   append: (lines) ->
+    return unless lines
+
     log   = @join lines
     log   = @escape log
     log   = @deansi log


Remove the sample file.

Remove the sample file.

sample.js does not exist!

--- spec/command-palette-spec.coffee
+++ spec/command-palette-spec.coffee
@@ -10,7 +10,7 @@
     activationPromise = null

     waitsForPromise ->
-      atom.workspace.open('sample.js')
+      atom.workspace.open()

     runs ->
       editorElement = atom.views.getView(atom.workspace.getActiveTextEditor())


:green_heart: Try fixing broken travis build

--- spec/minimap-element-spec.coffee
+++ spec/minimap-element-spec.coffee
@@ -500,7 +500,9 @@
           nextAnimationFrame()

           atom.config.set 'minimap.minimapScrollIndicator', true
-          nextAnimationFrame()
+
+          waitsFor -> minimapElement.frameRequested
+          runs -> nextAnimationFrame()

         it 'offsets the scroll indicator by the difference', ->
           indicator = minimapElement.shadowRoot.querySelector('.minimap-scroll-indicator')
@@ -509,7 +511,8 @@
       describe 'and when minimap.displayPluginsControls setting is true', ->
         beforeEach ->
           atom.config.set 'minimap.displayPluginsControls', true
-          nextAnimationFrame()
+          waitsFor -> minimapElement.frameRequested
+          runs -> nextAnimationFrame()

         it 'offsets the scroll indicator by the difference', ->
           openQuickSettings = minimapElement.shadowRoot.querySelector('.open-minimap-quick-settings')


footerにgithub urlを追加

footerにgithub urlを追加

--- app/assets/javascripts/components/footer.js.cjsx
+++ app/assets/javascripts/components/footer.js.cjsx
@@ -2,14 +2,14 @@
   render: ->
     now = new Date()
     year = now.getFullYear()
-

-

-
-
-
+

+

+
+
+
             Twitter

-        © IIDX☆12参考表 by839 2014-{year}
+        © IIDX☆12参考表 by839 2014-{year}


div.event-calendar #1476 (#1491)

div.event-calendar #1476 (#1491)

* event-calendar

* Add ".event-calendar"

* Use replaceWith()

--- app/assets/javascripts/event/lib/calendar.coffee
+++ app/assets/javascripts/event/lib/calendar.coffee
@@ -9,7 +9,7 @@
         cache: false,
         success: (res, status) ->
           html = "<div>" + res + "</div>"
-          $(".event-calendar").html($(html).find(".event-calendar"))
+          $(".event-calendar").replaceWith($(html).find(".event-calendar"))
           $(".calendar-nav a.paginate").on 'click', ->
             paginate(this)
             return false


#907 show toast error message on violated assertion

--- app/assets/javascripts/main/errorHandling.coffee
+++ app/assets/javascripts/main/errorHandling.coffee
@@ -1,6 +1,7 @@
 ### define
 jquery : $
 underscore : _
+../libs/toast : Toast
 ###

 ErrorHandling =
@@ -67,6 +68,8 @@
       error.params = assertionContext
       error.stack = @trimCallstack(error.stack)

+      Toast.error("Assertion violated - #{message}")
+
       if @throwAssertions
         # error will be automatically pushed to airbrake due to global handler
         throw error


use responseJSON

use responseJSON

--- app/assets/javascripts/me-add-to-playlist.coffee
+++ app/assets/javascripts/me-add-to-playlist.coffee
@@ -36,12 +36,12 @@
               end_time: $('#playlist_item_end').val()
           success: (response) ->
             $('#add_to_playlist_alert')[0].className = 'alert alert-success'
-            $('#add_to_playlist_result_message')[0].innerHTML = response['message']
+            $('#add_to_playlist_result_message')[0].innerHTML = response.responseJSON.message
             $('#add_to_playlist_alert').show(300)
             $('#add_to_playlist').hide(500)
           error: (response) ->
             $('#add_to_playlist_alert')[0].className = 'alert alert-danger'
-            $('#add_to_playlist_result_message')[0].innerHTML = response['message']
+            $('#add_to_playlist_result_message')[0].innerHTML = response.responseJSON.message
             $('#add_to_playlist_alert').show(300)

       $('.add_playlist_item_cancel').click (event) ->


regex match for uiActive

regex match for uiActive

--- app/assets/javascripts/monster/directives.coffee
+++ app/assets/javascripts/monster/directives.coffee
@@ -25,9 +25,8 @@
       update = ->
         if $state.current.templateUrl
           name = $state.current.templateUrl.replace /\//g, '_'
-          reg = new RegExp name
-          matched = name.match(attrs.uiActive)
-          if matched
+          reg = new RegExp attrs.uiActive
+          if name =~ reg
             element.addClass 'active'
           else
             element.removeClass 'active'


improvements

improvements

--- app/assets/javascripts/weather/weather.js.coffee
+++ app/assets/javascripts/weather/weather.js.coffee
@@ -70,7 +70,7 @@

 init_theme = (block) ->
-#  snowfall()
+  snowfall()

 wheater_proc = ->


DRYer prefixing

DRYer prefixing

--- hexaflip.coffee
+++ hexaflip.coffee
@@ -16,10 +16,8 @@
     return prefixed if testEl.style[prefixed]?
   false

-css =
-  transform: prefixProp 'Transform'
-  perspective: prefixProp 'Perspective'
-
+css = {}
+css[prop.toLowerCase()] = prefixProp prop for prop in ['Transform', 'Perspective']

 defaults =
   size: 280


Removing console log

Removing console log

--- app/controllers/interfaces/bugExpeditions.coffee
+++ app/controllers/interfaces/bugExpeditions.coffee
@@ -63,7 +63,6 @@
     @classification = new Classification subject: Subject.current

     Subject.current.location.small ?= Subject.current.location.standard
-    console.log(Subject.current);
     subjectImage = Subject.current.location.standard

     img = new Image


Improve title animation

Improve title animation

--- app/assets/javascripts/home.js.coffee
+++ app/assets/javascripts/home.js.coffee
@@ -28,8 +28,10 @@
     $(this).removeClass('is-animation-wiggle')

   $headerName.click ->
-    $(this).removeClass('is-animation-wiggle')
-    $(this).addClass('is-animation-hinge')
+    $(this)
+      .unbind('mouseenter').unbind('mouseleave')
+      .removeClass('is-animation-wiggle')
+      .addClass('is-animation-hinge')

   # infinite scroll
   baseAjaxURL = '/?max_tag_id='


Comment out ajax success log message

Comment out ajax success log message

Not needed at this time and it adds noise to the test output.

--- app/assets/javascripts/home.js.coffee
+++ app/assets/javascripts/home.js.coffee
@@ -71,7 +71,7 @@
       # TODO: need to check for errors returned by server
       set_dirty(false)
       # TODO: temporary logging for debugging
-      console.log "ajax done: jqXHR.responseText='#{jqXHR.reponseText}', textStatus='#{textStatus}'"
+      # console.log "ajax done: jqXHR.responseText='#{jqXHR.reponseText}', textStatus='#{textStatus}'"
   ).fail(
     ( jqXHR, textStatus, errorThrown ) ->
       # TODO: temporary logging for debugging


Fix issues creation tracking.

Fix issues creation tracking.

--- app/coffee/modules/issues/list.coffee
+++ app/coffee/modules/issues/list.coffee
@@ -50,11 +50,12 @@
         "$appTitle",
         "$tgNavUrls",
         "$tgEvents",
+        "$tgAnalytics",
         "tgLoader"
     ]

     constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appTitle,
-                  @navUrls, @events, tgLoader) ->
+                  @navUrls, @events, @analytics, tgLoader) ->
         @scope.sectionName = "Issues"
         @scope.filters = {}

@@ -80,8 +81,10 @@
             return @q.reject(xhr)

         @scope.$on "issueform:new:success", =>
+            @analytics.trackEvent("issue", "create", "create issue on issues list", 1)
             @.loadIssues()
             @.loadFilters()
+

     initializeSubscription: ->
         routingKey = "changes.project.#{@scope.projectId}.issues"


fix(kanban): calculate kanban size only with column that remains in the dom

--- app/coffee/modules/kanban/main.coffee
+++ app/coffee/modules/kanban/main.coffee
@@ -553,11 +553,16 @@
                 .split(' ')[1])

             resizeCb = (entries) =>
-                width = columns.toArray().reduce (acc, column) =>
-                    return acc + column.offsetWidth + columnMargin
-                , 0
-
-                document.body.style.setProperty('--kanban-width', (width - columnMargin) + 'px')
+                    width = columns.toArray().reduce (acc, column) =>
+                        if document.body.contains(column)
+                            return acc + column.offsetWidth + columnMargin
+
+                        resizeObserver.unobserve(column)
+                        return acc
+                    , 0
+
+                    if width > 0
+                        document.body.style.setProperty('--kanban-width', (width - columnMargin) + 'px')

             resizeObserver = new ResizeObserver(resizeCb)


Fix conditional for redirect

Fix conditional for redirect

--- app/components/project-card-list.cjsx
+++ app/components/project-card-list.cjsx
@@ -2,7 +2,7 @@
 PromiseRenderer = require '../components/promise-renderer'
 apiClient = require 'panoptes-client/lib/api-client'
 Select = require 'react-select'
-{Link, Router, Navigation, History} = require 'react-router'
+{Link, Router, History} = require 'react-router'
 Translate = require 'react-translate-component'
 debounce = require 'debounce'
 Filmstrip = require '../components/filmstrip'
@@ -94,7 +94,6 @@
     <Filmstrip increment={350} value={@props.value} onChange={@props.onChange}/>

 SearchSelector = React.createClass
-
   displayName: 'SearchSelector'
   mixins: [History]

@@ -106,7 +105,7 @@
   navigateToProject: (projectID) ->
     apiClient.type('projects').get(projectID)
       .then (project) ->
-        if project.redirect?
+        if project.redirect? and project.redirect.length isnt 0
           window.location.href = project.redirect
         else
           window.location.href = ['/projects', project.slug].join('/')


Added Pending Bin Tests

Added Pending Bin Tests

--- test/bin.coffee
+++ test/bin.coffee
@@ -75,3 +75,13 @@
       stderr.should.match /usage.*options/i
       done()

+  it 'should require an appPath'
+
+  it 'should fail with additional arguments'
+
+  it 'should compile a kdapp'
+
+  it 'should support coffee kdapps'
+
+  it 'should support commonjs kdapps'
+


interpolation settings for underscore templates

Former-commit-id: 9212e91aa23b43a9c4c680c952b2c526cdf596ec [formerly 7260df80ec4a4f708a23205f5fa1da59000cd94d] [formerly 5e5dabed9e7b76dfefd226de6c399e4bcf6c531e [formerly 4ac9a5eda7ff198c30a04431da43927fc797c9ae]] [formerly 9960f747d7c22cf9ff8238e882e083410b1fef19 [formerly 3cc11faa2f0fb7320033ee649ca3a2840637723e] [formerly 76cb9a07d08973320ea32f63e491764b36d5ea1a [formerly 76cb9a07d08973320ea32f63e491764b36d5ea1a [formerly 6ccff9b6b7ad86d41a96dd9eaa7130d14a09d746]]]]
Former-commit-id: 6105be9c3d4ec841b0eb8512d8f79d37ddfbced5 [formerly c69e0cd69323cc13422d0bdb798f6a0984f995d2] [formerly f4234841c3efe99d6c9f4ca2b3e081497900f11f [formerly 024e971b7868fb9e467900dc3c22452c10c3e4bc]]
Former-commit-id: e247579d1379ef8c479c5cf9387096b5c39e2ab6 [formerly cb681cee89567ae8f630601a89e72c53c8023dd1]
Former-commit-id: 0cad440ec0e570edf2b8432fd6ccda7d115a4062
Former-commit-id: 4eadcf8be129abdebc11023afa756ca8a0b33741

--- @client/vendor_extensions/underscore_extensions.js.coffee
+++ @client/vendor_extensions/underscore_extensions.js.coffee
@@ -1,7 +1,8 @@
 do (_) ->
   _.templateSettings =
-    interpolate : /\{\{(.+?)\}\}/g
+    escape : /\{\{(.+?)\}\}/g
     evaluate : /\(\((.+?)\)\)/g
+    interpolate : /\{SAFE\{(.+?)\}\}/g

   _.mixin
     compactObjectInPlace : (o) ->
@@ -20,3 +21,5 @@
         _.delay func, wait
       else
         func()
+}
+}


make sure we handle redirects after signup

--- models/logged_out_user.coffee
+++ models/logged_out_user.coffee
@@ -23,10 +23,10 @@
     new Backbone.Model().save (@pick 'email', 'password'), settings

   signup: (options = {}) ->
+    success = options.success
     settings = _.defaults options,
       url: "#{sd.API_URL}/api/v1/user"
-      success: injectToken -> location.reload()
-    settings.success = injectToken settings.success
+    settings.success = => @login(success: success)
     new Backbone.Model().save (@pick 'name', 'email', 'password'), settings

   # Alias #signup


Delegate cart functions to order model

Delegate cart functions to order model

--- app/assets/javascripts/sprangular/resources/cart.coffee
+++ app/assets/javascripts/sprangular/resources/cart.coffee
@@ -70,10 +70,11 @@
       $http.put '/api/cart/change_variant', params
         .success(@load)

-    totalQuantity: -> @current.totalQuantity()
+    clear:                   -> @current.clear()
+    totalQuantity:           -> @current.totalQuantity()
     findVariant: (variantId) -> @current.findVariant(variantId)
-    hasVariant: (variant) -> @current.hasVariant(variant)
-    isEmpty: -> @current.isEmpty()
+    hasVariant: (variant)    -> @current.hasVariant(variant)
+    isEmpty:                 -> @current.isEmpty()

   service.clear()
   service.reload()


fixes #120.

fixes #120.

--- app/assets/javascripts/views/application_view.js.coffee
+++ app/assets/javascripts/views/application_view.js.coffee
@@ -4,9 +4,13 @@
   keys:
     'return esc': 'onKeyPress'
     'f+alt left+alt right+alt space+alt': 'onAltKeyPress'
+    '1+alt 2+alt 3+alt': 'onGlobalMenuKeyPress'

-  onKeyPress: (event, name) =>
-    @trigger("keypress-#{name}", event, name)
+  onKeyPress: (ev,name) =>
+    @trigger("keypress-#{name}", ev,name)

-  onAltKeyPress: (event, name) =>
-    @onKeyPress(event, "alt+#{name}")
+  onAltKeyPress: (ev,name) =>
+    @onKeyPress(ev, "alt+#{name}")
+
+  onGlobalMenuKeyPress: (ev,name) =>
+    @$('header.page nav.main li a').get(name-1).click()


refactor refresh attachment method & editor instance creation

--- app/modules/components/wysiwyg/wysiwyg.directive.coffee
+++ app/modules/components/wysiwyg/wysiwyg.directive.coffee
@@ -410,129 +410,131 @@
         throttleChange = _.throttle(change, 200)

         create = (text, editMode=false) ->
+            html = ""
             if text.length
                 html = wysiwygService.getHTML(text)
+
+            promise = wysiwygService.refreshAttachmentURL(html)
+            promise.then (html) =>
                 editorMedium.html(html)

-            mediumInstance = new MediumEditor(editorMedium[0], {
-                imageDragging: false,
-                placeholder: {
-                    text: $scope.placeholder
-                },
-                toolbar: {
-                    buttons: [
-                        {
-                            name: 'bold',
-                            contentDefault: getIcon('editor-bold')
-                        },
-                        {
-                            name: 'italic',
-                            contentDefault: getIcon('editor-italic')
-                        },
-                        {
-                            name: 'strikethrough',
-                            contentDefault: getIcon('editor-cross-out')
-                        },
-                        {
-                            name: 'anchor',
-                            contentDefault: getIcon('editor-link')
-                        },
-                        {
-                            name: 'image',
-                            contentDefault: getIcon('editor-image')
-                        },
-                        {
-                            name: 'orderedlist',
-                            contentDefault: getIcon('editor-list-n')
-                        },
-                        {
-                            name: 'unorderedlist',
-                            contentDefault: getIcon('editor-list-o')
-                        },
-                        {
-                            name: 'h1',
-                            contentDefault: getIcon('editor-h1')
-                        },
-                        {
-                            name: 'h2',
-                            contentDefault: getIcon('editor-h2')
-                        },
-                        {
-                            name: 'h3',
-                            contentDefault: getIcon('editor-h3')
-                        },
-                        {
-                            name: 'quote',
-                            contentDefault: getIcon('editor-quote')
-                        },
-                        {
-                            name: 'removeFormat',
-                            contentDefault: getIcon('editor-no-format')
-                        },
-                        {
-                            name: 'rtl',
-                            contentDefault: getIcon('editor-rtl')
-                        },
-                        {
-                            name: 'code',
-                            contentDefault: getIcon('editor-code')
-                        }
-                    ]
-                },
-                extensions: {
-                    paste: new CustomPasteHandler(),
-                    code: new CodeButton(),
-                    autolist: new AutoList(),
-                    alignright: new AlignRightButton(),
-                    mediumMention: new MentionExtension({
-                        getItems: (mention, mentionCb) ->
-                            wysiwygMentionService.search(mention).then(mentionCb)
-                    })
-                }
-            })
-
-            refreshAttachments(mediumInstance)
-
-            $scope.changeMarkdown = throttleChange
-
-            mediumInstance.subscribe 'editableInput', (e) ->
-                $scope.$applyAsync(throttleChange)
-
-            mediumInstance.subscribe "editableClick", (e) ->
-                r = new RegExp('^(?:[a-z]+:)?//', 'i')
-
-                if e.target.href
-                    if r.test(e.target.getAttribute('href')) || e.target.getAttribute('target') == '_blank'
+                mediumInstance = new MediumEditor(editorMedium[0], {
+                    imageDragging: false,
+                    placeholder: {
+                        text: $scope.placeholder
+                    },
+                    toolbar: {
+                        buttons: [
+                            {
+                                name: 'bold',
+                                contentDefault: getIcon('editor-bold')
+                            },
+                            {
+                                name: 'italic',
+                                contentDefault: getIcon('editor-italic')
+                            },
+                            {
+                                name: 'strikethrough',
+                                contentDefault: getIcon('editor-cross-out')
+                            },
+                            {
+                                name: 'anchor',
+                                contentDefault: getIcon('editor-link')
+                            },
+                            {
+                                name: 'image',
+                                contentDefault: getIcon('editor-image')
+                            },
+                            {
+                                name: 'orderedlist',
+                                contentDefault: getIcon('editor-list-n')
+                            },
+                            {
+                                name: 'unorderedlist',
+                                contentDefault: getIcon('editor-list-o')
+                            },
+                            {
+                                name: 'h1',
+                                contentDefault: getIcon('editor-h1')
+                            },
+                            {
+                                name: 'h2',
+                                contentDefault: getIcon('editor-h2')
+                            },
+                            {
+                                name: 'h3',
+                                contentDefault: getIcon('editor-h3')
+                            },
+                            {
+                                name: 'quote',
+                                contentDefault: getIcon('editor-quote')
+                            },
+                            {
+                                name: 'removeFormat',
+                                contentDefault: getIcon('editor-no-format')
+                            },
+                            {
+                                name: 'rtl',
+                                contentDefault: getIcon('editor-rtl')
+                            },
+                            {
+                                name: 'code',
+                                contentDefault: getIcon('editor-code')
+                            }
+                        ]
+                    },
+                    extensions: {
+                        paste: new CustomPasteHandler(),
+                        code: new CodeButton(),
+                        autolist: new AutoList(),
+                        alignright: new AlignRightButton(),
+                        mediumMention: new MentionExtension({
+                            getItems: (mention, mentionCb) ->
+                                wysiwygMentionService.search(mention).then(mentionCb)
+                        })
+                    }
+                })
+
+                $scope.changeMarkdown = throttleChange
+
+                mediumInstance.subscribe 'editableInput', (e) ->
+                    $scope.$applyAsync(throttleChange)
+
+                mediumInstance.subscribe "editableClick", (e) ->
+                    r = new RegExp('^(?:[a-z]+:)?//', 'i')
+
+                    if e.target.href
+                        if r.test(e.target.getAttribute('href')) || e.target.getAttribute('target') == '_blank'
+                            e.stopPropagation()
+                            window.open(e.target.href)
+                        else
+                            $location.url(e.target.href)
+
+                mediumInstance.subscribe 'editableDrop', (event) ->
+                    $scope.onUploadFile({files: event.dataTransfer.files, cb: uploadEnd})
+
+                mediumInstance.subscribe 'editableKeydown', (e) ->
+                    code = if e.keyCode then e.keyCode else e.which
+
+                    mention = $('.medium-mention')
+
+                    if (code == 40 || code == 38) && mention.length
                         e.stopPropagation()
-                        window.open(e.target.href)
-                    else
-                        $location.url(e.target.href)
-
-            mediumInstance.subscribe 'editableDrop', (event) ->
-                $scope.onUploadFile({files: event.dataTransfer.files, cb: uploadEnd})
-
-            mediumInstance.subscribe 'editableKeydown', (e) ->
-                code = if e.keyCode then e.keyCode else e.which
-
-                mention = $('.medium-mention')
-
-                if (code == 40 || code == 38) && mention.length
-                    e.stopPropagation()
-                    e.preventDefault()
-
-                    return
-
-                if $scope.editMode && code == 27
-                    e.stopPropagation()
-                    $scope.$applyAsync($scope.cancelWithConfirmation)
-                else if code == 27
-                    editorMedium.blur()
-
-            setEditMode(editMode)
-
-            $scope.$applyAsync () ->
-                wysiwygCodeHightlighterService.addHightlighter(mediumInstance.elements[0])
-                refreshCodeBlocks(mediumInstance)
+                        e.preventDefault()
+
+                        return
+
+                    if $scope.editMode && code == 27
+                        e.stopPropagation()
+                        $scope.$applyAsync($scope.cancelWithConfirmation)
+                    else if code == 27
+                        editorMedium.blur()
+
+                setEditMode(editMode)
+
+                $scope.$applyAsync () ->
+                    wysiwygCodeHightlighterService.addHightlighter(mediumInstance.elements[0])
+                    refreshCodeBlocks(mediumInstance)

         $(editorMedium[0]).on 'mousedown', (e) ->
             if e.target.href


Only kill child process if it exists.

Only kill child process if it exists.

--- lib/build.coffee
+++ lib/build.coffee
@@ -16,7 +16,7 @@
     atom.workspaceView.command "build:stop", => @stop()

   deactivate: ->
-    @child.kill('SIGKILL')
+    @child.kill('SIGKILL') if @child

   buildCommand: ->
     cmd = 'make' if fs.existsSync @root + '/Makefile';


regex tweas

regex tweas

--- lib/build.coffee
+++ lib/build.coffee
@@ -120,7 +120,7 @@
           folderNamePath = path.replace ///#{fileBaseName}///, ''
           folderNamePath = folderNamePath.replace pathHeRegex, ''
           folderNamePath = folderNamePath.substring(1)
-          console.log('folderNamePath '+folderNamePaths)
+          console.log('folderNamePath '+folderNamePath)
           folderName =  if isWin then folderNamePath.split("\\") else folderNamePath.split "/"
           fileName = fileBaseName.split "."
           if(fileName.length > 1)


:art: Fix Lint Error

:art: Fix Lint Error

--- lib/godef.coffee
+++ lib/godef.coffee
@@ -48,7 +48,7 @@
     @reset(@editor)
     @gotoDefinitionForWord(word, done)

-  gotoDefinitionForWord: (word, callback = ->) ->
+  gotoDefinitionForWord: (word, callback = -> ) ->
     message = null
     done = (exitcode, stdout, stderr, messages) =>
       unless exitcode is 0


fix require paths

fix require paths

--- lib/index.coffee
+++ lib/index.coffee
@@ -1,6 +1,6 @@
 Promise = require 'bluebird'
 EventEmitter = require 'microemitter'
-bound_ = require './lib/bound'
+bound_ = require './bound'

 module.exports = class Bongo extends EventEmitter
@@ -17,20 +17,20 @@
   JsPath    = @JsPath = require 'jspath'

   @dnodeProtocol = require 'dnode-protocol'
-  @dnodeProtocol.Scrubber = require './lib/scrubber'
+  @dnodeProtocol.Scrubber = require './scrubber'

   {Store, Scrubber} = @dnodeProtocol

   @EventEmitter = EventEmitter
-  Model = @Model = require './lib/model'
-  @ListenerTree = require './lib/listenertree'
-  EventBus  = @EventBus = require './lib/eventbus'
-  OpaqueType = require './lib/opaquetype'
-  Signature = require './lib/signature'
-  @promibackify = require './lib/promibackify'
+  Model = @Model = require './model'
+  @ListenerTree = require './listenertree'
+  EventBus  = @EventBus = require './eventbus'
+  OpaqueType = require './opaquetype'
+  Signature = require './signature'
+  @promibackify = require './promibackify'

   # mixin the event emitter for the AMQP broker
-  Model::mixin require './lib/eventemitter/broker'
+  Model::mixin require './eventemitter/broker'
   # need these aliases:
   Model::off = Model::removeListener
   Model::addGlobalListener = Model::on
@@ -80,7 +80,7 @@

   isConnected: -> return @readyState is CONNECTED

-  cacheable: require './lib/cacheable'
+  cacheable: require './cacheable'

   cacheableAsync: (rest...) ->
     new Promise (resolve, reject) =>


remove unused log lines

remove unused log lines

--- lib/index.coffee
+++ lib/index.coffee
@@ -84,9 +84,6 @@
       spawn_opts.push '-a2' if options.type in ['right', 'full'] # Keep unpaired lines from right
       spawn_opts.push file_names... # Files to join

-      # console.log "FILE NAMES:", file_names
-      # console.log "SPAWN OPTS: join", spawn_opts.join ' '
-
       _.stream()
         .spawn('join', spawn_opts)
         .split('\n')


Ignore more

Ignore more

--- lib/logic.coffee
+++ lib/logic.coffee
@@ -42,15 +42,23 @@
 ]

 IGNORED_FACTOIDS = [
+  "he"
+  "hers"
+  "his"
+  "it"
+  "it's"
+  "its"
+  "she"
+  "that"
+  "them"
+  "these"
+  "they"
   "this"
   "those"
-  "these"
-  "they"
-  "that"
+  "what"
+  "when"
+  "where"
   "who"
-  "what"
-  "where"
-  "when"
   "why"
 ]


Update mutation observer to consider added and removed text nodes

--- src/trix/observers/mutation_observer.coffee
+++ src/trix/observers/mutation_observer.coffee
@@ -68,9 +68,34 @@
     @getTextMutationSummary()

   getTextMutationSummary: ->
-    additions = []
-    deletions = []
+    {additions, deletions} = @getTextChangesFromCharacterData()

+    textChanges = @getTextChangesFromTextNodes()
+    additions.push(textChanges.additions...)
+    deletions.push(textChanges.deletions...)
+
+    summary = {}
+    summary.textAdded = added if added = additions.join("")
+    summary.textDeleted = deleted if deleted = deletions.join("")
+    summary
+
+  getMutationsByType: (type) ->
+    mutation for mutation in @mutations when mutation.type is type
+
+  getTextChangesFromTextNodes: ->
+    nodesAdded = []
+    nodesRemoved = []
+
+    for mutation in @getMutationsByType("childList")
+      for node in mutation.removedNodes when node.nodeType is Node.TEXT_NODE
+        nodesRemoved.push(node)
+      for node in mutation.addedNodes when node.nodeType is Node.TEXT_NODE
+        nodesAdded.push(node)
+
+    additions: (node.data for node, index in nodesAdded when node.data isnt nodesRemoved[index]?.data)
+    deletions: (node.data for node, index in nodesRemoved when node.data isnt nodesAdded[index]?.data)
+
+  getTextChangesFromCharacterData: ->
     characterMutations = @getMutationsByType("characterData")

     if characterMutations.length
@@ -80,32 +105,5 @@
       newString = normalizeSpaces(endMutation.target.data)
       {added, removed} = summarizeStringChange(oldString, newString)

-      additions.push(added)
-      deletions.push(removed)
-
-    for node in @getRemovedTextNodes()
-      deletions.push(node.data)
-
-    summary = {}
-    summary.textAdded = added if added = additions.join("")
-    summary.textDeleted = deleted if deleted = deletions.join("")
-    summary
-
-  getMutationsByType: (type) ->
-    mutation for mutation in @mutations when mutation.type is type
-
-  getRemovedTextNodes: ->
-    added = []
-    removed = []
-    removedAndNotAdded = []
-
-    for mutation in @getMutationsByType("childList")
-      for node in mutation.removedNodes when node.nodeType is Node.TEXT_NODE
-        removed.push(node)
-      for node in mutation.addedNodes when node.nodeType is Node.TEXT_NODE
-        added.push(node)
-
-    for node, index in removed when node.data isnt added[index]?.data
-      removedAndNotAdded.push(node)
-
-    removedAndNotAdded
+    additions: if added then [added] else []
+    deletions: if removed then [removed] else []


获取当前课程学习进度状态

获取当前课程学习进度状态

--- app/assets/javascripts/kc_courses/ware_reading.coffee
+++ app/assets/javascripts/kc_courses/ware_reading.coffee
@@ -5,7 +5,7 @@
   finally: ($el)->

 class @WareReading
-  constructor: (@configs)->
+  constructor: (configs)->
     @$el = jQuery(config["selector"])
     @progress = new config["progress_class"]()