fixing news time to not have am/pm

fixing news time to not have am/pm

--- server.coffee
+++ server.coffee
@@ -408,7 +408,7 @@
 app.get '/data/news', (req, res) ->
   db.collection('news').find().sort({_id: -1}).toArray (err, data) ->
     for d in data
-      d.date = moment(d._id.getTimestamp().toISOString()).format("MM/DD/YYYY HH:mm a");
+      d.date = moment(d._id.getTimestamp().toISOString()).format("MM/DD/YYYY HH:mm");
       delete d._id
     res.json(200, data)


Fixed: the 'emit' was always sending data with the same fileName

--- server.coffee
+++ server.coffee
@@ -8,34 +8,45 @@
 config = require './config'
 spawn = require('child_process').spawn

+commands = []
+
+sendData = (socket, data, fileName, fileSlug, channel) ->
+    socket.emit 'new-data',
+        'fileSlug': fileSlug
+        'fileName': fileName
+        'channel': channel
+        'value': "#{data}"
+
+startProcess = (socket, fileName) ->
+
+    args = ['-f', "#{config.logPath}/#{fileName}"]
+    command = spawn "tail", args
+
+    # replace . by - to avoid conflicts in the frontend
+    fileSlug = fileName.replace /\./g, '-'
+
+    command.stdout.on 'data', (data) ->
+        sendData(socket, data, fileName, fileSlug, 'stdout')
+    command.stderr.on 'data', (data) ->
+        sendData(socket, data, fileName, fileSlug, 'stderr')
+
+    commands[fileName] = command
+
 io.sockets.on "connection", (socket) ->
-
-    commands = []

     fs.readdir config.logPath, (err, files) ->

         if !err
             for fileName in files
-                args = ['-f', "#{config.logPath}/#{fileName}"]
-                command = spawn "tail", args
-                commands.push(command)
+                startProcess(socket, fileName)

-                # replace . by - to avoid conflicts in the frontend
-                fileSlug = fileName.replace /\./g, '-'
-
-                sendData = (data) ->
-                    socket.emit 'new-data',
-                        'fileSlug': fileSlug
-                        'fileName': fileName
-                        'channel': 'stderr'
-                        'value': "#{data}"
-
-                command.stdout.on 'data', sendData
-                command.stderr.on 'data', sendData
         else
             console.log err

     socket.on "disconnect", () ->
         console.log "Client has disconnected, closing the processes..."
-        for command in commands
+
+        for fileName, command of commands
+
+            console.log "Killing process for #{fileName}..."
             command.kill 'SIGTERM'


tidied up

tidied up

--- server.coffee
+++ server.coffee
@@ -24,13 +24,13 @@

   # The server will server everything in the root folder it's launched from. If you want to test the built files
   # then you can do a quick and sneaky change here to point it to the `/build` directory instead.
-  app.use '/', express.static __dirname + '/'
+  app.use '/', express.static "#{__dirname}/"

 app.options '*', (request, response, next) ->
   response.send 200

 app.all '*', (request, response, next) ->
-  response.sendfile __dirname + '/index.html'
+  response.sendfile "#{__dirname}/index.html"

 app.listen port, () ->
-  console.log 'Listening on port: ' + port+  console.log "Listening on port: #{port}"


Remove redundant var

Remove redundant var

--- app/assets/javascripts/app/common/datetime-picker.coffee
+++ app/assets/javascripts/app/common/datetime-picker.coffee
@@ -5,7 +5,6 @@
     @$parent = @$calendarElement.parent()

   init : () ->
-    minDateTime = moment().subtract( "seconds", 1 )
     @$calendarElement.filthypillow()
     @_bindCalendarListeners()
     @_bindParentListeners()


chat: remove new window sound (temporarely)

--- app/assets/javascripts/app/controllers/layout_ref.coffee
+++ app/assets/javascripts/app/controllers/layout_ref.coffee
@@ -1779,7 +1779,6 @@

   propagateLayoutChange: (event) =>
     # adjust scroll position on layoutChange
-    console.log "propagateLayoutChange", event

     for chat in @chatWindows
       chat.trigger 'layout-changed'
@@ -1904,7 +1903,7 @@
     @lastTimestamp
     @lastAddedType
     @render()
-    @sound.window.play()
+    #@sound.window.play()

     @on 'layout-change', @scrollToBottom


fixed isDomainAvailable

fixed isDomainAvailable

--- client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
@@ -171,7 +171,7 @@
       KD.remote.api.JDomain.isDomainAvailable domain, tld, (avErr, result) =>
         status = result.status
         price = result.price
-        suggestions = result.price
+        suggestions = result.suggestions
         if avErr
           checkButton.hideLoader()
           log result


Environments: .data to .getData()

Environments: .data to .getData()

--- client/landing/app/Applications/Environments.kdapplication/views/scene/environmentsceneview.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/scene/environmentsceneview.coffee
@@ -30,8 +30,8 @@

     @askForApprove items, 'delete', (modal)=>
       return unless machine? # Remove a domain from a machine
-      jDomain = domain.dia.data.domain # JDomain
-      vmName  = machine.dia.data.title # JVM.hostnameAlias
+      jDomain = domain.dia.getData().domain # JDomain
+      vmName  = machine.dia.getData().title # JVM.hostnameAlias
       jDomain.unbindVM hostnameAlias: vmName, (err)=>
         modal.destroy()
         return KD.showError err  if err
@@ -48,13 +48,13 @@
     return unless domain

     if machine?
-      if domain.dia.data.domain.hostnameAlias.length > 0
+      if domain.dia.getData().domain.hostnameAlias.length > 0
         return new KDNotificationView
           title : "A domain name can only be bound to one VM."

     if machine? # Assign a domain to a machine
-      jDomain = domain.dia.data.domain # JDomain
-      vmName  = machine.dia.data.title # JVM.hostnameAlias
+      jDomain = domain.dia.getData().domain # JDomain
+      vmName  = machine.dia.getData().title # JVM.hostnameAlias
       jDomain.bindVM hostnameAlias: vmName, (err)=>
         return KD.showError err  if err
         jDomain.hostnameAlias.push vmName
@@ -63,7 +63,7 @@
   updateConnections:->
     for _mkey, machine of @boxes.machines.dias
       for _dkey, domain of @boxes.domains.dias
-        if domain.data.aliases and machine.data.title in domain.data.aliases
+        if domain.getData().aliases and machine.getData().title in domain.getData().aliases
           @connect {dia : domain , joint : 'right'}, \
                    {dia : machine, joint : 'left' }, yes

@@ -158,13 +158,17 @@
 class EnvironmentApprovalModal extends KDModalView

   getContentFor = ({domain, machine}, action)->
-    content = 'God knows.'
+    content     = 'God knows.'
+
+    domainTitle  = domain.dia.getData().title
+    machineTitle = machine.dia.getData().title
+
     if action is 'create'
-      content = """Do you want to assign #{domain.dia.data.title}
-                   to #{machine.dia.data.title} machine?"""
+      content = """Do you want to assign #{domainTitle}
+                   to #{machineTitle} machine?"""
     else if action is 'delete'
-      content = """Do you want to remove #{domain.dia.data.title}
-                   domain from #{machine.dia.data.title} machine?"""
+      content = """Do you want to remove #{domainTitle}
+                   domain from #{machineTitle} machine?"""
     return "

#{content}

"

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


Update to atom config

Update to atom config

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -48,8 +48,7 @@
     panelDisplayMode: "closed"
   "linter-eslint":
     useGlobalEslint: true
-  "linter-ui-default":
-    showPanel: true
+  "linter-ui-default": {}
   "prettier-atom":
     formatOnSaveOptions:
       enabled: true


yay more atom cruft

yay more atom cruft

--- atom.symlink/config.cson
+++ atom.symlink/config.cson
@@ -15,6 +15,7 @@
       "tmp"
       "vendor"
     ]
+    telemetryConsent: "no"
     themes: [
       "unity-ui"
       "glacier-syntax"


Migrate library publish function to PeerDB

--- server/publication.coffee
+++ server/publication.coffee
@@ -279,13 +279,13 @@
       assert.equal fields.user._id, @userId

       currentPersonId = id
-      publishPublications fields.library
+      publishPublications _.pluck fields.library, '_id'

     changed: (id, fields) =>
       # Person should already be added
       assert.notEqual currentPersonId, null

-      publishPublications fields.library
+      publishPublications _.pluck fields.library, '_id'

     removed: (id) =>
       # We cannot remove the person if we never added the person before
@@ -295,7 +295,7 @@
       handlePublications = null

       currentPersonId = null
-      removePublications currentLibrary
+      removePublications _.pluck currentLibrary, '_id'

   @ready()


Remove comma

Remove comma

--- desktop/apps/artwork/client/index.coffee
+++ desktop/apps/artwork/client/index.coffee
@@ -73,7 +73,7 @@
           require('../../../components/current_auctions/index.jsx').default unless context.is_closed
           require '../components/artist_artworks/index.coffee' if context.is_closed
           require '../components/related_artworks/index.coffee' if context.is_closed
-          require '../components/related_artists/index.coffee',
+          require '../components/related_artists/index.coffee'
         ]

       # Make some Auctions-only layout tweaks. See https://github.com/artsy/auctions/issues/360#event-1017559326


Only open annotations if the anchor is for an annotation

--- app/assets/javascripts/legislation_annotatable.js.coffee
+++ app/assets/javascripts/legislation_annotatable.js.coffee
@@ -100,7 +100,8 @@

   scrollToAnchor: ->
     annotationsLoaded: (annotations) ->
-      if anchor = $(location).attr('hash')
+      anchor = $(location).attr('hash')
+      if anchor && anchor.startsWith('#annotation')
         ann_id = anchor.split("-")[-1..]
         el = $("span[data-annotation-id='" + ann_id + "']")
         App.LegislationAllegations.show_comments()


F POST

F POST

--- app/assets/javascripts/mixins/model_ajax_mixin.js.coffee
+++ app/assets/javascripts/mixins/model_ajax_mixin.js.coffee
@@ -20,6 +20,6 @@

   post: (url, data) ->
-    @_ajax url, 'PUT', { data: data }
+    @_ajax url, 'POST', { data: data }


Test narrow and wide images

Test narrow and wide images

--- spec/Resize.coffee
+++ spec/Resize.coffee
@@ -104,16 +104,14 @@

     it 'should resize it right to default dimension', (done) ->
       expected =
-        width: 256
-        height: 256
-      out.on 'data', (data) ->
-        buffer = sharp data
-        buffer.metadata (err, meta) ->
-          chai.expect(meta.width).to.be.equal expected.width
-          chai.expect(meta.height).to.be.equal expected.height
-          done()
-
-      testutils.getBuffer __dirname + '/fixtures/lenna.png', (buffer) ->
+        width: c.defaultDimension
+      out.on 'data', (data) ->
+        buffer = sharp data
+        buffer.metadata (err, meta) ->
+          chai.expect(meta.width).to.be.equal expected.width
+          done()
+
+      testutils.getBuffer __dirname + '/fixtures/foo.jpeg', (buffer) ->
         ins.send buffer

     it 'should not resize up (just down, without enlargement)', (done) ->
@@ -178,16 +176,14 @@

     it 'should resize it right to default dimension', (done) ->
       expected =
-        width: 256
-        height: 256
-      out.on 'data', (data) ->
-        buffer = sharp data
-        buffer.metadata (err, meta) ->
-          chai.expect(meta.width).to.be.equal expected.width
-          chai.expect(meta.height).to.be.equal expected.height
-          done()
-
-      ins.send __dirname + '/fixtures/lenna.png'
+        width: c.defaultDimension
+      out.on 'data', (data) ->
+        buffer = sharp data
+        buffer.metadata (err, meta) ->
+          chai.expect(meta.width).to.be.equal expected.width
+          done()
+
+      ins.send __dirname + '/fixtures/foo.jpeg'

     it 'should not resize up (just down, without enlargement)', (done) ->
       expected =
@@ -228,6 +224,46 @@
       testutils.getBuffer __dirname + '/Resize.coffee', (buffer) ->
         ins.send buffer

