KDView: Getting draggedDistance of only drag allowed axis. Renamed a variable.

--- core/KDView.coffee
+++ core/KDView.coffee
@@ -682,7 +682,7 @@
     @setEmptyDragState()
     handle = if options.handle instanceof KDView then options.handle else @

-    @on "DragFinished", (e) => @isDragging = no
+    @on "DragFinished", (e) => @beingDragged = no

     handle.on "mousedown", (event)=>
       if "string" is typeof options.handle
@@ -728,13 +728,21 @@
     dragGlobDir  = dragDir.global
     dragCurDir   = dragDir.current

-    draggedDistance  = Math.max Math.abs(x), Math.abs(y)
-    if draggedDistance < 30 and not @isDragging
+    {axis}       = @getOptions().draggable
+    if axis
+      if axis is "x"
+        draggedDistance = Math.abs x
+      else
+        draggedDistance = Math.abs y
+    else
+      draggedDistance   = Math.max Math.abs(x), Math.abs(y)
+
+    if draggedDistance < 20 and not @beingDragged
       @dragIsAllowed = no
-      @isDragging    = no
+      @beingDragged  = no
     else
       @dragIsAllowed = yes
-      @isDragging    = yes
+      @beingDragged  = yes

     if x > dragRelPos.x
       dragCurDir.x  = 'right'


revive: activity feeds are working now as well

--- core/KDView.coffee
+++ core/KDView.coffee
@@ -401,7 +401,7 @@
       if shouldPrepend
       then @prepend subView, selector
       else @append subView, selector
-    else log "lazy view", subView
+    # else log "lazy view", subView

     subView.on "ViewResized", => subView.parentDidResize()


copy ecosystem_id when cloning task

copy ecosystem_id when cloning task

--- tutor/src/flux/task-plan.coffee
+++ tutor/src/flux/task-plan.coffee
@@ -267,7 +267,7 @@
   createClonedPlan: (newPlanId, {planId, courseId, due_at}) ->
     original = @_local[planId]
     originalPlan = {}