+  describe 'when passed a narrow image with default values', ->
+    it 'should resize to (? x default height)', (done) ->
+      original =
+        width: 736
+        height: 7337
+      expected =
+        height: c.defaultDimension
+      calculatedFactor = null
+      factor.on 'data', (data) ->
+        calculatedFactor = data
+      out.on 'data', (data) ->
+        buffer = sharp data
+        buffer.metadata (err, meta) ->
+          chai.expect(meta.height).to.be.equal expected.height
+          chai.expect(calculatedFactor).to.be.equal original.height / expected.height
+          done()
+
+      testutils.getBuffer __dirname + '/fixtures/narrow.jpg', (buffer) ->
+        ins.send buffer
+
+  describe 'when passed a wide image with default values', ->
+    it 'should resize to (default width x ?)', (done) ->
+      original =
+        width: 7337
+        height: 736
+      expected =
+        width: c.defaultDimension
+      calculatedFactor = null
+      factor.on 'data', (data) ->
+        calculatedFactor = data
+      out.on 'data', (data) ->
+        buffer = sharp data
+        buffer.metadata (err, meta) ->
+          chai.expect(meta.width).to.be.equal expected.width
+          chai.expect(calculatedFactor).to.be.equal original.width / expected.width
+          done()
+
+      testutils.getBuffer __dirname + '/fixtures/wide.jpg', (buffer) ->
+        ins.send buffer
+
   describe 'when passed a GIF buffer', ->
     original =
       width: 500


more elegant promise handling

more elegant promise handling

--- spec/client.coffee
+++ spec/client.coffee
@@ -62,24 +62,23 @@
           expect(response).toEqual(msg)

     it "can evaluate code and return the result", ->
-      [1..10].forEach (x) ->
-        waitsForPromise ->
-          evalsimple("#{x}^2").then (result) ->
-            expect(result).toBe(Math.pow(x, 2))
+      remote = [1..10].map (x) -> evalsimple("#{x}^2")
+      waitsForPromise ->
+        Promise.all(remote).then (remote) ->
+          expect(remote).toEqual (Math.pow(x, 2) for x in [1..10])

     it "can rpc into the frontend", ->
       client.handle test: (x) -> Math.pow(x, 2)
-      [1..10].forEach (x) ->
-        waitsForPromise ->
-          evalsimple("@rpc test(#{x})").then (result) ->
-            expect(result).toBe(Math.pow(x, 2))
+      remote = (evalsimple("@rpc test(#{x})") for x in [1..10])
+      waitsForPromise ->
+        Promise.all(remote).then (remote) ->
+          expect(remote).toEqual (Math.pow(x, 2) for x in [1..10])

     it "can retrieve promise values from the frontend", ->
-      client.handle test: (x) ->
-        Promise.resolve x
+      client.handle test: (x) -> Promise.resolve x
       waitsForPromise ->
         evalsimple("@rpc test(2)").then (x) ->
-          expect(x).toBe(2)
+          expect(x).toBe 2

     it "captures stdout", ->
       data = ''
@@ -129,14 +128,11 @@
           expect(doneSpy.calls.length).toBe(1)

     it "can handle a large number of concurrent callbacks", ->
-      n = 1000
+      n = 100
       cbs = (evalsimple("sleep(rand()); #{i}^2") for i in [0...n])
-      t = new Date().getTime()
-      [0...n].forEach (i) ->
-        waitsForPromise ->
-          cbs[i].then (result) -> expect(result).toBe(Math.pow(i, 2))
-      runs ->
-        expect(new Date().getTime() - t).toBeLessThan(2000)
+      waitsForPromise ->
+        Promise.all(cbs).then (xs) ->
+          expect(xs).toEqual (Math.pow(x, 2) for x in [0...n])

   it "handles shutdown correctly", ->
     waitsFor (done) ->


Avoid navigation on blocked projects

Avoid navigation on blocked projects

--- app/coffee/app.coffee
+++ app/coffee/app.coffee
@@ -780,7 +780,8 @@

 init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService,
-        loaderService, navigationBarService, errorHandlingService, lightboxService, $tgConfig) ->
+        loaderService, navigationBarService, errorHandlingService, lightboxService, $tgConfig,
+        projectService) ->
     $log.debug("Initialize application")

     $rootscope.$on '$translatePartialLoaderStructureChanged', () ->
@@ -869,6 +870,9 @@
     $rootscope.$on '$locationChangeStart',  (event) ->
         errorHandlingService.init()

+        if projectService.project?.get('blocked_code')
+            errorHandlingService.block()
+
         if lightboxService.getLightboxOpen().length
             event.preventDefault();

@@ -1001,5 +1005,6 @@
     "tgErrorHandlingService",
     "lightboxService",
     "$tgConfig",
+    "tgProjectService",
     init
 ])


removed outdated tag.coffee

removed outdated tag.coffee

--- app/coffee/tag.coffee
+++ app/coffee/tag.coffee
@@ -1,272 +0,0 @@
-# class window.TagsController
-#   constructor: (containerSelector, mode='edit')->
-#     @$container = $(containerSelector)
-#
-#
-#     switch mode
-#       when 'edit'
-#         @tagViewClass = EditingTagView
-#         @tagInputClass = TagInput
-#         @tagViewListClass = EditingTagViewList
-#         @$container.find('.tagAdd').click => $.publish 'tags:create', [@tagInput.value()]
-#       when 'show'
-#         @tagViewClass = ShowingTagView
-#         @tagInputClass = ShowingTagInput
-#         @tagViewListClass = TagViewList
-#         $('.tagAnalysis .trigger').live( 'click', (event)->
-#           $(this).parent().toggleClass('expanded')
-#         )
-#         #@$container.find('.tagAdd').click => $.publish 'tags:create', [@tagInput.value()]
-#
-#     @tagInput = new @tagInputClass(@$container.find('#newTag'))
-#     @tagViews = new @tagViewListClass(@tagViewClass)
-#
-#     $.subscribe 'tags:create', (tagName)=>
-#       # Check for empty tag & less than 3 char tag & default tag
-#       # Don't let them post unless they meet all the criteria
-#       if tagName isnt "" and tagName isnt "who/where/what" and tagName.length > 1
-#
-#         # Check for max tags
-#         if @tagViews.tagViews.length < 17
-#           @createTag(tagName)
-#
-#
-#   createTag: (tagName)->
-#     # if comma or slash delimited, split into an array of tags and append each one
-#     if /,/.test(tagName)
-#       tagNames = tagName.split(/,+/)
-#       @appendTag tag for tag in tagNames
-#     else if /\//.test(tagName)
-#       tagNames = tagName.split(/\/+/)
-#       @appendTag tag for tag in tagNames
-#     else
-#       @appendTag tagName
-#
-#   appendTag: (tagName)->
-#     tag = new Tag(tagName)
-#     tagView = new @tagViewClass(tag)
-#     if (!@tagViews.tagAlreadyExists(tagView))
-#       tagView.appendTo(@tagViews.$container)
-#       @tagViews.add(tagView)
-#
-# class TagInput
-#   constructor: ($input)->
-#     @$input = $input
-#
-#     @$input.keypress (event) =>
-#       if event.which is 13
-#         $.publish 'tags:create', [@value()]
-#         return false
-#
-#     $.subscribe 'tags:create', => @clear()
-#   value: -> @$input.val()
-#   clear: -> @$input.val('')
-#
-# class ShowingTagInput extends TagInput
-#   constructor: ($input)->
-#     super($input)
-#     @buttonMode = 'expand'
-#
-#     #$('.tagInput').css('width', '0px')
-#     $('.tagThisEntry').click =>
-#       @addExpandSubmitHandler()
-#     $('.tagHeader').click => @expandContractInputField()
-#
-#   expandContractInputField: ->
-#     switch @buttonMode
-#       when 'expand'
-#         @expandInputField()
-#       when 'submit'
-#         @contractInputField()
-#
-#   addExpandSubmitHandler: ->
-#     switch @buttonMode
-#       when 'expand'
-#         @expandInputField()
-#       when 'submit'
-#         # check if field is empty
-#         $.publish 'tags:create', [@value()]
-#
-#   expandInputField: ->
-#     @buttonMode = 'submit'
-#     $('.tagThisEntry').addClass('selected')
-#     $('.tagInput').animate({width: '200px'})
-#     $('#newTag').focus()
-#   contractInputField: ->
-#     @buttonMode = 'expand'
-#     $('.tagThisEntry').removeClass('selected')
-#     $('.tagInput').animate({width: '0px'})
-#     $('#newTag').blur()
-#
-#
-#
-#
-# class TagViewList
-#   constructor: (tagViewClass)->
-#     @$container = $('#tag-list')
-#     @tagViews = []
-#
-#     @tagViewClass = tagViewClass
-#     @addAllCurrentTags()
-#
-#     clickEvent = if (navigator.userAgent.match(/iPad/i)) then "touchstart" else "click"
-#
-#     @$container.find('.tag .close').live clickEvent, (event)=>
-#       @removeTag($(event.currentTarget).parent().data('id'))
-#
-#     #@$container.delegate 'div', 'click', (event)=>
-#     #  @removeTag($(event.currentTarget).data('id'))
-#
-#     $.subscribe 'tags:remove', (id) =>
-#       @findByTagId(id).startRemoveFromView()
-#
-#     # set up array functionality
-#     Array::remove = (e) -> @[t..t] = [] if (t = @.indexOf(e)) > -1
-#
-#   addAllCurrentTags: ->
-#     # Fill up @tagViews with tags for each currently displayed tags
-#     for currentElement in @$container.find('.tag')
-#       $element = $(currentElement)
-#       id = $element.data('id')
-#       name = $element.find('.tag-name').text()
-#       tag = new Tag(name, id)
-#       tagView = new @tagViewClass( tag )
-#       tagView.linkElement($element)
-#       @add(tagView)
-#
-#   findByTagId: (tagId)->
-#     return tagView for tagView in @tagViews when tagView.tag.id == tagId
-#   findByName: (tagName)->
-#     return tagView for tagView in @tagViews when tagView.tag.name == tagName
-#   add: (tagView)->
-#     if !@tagAlreadyExists(tagView)
-#       @tagViews.push(tagView)
-#       tagView.fadeIn()
-#
-#   tagAlreadyExists: (tagView) ->
-#     tagString = tagView.tag.name
-#     for tagViewz in @tagViews
-#       if tagViewz.tag.name is tagString
-#         return true
-#     return false
-#
-#   removeTag: (tagId) ->
-#     tagViewToRemove = @findByTagId(tagId)
-#     tagViewToRemove.remove()
-#     @tagViews.remove(tagViewToRemove)
-#
-# class EditingTagViewList extends TagViewList
-#   constructor: (tagViewClass)->
-#     @$container = $('#tag-list')
-#     @tagViews = []
-#
-#     @tagViewClass = tagViewClass
-#     @addAllCurrentTags()
-#
-#     @$container.find('.tag').live "click", (event)=>
-#       @removeTag($(event.currentTarget))
-#
-#     #@$container.delegate 'div', 'click', (event)=>
-#     #  @removeTag($(event.currentTarget).data('id'))
-#
-#     $.subscribe 'tags:remove', (id) =>
-#       @findByTagId(id).startRemoveFromView()
-#
-#     $.subscribe 'tags:removed', (id) =>
-#       @findByTagId(id).removeFromView()
-#
-#   removeTag: ($tag) ->
-#     tagName = $tag.find('.tag-name').text()
-#
-#
-#     new_tags = (tag for tag in @tagViews when tag.tag.name != tagName)
-#
-#     @tagViews = new_tags
-#     $tag.css('backgroundColor', '#ff0000')
-#     $tag.fadeOut('fast', =>
-#       $tag.remove()
-#     )
-#
-# class TagView
-#   constructor: (tag)->
-#     @tag = tag
-#
-#   tagNode: -> @tag
-#   linkElement: (element)->
-#     @$element = element
-#   setValue: (tagName) ->
-#     @$element.find('.tag-name').html(tagName)
-#   setId: (id) ->
-#     @$element.attr('data-id', id)
-#     @tag.setId(id)
-#   fadeIn: ->
-#     # TODO: This should pull the current bg color, change to dark, then animate up to the supposed-to color
-#     #currentBackground = @$element.css('backgroundColor');
-#     #@$element.css('backgroundColor', '#777');
-#     #setTimeout (=> @$element.animate {backgroundColor: currentBackground}, 'slow'), 200
-#   startRemoveFromView: ->
-#     @$element.css('backgroundColor', '#ff0000')
-#   removeFromView: ->
-#     @$element.fadeOut('fast', =>
-#       @$element.remove()
-#     )
-#
-#
-# class EditingTagView extends TagView
-#   inputHtml: '<input type="hidden" value=":tagName" name="what_tags[]" />'
-#   createElement: ->
-#     @$element = $('.emptyTag').clone()
-#     @$element.removeClass('hidden emptyTag')
-#     @setValue(@tag.name)
-#
-#     hiddenFieldString = @inputHtml.replace(/:tagName/, @tag.name)
-#     @$element.append(hiddenFieldString)
-#     @$element
-#   appendTo: ($container)->
-#     $container.append( @createElement() )
-#
-#   remove: ->
-#     #if $("#sorting").val() is "1"
-#     @removeFromView()
-#
-# class ShowingTagView extends TagView
-#   # ask for ajax stuff
-#   constructor: (tag) ->
-#     super(tag)
-#   appendTo: ($container)->
-#     @tag.create().then (response)=>
-#       @$element = $(response.html)
-#       @setId(response.what_id)
-#       $container.append( @$element )
-#
-#   remove: ->
-#     # FIX THIS HERE This if statement is not firing properly
-#     #if $("#sorting").val() is "1"
-#     @removeFromView()
-#     @tag.destroy()
-#
-#
-# # Tag Model
-# class Tag
-#   constructor: (name, id = '') ->
-#     @name = name
-#     @id = id
-#   entryId: -> $('#showEntry').data('id')
-#   setId: (id)-> @id = id
-#   create: ->
-#     deferred = $.post "/tags", { entry_id: @entryId(), what_name: @name }, (data)->
-#       @id = data.what_id
-#     return deferred.promise()
-#
-#   destroy: ->
-#     $.publish 'tags:remove', [@id]
-#
-#     $.ajax {
-#       type: 'DELETE'
-#       url: '/tags/what'
-#       data:
-#         entry_id: @entryId()
-#         what_id: @id
-#     }
-#
-#


add removeItem, length and toString to mockLocalStorage

--- src/mockLocalStorage.coffee
+++ src/mockLocalStorage.coffee
@@ -1,11 +1,29 @@
+DEBUG = false
+FILTER_OUT = ['getItem', 'setItem', 'removeItem', 'toString']
+
 localStorage = {}
 localStorage.getItem = (key) ->
-  #console.log "getItem(#{key})"
+  console.log "getItem(#{key})" if DEBUG
   return localStorage[key]
+  console.log "In localStorage: #{this.toString()}" if DEBUG

 localStorage.setItem = (key, value) ->
-  #console.log "setItem(#{key}, #{value})"
+  console.log "setItem(#{key}, #{value})" if DEBUG
   localStorage[key] = value + ""
+  console.log "In localStorage: #{this.toString()}" if DEBUG
+
+localStorage.removeItem = (key) ->
+  console.log "removeItem(#{key})" if DEBUG
+  delete localStorage[key]
+  console.log "In localStorage: #{this.toString()}" if DEBUG
+
+localStorage.toString = ->
+    return ("#{key}: #{value}" if not key in FILTER_OUT for own key, value of this).toString()
+
+ Object.defineProperty localStorage, "length",
+  get: ->
+    Object.keys(this).length - FILTER_OUT.length
+

 root = exports ? window
-root.localStorage = localStorage +root.localStorage = localStorage


Test notification use User preferences duration

--- packages/rocketchat-ui-account/account/accountPreferences.coffee
+++ packages/rocketchat-ui-account/account/accountPreferences.coffee
@@ -109,6 +109,7 @@

 	'click .test-notifications': ->
 		KonchatNotification.notify
+			duration: $('input[name=desktopNotificationDuration]').val()
 			payload:
 				sender:
 					username: 'rocket.cat'


Add Guess Who! feature

Add Guess Who! feature

--- scripts/nostalgia.coffee
+++ scripts/nostalgia.coffee
@@ -5,7 +5,11 @@
 #   nostalgiabot (Remind me of|Quote) <person> - Digs up a memorable quote from the past.
 #   nostalgiabot Remember that <person> said "<quote>" - Stores a new quote, to forever remain in the planes of Nostalgia.
 #   nostalgiabot Who do you remember? - See the memories the NostalgiaBot holds on to.
-#   nostalgiabot Make <person1> talk to <person2> -
+#   nostalgiabot Make <person1> talk to <person2> - Start a nonsensical convo
+#   nostalgiabot Start Guess Who - Start a game of Guess Who!
+#   nostalgiabot Show Guess Who - Show the current quote to guess
+#   nostalgiabot Guess <person> - Guess who said the current quote. Ends when guessed correctly.
+#   nostalgiabot End Guess Who - End the game of Guess Who!
 #   nostalgiabot Hacker me - Get a 100% real quote from a professional hacker.
 #   nostalgiabot BS me - Get a technobable quote that sounds almost real.
 #   nostalgiabot stats - See how memorable everyone is
@@ -49,7 +53,7 @@
             d = new Date()
             randomQuote = randomQuote.replace('$current_day', weekday[d.getDay()])

-        res.send "\"" + randomQuote + "\" - #{displayName}"
+        res.send "\"#{randomQuote}\" - #{displayName}"
     else
         res.send "I don't remember #{displayName}"

@@ -113,6 +117,41 @@

         res.send "Memory stored!"

+guessWhoPlaying = false
+guessWhoTarget = ''
+guessWhoQuote = ''
+startGuessWhoRespond = (res) ->
+    guessWhoPlaying = true
+    res.send "Guess Who game started!"
+
+    guessWhoTarget = res.random Object.keys(memories)
+    guessWhoQuote = res.random memories[guessWhoTarget]
+    res.send "Who said \"#{guessWhoQuote}\"?"
+
+showGuessWhoQuoteRespond = (res) ->
+    if guessWhoPlaying
+        res.send "Who said \"#{guessWhoQuote}\"?"
+    else
+        res.send "You are not playing Guess Who right now"
+
+guessRespond = (res) ->
+    senderName = res.message.user.name
+    guess = res.match[1].toLowerCase().trim()
+    if guessWhoPlaying
+        if guess == guessWhoTarget
+            res.send "Correct! #{toTitleCase senderName} correctly guessed that #{toTitleCase guessWhoTarget} said \"#{guessWhoQuote}\""
+            endGuessWhoRespond res
+        else
+            res.send "Wrong! Try again"
+    else
+        res.send "You are not playing Guess Who right now"
+
+endGuessWhoRespond = (res) ->
+    guessWhoPlaying = false
+    guessWhoTarget = ''
+    guessWhoQuote = ''
+    res.send "Guess Who game over"
+
 module.exports = (robot) ->
     robot.respond /Remember that (.*) said "(.*)"/i, rememberPerson

@@ -128,6 +167,11 @@
         res.send Object.keys(memories)

     robot.respond /stats/i, statsRespond
+
+    robot.respond /start guess who/i, startGuessWhoRespond
+    robot.respond /show guess who/i, showGuessWhoQuoteRespond
+    robot.respond /guess (.*)/i, guessRespond
+    robot.respond /end guess who/i, endGuessWhoRespond

     robot.hear /.*I found a bug.*/i, (res) ->
         res.send "There are no bugs, just happy little accidents!"


remove error monitoring changes as well

remove error monitoring changes as well

--- tutor/src/components/error-monitoring/index.cjsx
+++ tutor/src/components/error-monitoring/index.cjsx
@@ -4,6 +4,7 @@
 isEmpty = require 'lodash/isEmpty'

 ErrorHandlers = require './handlers'
+{ isReloaded } = require '../../helpers/reload';
 {AppStore, AppActions} = require '../../flux/app'

 Dialog = require '../tutor-dialog'
@@ -22,6 +23,7 @@
   bindUpdate: ->
     error = AppStore.getError()

+    return unless error and not isReloaded()
     dialogAttrs = ErrorHandlers.forError(error, @context)

     Dialog.show( dialogAttrs ).then(dialogAttrs.onOk, dialogAttrs.onCancel)


Slightly prettier forms appearing on course pages

--- app/assets/javascripts/leap.coffee
+++ app/assets/javascripts/leap.coffee
@@ -2,7 +2,7 @@
   $('#person_photo>img')
     .load(-> $(this).fadeIn())
     .mouseover(-> $(this).effect('shake', {times:2,distance:3},100))
-  $('#flash_notice').delay(4000).fadeOut()
+  $('#flash_notice').delay(4000).fadeOut('slow')
   $('#q').focus();
   $('#search_extended').click( -> $('#search_form').submit())
   $('.tabs').tabs()
@@ -25,10 +25,10 @@
   $('#person_header_selector').change ->
     if ($('#person_header_selector option:selected').val())
       $('#event_header_spinner').show()
-      $('#event_header').hide()
+      $('#event_header').hide('fast')
       $('#event_header').load $('#person_header_selector option:selected').val(), ->
         $('.tabs').tabs()
-        $('#event_header').show()
         $('#event_header_spinner').hide()
+        $('#event_header').show('fast')
     else
-      $('#event_header').empty()
+      $('#event_header').hide('fast', -> $('#event_header').empty())


implemented streaming committer and applied to write

--- redis-stream-storage.coffee
+++ redis-stream-storage.coffee
@@ -103,47 +103,11 @@

             write: (commit, callback) ->
-                id = getEventsKey(commit.streamId)
-                concurrency = client.stream()
-                check = (data, next) ->
-                    score = Number(data)
-                    #first record is likely the actual object
-                    return if isNaN score
-                    return next null, null if score == commit.checkRevision
-                    next new ConcurrencyError()
-
-
-                writeCommit = (data, next) ->
-                    reply = null
-                    writer = client.stream 'zadd', id, commit.streamRevision
-                    writer.on 'data', (data) ->
-                        reply = JSON.parse data
-                    writer.on 'end', ->
-                        callback null, reply
-                    #dont need to persist our check
-                    delete commit.checkRevision
-                    writer.write JSON.stringify commit
-                    writer.end()
-
-                finish = writeCommit
-
-                concurrency.on 'data', (reply) ->
-                    score = Number(reply)
-                    #first record is likely the actual object
-                    return if isNaN score
-                    return if score == commit.checkRevision
-                    finish = -> callback new ConcurrencyError()
-
-                concurrency.on 'end', ->
-                    finish()
-
-                concurrency.redis.write Redis.parse [
-                    'zrevrange'
-                    id
-                    0
-                    1
-                    'WITHSCORES'
-                ]
-                concurrency.end()
+                committer = @createCommitter()
+                committer.on 'commit', (data) ->
+                    callback null, data
+                committer.on 'error', (err) ->
+                    callback err
+                return committer.write commit
         cb null, new Storage()


Make wesabe.canvas.snapshot CommonJS-compatible.

--- application/chrome/content/wesabe/canvas/snapshot.coffee
+++ application/chrome/content/wesabe/canvas/snapshot.coffee
@@ -1,38 +1,34 @@
-wesabe.provide('canvas.snapshot')
-wesabe.require('io.file')
+file = require 'io/file'

-wesabe.canvas.snapshot =
-  TYPE: 'image/png',
+TYPE = 'image/png'

-  writeToFile: (window, path) ->
-    canvas = @canvasWithContentsOfWindow(window)
+writeToFile = (window, path) ->
+  write path, serializeCanvas(canvasWithContentsOfWindow(window))

-    data = @serializeCanvas(canvas)
-    file = wesabe.io.file.open(path)
+  return true

-    wesabe.io.file.write(file, data)
+canvasWithContentsOfWindow = (window) ->
+  document = window.document
+  canvas   = document.createElement 'canvas'
+  width    = window.innerWidth + window.scrollMaxX
+  height   = window.innerHeight + window.scrollMaxY

-    return true
+  canvas.setAttribute 'width', width
+  canvas.setAttribute 'height', height

-  canvasWithContentsOfWindow: (window) ->
-    document = window.document
-    canvas   = document.createElement('canvas')
-    width    = window.innerWidth + window.scrollMaxX
-    height   = window.innerHeight + window.scrollMaxY
+  context  = canvas.getContext '2d'
+  context.drawWindow window,
+    0,                 #left
+    0,                 #top
+    width,
+    height,
+    'rgb(255,255,255)' #bgcolor

-    canvas.setAttribute('width', width)
-    canvas.setAttribute('height', height)
+  return canvas

-    context  = canvas.getContext('2d')
-    context.drawWindow window,
-      0,                 #left
-      0,                 #top
-      width,
-      height,
-      'rgb(255,255,255)' #bgcolor
+serializeCanvas = (canvas) ->
+  dataurl = canvas.toDataURL TYPE
+  return atob(dataurl.substring(13 + TYPE.length)) # hack off scheme

-    return canvas

-  serializeCanvas: (canvas) ->
-    dataurl = canvas.toDataURL(wesabe.canvas.snapshot.TYPE)
-    return atob(dataurl.substring(13 + @TYPE.length)) # hack off scheme
+module.exports = {writeToFile}


Remove unused ResourceInput component

Remove unused ResourceInput component