-    copyKeys = ['title', 'description', 'is_feedback_immediate', 'settings']
+    copyKeys = ['title', 'description', 'is_feedback_immediate', 'settings', 'ecosystem_id']
     each(copyKeys, (key) ->
       originalPlan[key] = cloneDeep(original[key])
       # need to prevent false from being returned and ending the each loop early.


update jetstate

update jetstate

--- demo/scripts/stypi/ot/jetstate.coffee
+++ demo/scripts/stypi/ot/jetstate.coffee
@@ -1,6 +1,5 @@
 #= require diff_match_patch
 #= require coffee/ot/jetsync
-#= require coffee/editor/attribution

 DIFF_DELETE = -1
 DIFF_INSERT = 1
@@ -58,16 +57,17 @@

 class JetTextState extends JetState
-  constructor: (@editor, @client, shadow, @sessionId) ->
+  constructor: (@editor, @client, delta, @sessionId) ->
     @type = JetState.TEXT
     @userId = @client.settings.userId
-    this.reset(shadow)
-
-  reset: (shadow) ->
-    @editor.setText(shadow)
-    @arrived = JetDelta.getInitial(shadow)
-    @inFlight = JetDelta.getIdentity(shadow.length)
-    @inLine = JetDelta.getIdentity(shadow.length)
+    this.reset(delta)
+
+  reset: (delta) ->
+    delta = JetDelta.getInitial(delta) if _.isString(delta)
+    @editor.applyDelta(delta)
+    @arrived = delta
+    @inFlight = JetDelta.getIdentity(@arrived.endLength)
+    @inLine = JetDelta.getIdentity(@arrived.endLength)

   localUpdate: (delta) ->
     this.applyDeltaToCursors(delta, @userId)


fix syntax error: whatever, coffee-script

--- workers/auth/lib/auth/authworker.coffee
+++ workers/auth/lib/auth/authworker.coffee
@@ -152,8 +152,11 @@
     { serviceUniqueName, serviceGenericName, routingKey, method, callback
     username, correlationName, socketId, deadService } = options

-    params = { routingKey, username, correlationName
-               serviceGenericName, deadService, replyExchange: @authExchange }
+    params = {
+      routingKey, username, correlationName
+      serviceGenericName, deadService
+      replyExchange: @authExchange
+    }

     @publishToService serviceUniqueName, method, params, callback


sidebar: change workspace item to jview

sidebar: change workspace item to jview

--- client/Main/navigation/navigationworkspaceitem.coffee
+++ client/Main/navigation/navigationworkspaceitem.coffee
@@ -1,4 +1,4 @@
-class NavigationWorkspaceItem extends KDCustomHTMLView
+class NavigationWorkspaceItem extends JView

   constructor: (options = {}, data) ->

@@ -9,13 +9,16 @@

   init: ->

-    data = @getData()
+    @unsetClass 'kdview'

-    @setPartial """
-

-      #{data.title}
-      0
-    """
+    { href, title } = @getData()
+    href = KD.utils.groupifyLink href
+
+    @title = new CustomLinkView { href, title }
+
+    @unreadCount = new KDCustomHTMLView
+      tagName  : 'cite'
+      cssClass : 'count hidden'

   click: (event) ->
@@ -48,3 +51,11 @@

     new WorkspaceSettingsPopup { position, delegate: navItem }

+
+  pistachio: ->
+    """
+

+    {{> @title}}
+    {{> @unreadCount}}
+    """
+


Sidebar: Revive workspace plain objects

Sidebar: Revive workspace plain objects

--- client/Social/Activity/sidebar/activitysidebar.coffee
+++ client/Social/Activity/sidebar/activitysidebar.coffee
@@ -529,6 +529,9 @@
         KD.userWorkspaces.push @getDummyWorkspace machine

       KD.userWorkspaces.forEach (workspace) ->
+
+        unless workspace instanceof KD.remote.api.JWorkspace
+          workspace = KD.remote.revive workspace

         if workspace.machineUId is machine.uid
           ideRoute = "/IDE/#{machine.slug or machine.label}/#{workspace.slug}"


removed a little code duplication

removed a little code duplication

--- core/app/backbone/views/interactors_view.coffee
+++ core/app/backbone/views/interactors_view.coffee
@@ -26,14 +26,12 @@
       @render()

   templateHelpers: =>
-    past_action:
-      switch @collection.type
-        when 'weakening'
-          Factlink.Global.t.fact_disbelieve_past_action
-        when 'supporting'
-          Factlink.Global.t.fact_believe_past_action
-        when 'doubting'
-          Factlink.Global.t.fact_doubt_past_action
+    translation = switch @collection.type
+      when 'weakening' then 'fact_disbelieve_past_action'
+      when 'supporting' then 'fact_believe_past_action'
+      when 'doubting' then 'fact_doubt_past_action'
+
+    past_action: Factlink.Global.t[translation]

   showAll: (e) ->
     e.stopPropagation()


Fix sharing tests

Fix sharing tests

--- tests/sharing_tests.coffee
+++ tests/sharing_tests.coffee
@@ -121,7 +121,8 @@

         # We stub the `notifyRecipient` module to avoid calling it (if so it would
         # try to request the url we declare in the share object).
-        stubFn = (route, request, callback) ->
+        stubFn = (url, route, request, callback) ->
+            spyUrl = url
             spyRoute   = route
             spyRequest = request
             callback null # to mimick success
@@ -211,7 +212,7 @@
             # remove previously defined stub...
             notifyStub.restore()
             # ... and generate a new one that mimicks failure
-            stubFn = (route, request, callback) ->
+            stubFn = (url, route, request, callback) ->
                 callback "Error" # to mimick failure we return something
             notifyStub = sinon.stub Sharing, "notifyRecipient", stubFn

@@ -344,7 +345,7 @@

         # sharing.notifyRecipient stub (lib/sharing.coffee).
         # Returning `null` mimicks success.
-        notifyTargetFn   = (route, notification, callback) -> callback null
+        notifyTargetFn   = (url, route, notification, callback) -> callback null
         notifyTargetStub = {}

         beforeEach (done) ->
@@ -357,15 +358,12 @@
             done()

         it 'should define the notifications correctly and call the route
-        "services/sharing/cancel"', (done) ->
+        "services/sharing/revoke"', (done) ->
             notifyTargetStub.restore() # cancel stub
             # change to a custom stub that tests the values passed
-            testNotifyTargetFn = (route, notification, callback) ->
-                route.should.equal "services/sharing/cancel"
+            testNotifyTargetFn = (url, route, notification, callback) ->
+                route.should.equal "services/sharing/revoke"
                 urls.should.contain notification.recipientUrl
-                should.exist notification.token
-                tokens.should.contain notification.token
-                notification.shareID.should.equal req.share.shareID
                 notification.desc.should.equal "The sharing
                     #{req.share.shareID} has been deleted"
                 callback null
@@ -399,7 +397,7 @@
         it 'should return an error when a notification could not be sent',
         (done) ->
             notifyTargetStub.restore() # cancel stub
-            errNotifyTargetFn = (route, notification, callback) ->
+            errNotifyTargetFn = (url, route, notification, callback) ->
                 callback "Error"
             notifyTargetStub = sinon.stub Sharing, "notifyRecipient",
                 errNotifyTargetFn
@@ -415,20 +413,24 @@
         # Correct answer structure expected
         answer =
             id          : 'IdOfTheRecipientShareDocument'
-            shareID     : 'IdOfTheSharerShareDocument'
+            #shareID     : 'IdOfTheSharerShareDocument'
             accepted    : true
-            preToken    : 'preToken'
-            recipientUrl: 'urlOfTheRecipient'
-            sharerUrl   : 'urlOfTheSharer'
-            rules       : [{id: 1, docType: 'event'}, {id: 2, docType: 'event'}]
+            #preToken    : 'preToken'
+            #recipientUrl: 'urlOfTheRecipient'
+            #sharerUrl   : 'urlOfTheSharer'
+            #rules       : [{id: 1, docType: 'event'}, {id: 2, docType: 'event'}]

         # We stub the addAccess module from lib/token.coffee: we return an
         # error to avoid having our code run entirely if a test fails.
         addAccessStub = {}
-        # Same for the remove function
+        # Same for the remove and get function
         dbRemoveStub  = {}
+        dbGetStub = {}

         before (done) ->
+            dbGetStub    = sinon.stub db, "get", (id, callback) ->
+                callback null, _.cloneDeep answer
+
             addAccessStub = sinon.stub libToken, "addAccess",
                 (access, callback) ->
                     callback new Error "libToken.addAccess"
@@ -439,6 +441,7 @@
             done()

         after (done) ->
+            dbGetStub.restore()
             addAccessStub.restore()
             dbRemoveStub.restore()
             done()
@@ -463,15 +466,6 @@
                 res.body.error.should.equal "Bad request: body is incomplete"
                 done()

-        it 'should return an error when the req structure is incorrect: shareID
-        is missing or empty', (done) ->
-            data = _.cloneDeep answer
-            data.shareID = null
-            client.post "services/sharing/sendAnswer/", data,
-            (err, res, body) ->
-                res.statusCode.should.equal 400
-                res.body.error.should.equal "Bad request: body is incomplete"
-                done()

         it 'should return an error when the req structure is incorrect: accepted
         is missing or empty', (done) ->
@@ -481,68 +475,6 @@
             (err, res, body) ->
                 res.statusCode.should.equal 400
                 res.body.error.should.equal "Bad request: body is incomplete"
-                done()
-
-        it 'should return an error when the req structure is incorrect: preToken
-        is missing or empty', (done) ->
-            data = _.cloneDeep answer
-            data.preToken = ''
-            client.post "services/sharing/sendAnswer/", data,
-            (err, res, body) ->
-                res.statusCode.should.equal 400
-                res.body.error.should.equal "Bad request: body is incomplete"
-                done()
-
-        it 'should return an error when the req structure is incorrect: url is
-        missing or empty', (done) ->
-            data = _.cloneDeep answer
-            data.recipientUrl = null
-            client.post "services/sharing/sendAnswer/", data,
-            (err, res, body) ->
-                res.statusCode.should.equal 400
-                res.body.error.should.equal "Bad request: body is incomplete"
-                done()
-
-        it 'should return an error when the req structure is incorrect:
-        sharerUrl is missing or empty', (done) ->
-            data = _.cloneDeep answer
-            data.sharerUrl = null
-            client.post "services/sharing/sendAnswer/", data,
-            (err, res, body) ->
-                res.statusCode.should.equal 400
-                res.body.error.should.equal "Bad request: body is incomplete"
-                done()
-
-        it 'should return an error when the req structure is incorrect: rules is
-        missing or empty', (done) ->
-            data = _.cloneDeep answer
-            data.rules = []
-            client.post "services/sharing/sendAnswer/", data,
-            (err, res, body) ->
-                res.statusCode.should.equal 400
-                res.body.error.should.equal "Bad request: body is incomplete"
-                done()
-
-        it 'should return an error when the req structure is incorrect: a rule
-        is missing an id', (done) ->
-            data = _.cloneDeep answer
-            data.rules = [{id: 1, docType: 'event'},{id: '', docType: 'event'}]
-            client.post "services/sharing/sendAnswer/", data,
-            (err, res, body) ->
-                res.statusCode.should.equal 400
-                res.body.error.should.equal "Bad request: incorrect rule
-                    detected"
-                done()
-
-        it 'should return an error when the req structure is incorrect: a rule
-        is missing a docType', (done) ->
-            data = _.cloneDeep answer
-            data.rules = [{id: 1, docType: 'event'},{id: 2}]
-            client.post "services/sharing/sendAnswer/", data,
-            (err, res, body) ->
-                res.statusCode.should.equal 400
-                res.body.error.should.equal "Bad request: incorrect rule
-                    detected"
                 done()

         it 'should return an error when addAccess failed', (done) ->
@@ -594,12 +526,6 @@
                 should.exist req.share
                 should.exist req.share.token
                 req.share.id.should.equal answer.id
-                req.share.shareID.should.equal answer.shareID
-                req.share.preToken.should.equal answer.preToken
-                req.share.accepted.should.equal answer.accepted
-                req.share.recipientUrl.should.equal answer.recipientUrl
-                req.share.sharerUrl.should.equal answer.sharerUrl
-                req.share.rules.should.deep.equal answer.rules
                 done()

@@ -625,7 +551,7 @@

         beforeEach (done) ->
             notifyTargetStub = sinon.stub Sharing, "notifySharer",
-                (route, data, callback) ->
+                (url, route, data, callback) ->
                     callback new Error "Sharing.notifySharer"

             done()
@@ -648,7 +574,7 @@
         it 'should send success when notifySharer succeeded', (done) ->
             # We change the stub for one that succeeds
             notifyTargetStub.restore()
-            notifyTargetFnOk = (route, data, callback) ->
+            notifyTargetFnOk = (url, route, data, callback) ->
                 callback null
             notifyTargetStub = sinon.stub Sharing, "notifySharer",
                 notifyTargetFnOk
@@ -742,15 +668,6 @@
         missing/empty', (done) ->
             data = _.cloneDeep req.body
             data.preToken = null
-            client.post 'services/sharing/answer/', data, (err, res, body) ->
-                res.body.error.should.equal "Bad request: body is incomplete"
-                res.statusCode.should.equal 400
-                done()
-
-        it 'should return an error when the body is incomplete: token is
-        missing/empty', (done) ->
-            data = _.cloneDeep req.body
-            data.token = ''
             client.post 'services/sharing/answer/', data, (err, res, body) ->
                 res.body.error.should.equal "Bad request: body is incomplete"
                 res.statusCode.should.equal 400


Parse out the score given into an int.

Parse out the score given into an int.

--- common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -209,8 +209,8 @@
     fd.append('feedback', feedback)
     fd.append('submission_id', submission_id)
     fd.append('grader_id', grader_id)
-    if(score)
-      fd.append('score', score)
+    if(score && parseInt(score) != NaN)
+      fd.append('score', parseInt(score))

     settings =


redirect to user slug not username

redirect to user slug not username

--- components/new_channel/client/new_channel_view.coffee
+++ components/new_channel/client/new_channel_view.coffee
@@ -61,5 +61,5 @@
       success: =>
         analytics.track.click "New Channel created"
         @$('.grid__block').removeClass 'grid__block--loading'
-        document.location.href = "/#{@model.get('user').username}/#{@model.get('slug')}"
+        document.location.href = "/#{@model.get('user').slug}/#{@model.get('slug')}"


Lint fixes for HasAppInterface trait.

Lint fixes for HasAppInterface trait.


Education: video fullscreen on click

Education: video fullscreen on click

--- client/Education/AppView.coffee
+++ client/Education/AppView.coffee
@@ -30,6 +30,16 @@

   viewAppended: JView::viewAppended
+
+  click:(event)->
+
+    if $(event.target).is 'video'
+      video = @$('video')[0]
+      if KD.utils.isFullscreen()
+        KD.utils.exitFullscreen()
+      else
+        KD.utils.enterFullscreen video
+

   pistachio : ->
     """


 Merge branch 'master.kite' into 'master'

Master.Kite

--- client/Main/kite/newkite.coffee
+++ client/Main/kite/newkite.coffee
@@ -4,6 +4,7 @@

   {setAt} = Bongo.JsPath
   [NOTREADY, READY, CLOSED] = [0,1,3]
+  uniqueID = Bongo.createId()  # will be different for each tab

   constructor: (kite, authentication, options={})->
     super options
@@ -64,6 +65,14 @@
       authentication   : @authentication
       withArgs         : args
       responseCallback : cb
+      kite             :
+        username       : "#{KD.nick()}"
+        environment    : "#{KD.config.environment}"
+        name           : "browser"
+        version        : "1.0.#{KD.config.version}"
+        region         : "browser"
+        hostname       : "browser"
+        id             : uniqueID

     # Normally the request is made with the following statement:
     #   @proto.request method, [options, cb]


router: accumulate a breadcrumb for our users

--- client/Main/kodingrouter.coffee
+++ client/Main/kodingrouter.coffee
@@ -10,7 +10,7 @@
     if sectionName? then " - #{sectionName}" else ''

   constructor:(@defaultRoute)->
-
+    @breadcrumb = []
     @defaultRoute or= location.pathname + location.search
     @openRoutes     = {}
     @openRoutesById = {}
@@ -44,6 +44,8 @@
       console.warn "Contract warning: shared route #{route} is not implemented."

   handleRoute:(route, options={})->
+    @breadcrumb.push route
+
     entryPoint = options.entryPoint or KD.config.entryPoint
     frags      = route.split("?")[0].split "/"
     name       = if frags[1] is entryPoint?.slug then frags[2] else frags[1]


MainApp: utils extend fix

MainApp: utils extend fix

--- client/app/MainApp/utils.coffee
+++ client/app/MainApp/utils.coffee
@@ -1,5 +1,4 @@
-
-__utils.extend
+__utils.extend __utils,

   proxifyUrl:(url="", options={})->


AppController: Created default permissions map with initial value.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -465,6 +465,7 @@
     @activeTabView.emit 'DrawingPaneRequested', paneHash

   moveTab: (direction) ->
+
     return unless @activeTabView.parent?

     panel = @activeTabView.parent.parent
@@ -868,9 +869,11 @@
     nickname           = KD.nick()
     myWatchMapName     = "#{nickname}WatchMap"
     mySnapshotName     = "#{nickname}Snapshot"
+    defaultPermission  = default: 'edit'

     @participants      = @rtm.getFromModel 'participants'
     @changes           = @rtm.getFromModel 'changes'
+    @permissions       = @rtm.getFromModel 'permissions'
     @broadcastMessages = @rtm.getFromModel 'broadcastMessages'
     @pingTime          = @rtm.getFromModel 'pingTime'
     @myWatchMap        = @rtm.getFromModel myWatchMapName
@@ -878,6 +881,7 @@

     @participants      or= @rtm.create 'list',   'participants', []
     @changes           or= @rtm.create 'list',   'changes', []
+    @permissions       or= @rtm.create 'map',    'permissions', defaultPermission
     @broadcastMessages or= @rtm.create 'list',   'broadcastMessages', []
     @pingTime          or= @rtm.create 'string', 'pingTime'
     @myWatchMap        or= @rtm.create 'map',    myWatchMapName, {}


fix problem of saving int typed setting #701

--- client/views/admin/admin.coffee
+++ client/views/admin/admin.coffee
@@ -48,6 +48,8 @@
 			value = null
 			if setting.type is 'string'
 				value = _.trim(t.$("[name=#{setting._id}]").val())
+			else if setting.type is 'int'
+				value = parseInt(_.trim(t.$("[name=#{setting._id}]").val()))
 			else if setting.type is 'boolean' and t.$("[name=#{setting._id}]:checked").length
 				value = if t.$("[name=#{setting._id}]:checked").val() is "1" then true else false


auth: implement handler for kite.who

auth: implement handler for kite.who

--- workers/auth/lib/auth/authworker.coffee
+++ workers/auth/lib/auth/authworker.coffee
@@ -8,7 +8,7 @@
     type        : 'fanout'
     autoDelete  : yes

-  constructor:(@bongo, @resourceName, @presenceExchange='services-presence')->
+  constructor: (@bongo, @resourceName, @presenceExchange='services-presence') ->
     @services = {}
     @clients  = {
       bySocketId    : {}
@@ -19,26 +19,26 @@

   bound: require 'koding-bound'

-  authenticate:(messageData, routingKey, callback)->
+  authenticate: (messageData, routingKey, callback) ->
     {clientId, channel, event} = messageData
     @requireSession clientId, routingKey, callback

-  requireSession:(clientId, routingKey, callback)->
+  requireSession: (clientId, routingKey, callback) ->
     {JSession} = @bongo.models
-    JSession.fetchSession clientId, (err, session)=>
+    JSession.fetchSession clientId, (err, session) =>
       if err? or not session? then @rejectClient routingKey
       else
         tokenHasChanged = session.clientId isnt clientId
         @updateSessionToken session.clientId, routingKey  if tokenHasChanged
         callback session

-  updateSessionToken:(clientId, routingKey)->
+  updateSessionToken: (clientId, routingKey) ->
     @bongo.respondToClient routingKey,
       method      : 'updateSessionToken'
       arguments   : [clientId]
       callbacks   : {}

-  getNextServiceInfo:(serviceType)->
+  getNextServiceInfo: (serviceType) ->
     count = @counts[serviceType] ?= 0
     servicesOfType = @services[serviceType]
     return  unless servicesOfType?.length
@@ -46,11 +46,11 @@
     @counts[serviceType] += 1
     return serviceInfo

-  addService:({serviceGenericName, serviceUniqueName, loadBalancing})->
+  addService: ({serviceGenericName, serviceUniqueName, loadBalancing}) ->
     servicesOfType = @services[serviceGenericName] ?= []
     servicesOfType.push {serviceUniqueName, loadBalancing}

-  removeService:({serviceGenericName, serviceUniqueName})->
+  removeService: ({serviceGenericName, serviceUniqueName}) ->
     servicesOfType = @services[serviceGenericName]
     index = (i for s, i in servicesOfType \
                when s.serviceUniqueName is serviceUniqueName)[0]
@@ -58,7 +58,7 @@
     clientsByExchange = @clients.byExchange[serviceUniqueName]
     clientsByExchange?.forEach @bound 'cycleClient'

-  cycleClient:(client)->
+  cycleClient: (client) ->
     {routingKey} = client
     @bongo.respondToClient routingKey, {
       method      : 'cycleChannel'
@@ -66,7 +66,7 @@
       callbacks   : {}
     }

-  removeClient:(rest...)->
+  removeClient: (rest...) ->
     if rest.length is 1
       [client] = rest
       return @removeClient client.socketId, client.exchange, client.routingKey
@@ -75,7 +75,7 @@
     delete @clients.byExchange[exchange]
     delete @clients.byRoutingKey[routingKey]

-  addClient:(socketId, exchange, routingKey, sendOk=yes)->
+  addClient: (socketId, exchange, routingKey, sendOk=yes) ->
     if sendOk
       @bongo.respondToClient routingKey, {
         method    : 'authOk'
@@ -90,7 +90,7 @@
     clientsByRoutingKey.push client
     clientsByExchange.push client

-  rejectClient:(routingKey, message)->
+  rejectClient: (routingKey, message) ->
     return console.trace()  unless routingKey?
     @bongo.respondToClient routingKey, {
       method    : 'error'
@@ -98,31 +98,50 @@
       callbacks : {}
     }

-  publishToService:(exchangeName, routingKey, payload, callback)->
+  publishToService: (exchangeName, routingKey, payload, callback) ->
     {connection} = @bongo.mq
     connection.exchange exchangeName, AUTH_EXCHANGE_OPTIONS,
-      (exchange)=>
+      (exchange) =>
         exchange.publish routingKey, payload
         exchange.close() # don't leak a channel
         callback? null

+  sendAuthMessage: (options) ->
+    { serviceUniqueName, routingKey, method, callback
+    username, correlationName, socketId } = options
+
+    params = { routingKey, username, correlationName }
+
+    @publishToService serviceUniqueName, method, params, callback
+
+  sendAuthJoin: (options) ->
+    { socketId, serviceUniqueName, routingKey } = options
+    options.callback = => @addClient socketId, serviceUniqueName, routingKey
+    options.method = 'auth.join'
+    @sendAuthMessage options
+
+  sendAuthWho: (options) ->
+    options.method = 'auth.who'
+    @sendAuthMessage options
+
   joinClient: do ->

-    joinClientHelper =(messageData, routingKey, socketId)->
-      @authenticate messageData, routingKey, (session)=>
+    joinClientHelper = (messageData, routingKey, socketId) ->
+      @authenticate messageData, routingKey, (session) =>
         serviceInfo = @getNextServiceInfo messageData.name
-        {serviceUniqueName, loadBalancing} = serviceInfo
+        { serviceUniqueName, loadBalancing } = serviceInfo
+        params = {
+          serviceUniqueName
+          routingKey
+          username        : session.username
+          correlationName : messageData.correlationName
+          # maybe the callback wants this:
+          socketId
+        }
         if loadBalancing
-          @publishToService serviceUniqueName, 'auth.who', {
-            username    : session.username
-            routingKey  : routingKey
-          }
+        then @sendAuthWho params
         else if serviceUniqueName?
-          @publishToService serviceUniqueName, 'auth.join', {
-            username    : session.username
-            routingKey  : routingKey
-          }, =>
-            @addClient socketId, serviceUniqueName, routingKey
+        then @sendAuthJoin params
         else
           @bongo.respondToClient routingKey, {
             method    : 'error'
@@ -130,35 +149,35 @@
             callbacks : {}
           }

-    ensureGroupPermission =(group, account, roles, callback)->
+    ensureGroupPermission = (group, account, roles, callback) ->
       {JPermissionSet, JGroup} = @bongo.models
       client = {context: group.slug, connection: delegate: account}
       JPermissionSet.checkPermission client, "read activity", group,
-        (err, hasPermission)->
+        (err, hasPermission) ->
           if err then callback err
           else unless hasPermission
             callback {message: 'Access denied!', code: 403}
           else
             JGroup.fetchSecretChannelName group.slug, callback

-    joinClientGroupHelper =(messageData, routingKey, socketId)->
+    joinClientGroupHelper = (messageData, routingKey, socketId) ->
       {JAccount, JGroup} = @bongo.models
-      fail = (err)=>
+      fail = (err) =>
         console.error err  if err
         @rejectClient routingKey
-      @authenticate messageData, routingKey, (session)=>
+      @authenticate messageData, routingKey, (session) =>
         unless session then fail()
         else JAccount.one {'profile.nickname': session.username},
-          (err, account)=>
+          (err, account) =>
             if err or not account then fail err
-            else JGroup.one {slug: messageData.group}, (err, group)=>
+            else JGroup.one {slug: messageData.group}, (err, group) =>
               if err or not group then fail err
               else
-                group.fetchRolesByAccount account, (err, roles)=>
+                group.fetchRolesByAccount account, (err, roles) =>
                   if err or not roles then fail err
                   else
                     ensureGroupPermission.call this, group, account, roles,
-                      (err, secretChannelName)=>
+                      (err, secretChannelName) =>
                         if err or not secretChannelName
                           @rejectClient routingKey
                         else
@@ -166,7 +185,7 @@
                           message = JSON.stringify secretChannelName
                           @bongo.respondToClient setSecretNameEvent, message

-    joinClient =(messageData, socketId)->
+    joinClient = (messageData, socketId) ->
       {channel, routingKey, serviceType} = messageData
       switch serviceType
         when 'bongo', 'kite'
@@ -182,19 +201,19 @@
         else
           @rejectClient routingKey

-  cleanUpClient:(client)->
+  cleanUpClient: (client) ->
     @removeClient client
     @bongo.mq.connection.exchange client.exchange, AUTH_EXCHANGE_OPTIONS,
-      (exchange)->
+      (exchange) ->
         exchange.publish 'auth.leave', {
           routingKey: client.routingKey
         }
         exchange.close() # don't leak a channel!

-  cleanUpAfterDisconnect:(socketId)->
+  cleanUpAfterDisconnect: (socketId) ->
     @clients.bySocketId[socketId]?.forEach @bound 'cleanUpClient'

-  parseServiceKey =(serviceKey)->
+  parseServiceKey = (serviceKey) ->
     last = null
     serviceInfo = serviceKey.split('.').reduce (acc, edge, i)->
       unless i % 2 then last = edge
@@ -212,7 +231,7 @@

     return serviceInfo

-  monitorPresence:(connection)->
+  monitorPresence: (connection) ->
     Presence = require 'koding-rabbit-presence'
     @presence = new Presence {
       connection
@@ -227,17 +246,17 @@
       catch e then console.error e
     @presence.listen()

-  connect:->
+  connect: ->
     {bongo} = this
     bongo.mq.ready =>
       {connection} = bongo.mq
       @monitorPresence connection

-      connection.exchange 'authAll', AUTH_EXCHANGE_OPTIONS, (authAllExchange)=>
-        connection.queue '', {exclusive:yes}, (authAllQueue)=>
+      connection.exchange 'authAll', AUTH_EXCHANGE_OPTIONS, (authAllExchange) =>
+        connection.queue '', {exclusive:yes}, (authAllQueue) =>
           authAllQueue.bind authAllExchange, ''
           authAllQueue.on 'queueBindOk', =>
-            authAllQueue.subscribe (message, headers, deliveryInfo)=>
+            authAllQueue.subscribe (message, headers, deliveryInfo) =>
               {routingKey} = deliveryInfo
               messageStr = "#{message.data}"
               switch routingKey
@@ -245,11 +264,11 @@
                 when 'broker.clientDisconnected'
                   @cleanUpAfterDisconnect messageStr

-      connection.exchange 'auth', AUTH_EXCHANGE_OPTIONS, (authExchange)=>
+      connection.exchange 'auth', AUTH_EXCHANGE_OPTIONS, (authExchange) =>
         connection.queue  'auth', (authQueue)=>
           authQueue.bind authExchange, ''
           authQueue.on 'queueBindOk', =>
-            authQueue.subscribe (message, headers, deliveryInfo)=>
+            authQueue.subscribe (message, headers, deliveryInfo) =>
               {routingKey, correlationId} = deliveryInfo
               socketId = correlationId
               messageStr = "#{message.data}"
@@ -260,7 +279,13 @@
                 when 'kite.leave'
                   @removeService messageData
                 when 'kite.who'
-                  console.log 'got a kite.who', messageData
+                  # TODO: make sure this is OK for untrusted kites:
+                  @sendAuthJoin {
+                    serviceUniqueName : messageData.serviceUniqueName
+                    routingKey        : messageData.routingKey
+                    correlationName   : messageData.correlationName
+                    username          : messageData.username
+                  }
                 when "client.auth"
                   @joinClient messageData, socketId
                 else


changes to the auth worker

changes to the auth worker

--- workers/auth/lib/auth/authworker.coffee
+++ workers/auth/lib/auth/authworker.coffee
@@ -112,23 +112,33 @@
               exchange.close() # don't leak a channel
               @addClient socketId, exchange.name, routingKey

+    ensureGroupPermission =(group, account, roles)->
+      console.log 'ensure group permission', group, account, roles
+      {JPermissionSet} = @bongo.models
+      client = {context: group.slug}
+      JPermissionSet.checkPermission client, "read activities", group,
+        (err, hasPermission)->
+          if err then callback err
+          else
+            console.log {hasPermission}

     joinClientGroupHelper =(messageData, routingKey, socketId)->
+      console.trace()
       {JAccount, JGroup} = @bongo.models
-      fail = (err)=> @rejectClient routingKey; console.error err  if err
-      @authenticate messageData, routingKey, (session)->
+      fail = (err)=>
+        console.error err  if err
+        @rejectClient routingKey
+      @authenticate messageData, routingKey, (session)=>
         unless session then fail()
-        else
-          selector = {'profile.nickname': session.username}
-          JAccount.one selector, (err, account)->
+        else JAccount.one {'profile.nickname': session.username},
+          (err, account)=>
             if err or not account then fail err
-            else
-              JGroup.one {slug: messageData.group}, (err, group)->
-                if err or not group then fail err
-                else
-                  group.fetchRolesByAccount account, (err, roles)->
-                    if err or not roles then fail err
-                    else
+            else JGroup.one {slug: messageData.group}, (err, group)=>
+              if err or not group then fail err
+              else
+                group.fetchRolesByAccount account, (err, roles)=>
+                  if err or not roles then fail err
+                  else ensureGroupPermission.call this, group, account, roles

     joinClient =(messageData, socketId)->
       {channel, routingKey, serviceType} = messageData
@@ -189,6 +199,19 @@
     bongo.mq.ready =>
       {connection} = bongo.mq
       @monitorPresence connection
+
+      connection.exchange "#{@resourceName}All", AUTH_EXCHANGE_OPTIONS, (authAllExchange)=>
+        connection.queue '', {exclusive:yes}, (authAllQueue)=>
+          authAllQueue.bind authAllExchange, ''
+          authAllQueue.on 'queueBindOk', =>
+            authAllQueue.subscribe (message, headers, deliveryInfo)=>
+              {routingKey} = deliveryInfo
+              messageStr = "#{message.data}"
+              switch routingKey
+                when 'broker.clientConnected' then # ignore
+                when 'broker.clientDisconnected'
+                  @cleanUpAfterDisconnect messageStr
+
       connection.exchange @resourceName, AUTH_EXCHANGE_OPTIONS, (authExchange)=>
         connection.queue  @resourceName, (authQueue)=>
           authQueue.bind authExchange, ''
@@ -196,12 +219,8 @@
             authQueue.subscribe (message, headers, deliveryInfo)=>
               {routingKey, correlationId} = deliveryInfo
               socketId = correlationId
-              messageStr = "#{message.data}"
               messageData = (try JSON.parse messageStr) or message
               switch routingKey
-                when 'broker.clientConnected' then # ignore
-                when 'broker.clientDisconnected'
-                  @cleanUpAfterDisconnect messageStr
                 when 'kite.join'
                   @addService messageData
                 when 'kite.leave'


show deal image

show deal image

--- scripts/steam.coffee
+++ scripts/steam.coffee
@@ -34,7 +34,7 @@
   robot.respond /daily deal|сделка дня/i, (msg) ->
     getItems msg, location, parseDeals, 1, (deal) ->
       if deal.url
-        msg.send "#{deal.discount} // #{deal.url}"
+        msg.send "#{deal.discount} // #{deal.url} (#{deal.image})"
       else
         msg.send "Сегодня сделки дня нет"


more tests + testability improvements

more tests + testability improvements

--- tests/coffee/storage.test.coffee
+++ tests/coffee/storage.test.coffee
@@ -29,8 +29,14 @@
     mqtt = td.object ['publish']
     argIsFs = td.matchers.argThat (arg) -> arg is fs
     captor = td.matchers.captor()
-    storage agent, mqtt, dataDir: '/rootDir', domain: 'myDomain'
+    storage agent, mqtt, dataDir: '/rootDir', domain: 'myDomain', remotefsUrl: 'remotefsUrl'
     td.verify agent.on '/storage/delete', captor.capture()
+    captor.value {name:'name1'}, null, 'mycallback'
+    td.verify fs.writeFile '/rootDir/myDomain/.name1.delete.lock', td.matchers.anything(), captor.capture()
+    captor.value()
+    td.verify storageLib.remoteFs 'remotefsUrl', 'rm', {dir: '/myDomain/name1'}, captor.capture()
+    captor.value()
+    td.verify fs.unlink '/rootDir/myDomain/.name1.delete.lock', 'mycallback'

   it 'should periodically publish data storage statistics to mqtt', ->
     agent = td.object ['on']


Sidebar Spacing

Sidebar Spacing

--- docs/components/docs/DocsSidebar.cjsx
+++ docs/components/docs/DocsSidebar.cjsx
@@ -13,6 +13,9 @@
     'default':
       sidebar:
         paddingTop: '20px'
+
+      li:
+        paddingBottom: '8px'

       number:
         fontSize: '14px'
@@ -53,20 +56,22 @@
           id = /id: (.+)/.exec(file)[1]
           title = /title: (.+)/.exec(file)[1]
           sectionNumber = if fileName.split('-')[0].indexOf('.') is -1 then fileName.split('-')[0] else ''
-          <Tile key={ fileName } condensed>
-
{ sectionNumber }

+

+            <Tile key={ fileName } condensed>
+
{ sectionNumber }

-            { if sectionNumber
-                if @props.active is id
-                    { title }
-                  else
-                    { title }
-              else
-                if @props.active is id
-                    { title }
-                  else
-                    { title } }
+              { if sectionNumber
+                  if @props.active is id
+                      { title }
+                    else
+                      { title }
+                else
+                  if @props.active is id
+                      { title }
+                    else
+                      { title } }

-          </Tile> }
+            </Tile>
+
 }


clean-up attribute strings

clean-up attribute strings

--- app/components/frame-viewer.cjsx
+++ app/components/frame-viewer.cjsx
@@ -51,7 +51,7 @@
     frameDisplay = switch type
       when 'image'

-
+

           {if @state.loading

@@ -75,15 +75,15 @@

-                <button title={"draw"} title={"annotate"} className={"fa fa-mouse-pointer"} onClick={@togglePanOff}/>
+                <button title="annotate" className="fa fa-mouse-pointer" onClick={@togglePanOff}/>

-                <button title={"pan"} ref="pan" className={"fa fa-arrows"} onClick={@handleFocus.bind(this, "pan")} onFocus={@togglePanOn} onBlur={@togglePanOff}/>
+                <button title="pan" ref="pan" className="fa fa-arrows" onClick={@handleFocus.bind(this, "pan")} onFocus={@togglePanOn} onBlur={@togglePanOff}/>

               <button
-                title={ "zoom out" }
+                title="zoom out"
                 ref="zoomOut"
                 className={"zoom-out fa fa-minus" + if @cannotZoomOut() then " disabled" else "" }
                 onMouseDown={ @continuousZoom.bind(this, 1.1 ) }
@@ -97,9 +97,9 @@

               <button
-                title={ "zoom in" }
+                title="zoom in"
                 ref="zoomIn"
-                className={ "zoom-in fa fa-plus" }
+                className="zoom-in fa fa-plus"
                 onMouseDown={@continuousZoom.bind(this, .9)}
                 onMouseUp={@stopZoom}
                 onKeyDown={@keyDownZoomButton.bind(this,.9)}
@@ -110,7 +110,7 @@
               />

-              <button title={"reset zoom levels"} className={"reset fa fa-refresh" + if @cannotZoomOut() then " disabled" else ""} onClick={ this.zoomReset } ></button>
+              <button title="reset zoom levels" className={"reset fa fa-refresh" + if @cannotZoomOut() then " disabled" else ""} onClick={ this.zoomReset } ></button>

}


Uppercase in log sentence in server

Uppercase in log sentence in server

--- server/server.coffee
+++ server/server.coffee
@@ -69,7 +69,7 @@
       startGame socket, game

 http.listen port, ->
-  console.info "listening on http://localhost:#{port}"
+  console.info "Listening on http://localhost:#{port}"

 newGame = (socket) ->
   if not _.find(games, creator: socket.client.conn.id)


testing

testing

--- server/server.coffee
+++ server/server.coffee
@@ -39,7 +39,7 @@
 app.use loopback.favicon()

 # request pre-processing middleware
-app.use loopback.compress()
+#app.use loopback.compress()

 # boot scripts mount components like REST API
 boot app, __dirname


Fix #35

Fix #35

--- coffeequate/src/Equation.coffee
+++ coffeequate/src/Equation.coffee
@@ -65,16 +65,16 @@
 			return rightVars

 		sub: (substitutions, equivalencies) ->
-			if @left instanceof terminals.Variable
-				if equivalencies?
-					for equiv in equivalencies.get(@left.label)
-						if equiv of substitutions
-							expr = new operators.Add(@right, new operators.Mul("-1", @left))
-							return new Equation(expr.sub(substitutions, equivalencies))
-				else
-					if @left.label of substitutions
-						expr = new operators.Add(@right, new operators.Mul("-1", @left))
-						return new Equation(expr.sub(substitutions, equivalencies))
+			# if @left instanceof terminals.Variable
+			# 	if equivalencies?
+			# 		for equiv in equivalencies.get(@left.label)
+			# 			if equiv of substitutions
+			# 				expr = new operators.Add(@right, new operators.Mul("-1", @left))
+			# 				return new Equation(expr.sub(substitutions, equivalencies))
+			# 	else
+			# 		if @left.label of substitutions
+			# 			expr = new operators.Add(@right, new operators.Mul("-1", @left))
+			# 			return new Equation(expr.sub(substitutions, equivalencies))
 			return new Equation(@left, @right.sub(substitutions, equivalencies))

 		substituteExpression: (source, variable, equivalencies, eliminate=false) ->
@@ -98,13 +98,13 @@

 			results = []
 			for s in sources
-				if @left instanceof terminals.Variable and (@left.label == variable or @left.label in variableEquivalencies)
-					expr = new operators.Add(@right, new operators.Mul("-1", @left))
-					for i in (expr.substituteExpression(s, variable, equivalencies))
-						results.push(new Equation(i))
-				else
-					for i in @right.substituteExpression(s, variable, equivalencies)
-						results.push(new Equation(@left, i.expandAndSimplify(equivalencies)))
+				# if @left instanceof terminals.Variable and (@left.label == variable or @left.label in variableEquivalencies)
+				# 	expr = new operators.Add(@right, new operators.Mul("-1", @left))
+				# 	for i in (expr.substituteExpression(s, variable, equivalencies))
+				# 		results.push(new Equation(i))
+				# else
+				for i in @right.substituteExpression(s, variable, equivalencies)
+					results.push(new Equation(@left, i.expandAndSimplify(equivalencies)))
 			return results

 		expandAndSimplify: (equivalencies) ->


Update electron to v0.30.5

Update electron to v0.30.5

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -1,5 +1,5 @@
 POI_VERSION = '3.3.1'
-ELECTRON_VERSION = '0.30.0'
+ELECTRON_VERSION = '0.30.5'
 SYSTEM_BIT =
   win32: 'ia32'
   linux: 'x64'


add a css minification task

add a css minification task

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -6,6 +6,7 @@
 coffee = require('gulp-coffee')
 rename = require('gulp-rename')
 uglify = require('gulp-uglifyjs')
+minifycss = require('gulp-minify-css')
 sass = require('gulp-sass')
 autoprefixer = require('gulp-autoprefixer')
 header = require('gulp-header')
@@ -22,10 +23,10 @@
 sassBuildDir = './assets'
 pluginScript = './src/coffeescript/notifyr.coffee'
 pluginBuildDir = './dist'
-pluginName = 'jquery.notifyr.js'
-pluginNameMin = 'jquery.notifyr.min.js'
-pluginCssName = 'jquery.notifyr.css'
-pluginCssNameMin = 'jquery.notifyr.min.css'
+pluginName = 'notifyr.js'
+pluginNameMin = 'notifyr.min.js'
+pluginCssName = 'notifyr.css'
+pluginCssNameMin = 'notifyr.min.css'
 banner = "/*!\n
         #{pak.name} v#{pak.version} (#{pak.homepage})\n
         Copyright (c) #{new Date().getFullYear()} #{pak.author}\n
@@ -116,7 +117,7 @@
   )
   return

-gulp.task 'minify', ->
+gulp.task 'minify-js', ->
   gulp
     .src("#{pluginBuildDir}/#{pluginName}")
     .pipe(uglify())
@@ -125,6 +126,16 @@
     .pipe(size({showFiles: true}))
     .pipe(size({showFiles: true, gzip: true}))
     .pipe(gulp.dest(pluginBuildDir))
+
+gulp.task 'minify-css', ->
+  gulp
+    .src("#{pluginBuildDir}/#{pluginCssName}")
+    .pipe(minifycss())
+    .pipe(rename(pluginCssNameMin))
+    .pipe(size({showFiles: true}))
+    .pipe(size({showFiles: true, gzip: true}))
+    .pipe(gulp.dest(pluginBuildDir))
+    .on 'error', (e) -> log.error e

 gulp.task 'watch', ->
   gulp
@@ -143,7 +154,8 @@
     'compile-js',
     'compile-css',
     'browserify',
-    'minify',
+    'minify-js',
+    'minify-css',
     callback
   )
   return


fix(imagemin): add .png files

fix(imagemin): add .png files

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -65,6 +65,7 @@
 			'.gif'
 			'.jpeg'
 			'.jpg'
+			'.png'
 		]
 	SCRIPTS:
 		COMPILED: [


Switched to use configurable paths for the website.

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -53,6 +53,10 @@
     fonts: 'src/assets/fonts'
     icons: 'src/assets/images/icons'
     images: 'src/assets/images'
+  website:
+    base: 'src/website'
+    partials: 'src/website/partials'
+    layouts: 'src/website/layouts'
   build:
     base: 'build'
     assets: 'build/assets'
@@ -244,17 +248,17 @@
   hbs = require 'express-hbs'
   through = require 'through2'

-  gulp.src 'src/website/**/*.html'
+  gulp.src "#{paths.website.base}/**/*.html"
     .pipe through.obj (file, enc, cb) ->
       render = hbs.create().express3
-        viewsDir: "src/website"
-        partialsDir: 'src/website/partials'
-        layoutDir: 'src/website/layouts'
-        defaultLayout: 'src/website/layouts/default.html'
+        viewsDir: paths.website.base
+        partialsDir: paths.website.partials
+        layoutDir: paths.website.layouts
+        defaultLayout: "#{paths.website.layouts}/default.html"
         extName: 'html'
       locals = {
         settings: {
-          views: 'src/website'
+          views: paths.website.base
         },
         version: pkg.version
       }


logging

logging

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -18,6 +18,9 @@

 config_file = if args.config? then args.config else path.resolve('./config.json')
 config = JSON.parse(fs.readFileSync(config_file, 'utf8'));
+console.log 'READ CONFIG VALUES: '
+for key, value of config
+  console.log "#{key} - #{value}"

 gulp.task 'clean', ->
   files = [ config.app + '/*.js',
@@ -39,7 +42,7 @@
   gulp.src config.styles
     .pipe stylus (use: [nib()], errors: true)
     .pipe minifyCSS keepBreaks: true
-    .pipe concat config.app_style
+    .pipe concat config.final_style_name
     .pipe gulp.dest config.app
     .pipe connect.reload()

@@ -47,13 +50,14 @@
   gulp.src config.scripts
     .pipe changed config.app
     .pipe uglify()
-    .pipe concat config.app_script
+    .pipe concat config.final_script_name
     .pipe gulp.dest config.app
     .pipe connect.reload()

-gulp.task 'copy-brush-assets', ->
+gulp.task 'refresh-brush-assets', ->
+  dest = if args.dest? then args.config else console.error 'Enter destination folder: --dest=<..>'
   gulp.src 'dist'
-    .pipe gulp.dest 'examples/app'
+    .pipe gulp.dest dest

 gulp.task 'start-server', ->
   connect.server


Cross platform support Part.1

Cross platform support Part.1

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -2,6 +2,7 @@

 argv = require('yargs').argv
 fs = require 'fs-extra'
+path = require 'path'
 plugins = require('gulp-load-plugins')()
 yamljs = require 'yamljs'

@@ -32,8 +33,8 @@
   rmdirp dir_path

   fs.mkdirSync dir_path
-  fs.mkdirSync dir_path + '/' + lang.css.dest
-  fs.mkdirSync dir_path + '/' + lang.js.dest
+  fs.mkdirSync path.join(dir_path, lang.css.dest)
+  fs.mkdirSync path.join(dir_path, lang.js.dest)

 # recursively remove folder if exist
 rmdirp = (path) ->
@@ -48,14 +49,14 @@
 compileLangHandler = (this_lang, source_path, dest_dir, options) ->
   gulp.src source_path
     .pipe plugins[this_lang.plugin](options)
-    .pipe gulp.dest(dest_dir + '/' + this_lang.dest)
+    .pipe gulp.dest(path.join(dest_dir, this_lang.dest))
 compileManifest = (dest_dir, update_fn) ->
-  manifest_json = yamljs.load resources_path + '/manifest.yml'
+  manifest_json = yamljs.load path.join(resources_path, 'manifest.yml')
   update_fn manifest_json

-  dest_path = dest_dir + '/manifest.json'
+  dest_path = path.join dest_dir, 'manifest.json'
   fs.writeJSONSync dest_path, manifest_json
-getSourcePath = (this_lang) -> this_lang.source + '/*.' + this_lang.extName
+getSourcePath = (this_lang) -> path.join this_lang.source, '*.' + this_lang.extName
 watchLang = (lang_name, dest_dir, options) ->
   this_lang = lang[lang_name]

@@ -96,13 +97,13 @@
     manifest_json.version = argv.version

   for file_name in ['font', '_locales', 'license.txt']
-    fs.copySync file_name, compile_path + '/' + file_name
-  fs.copySync resources_path + '/img', compile_path + '/img'
+    fs.copySync file_name, path.join(compile_path, file_name)
+  fs.copySync path.join(resources_path, 'img'), path.join(compile_path, 'img')

 gulp.task 'compile-zip', ['compile-main', 'compile-others'], ->
-  gulp.src(compile_path + '/**')
-    .pipe(plugins.zip(argv.version + '.zip'))
-    .pipe(gulp.dest('.'))
+  gulp.src path.join(compile_path, '**')
+    .pipe plugins.zip(argv.version + '.zip')
+    .pipe gulp.dest('.')

 gulp.task 'compile', ['compile-zip'], ->
   rmdirp compile_path # useless after zipped
@@ -115,14 +116,14 @@
   watchLang 'html', dev_path, pretty: true

   # for js only (no compile, just soft link)
-  js_dest_path = dev_path + '/' + lang.js.dest
+  js_dest_path = path.join(dev_path, lang.js.dest)
   rmdirp js_dest_path
-  fs.symlinkSync '../' + lang.js.source, js_dest_path
+  fs.symlinkSync path.join('..', lang.js.source), js_dest_path

   compileManifest dev_path, (manifest_json) ->
     manifest_json.name += '(dev)'
     manifest_json.version = '0.0.0.0'

   for file_name in ['font', '_locales']
-    fs.copySync file_name, dev_path + '/' + file_name
-  fs.copySync resources_path + '/img-dev', dev_path + '/img'
+    fs.copySync file_name, path.join(dev_path, file_name)
+  fs.copySync path.join(resources_path, 'img-dev'), path.join(dev_path, 'img')


style(changelog): uppercase CHANGELOG.md filename

Be consistent with default naming of conventional-changelog package

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -21,7 +21,7 @@
 		version: pkg.version

 	changelog config, (err, log) ->
-		fs.writeFile './changelog.md', log
+		fs.writeFile './CHANGELOG.md', log

 gulp.task 'default', ['scripts', 'styles', 'views'], ->


Refs #3. Remove unused img task

Refs #3. Remove unused img task

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -6,16 +6,11 @@
 watch = require 'gulp-watch'
 insert = require 'gulp-insert'

-gulp.task 'default', ['coffee', 'img']
+gulp.task 'default', ['coffee']

 gulp.task 'watch', ->
   gulp.watch('./src/**/*.coffee', ['default'])
   gulp.watch('./test/fixtures/**/*.coffee', ['default'])
-
-gulp.task 'img', ->
-  gulp
-    .src('./src/**/*.png')
-    .pipe gulp.dest('./dist/')

 gulp.task 'coffee', ->
   gulp


Add icons to Gulpfile (maybe)

Add icons to Gulpfile (maybe)

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -95,6 +95,8 @@
       version: packageInfo.devDependencies["electron-prebuilt"]
       platform: platform
       arch: arch
+      winIcon: "./icon/ReliefValve.ico"
+      darwinIcon: "./icon/ReliefValve.icns"
       quiet: yes
     .pipe $.symdest "./dist/#{id}/Relief Valve v#{packageInfo.version}/"
   gulp.task "build:#{id}", ["clean:build", "dist:#{id}"], ->


removed process.coffee

removed process.coffee

--- data/process.coffee
+++ data/process.coffee
@@ -1,2 +0,0 @@
-if process.argv?[3] == '--csv'
-  console.log 'ues!'


List all on empty searches

List all on empty searches

--- app/scripts/views/results.coffee
+++ app/scripts/views/results.coffee
@@ -74,6 +74,7 @@
     _renderMatches: (search) ->
       return if @_lastSearch is search
       @_lastSearch = search
+      return @listAll() if search is ""

       @commandViews = @collection.filterMatches(search)
         .map (model) -> model.view.render()


Change config index to load json.

Change config index to load json.

--- config/index.coffee
+++ config/index.coffee
@@ -5,9 +5,9 @@
 configDir = process.env.CONFIG_DIR or './deploy'

 # Default, app mode, deploy and environment configs
-defaults   = require './defaults.coffee'
-deployConf = (try require "#{configDir}/#{appMode}.coffee") or null
-modeConf   = (try require "./modes/#{appMode}.coffee") or null
+defaults   = require './defaults.json'
+deployConf = (try require "#{configDir}/#{appMode}.json") or null
+modeConf   = (try require "./modes/#{appMode}.json") or null
 envConf    = require './env'

 Config   = _.extend {}, defaults, modeConf, deployConf, envConf


should trim the version string

should trim the version string

--- config/kites.coffee
+++ config/kites.coffee
@@ -2,7 +2,7 @@
 { join: joinPath } = require 'path'

 versionFile = (kiteName) ->
-  fs.readFileSync (joinPath __dirname, "../versions/#{ kiteName }.version"), 'utf-8'
+  (fs.readFileSync (joinPath __dirname, "../versions/#{ kiteName }.version"), 'utf-8').trim()

 module.exports =
   os:


test: Have command ouput in the right order

When concatenating stdout and stderr to get the whole output, then
the output contains first stdout and then stderr. For output to be
useful, it needs to contain stdout and stderr entwined, in the same
order as can be usually seen when the command is ran in the console.

--- test/integration/helpers.coffee
+++ test/integration/helpers.coffee
@@ -169,14 +169,21 @@

   stdout = ''
   stderr = ''
+  output = ''

   cli = spawn(command, args, spawnOptions)

-  cli.stdout.on('data', (data) -> stdout += data)
-  cli.stderr.on('data', (data) -> stderr += data)
+  cli.stdout.on('data', (data) ->
+    stdout += data
+    output += data
+  )
+  cli.stderr.on('data', (data) ->
+    stderr += data
+    output += data
+  )

   cli.on('close', (exitStatus) ->
-    callback(null, {stdout, stderr, output: stdout + stderr, exitStatus})
+    callback(null, {stdout, stderr, output, exitStatus})
   )


test the version property exists

test the version property exists

--- test/lib/testdouble-test.coffee
+++ test/lib/testdouble-test.coffee
@@ -6,3 +6,4 @@
     Then -> @subject.create == requireSubject('lib/create')
     Then -> @subject.matchers == requireSubject('lib/matchers')
     Then -> @subject.explain == requireSubject('lib/explain')
+    Then -> !!@subject.version.match(/\d+\.\d+\.\d+/)


fix routes that were missing data/bad id

--- tutor/src/api/index.coffee
+++ tutor/src/api/index.coffee
@@ -95,7 +95,9 @@
   connectRead(TocActions, pattern: 'ecosystems/{id}/readings')
   connectRead(CourseGuideActions, pattern: 'courses/{id}/guide')
   connectRead(CourseActions, pattern: 'courses/{id}')
-  connectUpdate(CourseActions, pattern: 'courses/{id}')
+  connectUpdate(CourseActions, pattern: 'courses/{id}',
+    data: (id, data) -> data
+  )

   connectRead(CCDashboardActions, pattern: 'courses/{id}/cc/dashboard')
   connectRead(CoursePracticeActions, pattern: 'courses/{id}/practice')
@@ -139,29 +141,37 @@
     pattern: 'teachers/{id}', trigger: 'teacherDelete', onSuccess: 'teacherDeleted'
   )
   connectDelete(RosterActions, pattern: 'students/{id}')
-  connectUpdate(RosterActions, pattern: 'students/{id}')
-
-  connectModify(RosterActions, pattern: 'students/{id}/undrop', trigger: 'undrop', onSuccess: 'undropped',
+  connectUpdate(RosterActions, pattern: 'students/{id}',
+    data: (id, data) -> data
+  )
+
+  connectModify(RosterActions, pattern: 'students/{studentId}/undrop', trigger: 'undrop', onSuccess: 'undropped',
     errorHandlers:
       already_active: 'onUndropAlreadyActive'
       student_identifier_has_already_been_taken: 'recordDuplicateStudentIdError'
   )
   connectUpdate(RosterActions,
-    pattern: 'students/{id}', trigger: 'saveStudentIdentifier', onSuccess: 'savedStudentIdentifier',
+    pattern: 'students/{studentId}', trigger: 'saveStudentIdentifier', onSuccess: 'savedStudentIdentifier',
     errorHandlers:
       student_identifier_has_already_been_taken: 'recordDuplicateStudentIdError'
     data: ({courseId, studentId}) ->
       student_identifier: RosterStore.getStudentIdentifier(courseId, studentId)
   )
-  connectCreate(RosterActions, pattern: 'courses/{id}/roster')
+  # this isn't currently used, it's the old endpoint for a teacher adding a student
+  # connectCreate(RosterActions, pattern: 'courses/{id}/roster')
   connectRead(RosterActions, pattern: 'courses/{id}/roster')
   connectUpdate(StudentIdActions, pattern: 'user/courses/{id}/student',
     handledErrors: ['*'], handleError: StudentIdActions.errored
     data: (id, data) -> data
   )

-  connectCreate(PeriodActions, pattern: 'courses/{courseId}/periods')
-  connectUpdate(PeriodActions, pattern: 'periods/{id}')
+  connectCreate(PeriodActions, pattern: 'courses/{id}/periods',
+    data: (id, data) -> data
+  )
+  connectUpdate(PeriodActions,
+    url: (courseId, periodId, data) -> "periods/#{periodId}"
+    data: (courseId, periodId, data) -> data
+  )
   connectDelete(PeriodActions, pattern: 'periods/{id}')
   connectModify(PeriodActions, pattern: 'periods/{id}/restore', trigger: 'restore', onSuccess: 'restored')


Port GetCannyEdges to Process API

Port GetCannyEdges to Process API

--- components/GetCannyEdges.coffee
+++ components/GetCannyEdges.coffee
@@ -1,45 +1,37 @@
 noflo = require 'noflo'
 jsfeat = require 'jsfeat'

-class GetCannyEdges extends noflo.Component
-  description: 'Canny edge detector.'
-  icon: 'file-image-o'
-
-  constructor: ->
-    @low = 20
-    @high = 50
-    @kernel = 6
-    @canvas = null
-
-    @inPorts =
-      canvas: new noflo.Port 'object'
-      low: new noflo.Port 'number'
-      high: new noflo.Port 'number'
-      kernel: new noflo.Port 'number'
-    @outPorts =
-      canvas: new noflo.Port 'object'
-
-    @inPorts.canvas.on 'data', (canvas) =>
-      @canvas = canvas
-      @computeCanny()
-
-    @inPorts.low.on 'data', (low) =>
-      @low = low
-      @computeCanny()
-
-    @inPorts.high.on 'data', (high) =>
-      @high = high
-      @computeCanny()
-
-    @inPorts.kernel.on 'data', (kernel) =>
-      @kernel = kernel
-      @computeCanny()
-
-  computeCanny: ->
-    return unless @outPorts.canvas.isAttached()
-    return unless @canvas?
-
-    canvas = @canvas
+exports.getComponent = ->
+  c = new noflo.Component
+  c.description = 'Canny edge detector.'
+  c.icon = 'file-image-o'
+  c.inPorts.add 'canvas',
+    datatype: 'object'
+  c.inPorts.add 'low',
+    datatype: 'number'
+    control: true
+    default: 20
+  c.inPorts.add 'high',
+    datatype: 'number'
+    control: true
+    default: 50
+  c.inPorts.add 'kernel',
+    datatype: 'number'
+    control: true
+    default: 6
+  c.outPorts.add 'canvas',
+    datatype: 'object'
+  c.forwardBrackets =
+    canvas: ['canvas']
+  c.process (input, output) ->
+    return unless input.hasData 'canvas'
+    return if input.attached('low').length and not input.hasData 'low'
+    return if input.attached('high').length and not input.hasData 'high'
+    return if input.attached('kernel').length and not input.hasData 'kernel'
+    low = if input.hasData('low') then input.getData('low') else 20
+    high = if input.hasData('high') then input.getData('high') else 50
+    kernel = if input.hasData('kernel') then input.getData('kernel') else 6
+    canvas = input.getData 'canvas'

     context = canvas.getContext '2d'
     img = context.getImageData 0, 0, canvas.width, canvas.height
@@ -60,9 +52,6 @@

     context.putImageData img, 0, 0

-    @outPorts.canvas.send canvas
-
-exports.getComponent = -> new GetCannyEdges
-
-
-
+    output.sendDone
+      canvas: canvas
+    return


Remove console.log

Remove console.log

--- lib/assets/javascripts/nested_form_fields.js.coffee
+++ lib/assets/javascripts/nested_form_fields.js.coffee
@@ -37,7 +37,6 @@
     return false unless $.rails.allowAction($link)
     object_class = $link.data('object-class')
     delete_association_field_name = $link.data('delete-association-field-name')
-    console.log(delete_association_field_name);
     removed_index = parseInt(delete_association_field_name.match('(\\d+\\]\\[_destroy])')[0].match('\\d+')[0])
     $.event.trigger("fields_removing.nested_form_fields",{object_class: object_class, delete_association_field_name: delete_association_field_name, removed_index: removed_index });
     $nested_fields_container = $link.parents(".nested_fields").first()


cleanup and docs

cleanup and docs

--- lib/assets/javascripts/stark/view.js.coffee
+++ lib/assets/javascripts/stark/view.js.coffee
@@ -88,7 +88,7 @@
   dispose: ->
     @log "disposing of view ", @
     @$el.html("")
-    @stopListening();
+    @stopListening()
     @unstickit()
     @unbind_app_events()
     @undelegateEvents()
@@ -98,7 +98,9 @@
     @views = []

+
   # Rendering methods
+  # ================================================================================================

   # Default implementation of Backbone.View's render() method. Simply renders
   # the @template into the element defined by @selector.
@@ -161,7 +163,9 @@
     @[key] = val

-  # Events
+
+  # DOM Events & model binding
+  # ================================================================================================

   # Bind all events
   bind_events: ->
@@ -306,11 +310,20 @@

+
   # Methods dealing with partials, includes, etc.
+  # ================================================================================================
+
+  # Summary:
+  #
+  # include            Raw include of the contents of some other template (used in templates)
+  # include_partial    Create partial class and return HTML (used in templates)
+  # partial            Render a partial (sub) view into the given selector (used in views)
+

   # A raw include of the contents of some other template. It will be bound with
   # the the same variables in this view.
-  # The target template is rendered as plain text with no  events or other bindings.
+  # The target template is rendered as plain text with no events or other bindings.
   #
   # NOTE: This should be called from within a template
   #
@@ -381,7 +394,9 @@
     return v

+
   # Utility methods for use within view & view helpers
+  # ================================================================================================

   # Proxy for Stark.state#transition
   transition: (state_name, state_data) ->


Mocha 3 compat

Mocha 3 compat

--- spec/Thenable.coffee
+++ spec/Thenable.coffee
@@ -868,6 +868,7 @@
             chai.expect(called).to.equal 2
             done()
           , 1
+        return

     describe 'with qualified race, branches  & return values', ->
       it 'should resolve', (done) ->
@@ -897,6 +898,7 @@
             chai.expect(called).to.equal 4
             done()
           , 1
+        return

     describe 'with race & abort', ->
       it 'should resolve', (done) ->


Change simultaneous uploads to 1

Change simultaneous uploads to 1

Seems to improve performance at least with http/2 based on Vicky's
testing

--- web/service/upload.coffee
+++ web/service/upload.coffee
@@ -9,7 +9,7 @@
       target: router.controllers.uploadChunk.route()
       method: 'octet'
       chunkSize: 4194304
-      simultaneousUploads: 3
+      simultaneousUploads: 1
       testChunks: false
       chunkRetryInterval: 5000
       permanentErrors: [400, 403, 404, 409, 415, 500, 501]


Flush (tested) & Four of a kind (not tested) hand types implemented

--- KataPokerHands.coffee
+++ KataPokerHands.coffee
@@ -22,8 +22,10 @@

     result = '%1 wins. - with %2'

+    # same type
     if (p[1].type[0] == p[2].type[0])
         # check if all cards have same values
+        # or apply the 'highest card' rule
         for i in [1..5]
             if (p[1].type[i] > p[2].type[i])
                 return result.replace('%1', p[1].name)
@@ -35,6 +37,13 @@

             return 'Tie.' if (i == 5)

+    else if (p[1].type[0] > p[2].type[0])
+        return result.replace('%1', p[1].name)
+                     .replace('%2', type_to_s(p[1].type[0]))
+
+    else
+        return result.replace('%1', p[2].name)
+                     .replace('%2', type_to_s(p[2].type[0]))

@@ -102,8 +111,19 @@
     # Straight Flush
     if (same_suit)
         # check if all cards have consecutive values
-        if (!![1..4].reduce (b, i) -> b & (hand[i-1].value + 1 == hand[i].value))
+        if (!![1..4].reduce (b, i) -> b && (hand[i-1].value + 1 == hand[i].value))
             return [7].concat(values)
+
+        # Flush
+        return [5].concat(values)
+    else
+        # Four of a kind
+        # card are ordered, so we check
+        #    if values[1] == values[2] == values[3] == values[0]
+        # or if values[1] == values[2] == values[3] == values[4]
+        for i in [0, 4]
+            if (!!values[1..3].reduce (b, c) -> b && (c == values[i]))
+                return [6].concat(values)

     # TODO

@@ -112,6 +132,6 @@

     #renamining hand_types : [ 'High Card', 'Pair', 'Two Pairs', 'Three of a Kind', 'Straight',
-    #'Flush', 'Full House', 'Four of a kind' ]
+    #'Flush', 'Full House', ]

 exports.compare_hands = compare_hands


step completion should submit answer_id and free_response

--- tutor/src/api/index.coffee
+++ tutor/src/api/index.coffee
@@ -18,7 +18,7 @@

 {TaskActions} = require '../flux/task'
 {TaskPanelActions} = require '../flux/task-panel'
-{TaskStepActions} = require '../flux/task-step'
+{TaskStepActions, TaskStepStore} = require '../flux/task-step'
 {TaskPlanActions, TaskPlanStore} = require '../flux/task-plan'

 {PastTaskPlansActions} = require '../flux/past-task-plans'
@@ -139,7 +139,11 @@
       TaskStepActions.loadedNoPersonalized(args...)
       true
   )
-  connectModify(TaskActions, pattern: 'steps/{id}/completed', trigger: 'completeStep', onSuccess: 'stepCompleted')
+  connectModify(TaskActions, pattern: 'steps/{id}/completed', trigger: 'completeStep', onSuccess: 'stepCompleted',
+    data: (id) ->
+      step = TaskStepStore.get(id)
+      pick(step, ['free_response', 'answer_id'])
+  )

   connectUpdate(TaskStepActions, pattern: 'steps/{id}', trigger: 'setFreeResponseAnswer',
     data: (id, freeResponse) ->


include current dir in `csi test --testconfig`

this was getting the test config for every module except the current
directory.

--- csi.coffee
+++ csi.coffee
@@ -107,13 +107,14 @@
       ).filter(_.identity).map (d) -> {path: d}
   directories

-allComponents = () ->
+allComponents = (opts) ->
+  opts = _.extend({includeThis: false}, opts)
   results = []
   names = () -> component.json.name for component in results
   t.bfs allNodeModules(), (m) ->
     if m.json and isComponent(null, m.json) and m.json.name not in names()
       results.push(m)
-  _.filter results, (m) -> m.path isnt "."
+  _.filter results, (m) -> opts.includeThis or m.path isnt "."

 provide = (pth) ->
   if not exists pth
@@ -294,7 +295,7 @@
         return listTests(discoverTests(argv.staticpath), argv.host, argv.port)
       if argv.testconfig
         return console.log(JSON.stringify(
-          components.reduce((config, component) ->
+          allComponents(includeThis: true).reduce((config, component) ->
             addQualifier = (name) ->
               if component.json?.csi?[name]
                 config[name] ||= []


Fixing check for server nonce

Fixing check for server nonce

OTS by @vexocide

--- drivers/javascript/net.coffee
+++ drivers/javascript/net.coffee
@@ -1142,7 +1142,7 @@
                             auth_salt = new Buffer(authentication.s, 'base64')
                             auth_i = parseInt(authentication.i)

-                            if not auth_r.substr(0, r_string) == r_string
+                            if not (auth_r.substr(0, r_string.length) == r_string)
                                 throw new err.ReqlAuthError("Invalid nonce from server")

                             client_final_message_without_proof = "c=biws,r=" + auth_r


Add workaround for xhr in Sandstorm

Add workaround for xhr in Sandstorm

--- www/coffee/servers.coffee
+++ www/coffee/servers.coffee
@@ -363,6 +363,17 @@
 						return new _OriginalWebSocket(url);
 					}
 				};
+
+				XMLHttpRequest._originalOpen = XMLHttpRequest._originalOpen || XMLHttpRequest.prototype.open;
+				XMLHttpRequest.prototype.open = function () {
+				  var res = XMLHttpRequest._originalOpen.apply(this, arguments);
+					if (arguments[1].indexOf("#{baseUrl}") === 0 && arguments[1].indexOf("#{baseUrl}/.sandstorm-login") !== 0) {
+						// Only send sandstorm auth for urls in the base url.
+						// Confusingly, /.sandstorm-login already sets the auth header, so skip that route.
+						this.setRequestHeader("Authorization", "Bearer #{urlObj.auth}");
+					}
+					return res;
+				}
 				""")
 			file = file.replace /(<\/head>)/gm, """
 				<link rel="stylesheet" href="/shared/css/servers-list.css"/>


Teamwork: Moved Teamwork related logic in CollaborativeWorkspace finder.

--- client/landing/CollaborativeWorkspace/panes/collaborativefinderpane.coffee
+++ client/landing/CollaborativeWorkspace/panes/collaborativefinderpane.coffee
@@ -1,9 +1,3 @@
-class TeamworkFinderItem extends NFinderItem
-
-  removeHighlight: ->
-    super
-    @exporter?.destroy()
-
 # TODO: Should implement non-collaborative finder
 # TODO: Should extend this class from NonCollab one.

@@ -20,8 +14,8 @@
       nodeParentIdPath    : "parentPath"
       contextMenu         : yes
       useStorage          : no
-      treeControllerClass : CollaborativeFinderTreeController
-      treeItemClass       : TeamworkFinderItem
+      treeControllerClass : options.treeControllerClass or CollaborativeFinderTreeController
+      treeItemClass       : options.treeItemClass       or NFinderItem

     @container?.destroy()
     @finder = @container = @finderController.getView()
@@ -128,13 +122,3 @@

     file.fetchContents (err, contents) =>
       delegate.emit "OpenedAFile", file, contents
-
-  selectNode: (node) ->
-    super
-
-    if node.getData().type is "folder"
-      node.exporter = new KDCustomHTMLView
-        cssClass    : "tw-export"
-        click       : => @emit "ExportRequested", node
-
-      node.addSubView node.exporter


Let sort can work with same key but different values

--- client/landing/app/Applications/Feeder.kdapplication/FeedController.coffee
+++ client/landing/app/Applications/Feeder.kdapplication/FeedController.coffee
@@ -102,7 +102,7 @@
     filter  = @selection
     sort    = @sorts[@selection.activeSort] or @defaultSort

-    options.sort[sort.name] = sort.direction
+    options.sort[sort.name.split('|')[0]] = sort.direction
     options.limit = @getOptions().limitPerPage
     options.skip  = @resultsController.listControllers[filter.name].itemsOrdered.length
     options


wip from home

wip from home

--- core/utils.coffee
+++ core/utils.coffee
@@ -273,6 +273,24 @@
     parentPath = path.split('/')
     parentPath.pop()
     return parentPath.join('/')
+
+  defer:do ->
+    # this was ported from browserify's implementation of "process.nextTick"
+    queue = []
+    canPost = window?.postMessage and window.addEventListener
+    if canPost
+      window.addEventListener "message", ((ev) ->
+        if ev.source is window and ev.data is "kd-tick"
+          ev.stopPropagation()
+          if queue.length > 0
+            fn = queue.shift()
+            fn()
+      ), true
+      (fn) ->
+        queue.push fn
+        window.postMessage "kd-tick", "*"
+    else
+      (fn) -> setTimeout fn, 0

   wait: (duration, fn) ->
     if "function" is typeof duration


show more/less everywhere

show more/less everywhere

--- core/utils.coffee
+++ core/utils.coffee
@@ -84,9 +84,11 @@
   stripTags:(value)->
     value.replace /<(?:.|\n)*?>/gm, ''

-  applyTextExpansions: (text)->
+  applyTextExpansions: (text, shorten)->
     return null unless text
     # @expandWwwDotDomains @expandUrls @expandUsernames @expandTags text
+    text = text.replace '
', ' '
+    text = __utils.putShowMore text if shorten
     @expandWwwDotDomains @expandUrls @expandUsernames text

   expandWwwDotDomains: (text) ->
@@ -110,6 +112,18 @@
     return null unless text
     text.replace /[A-Za-z]+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+/g, (url) ->
       "#{url}"
+
+  putShowMore: (text)->
+    shortenedText = __utils.shortenText text,
+      minLength : 500
+      maxLength : 600
+      suffix    : ''
+
+    text = if text.length > 500
+      morePart = "show more...#{text.substr 500}...show less"
+      shortenedText + morePart
+    else
+      shortenedText

   shortenText:do ->
     tryToShorten = (longText, optimalBreak, suffix)->
@@ -120,6 +134,9 @@
       return unless longText
       minLength = options.minLength or 450
       maxLength = options.maxLength or 600
+      suffix    = options.suffix     ? '...'
+
+      longText  = Encoder.htmlDecode longText

       return longText if longText < minLength or longText < maxLength

@@ -127,12 +144,12 @@

       # prefer to end the teaser at the end of a sentence (a period).
       # failing that prefer to end the teaser at the end of a word (a space).
-      candidate = tryToShorten(longText, '.') or tryToShorten longText, ' ', '...'
+      candidate = tryToShorten(longText, '. ', suffix) or tryToShorten longText, ' ', suffix

       if candidate?.length > minLength
-        candidate
+        Encoder.htmlEncode candidate
       else
-        longText
+        Encoder.htmlEncode longText

   getMonthOptions : ()->
     ((if i > 9 then { title : "#{i}", value : i} else { title : "0#{i}", value : i}) for i in [1..12])


Tests: IDE: Terminal: Improve renameTerminalTab test.

--- client/test/lib/ide/terminal.coffee
+++ client/test/lib/ide/terminal.coffee
@@ -60,7 +60,7 @@

 module.exports =
-
+
   terminateAll: (browser) ->

     user = helpers.beginTest(browser)
@@ -137,11 +137,11 @@
   renameTerminalTab: (browser) ->

     name            = helpers.getFakeText().split(' ')[0]
-    tabSelector     = ".kdtabhandle.terminal"
-    optionsSelector = tabSelector + " .options"
+    tabSelector     = "#{paneSelector} .kdtabhandle.terminal"
+    optionsSelector = "#{tabSelector} .options"
     renameSelector  = ".kdcontextmenu.terminal-context-menu .rename"
-    editSelector    = tabSelector + ".edit-mode .hitenterview.tab-handle-input"
-    tabNameSelector = tabSelector + " .tab-handle-text"
+    editSelector    = "#{tabSelector}.edit-mode .hitenterview.tab-handle-input"
+    tabNameSelector = "#{tabSelector} .tab-handle-text"

     user = helpers.beginTest(browser)
     helpers.waitForVMRunning(browser)
@@ -155,6 +155,7 @@
       .waitForElementVisible    renameSelector, 20000
       .click                    renameSelector
       .waitForElementPresent    editSelector, 20000
+      .clearValue               editSelector
       .setValue                 editSelector, [name,browser.Keys.RETURN]
       .waitForElementNotPresent editSelector, 20000
       .assert.containsText      tabNameSelector, name #Assertion


Removed fake "name"

Removed fake "name"

--- js/login.coffee
+++ js/login.coffee
@@ -1,6 +1,6 @@
 class @LoginController
 	constructor: ->
-		@background = $("

Who are you?
<input value=name type=text />

OK

").show()
+		@background = $("

Who are you?
<input type=text />

OK

").show()

 		@button = @background.children('.message-button')
 		@input 	= @background.children('input')


Screw global polyfills on ie 6-8

Screw global polyfills on ie 6-8

--- js/outer.coffee
+++ js/outer.coffee
@@ -222,13 +222,6 @@
       $("body").append HAML['footer'](renderContext)

-
-# Global polyfills
-if $.browser.msie and $.browser.version > 6 and $.browser.version < 9
-  $.getScript(renderContext.assetsRoot + "/js/vendor/selectivizr-1.0.2.js")
-# `!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");`
-
-
 # Run the application
 $ ->
   circle.run window.location.pathname.replace(/\/$/, '')


Delete unused file.

Delete unused file.

--- app/client/js/users/user.coffee
+++ app/client/js/users/user.coffee
@@ -1,35 +0,0 @@
-goog.provide 'app.users.User'
-
-class app.users.User
-
-  ###*
-    @constructor
-  ###
-  constructor: ->
-
-  ###*
-    ID is an empty string until first sync.
-    @type {string}
-    @expose
-  ###
-  id: ''
-
-  ###*
-    ISO 8601 string representation of date/time.
-    @type {string}
-    @expose
-  ###
-  created: ''
-
-  ###*
-    ISO 8601 string representation of date/time.
-    @type {string}
-    @expose
-  ###
-  updated: ''
-
-  ###*
-    @type {string}
-    @expose
-  ###
-  email: ''


added more logging

added more logging

--- app/coffee/FileConverter.coffee
+++ app/coffee/FileConverter.coffee
@@ -21,6 +21,10 @@
 			timeout: twoMinsInMs
 		exec args, opts, (err, stdout, stderr)->
 			timer.done()
+			if err?
+				logger.err err:err, stderr:stderr, sourcePath:sourcePath, requestedFormat:requestedFormat, "something went wrong converting file"
+			else
+				logger.log  sourcePath:sourcePath, requestedFormat:requestedFormat, "finished converting file"
 			callback(err, destPath)

 	thumbnail: (sourcePath, callback)->
@@ -36,6 +40,10 @@
 		opts =
 			timeout: twoMinsInMs
 		exec args, opts,(err, stdout, stderr)->
+			if err?
+				logger.err err:err, stderr:stderr, sourcePath:sourcePath, "something went wrong converting file to preview"
+			else
+				logger.log  sourcePath:sourcePath, "finished thumbnailing file"
 			callback(err, destPath)

 	preview: (sourcePath, callback)->
@@ -51,4 +59,8 @@
 		opts =
 			timeout: twoMinsInMs
 		exec args, opts,(err, stdout, stderr)->
+			if err?
+				logger.err err:err, stderr:stderr, sourcePath:sourcePath, "something went wrong converting file to preview"
+			else
+				logger.log  sourcePath:sourcePath, "finished converting file to preview"
 			callback(err, destPath)


Simplify config reading

Simplify config reading

--- app.coffee
+++ app.coffee
@@ -51,11 +51,7 @@
 	filename = "#{__dirname}/config.js"

 	# configuration file was passed via `process.argv`
-	if process.argv[2]
-		if process.argv[2].substring(0, 1) is '/'
-			filename = process.argv[2]
-		else
-			filename = "#{__dirname}/../#{process.argv[2]}"
+	filename = (require 'path').resolve process.argv[2] if process.argv[2]?

 	filename = fs.realpathSync filename


Fix default values for configuration

Fix default values for configuration

--- app.coffee
+++ app.coffee
@@ -41,12 +41,12 @@

 config.port ?= 9001
 config.ip ?= '0.0.0.0'
+config.enableFrontend ?= yes
+config.database ?= __dirname + '/storage.sqlite3'

 config.securityToken = ''
 config.userID = null
 config.upSince = new Date()
-config.enableFrontend = yes
-config.database = __dirname + '/storage.sqlite3'

 db = require './db'
 api = require './api'


Fix formatting

Fix formatting

--- app.coffee
+++ app.coffee
@@ -107,7 +107,7 @@
 	return if updating
 	updating = yes
 	updateStart = do (new Date).getTime
-
+
 	fs.readdir config.packageFolder, (err, files) ->
 		logger.log "info", "Starting update"
 		if err?
@@ -126,7 +126,7 @@
 				logger.log "notice", "Skipping #{config.packageFolder}#{file}, as it does not match a valid package identifier"
 				fileCallback null
 				return
-
+
 			packageFolder = config.packageFolder + file
 			logger.log "debug", "Parsing #{packageFolder}"
 			fs.stat packageFolder, (err, packageFolderStat) ->
@@ -186,7 +186,7 @@
 									logger.log "notice", "Skipping #{packageFolder}/#{versionFile}, as it does not match a valid version number"
 									versionsCallback null
 									return
-
+
 								versionFile = packageFolder + '/' + versionFile
 								logger.log "debug", "Parsing #{versionFile}"
 								fs.stat versionFile, (err, versionFileStat) ->


Небольшой рефакторинг:  # исправил несколько багов с отображением заголовка, когда ничего нет и с загрузкой фотографии в то время как мы уже находимся на другой  # вынес получение не изменяемых размеров в отдельный метод, и запускаем его при инициализации  # убрал лишний код  # убрал приватные методы вниз  # добавил комментарии

--- src/chocolate.coffee
+++ src/chocolate.coffee
@@ -22,29 +22,31 @@
   constructor: (images, options = {}) ->
     throw "You don't have defaultOptions or templates variables" if not defaultOptions or not templates

-    @options  = $.extend defaultOptions, options
-    isHistory = false if not @options.history
-
+    @options = $.extend defaultOptions, options
+
+    ###
+     Подготовка шаблонов
+    ###
     template = templates['overlay']
     template = template.replace '{{spinner}}', templates['spinner']
     template = template.replace '{{thumbnails}}', if @options.thumbnails then templates['thumbnails'] else ''

     @overlay = $(template).appendTo 'body'

-    elements = ['container', 'spinner', 'leftside', 'rightside', 'header']
-    elements.push 'thumbnails' if @options.thumbnails
-
-    @[element] = @overlay.find '.choco-' + element for element in elements
-
-    @overlay.find('.choco-close').click (event) => @close()
-
-    @_prepareActionFor element for element in ['overlay', 'container', 'leftside', 'rightside']
-
-    if isHistory
-      $(window).bind 'popstate', (event) =>
-        cid = @getImageFromUri()
-        if cid > 0 and cid isnt @current
-          if @current is null then @show cid else @open cid, false
+    ###
+     Получаем необходимые контейнеры
+    ###
+    containers = ['container', 'spinner', 'leftside', 'rightside', 'header']
+    containers.push 'thumbnails' if @options.thumbnails
+
+    @[container] = @overlay.find '.choco-' + container for container in containers
+
+    ###
+     Добавляем события по-умолчанию для контейнеров
+    ###
+    @overlay.find('.choco-close').click => @close()
+
+    @_prepareActionFor container for container in ['overlay', 'container', 'leftside', 'rightside']

     $(window).bind 'keyup', (event) =>
       if @overlay.hasClass 'show'
@@ -56,23 +58,30 @@
           when 39    # Right arrow
             @next()

+    ###
+     Если можно использовать History API добавляем событие на отслеживание изменений в адресе
+    ###
+    isHistory = false if isHistory and not @options.history
+
+    if isHistory
+      $(window).bind 'popstate', =>
+        cid = @_getImageFromUri()
+
+        if cid > 0 and cid isnt @current
+          if @current is null
+            @show cid
+          else
+            @open cid, false
+
+    ###
+     Получаем параметры отступов и другие неизменяемые размеры
+    ###
+    @dimensions = @_getInitialParams()
+
+    ###
+     Добавляем изображения для галереи
+    ###
     @add images if images
-
-  _prepareActionFor: (element) ->
-    method = if @options.actions[element] in existActions then @options.actions[element] else false
-    if method
-      verify = @[element].attr 'class'
-      @[element].click (event) => @[method]() if $(event.target).hasClass verify
-      if @options.actions[element] is 'close'
-        @[element].bind 'mouseenter mouseleave', (event) => @_hoverCloseButton()
-    @
-
-  _hoverCloseButton: ->
-    @overlay.find('.choco-close').toggleClass 'hover'
-
-  getImageFromUri: ->
-    hash = window.location.hash
-    if hash then toInt hash.replace('#image', '') else 0

   ###
    Добавляем список изображений для работы в галереи
@@ -92,40 +101,11 @@
       @_addToGallery object, image
     @

-  _addToGallery: (data, image) ->
-    return unless data.source
-
-    cid = ++counter
-
-    data.thumbnail = data.source unless data.thumbnail
-    @images[cid] = data
-    @length++
-
-    if image
-      showFirstImage = (event, cid) =>
-        event.stopPropagation()
-        event.preventDefault()
-        @show cid
-
-      image.addClass('choco-item').click (event) -> showFirstImage event, cid
-
-      preload = new Image()
-      preload.src = data.thumbnail
-      preload.onload = ->
-        image.after templates['image-hover'].replace '{{cid}}', cid
-
-        popover = $('[data-pid=' + cid + ']').css
-          'width':      image.width()
-          'height':     image.height()
-          'margin-top': '-' + image.height() + 'px'
-
-        popover.click (event) -> showFirstImage event, cid
-
   ###
    Показать изображение на большом экране
   ###
   show: (cid) ->
-    cid = @getImageFromUri() unless cid?
+    cid = @_getImageFromUri() unless cid?
     cid = 1 if cid <= 0
     throw 'Image not found' unless @images[cid]?

@@ -175,6 +155,7 @@
     @current = cid

     @container.removeClass 'show'
+    @header.removeClass 'show' if @header.hasClass 'show'

     if isHistory and updateHistory
       title = if @images[cid].title then 'Image: ' + @images[cid].title else null
@@ -191,27 +172,6 @@

       @container.css 'background-image', 'url(' + image.source + ')'
       @header.html if image.title then templates['image-title'].replace '{{title}}', image.title else ''
-    @
-
-  ###
-   Обновление миниатюр
-  ###
-  updateThumbnails: ->
-    @thumbnails.find('.selected').removeClass 'selected'
-
-    thumbnail = @thumbnails.find('[data-cid=' + @current + ']').addClass 'selected'
-
-    width = thumbnail.outerWidth() + toInt thumbnail.css('margin-right')
-    left  = thumbnail.offset().left
-
-    if @thumbnails.width() < width + left
-      offset = @thumbnails.scrollLeft() + width
-      offset = width + left if @thumbnails.width() + offset < width + left
-
-    else if @thumbnails.scrollLeft() > left
-      offset = if left < width then 0 else @thumbnails.scrollLeft() - width
-
-    @thumbnails.scrollLeft offset if offset > 0
     @

   ###
@@ -245,24 +205,17 @@
   updateDimensions: (width, height) ->
     title = not not @images[@current].title

-    thumbnails = if not @options.thumbnails or @thumbnails.hasClass('hide') then 0 else @thumbnails.height()
-
-    # TODO оптимизация
-    horizontal = toInt(@overlay.css('padding-left')) + toInt(@overlay.css('padding-right'))
-    vertical   = toInt(@overlay.css('padding-top')) + toInt(@overlay.css('padding-bottom'))
-
-    if title
-      headerHeight = toInt @header.css('height')
-      if headerHeight is 0
-        headerHeight = 40
-        @header.css('height', headerHeight)
-    else
-      headerHeight = 0
+    thumbnails = headerHeight = 0
+
+    if @dimensions.thumbnails isnt false and not @thumbnails.hasClass('hide')
+      thumbnails = @dimensions.thumbnails
+
+    headerHeight = @dimensions.header if title

     innerWidth   = window.innerWidth
-    windowWidth  = innerWidth - horizontal
+    windowWidth  = innerWidth - @dimensions.horizontal
     innerHeight  = window.innerHeight
-    windowHeight = innerHeight - vertical - thumbnails - headerHeight
+    windowHeight = innerHeight - @dimensions.vertical - thumbnails - headerHeight

     if width > windowWidth
       height = windowWidth * height / width
@@ -274,9 +227,9 @@

     left = toInt width / 2

-    top  = toInt height / 2
-    top += toInt thumbnails / 2 if thumbnails > 0
-    top -= toInt headerHeight / 2 if title
+    top  = height / 2
+    top += thumbnails / 2   if thumbnails > 0
+    top -= headerHeight / 2 if headerHeight > 0

     style =
       'width':  toInt(innerWidth / 2 - left) + 'px'
@@ -284,20 +237,18 @@

     @leftside.css  style
     @rightside.css style
+
+    if title
+      @header.addClass('show').css
+        'width':       width
+        'margin-left': '-' + left + 'px'
+        'margin-top':  '-' + toInt(top + headerHeight) + 'px'

     style =
       'width':       width
       'height':      height
       'margin-left': '-' + left + 'px'
-      'margin-top':  '-' + top + 'px'
-
-    if title
-      @header.css
-        'width':       width
-        'margin-left': '-' + left + 'px'
-        'margin-top':  '-' + (top + headerHeight) + 'px'
-
-    @header[if title then 'addClass' else 'removeClass'] 'show'
+      'margin-top':  '-' + toInt(top) + 'px'

     @container.css style
     @spinner.css   style
@@ -321,10 +272,10 @@
                          .replace('{{thumbnail}}', image.thumbnail)
                          .replace('{{title}}', if image.title then ' title="' + image.title + '"' else '')

-    @thumbnails.html(content).find('.choco-thumbnail').click (event) ->
+    @thumbnails.html(content).find('.choco-thumbnail').click ->
       _this.open toInt $(@).attr('data-cid')

-    @overlay.find('.choco-thumbnails-toggle').click (event) ->
+    @overlay.find('.choco-thumbnails-toggle').click ->
       current = _this.images[_this.current]
       method  = if _this.thumbnails.hasClass 'hide' then 'removeClass' else 'addClass'

@@ -334,6 +285,108 @@
       _this.updateDimensions current.width, current.height

     @
+
+  ###
+   Обновление тумбнейлов
+  ###
+  updateThumbnails: ->
+    @thumbnails.find('.selected').removeClass 'selected'
+
+    thumbnail = @thumbnails.find('[data-cid=' + @current + ']').addClass 'selected'
+
+    width = thumbnail.outerWidth() + toInt thumbnail.css('margin-right')
+    left  = thumbnail.offset().left
+
+    if @thumbnails.width() < width + left
+      offset = @thumbnails.scrollLeft() + width
+      offset = width + left if @thumbnails.width() + offset < width + left
+
+    else if @thumbnails.scrollLeft() > left
+      offset = if left < width then 0 else @thumbnails.scrollLeft() - width
+
+    @thumbnails.scrollLeft offset if offset > 0
+    @
+
+
+  ###
+   -- Private methods --
+  ###
+
+
+  ###
+   Private method
+  ###
+  _addToGallery: (data, image) ->
+    return unless data.source
+
+    cid = ++counter
+
+    data.thumbnail = data.source unless data.thumbnail
+    @images[cid]   = data
+
+    @length++
+
+    if image
+      showFirstImage = (event, cid) =>
+        event.stopPropagation()
+        event.preventDefault()
+        @show cid
+
+      image.addClass('choco-item').click (event) ->
+        showFirstImage event, cid
+
+      preload        = new Image()
+      preload.src    = data.thumbnail
+      preload.onload = ->
+        image.after templates['image-hover'].replace '{{cid}}', cid
+
+        popover = $('[data-pid=' + cid + ']').css
+          'width':      image.width()
+          'height':     image.height()
+          'margin-top': '-' + image.height() + 'px'
+
+        popover.click (event) -> showFirstImage event, cid
+
+  ###
+   Private method
+  ###
+  _prepareActionFor: (container) ->
+    method = @options.actions[container] if @options.actions[container] in existActions
+
+    if method
+      verify = @[container].attr 'class'
+
+      @[container].click (event) =>
+        @[method]() if $(event.target).hasClass verify
+
+      if @options.actions[container] is 'close'
+        @[container].bind 'mouseenter mouseleave', =>
+          @overlay.find('.choco-close').toggleClass 'hover'
+    @
+
+  ###
+   Private method
+  ###
+  _getImageFromUri: ->
+    hash = window.location.hash
+    if hash then toInt hash.replace('#image', '') else 0
+
+  ###
+   Private method
+  ###
+  _getInitialParams: ->
+    thumbnails = if not @options.thumbnails then false else @thumbnails.height()
+
+    horizontal = toInt(@overlay.css('padding-left')) + toInt(@overlay.css('padding-right'))
+    vertical   = toInt(@overlay.css('padding-top')) + toInt(@overlay.css('padding-bottom'))
+
+    header = toInt @header.css('height')
+    if header is 0
+      header = 40
+      @header.css 'height', header
+
+    {horizontal, vertical, thumbnails, header}
+

 toInt = (string) -> parseInt string, 10


Исправил баг при долгой загрузке изображений и переключении на следующую

--- src/chocolate.coffee
+++ src/chocolate.coffee
@@ -201,12 +201,13 @@
       element        = new Image()
       element.src    = image.source
       element.onload = =>
-        @images[cid].width  = element.width
-        @images[cid].height = element.height
-
-        @spinner.addClass 'hide' if not @spinner.hasClass 'hide'
-
-        fn()
+        if cid is @current
+          @images[cid].width  = element.width
+          @images[cid].height = element.height
+
+          @spinner.addClass 'hide' if not @spinner.hasClass 'hide'
+
+          fn()
     else
       fn()


Small fix for thumbnails template

Small fix for thumbnails template

--- src/chocolate.coffee
+++ src/chocolate.coffee
@@ -3,9 +3,11 @@
     @options = merge defaultOptions, options
     @storage = new Storage @options.repeat

-    template = templates.overlay.replace '{{thumbnails}}', if @options.thumbnails then templates.thumbnails else ''
-
+    thumbnailTemplate = if @options.thumbnails and templates.thumbnails then templates.thumbnails else ''
+
+    template = templates.overlay.replace '{{thumbnails}}', thumbnailTemplate
     @overlay = beforeEnd document.body, template
+
     @slider = getTarget @overlay, ".#{choco}slider"

     startTouch @
@@ -314,7 +316,6 @@

       return

-
     setSize = (item) ->
       return unless item.w > 0 and item.h > 0


Clampable#clampToBounds

Clampable#clampToBounds

--- src/clampable.coffee
+++ src/clampable.coffee
@@ -12,4 +12,4 @@
   clampToBounds: (bounds) ->
     bounds ||= Rectangle x: 0, y: 0, width: App.width, height: App.height

-    +    self.clamp


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/collision.coffee
+++ src/collision.coffee
@@ -221,7 +221,7 @@
             xw: target.width/2
             yw: target.height/2
         else
-          error
+          error()
           return

       xw = target.xw