--- app/components/resource-input.cjsx
+++ app/components/resource-input.cjsx
@@ -1,79 +0,0 @@
-React = require 'react'
-ChangeListener = require './change-listener'
-handleInputChange = require '../lib/handle-input-change'
-
-module.exports = React.createClass
-  displayName: 'ResourceInput'
-
-  getDefaultProps: ->
-    type: ''
-    resource: null
-    attribute: ''
-    autoSaveDelay: 5000
-
-  getInitialState: ->
-    edited: false
-    saving: false
-    saved: false
-    error: null
-
-  render: ->
-
-      <ChangeListener target={@props.resource} handler={@renderComponent} />
-
-
-        {if @state.error?
-          {@state.error.message}
-        else if @state.saving
-          Saving...
-        else if @state.edited
-          Edited
-        else if @state.saved
-          Saved}
-
-
-
-  renderComponent: ->
-    component = switch @props.type
-      when 'textarea' then 'textarea'
-      else 'input'
-
-    React.createElement component,
-      type: @props.type
-      className: @props.className
-      name: @props.attribute
-      value: @props.resource[@props.attribute]
-      onChange: @handleChange
-      onBlur: @handleBlur
-
-  handleChange: ->
-    handleInputChange.apply @props.resource, arguments
-
-    clearTimeout @state.saveTimeout
-    @setState
-      edited: true
-      saved: false
-      error: null
-      saveTimeout: setTimeout @saveResource, @props.autoSaveDelay
-
-  handleBlur: ->
-    if @state.edited
-      clearTimeout @state.saveTimeout
-      @saveResource()
-
-  saveResource: ->
-    @setState
-      saving: true
-      saved: false
-      error: null
-
-    @props.resource.save()
-      .catch (error) =>
-        @setState
-          error: error
-      .then =>
-        @setState
-          edited: false
-          saved: true
-      .then =>
-        @setState saving: false


Don't display "_"-prefixed metadata

Don't display "_"-prefixed metadata

--- app/components/subject-viewer.cjsx
+++ app/components/subject-viewer.cjsx
@@ -130,7 +130,7 @@
     alert

       Subject metadata

-        {for key, value of @props.subject?.metadata
+        {for key, value of @props.subject?.metadata when key.charAt(0) isnt '_'

             {key}

{JSON.stringify value, null, 2}


refactor(actions): add error as payload

refactor(actions): add error as payload

--- src/Actions.coffee
+++ src/Actions.coffee
@@ -142,7 +142,7 @@
     (dispatch, getState) ->
       dispatch type: T.SHOW_IO_WAIT
       WebAPI.getEntries [getState().search.current], (err, res) ->
-        unless err?
+        unless err
           dispatch
             type    : T.ENTRIES_RESULT
             payload : res
@@ -150,8 +150,7 @@
           dispatch
             type: T.EDIT_CURRENT_ENTRY
             payload : state.entries[state.search.current]
-            err: err
         else
-          dispatch type: T.EDIT_CURRENT_ENTRY, error: err
+          dispatch type: T.EDIT_CURRENT_ENTRY, payload: err, error: yes

 module.exports = Actions


fixed a bug in buildCommand when using String inputs

--- app/helper/executableHelper.coffee
+++ app/helper/executableHelper.coffee
@@ -50,7 +50,10 @@
     execType = getExecutionType programType
     # return the command line call
     #dataPath = _.values(data).join(' ')
-    paramsPath = _.values(params).join(' ')
+    paramsPath = ""
+    for param in _.values(params)
+      paramsPath += '"' + param + '" '
+
     return execType + ' ' + executablePath + ' ' + paramsPath

   buildRemoteCommand = (process) ->


Social: Add searchChats xhr wrapper

Social: Add searchChats xhr wrapper

--- workers/social/lib/social/models/socialapi/requests.coffee
+++ workers/social/lib/social/models/socialapi/requests.coffee
@@ -273,6 +273,12 @@
   if not data.name
     return callback { message: "Name should be set for topic search"}
   url = "/channel/search"
+  get url, data, callback
+
+searchChats = (data, callback)->
+  if not data.name
+    return callback { message: "Name should be set for chat search"}
+  url = "/privatemessage/search"
   get url, data, callback

 fetchProfileFeed = (data, callback)->
@@ -391,6 +397,7 @@
   updateLastSeenTime
   fetchProfileFeed
   searchTopics
+  searchChats
   fetchPrivateMessages
   initPrivateMessage
   sendPrivateMessage


activity: exponential decay for trying to get notifications db.

--- salvus/page/activity.coffee
+++ salvus/page/activity.coffee
@@ -67,14 +67,17 @@
                             cb(undefined, db)

 _init_notifications_done = false
+_init_notifications_retry_interval = 2000
 init_notifications = () ->
     if _init_notifications_done or DISABLE_NOTIFICATIONS
         return
     #console.log('initializing notifications')
     get_notifications_syncdb (err, db) ->
         if err
+            console.log("init_notification: err=#{err}")
             $(".salvus-notification-indicator").hide()
-            setTimeout(init_notifications, 30000) # try again later
+            _init_notifications_retry_interval = Math.min(30000,_init_notifications_retry_interval*1.5)
+            setTimeout(init_notifications, _init_notifications_retry_interval) # try again later
             return
         $(".salvus-notification-indicator").show()
         if _init_notifications_done


fixed resetting of global filters

fixed resetting of global filters

--- app/scripts/struct_services.coffee
+++ app/scripts/struct_services.coffee
@@ -174,7 +174,6 @@

     makeCqp = () ->
         exprs = []
-        console.log("dataObj", dataObj)
         andArray = for attrKey, attrValues of dataObj.filterValues
             attrType = dataObj.attributes[attrKey].settings.type
             op = if attrType is "set" then "contains" else "="
@@ -196,17 +195,18 @@
             $rootScope.globalFilter = null

     $rootScope.$on "corpuschooserchange", () ->
-        console.log("corpuschooserchange struct_services")
         if settings.corpusListing.selected.length is 0
             dataObj.showDirective = false
         else
             [newDefaultFilters, defAttributes] = settings.corpusListing.getDefaultFilters()
             [newOptionalFilters, possAttributes] = settings.corpusListing.getCurrentFilters()

-            console.log("newDefaultFilters) and (_.isEmpty newOptionalFilters)", (_.isEmpty newDefaultFilters), (_.isEmpty newOptionalFilters))
             if (_.isEmpty newDefaultFilters) and (_.isEmpty newOptionalFilters)
                 dataObj.showDirective = false
                 $location.search("global_filter", null)
+                for filter in dataObj.selectedFilters
+                    dataObj.filterValues[filter].value = []
+
             else
                 dataObj.showDirective = true
                 if not (_.isEqual newDefaultFilters, dataObj.defaultFilters) and (_.isEqual newOptionalFilters, dataObj.optionalFilters)


Support for reactive sorting. Fixes #477

--- server/lib/search.coffee
+++ server/lib/search.coffee
@@ -31,37 +31,163 @@

   initializing = results.length

-  order = 0
   counts = []
   resultsHandles = []
   countsHandles = []
   for result, i in results
     do (result, i) ->
+      orderMap = {}
+      orderList = []
+
+      assertOrder = ->
+        assert.equal orderList.length, _.size orderMap
+        for orderId, orderIndex in orderList
+          assert.equal orderMap[orderId], orderIndex
+
       counts[i] = 0

       resultsHandles[i] = result.cursor.observeChanges
-        added: (id, fields) =>
+        addedBefore: (id, fields, before) =>
+          if before
+            beforeIndex = orderMap[before]
+
+            # Insert
+            orderList.splice beforeIndex, 0, id
+            orderMap[id] = beforeIndex
+
+            # Reindex all after the insertion point
+            for orderId, orderIndex in orderList[(beforeIndex + 1)..]
+              orderIndex += beforeIndex + 1
+              orderMap[orderId] = orderIndex
+              publish.changed result.cursor._cursorDescription.collectionName, orderId,
+                searchResult:
+                  _id: queryId
+                  order: orderIndex
+
+          else # Added at the end
+            orderList.push id
+            orderMap[id] = orderList.length - 1
+
           fields.searchResult =
             _id: queryId
-            # TODO: Implement
-            order: ++order
-
-          fields = result.added id, fields if result.added
+            order: orderMap[id]
+
+          fields = result.added id, fields if result.added # Optional preprocessing callback

           publish.added result.cursor._cursorDescription.collectionName, id, fields

+          assertOrder()
+
         changed: (id, fields) =>
-          # TODO: Maybe order changed now?
-
-          fields = result.changed id, fields if result.changed
+          fields = result.changed id, fields if result.changed # Optional preprocessing callback

           publish.changed result.cursor._cursorDescription.collectionName, id, fields unless _.isEmpty fields

+        movedBefore: (id, before) =>
+          idIndex = orderMap[id]
+
+          # TODO: Can be before null?
+          unless before # Moved to the end
+            # Remove from the current position
+            orderList.splice idIndex, 1
+            delete orderMap[id]
+
+            # Reindex all from the deletion point on
+            for orderId, orderIndex in orderList[idIndex..]
+              orderIndex += idIndex
+              orderMap[orderId] = orderIndex
+              publish.changed result.cursor._cursorDescription.collectionName, orderId,
+                searchResult:
+                  _id: queryId
+                  order: orderIndex
+
+            # Add at the end
+            orderList.push id
+            orderMap[id] = orderList.length - 1
+
+            publish.changed result.cursor._cursorDescription.collectionName, id,
+              searchResult:
+                _id: queryId
+                order: orderMap[id]
+
+          else
+            beforeIndex = orderMap[before]
+
+            if idIndex < beforeIndex # Moved after current position
+              # Remove from the current position
+              orderList.splice idIndex, 1
+              delete orderMap[id]
+
+              # Reindex all from the deletion point to, but excluding the new position (we
+              # will reinsert there and push everything afterwards back to the right position)
+              for orderId, orderIndex in orderList[idIndex...(beforeIndex - 1)]
+                orderIndex += idIndex
+                orderMap[orderId] = orderIndex
+                publish.changed result.cursor._cursorDescription.collectionName, orderId,
+                  searchResult:
+                    _id: queryId
+                    order: orderIndex
+
+              # Add at the new position
+              orderList.splice beforeIndex - 1, 0, id
+              orderMap[id] = beforeIndex - 1
+
+              publish.changed result.cursor._cursorDescription.collectionName, id,
+                searchResult:
+                  _id: queryId
+                  order: orderMap[id]
+
+            else if beforeIndex < idIndex # Move before current position
+              # Remove from the current position
+              orderList.splice idIndex, 1
+              delete orderMap[id]
+
+              # Add at the new position
+              orderList.splice beforeIndex, 0, id
+              orderMap[id] = beforeIndex
+
+              publish.changed result.cursor._cursorDescription.collectionName, id,
+                searchResult:
+                  _id: queryId
+                  order: orderMap[id]
+
+              # Reindex all after the insertion point on, including
+              # the old position (to where everything was pushed to)
+              for orderId, orderIndex in orderList[(beforeIndex + 1)..idIndex]
+                orderIndex += beforeIndex + 1
+                orderMap[orderId] = orderIndex
+                publish.changed result.cursor._cursorDescription.collectionName, orderId,
+                  searchResult:
+                    _id: queryId
+                    order: orderIndex
+
+            else
+              assert false # Moved to the same position?
+
+          assertOrder()
+
         removed: (id) =>
-          result.removed id if result.removed
+          result.removed id if result.removed # Optional preprocessing callback

           # We remove from the search results and leave to some other publish function to remove whole document
           publish.changed result.cursor._cursorDescription.collectionName, id, searchResult: undefined
+
+          idIndex = orderMap[id]
+
+          # Remove
+          orderList.splice idIndex, 1
+          delete orderMap[id]
+
+          # Reindex all from the deletion point on
+          for orderId, orderIndex in orderList[idIndex..]
+            orderIndex += idIndex
+            orderMap[orderId] = orderIndex
+            publish.changed result.cursor._cursorDescription.collectionName, orderId,
+              searchResult:
+                _id: queryId
+                order: orderIndex
+
+          assertOrder()

       # For counting we want only _id field and no skip or limit restrictions
       result.cursor._cursorDescription.options.fields =


Better gravity. Need a scale factor (pixels != meters)

--- app/js/physics.coffee
+++ app/js/physics.coffee
@@ -15,10 +15,10 @@
     update: ->
       rect.reset()
       rect.resetDebug()
-      contacts = rect.contactPoints(floor)

       unless paused
         rect.integrate ctx.dt / 1000, controls
+      contacts = rect.contactPoints(floor)

       if contacts.length > 0
         unless paused
@@ -64,6 +64,7 @@
   inverseMass: 0 # required value
   inverseMoment: 0 # calculated value

+  acceleration: [0, -100] # gravity
   damping: 0.999 # minimal

   vertices: -> []
@@ -77,11 +78,10 @@
     return if @inverseMass <= 0 or dt <= 0

     @position = Vec.add @position, Vec.scale @velocity, dt
-    acceleration = [0, -15]
-    @velocity = Vec.add @velocity, Vec.scale acceleration, dt
+
+
+    @velocity = Vec.add @velocity, Vec.scale @acceleration, dt
     @velocity = Vec.scale @velocity, Math.pow(@damping, dt)
-
-

   draw: (ctx) ->
     vertices = @vertices()


Added global waiton users

Added global waiton users

--- app/lib/router.coffee
+++ app/lib/router.coffee
@@ -4,6 +4,7 @@
   layoutTemplate: 'layout'
   loadingTemplate: 'loading'
   notFoundTemplate: 'notFound'
+  waitOn: -> Meteor.subscribe 'users'

 # filters
@@ -68,7 +69,6 @@
           [
             Meteor.subscribe 'challenges'
             Meteor.subscribe 'games'
-            Meteor.subscribe 'users'
           ]

     # highscore
@@ -138,7 +138,6 @@
         [
           Meteor.subscribe 'games'
           Meteor.subscribe 'quizzes'
-          Meteor.subscribe 'users'
         ]

       data: -> Games.findOne @params._id


AppController: We don't need to remove modified mark manually. Ace will take care of it.

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -931,8 +931,6 @@
   saveAllFiles: ->

     @forEachSubViewInIDEViews_ 'editor', (editorPane) ->
-      {ace} = editorPane.aceView
-      ace.once 'FileContentRestored', -> ace.removeModifiedFromTab()
       editorPane.emit 'SaveRequested'


Layout: Resurrection: Remember layouts for participants on permanent machines

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -517,12 +517,12 @@

             # Just resurrect snapshot for host or without collaboration.
             # Because we need check the `@myWatchMap` and it is not possible here.
-            if (snapshot and @amIHost) or @mountedMachine.isPermanent()
+            if snapshot and (@amIHost or @mountedMachine.isPermanent())
               return @layoutManager.resurrectSnapshot snapshot

             # Be quiet. Don't write initial views's changes to snapshot.
             # After that, get participant's snapshot from collaboration data and build workspace.
-            @silent = yes  if @isInSession and not @amIHost
+            @silent = yes  if @isInSession and not @amIHost and not @mountedMachine.isPermanent()

             @splitTabView type: 'horizontal', dontSave: yes


Resurrection: Save layout size to KiteStorage

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -893,6 +893,14 @@
     @mountedMachine.getBaseKite()?.storageSetQueued key, value

+  saveLayoutSize: ->
+
+    username  = nick()
+    key       = @getWorkspaceStorageKey "#{username}-LayoutSize"
+    value     = @getLayoutSizeData()
+
+    @writeToKiteStorage key, value
+    @emit 'LayoutSizeChangesSaved' # TODO: Naming.
   removeWorkspaceSnapshot: (username = nick()) ->

     key = @getWorkspaceStorageKey username


Dont check course instances as it is unreliable.

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -1083,7 +1083,7 @@

   applyCourseLogicToLevels: (orderedLevels) ->
     nextSlug = @courseStats.levels.next?.get('slug')
-    nextSlug ?= @courseStats.levels.first.get('slug')
+    nextSlug ?= @courseStats.levels.first?.get('slug')
     return unless nextSlug

     courseOrder = _.sortBy orderedLevels, 'courseIdx'


Keycode and event now should be capturing for IE8, addresses #406

--- app/assets/javascripts/cleantheme/surveyAnimation.js.coffee
+++ app/assets/javascripts/cleantheme/surveyAnimation.js.coffee
@@ -157,26 +157,28 @@
   #####################

   $("body").keydown (e) ->
+    e = e || window.event
+    keyCode = if window.event then e.which else e.keyCode
     if $('.survey-container').length
       # Prevent default up and down and enter on keydown
-      if e.keyCode is 38 or e.keyCode is 40 or e.keyCode is 13
+      if keyCode is 38 or keyCode is 40 or keyCode is 13
         e.preventDefault()
       # Specifically targeting custom date input
       if $(".survey-custom-date").is(":focus")
         # Allow regular command and left/right keys
-        if e.metaKey or e.keyCode is 37 or e.keyCode is 39
+        if e.metaKey or keyCode is 37 or keyCode is 39
           return
         # Ensure proper blurring of date input
-        else if e.keyCode is 13
+        else if keyCode is 13
           $(".survey-custom-date").blur()
           return
         # Autocomplete date when tab or / is pressed
-        else if e.keyCode is 9 or e.keyCode is 191
+        else if keyCode is 9 or keyCode is 191
           e.preventDefault()
           autocompleteDate()
           return
         # Allow deleting, with dynamic string completion
-        else if e.keyCode is 46 or e.keyCode is 8
+        else if keyCode is 46 or keyCode is 8
           writeDate(e.keyCode)
           return
         else if (/^[a-zA-Z]*$/.test(+String.fromCharCode(e.keyCode)))
@@ -188,16 +190,18 @@
           writeDate(e.keyCode)

   $("body").keyup (e) ->
+    e = e || window.event
+    keyCode = if window.event then e.which else e.keyCode
     if $('.survey-container.active').length
       # Handle navigating using the up/down arrow keys
-      if e.keyCode is 38
+      if keyCode is 38
         e.preventDefault()
         if $(".survey-container.active").hasClass "multiple-question-parts" then assignMultipleQuestion(false, true) else assignQuestion(false, true)
-      else if e.keyCode is 40
+      else if keyCode is 40
         e.preventDefault()
         if $(".survey-container.active").hasClass "multiple-question-parts" then assignMultipleQuestion(true, false) else assignQuestion(true, false)
       # Progress to next question for enter
-      else if e.keyCode is 13
+      else if keyCode is 13
         assignQuestion(true, false)
         return
       # Handle hidden inputs first to prevent extra entering
@@ -209,8 +213,9 @@
       # Handle radio_input_multiple
       else if $(".survey-container.active").hasClass "multiple-question-parts"
         inputs = $(".survey-container.active .multiple-question-container.current").find("input:radio")
+        keyCode = if window.event then e.which else e.keyCode
         inputs.each (index) ->
-          if e.keyCode is $(inputs[index]).data("hotkey").toString().charCodeAt(0)
+          if keyCode is $(inputs[index]).data("hotkey").toString().charCodeAt(0)
             $(inputs[index]).prop "checked", true
             handleChangedValue($(inputs[index]))
             assignMultipleQuestion(true, false)
@@ -219,17 +224,19 @@
         # Progress to next question if applicable
         if $(".survey-container.active").data('progress')
           inputs = $(".survey-container.active").find("input:radio")
+          keyCode = if window.event then e.which else e.keyCode
           inputs.each (index) ->
-            if e.keyCode is $(this).data("hotkey").toString().charCodeAt(0)
+            if keyCode is $(this).data("hotkey").toString().charCodeAt(0)
               $(inputs[index]).prop "checked", true
               handleChangedValue($(inputs[index]))
               if $(inputs[index]).data('object') == "reveal-next-input" then revealNextInput($(inputs[index]).data('target')) else assignQuestion(true, false)
         # Otherwise, check answer
         else
           inputs = $(".survey-container.active").find("input:checkbox")
+          keyCode = if window.event then e.which else e.keyCode
           if $(inputs).data('hotkey')
             inputs.each (index) ->
-              if e.keyCode is $(inputs[index]).data("hotkey").toString().charCodeAt(0)
+              if keyCode is $(inputs[index]).data("hotkey").toString().charCodeAt(0)
                 if !($(inputs[index]).prop "checked") and ($(inputs[index]).data('object') == "reveal-next-input")
                   revealNextInput($(inputs[index]).data('target'))
                 $(inputs[index]).prop "checked", !$(inputs[index]).prop("checked")


mmd cayw

mmd cayw

--- chrome/content/zotero-better-bibtex/web-endpoints.coffee
+++ chrome/content/zotero-better-bibtex/web-endpoints.coffee
@@ -235,6 +235,7 @@
       keypostfix: ''
       separator: ','
       clipboard: false
+      format: ''
     }

     for own key of @config
@@ -245,10 +246,25 @@
   accept: (progressCallback) ->
     progressCallback.call(null, 100) if progressCallback
     citation = []
-    for item in @citation.citationItems
-      cite = Zotero.BetterBibTeX.keymanager.get({itemID: item.id}, 'on-export').citekey
-      citation.push(@config.keyprefix + cite + @config.keypostfix)
-    citation = @config.citeprefix + citation.join(@config.separator) + @config.citepostfix
+
+    # {"citationItems":[{"id":"5","locator":"page","prefix":"prefix","suffix":"suffix","suppress-author":true}],"properties":{}}
+    switch @config.format
+      when 'mmd'
+        for item in @citation.citationItems
+          citekey = Zotero.BetterBibTeX.keymanager.get({itemID: item.id}, 'on-export')
+          continue unless citekey
+          if item.prefix
+            citation.push("[#{item.prefix}][##{citekey.citekey}]")
+          else
+            citation.push("[##{citekey.citekey}][]")
+        citation = citation.join('')
+
+      else
+        for item in @citation.citationItems
+          cite = Zotero.BetterBibTeX.keymanager.get({itemID: item.id}, 'on-export').citekey
+          citation.push(@config.keyprefix + cite + @config.keypostfix)
+        citation = @config.citeprefix + citation.join(@config.separator) + @config.citepostfix
+
     Zotero.Utilities.Internal.copyTextToClipboard(citation) if @config.clipboard
     @deferred.fulfill(citation)
     Zotero.Integration.currentWindow.close()


Client: Activity: Deleted unnecessary state and mouse event handlers

--- client/activity/lib/components/chatlistitem/index.coffee
+++ client/activity/lib/components/chatlistitem/index.coffee
@@ -38,7 +38,6 @@
     hover                         : no
     account                       : null
     isDeleting                    : no
-    isMenuOpen                    : no
     channelName                   : ''
     isUserMarkedAsTroll           : no
     isBlockUserModalVisible       : no
@@ -56,7 +55,6 @@
       account                       : @props.account
       editMode                      : @props.message.get '__isEditing'
       isDeleting                    : @props.isDeleting
-      isMenuOpen                    : @props.isMenuOpen
       isUserMarkedAsTroll           : @props.message.get('account').isExempt
       isBlockUserModalVisible       : @props.isBlockUserModalVisible
       isMarkUserAsTrollModalVisible : @props.isMarkUserAsTrollModalVisible
@@ -100,15 +98,7 @@
     key               : @props.message.get 'id'
     className         : classnames
       'ChatItem'      : yes
-      'mouse-enter'   : @state.hover
-      'is-menuOpen'   : @state.isMenuOpen
       'is-selected'   : @props.isSelected
-    onMouseEnter      : =>
-      @setState hover : yes
-    onMouseLeave      : ->
-      kd.utils.debounce 10, =>
-        @setState hover : no
-

   getMenuItems: ->

@@ -273,9 +263,6 @@

     messageId = @props.message.get '_id'
     ActivityFlux.actions.message.unsetMessageEditMode messageId
-
-
-  onMenuToggle: (isMenuOpen) -> @setState { isMenuOpen }

   getEditModeClassNames: -> classnames
@@ -328,11 +315,7 @@

     { message } = @props
     if (message.get('accountId') is whoami().socialApiId) or checkFlag('super-admin')
-      <ButtonWithMenu
-        items       = {@getMenuItems()}
-        onMenuOpen  = {=> @onMenuToggle yes}
-        onMenuClose = {=> @onMenuToggle no}
-      />
+      <ButtonWithMenu items={@getMenuItems()} />

   renderEmbedBox: ->


fix #128

fix #128

--- client/views/common/editShiftModal/editShiftModal.coffee
+++ client/views/common/editShiftModal/editShiftModal.coffee
@@ -98,6 +98,7 @@
 		teamId = $(e.target).closest('.team').attr('teamId')
 		newTeamId = @_id
 		newTeamName = @name
+		newTeamDescription = @description

 		if teamId != newTeamId
 			Meteor.call 'updateShiftItem', shiftId, 'teams', teamId, '_id', newTeamId, (e) ->
@@ -105,6 +106,7 @@
 					handleError e
 				else
 					Meteor.call 'updateShiftItem', shiftId, 'teams', newTeamId, 'name', newTeamName, handleError
+					Meteor.call 'updateShiftItem', shiftId, 'teams', newTeamId, 'description', newTeamDescription, handleError

 	'click .decreaseCount': (e) ->
 		shiftId = FlowRouter.getQueryParam('editShift')


Auto hide active_admin translation inputs if native_informations is checked when loading page

--- vendor/assets/javascripts/active_admin.js.coffee
+++ vendor/assets/javascripts/active_admin.js.coffee
@@ -39,9 +39,12 @@
     $(this).parents('.has_many_delete.boolean').siblings().slideToggle()

   # VideoPlatform title and description
-  $('#video_platform_native_informations_input input').on 'click', ->
-    $this = $(this)
-    if $this.is(':checked')
-      $this.parents('li.input.boolean').next('div').slideUp()
-    else
-      $this.parents('li.input.boolean').next('div').slideDown()
+  if $('#video_platform_native_informations_input input').length
+    $('#video_platform_native_informations_input input').on 'click', ->
+      $this = $(this)
+      if $this.is(':checked')
+        $this.parents('li.input.boolean').next('div').slideUp()
+      else
+        $this.parents('li.input.boolean').next('div').slideDown()
+
+    $('.activeadmin-translations').slideUp() if $('#video_platform_native_informations_input input').is(':checked')


Got rid of the lag before first tick

Got rid of the lag before first tick

--- views/components/main/parts/countdown-timer.cjsx
+++ views/components/main/parts/countdown-timer.cjsx
@@ -34,16 +34,24 @@
     completeTime: -1
     tickCallback: null
     completeCallback: null
+  getTimeRemaining: (completeTime, currentTime = (new Date()).getTime()) ->
+    if completeTime < 0
+      -1
+    else if completeTime <= currentTime
+      0
+    else
+      Math.floor((completeTime - currentTime) / 1000)
   getInitialState: ->
-    @timeRemaining = -1
+    @timeRemaining = @getTimeRemaining(@props.completeTime)

     completeTime: @props.completeTime
   componentDidMount: ->
     @startTick()
   componentWillReceiveProps: (nextProps) ->
-    console.log @props.completeTime
     @stopTick() if nextProps.countdownId isnt @props.countdownId
-    @setState {completeTime: nextProps.completeTime} if nextProps.completeTime isnt @state.completeTime
+    if nextProps.completeTime isnt @state.completeTime
+      @setState {completeTime: nextProps.completeTime}
+      @timeRemaining = @getTimeRemaining(nextProps.completeTime)
   shouldComponentUpdate: (nextProps, nextState) ->
     nextProps.countdownId isnt @props.countdownId or nextState.completeTime isnt @state.completeTime
   componentDidUpdate: ->
@@ -55,16 +63,12 @@
   stopTick: ->
     ticker.unreg @props.countdownId
   tick: (currentTime) ->
-    if @state.completeTime < 0
-      @timeRemaining = -1
-      @stopTick()
-    else
-      @timeRemaining = Math.max 0, Math.floor((@state.completeTime - currentTime) / 1000)
+    @timeRemaining = @getTimeRemaining(@state.completeTime, currentTime)
+    @stopTick() if @timeRemaining < 1
+    if @state.completeTime >= 0
       @textLabel.textContent = resolveTime @timeRemaining if @textLabel?
       @props.tickCallback(@timeRemaining) if @props.tickCallback?
-      if @timeRemaining < 1
-        @stopTick()
-        @props.completeCallback() if @props.completeCallback?
+      @props.completeCallback() if @timeRemaining < 1 and @props.completeCallback?
   render: ->
      @textLabel = ref}>{resolveTime @timeRemaining}


Eliminated currentChannel from ChannelController.

--- core/app/backbone/controllers/channels_controller.coffee
+++ core/app/backbone/controllers/channels_controller.coffee
@@ -63,8 +63,6 @@
     channel

   commonChannelViews: (channel) ->
-    window.currentChannel = channel
-
     @showRelatedChannels channel
     @showUserProfile channel.user()
     FactlinkApp.Sidebar.showForChannelsOrTopicsAndActivateCorrectItem window.Channels, channel, channel.user()


Calculate pixel position of both bracket highlights before adding them

Before, we were causing a layout to occur twice because we needed to
read the DOM for the pixel position of the second highlight view after
invalidating the DOM when adding the first highlight view.

--- src/packages/bracket-matcher/lib/bracket-matcher.coffee
+++ src/packages/bracket-matcher/lib/bracket-matcher.coffee
@@ -50,12 +50,23 @@
     else if previousPosition.isEqual(endPosition)
       editor.setCursorBufferPosition(startPosition)

-  createView: (editor, bufferPosition) ->
-    pixelPosition = editor.pixelPositionForBufferPosition(bufferPosition)
+  addHighlightViews: (editor, bufferRange) ->
+    { start, end } = Range.fromObject(bufferRange)
+    startPixelPosition = editor.pixelPositionForBufferPosition(start)
+    endPixelPosition = editor.pixelPositionForBufferPosition(end)
+    @addHighlightViewAtPixelPosition(editor, bufferPosition: start, pixelPosition: startPixelPosition)
+    @addHighlightViewAtPixelPosition(editor, bufferPosition: end, pixelPosition: endPixelPosition)
+
+  addHighlightViewAtPixelPosition: (editor, {bufferPosition, pixelPosition}) ->
     view = $$ -> @div class: 'bracket-matcher'
     view.data('bufferPosition', bufferPosition)
-    view.css('top', pixelPosition.top).css('left', pixelPosition.left)
-    view.width(editor.charWidth).height(editor.lineHeight)
+    view.css(
+      top: pixelPosition.top
+      left: pixelPosition.left
+      width: editor.charWidth
+      height: editor.lineHeight
+    )
+    editor.underlayer.append(view)

   findCurrentPair: (editor, buffer, matches) ->
     position = editor.getCursorBufferPosition()
@@ -117,12 +128,7 @@
         matchPosition = @findMatchingStartPair(buffer, position, matchingPair, currentPair)

     if position? and matchPosition?
-      if position.isLessThan(matchPosition)
-        underlayer.append(@createView(editor, position))
-        underlayer.append(@createView(editor, matchPosition))
-      else
-        underlayer.append(@createView(editor, matchPosition))
-        underlayer.append(@createView(editor, position))
+      @addHighlightViews(editor, [position, matchPosition])
       @pairHighlighted = true

   subscribeToEditSession: (editSession) ->


collision with regular bounds

collision with regular bounds

--- test/collision.coffee
+++ test/collision.coffee
@@ -54,6 +54,6 @@
     width: 10
     height: 10

-  Collision.ray
+  Collision.rayRectangle
 module()


Refactor / fix page analytics

Refactor / fix page analytics

--- assets/javascripts/lib/page.coffee
+++ assets/javascripts/lib/page.coffee
@@ -41,8 +41,7 @@
   currentState = context.state
   page.dispatch(context)
   context.pushState()
-  ga?('send', 'pageview', location.pathname + location.search + location.hash)
-  _gauges?.push(['track'])
+  track()
   context

 page.replace = (path, state, skipDispatch, init) ->
@@ -51,8 +50,7 @@
   currentState = context.state
   page.dispatch(context) unless skipDispatch
   context.replaceState()
-  ga?('send', 'pageview', location.pathname + location.search + location.hash) unless init
-  _gauges?.push(['track'])
+  track() unless init or skipDispatch
   context

 page.dispatch = (context) ->
@@ -163,3 +161,8 @@

 isSameOrigin = (url) ->
   url.indexOf("#{location.protocol}//#{location.hostname}") is 0
+
+track = ->
+  ga?('send', 'pageview', location.pathname + location.search + location.hash)
+  _gauges?.push(['track'])
+  return


Remove now-unused translation parameter

Remove now-unused translation parameter

--- resources/assets/coffee/react/profile-page/beatmaps.coffee
+++ resources/assets/coffee/react/profile-page/beatmaps.coffee
@@ -44,7 +44,7 @@
       key: section
       h3
         className: 'page-extra__title page-extra__title--small'
-        osu.trans("users.show.extra.beatmaps.#{sectionSnaked}.title", count: count)
+        osu.trans("users.show.extra.beatmaps.#{sectionSnaked}.title")
         ' '
         if count > 0
           span


Remove unnecessary render call when pasting

EditorController#inputControllerDidHandleInput will perform the necessary render

--- src/trix/controllers/editor_controller.coffee
+++ src/trix/controllers/editor_controller.coffee
@@ -187,9 +187,7 @@
     range = @pastedRange
     @pastedRange = null
     @pasting = null
-
     @editorElement.notify("paste", {pasteData, range})
-    @render()

   inputControllerWillMoveText: ->
     @editor.recordUndoEntry("Move")


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/expirable.coffee
+++ test/expirable.coffee
@@ -20,9 +20,13 @@
   obj = GameObject
     duration: 10
     alpha: 0.8
+    fadeOut;

   obj.include Expirable

-  5.times
+  5.times ->
+    obj.update()
+
+

 module()


Note that visual mode is a beta feature.

--- background_scripts/commands.coffee
+++ background_scripts/commands.coffee
@@ -289,8 +289,8 @@
   openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true, repeatLimit: 20 }]

   enterInsertMode: ["Enter insert mode", { noRepeat: true }]
-  enterVisualMode: ["Enter visual mode", { noRepeat: true }]
-  enterVisualLineMode: ["Enter visual line mode", { noRepeat: true }]
+  enterVisualMode: ["Enter visual mode (beta feature)", { noRepeat: true }]
+  enterVisualLineMode: ["Enter visual line mode (beta feature)", { noRepeat: true }]
   # enterEditMode: ["Enter vim-like edit mode (not yet implemented)", { noRepeat: true }]

   focusInput: ["Focus the first text box on the page. Cycle between them using tab",


bugfix publish myuserid

bugfix publish myuserid

--- lib/generators/webrtc_rails/templates/main.js.coffee
+++ lib/generators/webrtc_rails/templates/main.js.coffee
@@ -1,4 +1,6 @@
 class @WebRTC
+  myUserID: null
+
   onWebSocketConnected: ->
   onWebSocketReconnectingStarted: ->
   onWebSocketReConnected: ->
@@ -19,7 +21,7 @@
     if !@_peerStarted && @_localStream
       @_sendMessage(
         type: 'call'
-        remoteUserID: @_myUserID
+        remoteUserID: @myUserID
       )
       @_sendOffer()
       @_peerStarted = true
@@ -51,6 +53,8 @@
     'OfferToReceiveVideo': true

   _webSocketInitialize: (url, userToken) ->
+    @_url = url
+    @_userToken = userToken
     @_webSocket = new WebSocket(url)
     @_webSocket.onopen = =>
       @_startHeartbeat()
@@ -62,13 +66,13 @@

     @_webSocket.onclose = (event) =>
       @onWebSocketReconnectingStarted()
-      @_webSocketInitialize(userToken)
+      @_webSocketInitialize(url, userToken)

     @_webSocket.onmessage = (data) =>
       event = JSON.parse(data.data)
       switch event['type']
         when 'myUserID'
-          @_myUserID = event['myUserID']
+          @myUserID = event['myUserID']
           if @_webSocketConnected
             @onWebSocketReConnected()
           else
@@ -95,10 +99,6 @@
     window.addEventListener('online', (event) =>
       @_webSocket.close()
     )
-    window.addEventListener('online', (event) =>
-      @_webSocket = null
-      @_webSocketInitialize()
-    )

   _startHeartbeat: ->
     if !@_heartbeating


check that content exists before adding it to getAllDocs.

causing error which I think is blowing up the mongo driver elsewhere

--- services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
+++ services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
@@ -48,12 +48,13 @@
 				for folderPath, folder of folders
 					for doc in folder.docs
 						content = docContents[doc._id.toString()]
-						docs[path.join(folderPath, doc.name)] = {
-							_id:   doc._id
-							name:  doc.name
-							lines: content.lines
-							rev:   content.rev
-						}
+						if content?
+							docs[path.join(folderPath, doc.name)] = {
+								_id:   doc._id
+								name:  doc.name
+								lines: content.lines
+								rev:   content.rev
+							}
 				logger.log count:_.keys(docs).length, project_id:project_id, "returning docs for project"
 				callback null, docs


Tests: Routes: Refactor tests to use mocks.

--- client/ide/test/routes.test.coffee
+++ client/ide/test/routes.test.coffee
@@ -1,9 +1,10 @@
 kd           = require 'kd'
+mock         = require '../../mocks/mockingjay'
 expect       = require 'expect'
 routes       = require '../lib/routes'
 dataProvider = require 'app/userenvironmentdataprovider'

-spy                    = null
+
 ROUTE_PARAMS           =
   machine              : { params: { machineLabel: 'koding-vm-0' } }
   noMachine            : { params: { machineLabel: 'not-existing-machine' } }
@@ -13,21 +14,17 @@
   noMachineNoWorkspace : { params: { machineLabel: 'not-existing-machine', workspaceSlug: 'not-existing-workspace' } }

-createSpyAndAssert = (spyOn, spyFor, routeType, routeParams, done) ->
+createSpyAndAssert = (spyOn, spyFor, routeType, routeParams) ->

-  spy = expect.spyOn spyOn, spyFor
+  expect.spyOn spyOn, spyFor
   routes.routeHandler routeType, routeParams

-  kd.utils.wait 333, ->
-    expect(spyOn[spyFor]).toHaveBeenCalled()
-    done()
+  expect(spyOn[spyFor]).toHaveBeenCalled()

 describe 'IDE.routes', ->

-  beforeEach -> spy = null
-
-  afterEach -> expect.restoreSpies() if spy
+  afterEach -> expect.restoreSpies()

   describe '.routeHandler', ->

@@ -35,60 +32,97 @@

       it 'should routeToLatestWorkspace', ->

-        spy = expect.spyOn routes, 'routeToLatestWorkspace'
-        routes.routeHandler 'home'
-        expect(routes.routeToLatestWorkspace).toHaveBeenCalled()
+        createSpyAndAssert routes, 'routeToLatestWorkspace', 'home'

     describe 'when machine, eg. /IDE/6075649037833338981 or /IDE/koding-vm-0', ->

-      it 'should loadCollaborativeIDE if machine label is all digits which we assume it is a channel id related with a collaboration session', (done) ->
+      it 'should loadCollaborativeIDE if machine label is all digits which we assume it is a channel id related with a collaboration session', ->

-        createSpyAndAssert routes, 'loadCollaborativeIDE', 'machine', ROUTE_PARAMS.collaboration, done
+        createSpyAndAssert routes, 'loadCollaborativeIDE', 'machine', ROUTE_PARAMS.collaboration

-      it 'should fetch machine if machine label contains chars', (done) ->
+      it 'should fetch machine if machine label contains chars', ->

-        createSpyAndAssert dataProvider, 'fetchMachine', 'machine', ROUTE_PARAMS.machine, done
+        createSpyAndAssert dataProvider, 'fetchMachine', 'machine', ROUTE_PARAMS.machine

-      it 'should routeToMachineWorkspace if machine is fetched', (done) ->
+      it 'should routeToMachineWorkspace if machine is fetched', ->

-        createSpyAndAssert routes, 'routeToMachineWorkspace', 'machine', ROUTE_PARAMS.machine, done
+        mock.envDataProvider.fetchMachine.toReturnMachine()
+
+        expect.spyOn routes, 'routeToMachineWorkspace'
+        routes.routeHandler 'machine', ROUTE_PARAMS.machine
+        expect(routes.routeToMachineWorkspace).toHaveBeenCalled()

-      it 'should routeToLatestWorkspace if no machine is fetched', (done) ->
+      it 'should routeToLatestWorkspace if no machine is fetched', ->

-         createSpyAndAssert routes, 'routeToLatestWorkspace', 'machine', ROUTE_PARAMS.noMachine, done
+        mock.envDataProvider.fetchMachine.toReturnNull()
+        expect.spyOn routes, 'routeToLatestWorkspace'
+        routes.routeHandler 'machine', ROUTE_PARAMS.noMachine
+        expect(routes.routeToLatestWorkspace).toHaveBeenCalled()

     describe 'when workspace, eg. IDE/koding-vm-0/my-workspace', ->

-      it 'should fetchMachine for the given machine id', (done) ->
+      it 'should fetchMachine for the given machine id', ->

-        createSpyAndAssert dataProvider, 'fetchMachine', 'workspace', ROUTE_PARAMS.workspace, done
+        createSpyAndAssert dataProvider, 'fetchMachine', 'workspace', ROUTE_PARAMS.workspace

-      it 'should ensureDefaultWorkspace', (done) ->
+      it 'should ensureDefaultWorkspace', ->

-        createSpyAndAssert dataProvider, 'ensureDefaultWorkspace', 'workspace', ROUTE_PARAMS.workspace, done
+        mock.envDataProvider.fetchMachine.toReturnMachine()
+        mock.envDataProvider.ensureDefaultWorkspace()
+
+        expect.spyOn dataProvider, 'ensureDefaultWorkspace'
+
+        routes.routeHandler 'workspace', ROUTE_PARAMS.workspace
+        expect(dataProvider.ensureDefaultWorkspace).toHaveBeenCalled()

-      it 'should routeToLatestWorkspace if there is no machine', (done) ->
+      it 'should routeToLatestWorkspace if there is no machine', ->

-        createSpyAndAssert routes, 'routeToLatestWorkspace', 'workspace', ROUTE_PARAMS.noMachineNoWorkspace, done
+        mock.envDataProvider.fetchMachine.toReturnNull()
+        mock.envDataProvider.ensureDefaultWorkspace()
+
+        expect.spyOn routes, 'routeToLatestWorkspace'
+
+        routes.routeHandler 'workspace', ROUTE_PARAMS.noMachineNoWorkspace
+        expect(routes.routeToLatestWorkspace).toHaveBeenCalled()

-      it 'should fetchWorkspaceByMachineUId', (done) ->
+      it 'should fetchWorkspaceByMachineUId', ->

-        createSpyAndAssert dataProvider, 'fetchWorkspaceByMachineUId', 'workspace', ROUTE_PARAMS.workspace, done
+        mock.envDataProvider.fetchMachine.toReturnMachine()
+        mock.envDataProvider.ensureDefaultWorkspace()
+
+        expect.spyOn dataProvider, 'fetchWorkspaceByMachineUId'
+
+        routes.routeHandler 'workspace', ROUTE_PARAMS.workspace
+        expect(dataProvider.fetchWorkspaceByMachineUId).toHaveBeenCalled()

-      it 'should loadIDE if machine and workpsace is found', (done) ->
+      it 'should loadIDE if machine and workpsace is found', ->

-        createSpyAndAssert routes, 'loadIDE', 'workspace', ROUTE_PARAMS.workspace, done
+        mock.envDataProvider.fetchMachine.toReturnMachine()
+        mock.envDataProvider.ensureDefaultWorkspace()
+        mock.envDataProvider.fetchWorkspaceByMachineUId.toReturnWorkspace()
+
+        expect.spyOn routes, 'loadIDE'
+
+        routes.routeHandler 'workspace', ROUTE_PARAMS.workspace
+        expect(routes.loadIDE).toHaveBeenCalled()

-      it 'should routeToMachineWorkspace if workspace is not found', (done) ->
+      it 'should routeToMachineWorkspace if workspace is not found', ->

-        createSpyAndAssert routes, 'routeToMachineWorkspace', 'workspace', ROUTE_PARAMS.noWorkspace, done
+        mock.envDataProvider.fetchMachine.toReturnMachine()
+        mock.envDataProvider.ensureDefaultWorkspace()
+        mock.envDataProvider.fetchWorkspaceByMachineUId.toReturnNull()
+
+        expect.spyOn routes, 'routeToMachineWorkspace'
+
+        routes.routeHandler 'workspace', ROUTE_PARAMS.workspace
+        expect(routes.routeToMachineWorkspace).toHaveBeenCalled()


fix settign accounts-list

fix settign accounts-list

--- app/assets/javascripts/mno_enterprise/angular/directives/impac/widgets/settings/accounts-list.coffee
+++ app/assets/javascripts/mno_enterprise/angular/directives/impac/widgets/settings/accounts-list.coffee
@@ -54,7 +54,10 @@
           acc = _.find(w.content.complete_list, (acc) ->
             acc.uid == accUid
           )
-          w.moveAccountToAnotherList(acc,w.remainingAccounts,w.selectedAccounts,false)
+          # Quick-fix: replace after update of settings update process
+          # w.moveAccountToAnotherList(acc,w.remainingAccounts,w.selectedAccounts,false)
+          removeAccountFromList(acc, w.remainingAccounts)
+          addAccountToList(acc, w.selectedAccounts)
         )

       setting.isInitialized = true


More fixing stuff

More fixing stuff

--- test/spec/portfoliokanban/PortfolioKanbanAppSpec.coffee
+++ test/spec/portfoliokanban/PortfolioKanbanAppSpec.coffee
@@ -188,13 +188,13 @@
 # Due to how the tests are wired up, they only failed once we added the type picker to other pages(S69900).
 # NOTE: these only seem to fail when the whole test suite is run.

-  describe 'when the type is changed', ->
-    it 'should reload the gridboard', ->
-      @_createApp().then =>
-        loadGridBoardSpy = @spy @app, 'loadGridBoard'
-        @app.piTypePicker.setValue(Rally.util.Ref.getRelativeUri(@theme._ref))
-        @waitForCallback(loadGridBoardSpy).then =>
-          expect(loadGridBoardSpy).toHaveBeenCalledOnce()
+#  describe 'when the type is changed', ->
+#    it 'should reload the gridboard', ->
+#      @_createApp().then =>
+#        loadGridBoardSpy = @spy @app, 'loadGridBoard'
+#        @app.piTypePicker.setValue(Rally.util.Ref.getRelativeUri(@theme._ref))
+#        @waitForCallback(loadGridBoardSpy).then =>
+#          expect(loadGridBoardSpy).toHaveBeenCalledOnce()
 #
 #    beforeEach ->
 #      @ajax.whenQuerying('state').respondWith(@initiativeStates)


fix: api scan modal

fix: api scan modal

--- app/components/vnc-viewer.coffee
+++ app/components/vnc-viewer.coffee
@@ -132,6 +132,8 @@
       if ENUMS.PLATFORM.IOS is @get "file.project.platform" # TEMPIOSDYKEY
         @send "doNotRunAPIScan"
       else
+        @set "showURLFilter", false
+        @set "showAPIScan", true
         @set "showAPIScanModal", true

     closeModal: ->
@@ -158,8 +160,6 @@
         that.send "closeModal"
         that.send "dynamicScan"
         that.get("notify").success "Successfully added the url filter & Starting the scan"
-        that.set "showAPIScan", true
-        that.set "showURLFilter", false
       .catch (error) ->
         for error in error.errors
           that.get("notify").error error.detail?.message


Keep root doc list in sync with file tree updates

--- services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
+++ services/web/public/coffee/ide/file-tree/FileTreeManager.coffee
@@ -23,49 +23,55 @@
 		_bindToSocketEvents: () ->
 			@ide.socket.on "reciveNewDoc", (parent_folder_id, doc) =>
 				parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
-				@$scope.$apply () ->
+				@$scope.$apply () =>
 					parent_folder.children.push {
 						name: doc.name
 						id:   doc._id
 						type: "doc"
 					}
+					@recalculateDocList()

 			@ide.socket.on "reciveNewFile", (parent_folder_id, file) =>
 				parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
-				@$scope.$apply () ->
+				@$scope.$apply () =>
 					parent_folder.children.push {
 						name: file.name
 						id:   file._id
 						type: "file"
 					}
-
+					@recalculateDocList()
+
 			@ide.socket.on "reciveNewFolder", (parent_folder_id, folder) =>
 				parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
-				@$scope.$apply () ->
+				@$scope.$apply () =>
 					parent_folder.children.push {
 						name: folder.name
 						id:   folder._id
 						type: "folder"
 						children: []
 					}
+					@recalculateDocList()

 			@ide.socket.on "reciveEntityRename", (entity_id, name) =>
 				entity = @findEntityById(entity_id)
 				return if !entity?
-				@$scope.$apply () ->
+				@$scope.$apply () =>
 					entity.name = name
+					@recalculateDocList()

 			@ide.socket.on "removeEntity", (entity_id) =>
 				entity = @findEntityById(entity_id)
 				return if !entity?
 				@$scope.$apply () =>
 					@_deleteEntityFromScope entity
+					@recalculateDocList()

 			@ide.socket.on "reciveEntityMove", (entity_id, folder_id) =>
 				entity = @findEntityById(entity_id)
 				folder = @findEntityById(folder_id)
 				@$scope.$apply () =>
 					@_moveEntityInScope(entity, folder)
+					@recalculateDocList()

 		selectEntity: (entity) ->
 			@selected_entity_id = entity.id # For reselecting after a reconnect


Add reset() function to OuterScenarioViewModel.

--- src/Witness/Scripts/witness/ViewModels/OuterScenarioViewModel.coffee
+++ src/Witness/Scripts/witness/ViewModels/OuterScenarioViewModel.coffee
@@ -17,6 +17,9 @@
 	run: (context, done, fail) ->
 		@outerScenario.run {}, done, fail

+	reset: ->
+		scenario.reset() for scenario in @innerScenarios
+
 	scenarioTemplate: (item) ->
 		if item instanceof Witness.ViewModels.OuterScenarioViewModel
 			"outer-scenario"


remove reference to 'user_builds_table'

remove reference to 'user_builds_table'

--- js/app.coffee
+++ js/app.coffee
@@ -155,9 +155,7 @@
     if window._gaq? # we dont use ga in test mode
       _gaq.push(['_trackPageview', '/dashboard'])
     mixpanel.track("Dashboard")
-    display "dashboard",
-      builds_table: 'user_builds_table'
-
+    display "dashboard"

   loadAddProjects: (cx) =>
     @current_user().loadOrganizations()


Lock special keys (node ids)

Lock special keys (node ids)

--- graphedit/coffee/graphedit.coffee
+++ graphedit/coffee/graphedit.coffee
@@ -143,21 +143,41 @@
     @TOOLBAR.find('.graphedit-toolbar-new-edge').attr('disabled', 'disabled')
     @drawSelection()

-  _propertyForm : (data) =>
+  _propertyForm : (data, type) =>
+
+    if type == "edge"
+      locked_keys = ["src", "dest"]
+    else
+      locked_keys = ["node_id"]
+
     str = '<form class="form-horizontal graphedit-property-form" role="form">'

     str += '
'
     for k,v of data
+
       str += """

           #{k}

+
+      """
+
+      if k.toLowerCase() in locked_keys
+        str += """
+            <input id="#{k}" name="#{k}-visible" class="form-control input-sm" value="#{v}" disabled>
+            <input id="#{k}" name="#{k}" value="#{v}" type="hidden">
+          """
+      else
+        str += """

               <input id="#{k}" name="#{k}" class="form-control input-sm" value="#{v}">
-

-                  <button class="btn btn-default btn-sm graphedit-remove-property" type="submit"></button>
-

+

+                <button class="btn btn-default btn-sm graphedit-remove-property"></button>
+

+          """
+
+      str += """

       """
@@ -186,20 +206,20 @@
   _setDataviewBindings: () =>
     $('.add_property').on 'click', @clickNewProperty
     $('.graphedit-property-form').on 'submit', @submitPropertyForm
-    $('.graphedit-remove-property').on 'click', @clickRemoveProperty
-
-  # display provided data, or whatever is currently selected
-  displayData : (data) =>
+    $('.graphedit-remove-property').on 'mouseup', @clickRemoveProperty
+
+  # display provided data (for type in {'edge', 'node'}), or whatever is currently selected
+  displayData : (data, type) =>
     @_clearDataviewBindings()

     if data
-      @DATAVIEW.html(@_propertyForm(data.properties))
+      @DATAVIEW.html(@_propertyForm(data.properties, type))
     else if @selected_nodes.length + @selected_edges.length == 0
       @DATAVIEW.html('
No items selected
')
     else if @selected_nodes.length == 1 and @selected_edges.length == 0
-      @DATAVIEW.html(@_propertyForm(d3.select(@selected_nodes[0]).data()[0].properties))
+      @DATAVIEW.html(@_propertyForm(d3.select(@selected_nodes[0]).data()[0].properties, 'node'))
     else if @selected_nodes.length == 0 and @selected_edges.length == 1
-      @DATAVIEW.html(@_propertyForm(d3.select(@selected_edges[0]).data()[0].properties))
+      @DATAVIEW.html(@_propertyForm(d3.select(@selected_edges[0]).data()[0].properties, 'edge'))
     else if @selected_nodes.length == 2 and @selected_edges.length == 0
       @DATAVIEW.html('
Two nodes selected, click  to create an edge
')
     else
@@ -439,7 +459,7 @@

           <input class="form-control input-sm">

-              <button class="btn btn-default btn-sm graphedit-remove-property" type="submit"></button>
+              <button class="btn btn-default btn-sm graphedit-remove-property"></button>

@@ -447,7 +467,7 @@

     """
     @DATAVIEW.find('.properties').append(str)
-    @DATAVIEW.find('.graphedit-remove-property').on 'click', @clickRemoveProperty
+    @DATAVIEW.find('.graphedit-remove-property').on 'mouseup', @clickRemoveProperty

   validatePropertyForm: (form_elm) =>
     #existing properties
@@ -459,7 +479,7 @@
     is_valid = true

     $.each form, (i, d) => data[d.name] = d.value
-
+
     #new properties
     $(form_elm).find('.graphedit-new-property').each (i, elm) =>
       key = $(elm).find('input').first().val()


:lipstick: Sort requires

:lipstick: Sort requires

--- spec/git-diff-spec.coffee
+++ spec/git-diff-spec.coffee
@@ -1,7 +1,7 @@
 path = require 'path'
 {WorkspaceView} = require 'atom'
+fs = require 'fs-plus'
 temp = require 'temp'
-fs = require 'fs-plus'

 describe "GitDiff package", ->
   [editor, editorView, projectPath] = []


LEV-46 static cells added

LEV-46 static cells added

--- modules/levelcreator/app/assets/javascripts/level_creator/plugins/set_segments_static_display.coffee
+++ modules/levelcreator/app/assets/javascripts/level_creator/plugins/set_segments_static_display.coffee
@@ -25,9 +25,4 @@

     mission.staticDisplay = []
     for segment in activeSegments
-      #segment.staticDisplay = false
       mission.staticDisplay.push segment.value
-
-
-    #for segment in activeSegments
-    #  segment.staticDisplay = true


fix device orientation changes

fix device orientation changes

--- framer/Components/DeviceComponent.coffee
+++ framer/Components/DeviceComponent.coffee
@@ -477,6 +477,7 @@
 		@emit("change:orientation", @_orientation)

 	_orientationChange: =>
+		@_orientation = window.orientation
 		@_update()
 		@emit("change:orientation", window.orientation)


sort doesn't return the sorted data

sort doesn't return the sorted data

--- vendor/assets/javascripts/fustrate/generic_table.coffee
+++ vendor/assets/javascripts/fustrate/generic_table.coffee
@@ -10,9 +10,9 @@
   reloadTable: ->

   sortRows: (rows, sortFunction = ->) ->
-    ([sortFunction(row), row] for row in rows)
-      .sort (x, y) -> if x[0] == y[0] then 0 else x[0] > y[0]
-      .map (row) -> row[1]
+    sorted = ([sortFunction(row), row] for row in rows)
+    sorted.sort (x, y) -> if x[0] == y[0] then 0 else x[0] > y[0]
+    sorted.map (row) -> row[1]

   createRow: (item) =>
     @updateRow @constructor.blankRow.clone(), item


:green_heart: Change button options

:green_heart: Change button options

--- spec/tool-bar-spec.coffee
+++ spec/tool-bar-spec.coffee
@@ -172,8 +172,7 @@
         it 'stops event bubbling', ->
           clickSpy = jasmine.createSpy()
           toolBar.onclick = clickSpy
-          button = toolBarAPI.addButton
-            callback: 'application:about'
+          button = toolBarAPI.addButton {}
           button.click()
           expect(clickSpy).not.toHaveBeenCalled()


Reference issues in comments where appropriate

--- spec/vim-mode-spec.coffee
+++ spec/vim-mode-spec.coffee
@@ -46,7 +46,7 @@
       keydown('\\', element: editor[0])
       expect(editor.getText()).toEqual('')

-    # FIXME: Need to discuss this with probablycorey or nathansobo
+    # FIXME: See atom/vim-mode#2
     xit "does not allow the cursor to be placed on the \n character, unless the line is empty", ->
       editor.setText("012345\n\nabcdef")
       editor.setCursorScreenPosition([0, 5])
@@ -310,6 +310,8 @@
           # Therefore it picks the end of the next word here (which is [3,3])
           # to start looking for the next word, which is also the end of the
           # buffer so the cursor never advances.
+          #
+          # See atom/vim-mode#3
           keydown('w', element: editor[0])
           expect(editor.getCursorScreenPosition()).toEqual([3,0])

@@ -386,6 +388,8 @@
           # FIXME: The definition of Cursor#getMovePreviousWordBoundaryBufferPosition
           # will always stop on the last word in the buffer. The question is should
           # we change this behavior.
+          #
+          # See atom/vim-mode#3
           keydown('b', element: editor[0])
           expect(editor.getCursorScreenPosition()).toEqual [0,0]

@@ -431,7 +435,7 @@
           editor.setText("  abcde\n")
           editor.setCursorScreenPosition([0,4])

-        # FIXME: this is related to properly handling line endings
+        # FIXME: See atom/vim-mode#2
         xit 'moves the cursor to the end of the line', ->
           keydown('$', element: editor[0])
           expect(editor.getCursorScreenPosition()).toEqual [0,6]
@@ -441,7 +445,7 @@
           keydown('$', element: editor[0])

           expect(editor.getText()).toBe "  ab\n"
-          # FIXME: this is related to properly handling line endings
+          # FIXME: See atom/vim-mode#2
           #expect(editor.getCursorScreenPosition()).toEqual [0,3]

     describe "numeric prefix bindings", ->


Tweaking command language

Tweaking command language

--- scripts/time-tracker.coffee
+++ scripts/time-tracker.coffee
@@ -1,6 +1,6 @@
 # Track time spent on stories by devs
 #
-# i am starting on <story number> - Start a timer for the given effort
+# i am starting <story number> - Start a timer for the given effort
 # i am done with <story number> - Stop timer for the given effort
 # show my timesheet - Display the time hubot has accumulated for you
 # reset my timesheet - Clear all entries from my timesheet
@@ -64,11 +64,11 @@
   robot.respond /show my time(sheet)?/i, (msg) ->
     msg.send new Effort(msg.message.user).duration()

-  robot.respond /i am starting on (\d+)/i, (msg) ->
+  robot.respond /i am starting (.*)/i, (msg) ->

     #msg.message.user.

-  robot.respond /i am done with (\d+)/i, (msg) ->
+  robot.respond /i am done with (.*)/i, (msg) ->

 (exports ? this).Effort = Effort
 (exports ? this).Timesheets = Timesheets


Added case to make sure that static class properties work as expected.

--- closure/test/abstractperson.coffee
+++ closure/test/abstractperson.coffee
@@ -11,4 +11,7 @@
     @getFriends_().push(friend)

   hasFriend: (friend) ->
-    goog.array.contains @getFriends_(), friend+    goog.array.contains @getFriends_(), friend
+
+  @isMutalFriends: (friendA, friendB) ->
+    friendA.hasFriend(friendB) and friendB.hasFriend(friendA)


Updating schedule.

Updating schedule.

--- scripts/common.coffee
+++ scripts/common.coffee
@@ -10,7 +10,7 @@

 module.exports = (robot) ->
     robot.respond /schedule$/i, (msg) ->
-        msg.send "Our current schedule is Tuesday, Thursday, Saturday 10pm-1am GMT and Sunday 10pm-12am GMT."
+        msg.send "Our current schedule is Monday, Tuesday, Wednesday and Thursday from 10pm until about 2/3am GMT."

     robot.respond /social$/i, (msg) ->
         msg.send "You can find me on Twitter at http://twitter.com/masonest_ and on Facebook at http://facebook.com/masonest"


completely rewrite PR https://github.com/sagemathinc/smc/pull/807

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -1007,19 +1007,40 @@

Course packages

-
Educator/institution pays

-                We offer course packages for teaching classes in <SiteName/>.
-                Such plans start right after purchase and last for the full indicated period without auto-renewal.
-                Through the interface of <SiteName/>, you start teaching by creating a course.
-                Each time you add a student, a project will be automatically created for that student.
-                After upgrading your student{"'"}s projects, you can create and distribute assignments,
+

+                We offer course packages to support teaching using <SiteName/>.
+                They start right after purchase and last for the indicated period and do not auto-renew.
+                Following this
+                guide, create a course file.
+                Each time you add a student to your course, a project will be automatically created for that student.
+                You can create and distribute assignments,
                 students work on assignments inside their project (where you can see their progress
                 in realtime and answer their questions),
                 and you later collect and grade their assignments, then return them.
-
Alteratively students pay

-                When creating a course one can require that all students in the course pay a one-time $9 fee to move their
-                projects to members only hosts and enable full internet access, for four months. This is optional,
-                but will ensure that your students have a better experience and receive priority support.
+

+
+

+                Paying is optional, but will ensure that your students have a better
+                experience, network access, and receive priority support.  The cost
+                is between $4 and $9 per student, depending on class size and whether
+                you or your students pay.  Start right now: you can fully setup your class
+                and add students immediately before you pay us anything!
+
+

+
+
Your or your institution pays

+                You or your institution may pay for one of the course plans.  You then use your plan to upgrade
+                all projects in the course in the settings tab of the course file.
+
+
Students pay

+                In the settings tab of your course, you require that all students
+                pay a one-time $9 fee to move their
+                projects to members only hosts and enable full internet access.
+
+

+
+

+