refactor (fetch_remote_note() already updates db, no need to update db again)

--- app/assets/javascripts/app.coffee
+++ app/assets/javascripts/app.coffee
@@ -828,12 +828,8 @@
                 # fetch the whole note from server and update local
                 console.log('local note ' + guid + ' is SYNCED_ALL, about to fetch from remote for updating')
                 @fetch_remote_note(guid).then(
-                  (data) =>
-                    note =
-                      guid: data.note.guid
-                      title: data.note.title
-                      md: data.note.md
-                    p = @update_note(note)
+                  (note) =>
+                    console.log('note ' + guid + ' (SYNCED_ALL) updated from remote')
                     # maybe FIXME did not add to promise waiting list
                     #_find_p_must_finish_promise_list.push(p)
                     #console.log('pushed to _find_p_must_finish_promise_list. TAG: C')


fix on awesome job message showing

fix on awesome job message showing

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -624,36 +624,41 @@

   #helper method to showSummary
   summarizeKnownObjects: ->
-
     objectsData = @Subject.current?.metadata?.known_objects
     seenKnowns = new Object
     for frame in ['0001', '0002', '0003', '0004'] when objectsData[frame] isnt undefined
       for knownObject in [objectsData[frame]...]  when knownObject.good_known
         objectName = knownObject.object
         if seenKnowns[objectName]
-          seenKnown = seenKnowns[objectName]
-          #accumulate x and y values when seen
-          seenKnown.x_accum = seenKnown.x_accum + Math.round(knownObject.x)
-          seenKnown.y_accum = seenKnown.y_accum + Math.round(knownObject.y)
-          #increment seen counter
-          seenKnown.counter = seenKnown.counter  + 1
+          @updateSeenObject(seenKnowns[objectName], knownObject)
         else # first time seen
           seenKnowns[objectName] = @firstSeen(knownObject)
-
-    @knownAsteroidMessage.show() if objectsData
-
+      #if we found known object show message
+      @knownAsteroidMessage.show()
+
+    #for each known Object
     for objectName, seenKnown of seenKnowns
       seenKnown = @averageSeenKnownPoints(seenKnown)
       radius = 10
+      #process marking surface
       for surface in [@markingSurfaceList...]
         surface.addShape 'ellipse', class: "known-asteroid", opacity: 0.75, cx: seenKnown.x, cy: seenKnown.y, rx: radius, ry: radius, fill: "none", stroke: "rgb(20,200,20)", 'stroke-width': 2
-        @evaluateAnnotations(seenKnown.P_ref)
+      #and evaluate annotations for users marking known objects
+      @evaluateAnnotations(seenKnown.P_ref)

   #helper method to summarizeKnownObjects
   firstSeen:(knownObject) ->
     x = Math.round(knownObject.x)
     y = Math.round(knownObject.y)
     {counter: 1, x_accum: x, y_accum: y}
+
+  #helper method to summarizeKnownObjects
+  updateSeenObject:(seenKnown, knownObject) ->
+    #accumulate x and y values when seen
+    seenKnown.x_accum = seenKnown.x_accum + Math.round(knownObject.x)
+    seenKnown.y_accum = seenKnown.y_accum + Math.round(knownObject.y)
+    #increment seen counter
+    seenKnown.counter = seenKnown.counter  + 1

   #helper method to summarizeKnownObjects
   averageSeenKnownPoints: (seenKnown) ->


Update classifier.coffee

Update classifier.coffee

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -716,7 +716,7 @@
     @destroyFrames()

     if @shouldShowTraining()
-      Subject.group = TRAINING_SUBJECT_GRO  UP
+      Subject.group = TRAINING_SUBJECT_GROUP
       Subject.fetch limit: 1, (subject) ->
         Subject.current.destroy()
         subject.select()


remove prior asteroids when appropriate

remove prior asteroids when appropriate

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -95,6 +95,7 @@
       exit: ->
         @el.find('button[name="to-select"]').removeClass 'hidden'
         @el.find('.what-kind').hide()
+        @removePriorAsteroids()

     asteroidTool:
       enter: ->
@@ -502,11 +503,17 @@
   stopLoading: ->
     @el.removeClass 'loading'

+  removePriorAsteroids: ->
+    console.log "removing prior asteroids"
+    for priorAsteroid in [@el.find('.prior-asteroid')...]
+      priorAsteroid.remove()
+
   showExistingAsteroids: ->
     return if @TRAINING_SUBJECT is null
     xs = []
     ys = []
-    x_sum = y_sum = null
+    x_sum = null
+    y_sum = null
     for priorAsteroid, i in [@TRAINING_SUBJECT.metadata.asteroids...]
       xs[i] = priorAsteroid.x
       ys[i] = priorAsteroid.y
@@ -516,7 +523,7 @@
     y_avg = Math.round(y_sum/@numFrames)
     rad_x = Math.abs( (Math.max.apply null, [xs...]) - (Math.min.apply null, [xs...]) ) * 4
     rad_y = Math.abs( (Math.max.apply null, [ys...]) - (Math.min.apply null, [ys...]) )* 4
-
+
     for surface in [@markingSurfaceList...]
       svgElement = surface.addShape 'ellipse', class: "prior-asteroid", opacity: 0.75, cx: x_avg, cy: y_avg, rx: rad_x, ry: rad_y, fill: "none", stroke: "rgb(20,200,20)", 'stroke-width': 4


revert bad delegate pay code

revert bad delegate pay code

--- app/js/controllers/account.coffee
+++ app/js/controllers/account.coffee
@@ -5,9 +5,6 @@
     name = $stateParams.name
     $scope.utils = Utils
     $scope.account = Wallet.accounts[name]
-    if $scope.account.delegate_info
-        Blockchain.get_asset(0).then (asset_type) ->
-            $scope.account.delegate_info.pay_balance_asset = Utils.asset($scope.account.delegate_info.pay_balance, asset_type)
     $scope.balances = Wallet.balances[name]
     $scope.formatAsset = Utils.formatAsset
     $scope.symbol = Info.symbol


Retry on status code of 0 (canceled request)

--- assets/javascripts/http.js.coffee
+++ assets/javascripts/http.js.coffee
@@ -62,7 +62,7 @@
     @request.on 'complete', (xhr) =>
       delete HTTP.active_requests[@key]

-      if xhr.status == 503 and (@retry_count < @MAX_NUM_RETRIES)
+      if (xhr.status == 503 or xhr.status == 0) and (@retry_count < @MAX_NUM_RETRIES)
         @retry_count += 1
         @retry()
         return


Map XML fence to grammar

Map XML fence to grammar

--- lib/markdown-preview-view.coffee
+++ lib/markdown-preview-view.coffee
@@ -18,6 +18,7 @@
   "bash": "sh"
   "js": "js"
   "javascript": "js"
+  "xml": "xml"

 module.exports =
 class MarkdownPreviewView extends ScrollView


core module post support

core module post support

--- core-module.coffee
+++ core-module.coffee
@@ -1,4 +1,4 @@
-define ['require'], (require) ->
+define ['require', 'zest'], (require, zest) ->
   routes:
     '/component/{moduleId*}':
       structure:
@@ -11,4 +11,30 @@
             render()
         render: (o) ->
           render: o.com
-          options: o._query || {}+          options: o._query || {}
+
+  # handle post requests to /component/moduleId, with JSON POST options
+    if (req.method != 'POST')
+      return next();
+
+    var routeMatch;
+    if (!(routeMatch = req.url.match(/^\/component\/(.*)/))
+      return next();
+
+    moduleId = routeMatch[1]
+
+    postData = []
+    req.on 'data', (chunk) ->
+      postData.push(chunk)
+      if postData.length > 1e4
+        res.writeHead(413, {'Content-Type': 'text/html'});
+        req.connection.destroy()
+
+    req.on 'end', () ->
+      require [moduleId], (com) ->
+        zest.renderPage JSON.parse(postData.join('')), res
+      , (err) ->
+        zest.renderPage
+          structure: err.toString()
+        , res
+


KDView: Added threshold for starting to drag.

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

+    @on "DragFinished", (e) => @isDragging = no
+
     handle.on "mousedown", (event)=>
       if "string" is typeof options.handle
         return if $(event.target).closest(options.handle).length is 0
@@ -725,6 +727,14 @@
     dragDir      = @dragState.direction
     dragGlobDir  = dragDir.global
     dragCurDir   = dragDir.current
+
+    draggedDistance  = Math.max Math.abs(x), Math.abs(y)
+    if draggedDistance < 30 and not @isDragging
+      @dragIsAllowed = no
+      @isDragging    = no
+    else
+      @dragIsAllowed = yes
+      @isDragging    = yes

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


Articles on mobile work; use them

Articles on mobile work; use them

--- lib/middleware/redirect_mobile.coffee
+++ lib/middleware/redirect_mobile.coffee
@@ -23,9 +23,8 @@
   else
     next()

-router.get '/article/:id', isResponsive
 router.get '/apply*', isResponsive
 router.get '/gallery-insights*', isResponsive
 router.get '/auction/:id/buyers-premium', isResponsive
 router.use redirect
-module.exports = router+module.exports = router


Fixed #356.

Fixed #356.

--- server/commons/Handler.coffee
+++ server/commons/Handler.coffee
@@ -252,9 +252,13 @@
       return @sendDatabaseError(res, err) if err
       return @sendNotFoundError(res) unless parentDocument?
       return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, parentDocument)
+      editableProperties = @getEditableProperties req, parentDocument
       updatedObject = parentDocument.toObject()
-      changes = _.pick req.body, @getEditableProperties(req, parentDocument)
-      _.extend updatedObject, changes
+      for prop in editableProperties
+        if (val = req.body[prop])?
+          updatedObject[prop] = val
+        else if updatedObject[prop]?
+          delete updatedObject[prop]
       delete updatedObject._id
       major = req.body.version?.major

@@ -304,8 +308,11 @@
       done(err, document)

   saveChangesToDocument: (req, document, done) ->
-    for prop in @getEditableProperties(req, document)
-      document.set(prop, req.body[prop]) if req.body[prop]?
+    for prop in @getEditableProperties req, document
+      if (val = req.body[prop])?
+        document.set prop, val
+      else if document.get(prop)? and req.method isnt 'PATCH'
+        document.unset prop
     obj = document.toObject()

     # Hack to get saving of Users to work. Probably should replace these props with strings


Actual voice connection count

Actual voice connection count

--- modules/status.coffee
+++ modules/status.coffee
@@ -6,7 +6,7 @@
     @statusList = [
       => "#{@prefix}help"
       => "#{@prefix}filters"
-      => "music in #{@bot.Guilds.length} servers!" if @bot.Guilds.length
+      => "music in #{@bot.VoiceConnections.length} servers!" if @bot.VoiceConnections.length
       => @engine.version if @engine.version.indexOf('dev') >= 0
       => if @engine.version.indexOf('dev') >= 0 then "with cutting-edge seals!" else "with seals!"
       => "#{moment().from @engine.bootDate, true} since last restart." if @engine.version.indexOf('dev') >= 0


'Updating for version 0.12.5dev5'

'Updating for version 0.12.5dev5'

--- bokehjs/src/coffee/version.coffee
+++ bokehjs/src/coffee/version.coffee
@@ -1 +1 @@
-export version = '0.12.5dev4'
+export version = '0.12.5dev5'


Finish up the processing of the outcome service

--- src/outcome-service.coffee
+++ src/outcome-service.coffee
@@ -102,7 +102,6 @@
       res.setEncoding 'utf8'
       res.on 'data', (chunk) => body += chunk
       res.on 'end', () =>
-        console.log body
         if res.statusCode == 200
           @_process_response body, callback
         else
@@ -134,8 +133,12 @@
   _process_response: (body, callback) ->
     xml2js.parseString body, trim: true, (err, result) =>
       return callback new errors.OutcomeResponseError('The server responsed with an invalid XML document'), false if err
-      info = result?.imsx_POXHeader?.imsx_POXResponseHeaderInfo
-      callback null, true
+      info = result?.imsx_POXEnvelopeResponse?.imsx_POXHeader?.imsx_POXResponseHeaderInfo?.imsx_statusInfo
+
+      if info?.imsx_codeMajor == 'success'
+        callback null, true
+      else
+        callback new errors.OutcomeResponseError('The request provided was invalid'), false


Call the correct parameter

Call the correct parameter

--- bokehjs/test/models/transforms/jitter_transform.coffee
+++ bokehjs/test/models/transforms/jitter_transform.coffee
@@ -11,7 +11,7 @@
   generate_jitter = ->
     new Jitter({
       width: 1,
-      center: 0,
+      mean: 0,
       distribution: 'uniform'
     })


dashboard: removed membershippolicyview

dashboard: removed membershippolicyview

--- client/app/Applications/Groups.kdapplication/Views/groupsmembershippolicydetailview.coffee
+++ client/app/Applications/Groups.kdapplication/Views/groupsmembershippolicydetailview.coffee
@@ -15,8 +15,7 @@
             'updateMembershipPolicy',
             group,
             policy,
-            formData,
-            membershipPolicyView
+            formData
         @createSubViews policy

@@ -90,7 +89,6 @@
     @showPolicyLanguageLink = new CustomLinkView
       cssClass  : "edit-link #{if policyLanguageExists then 'hidden' else ''}"
       title     : 'Edit'
-      href      : './edit'
       click     :(event)=>
         event.preventDefault()
         @showPolicyLanguageLink.hide()


Fixed issue where other user wouldn't see banner data in left panel

--- client/views/app/sideNav/chatRoomItem.coffee
+++ client/views/app/sideNav/chatRoomItem.coffee
@@ -59,7 +59,11 @@
 		me = Meteor.user().username
 		otherUser = _.without(Template.instance().room.get().usernames, me)[0]
 		if otherUser?
-			count = ChatRoom.find({t: 'd', usernames: [me, otherUser]}).count()
+			count = ChatRoom.find
+				t: 'd'
+				usernames:
+					$all: [me, otherUser]
+			.count()
 			if count > 1
 				result = true
 		return result


Groups.sharedVM: removed unnecessary fat arrow.

--- client/Environments/views/environmentsmainscene.coffee
+++ client/Environments/views/environmentsmainscene.coffee
@@ -86,7 +86,7 @@

       vmController.createNewVM()

-    @machinesContainer.on 'PlusButtonForGroupsClicked', =>
+    @machinesContainer.on 'PlusButtonForGroupsClicked', ->
       return unless KD.isLoggedIn()
         new KDNotificationView
           title: "You need to login to create a new machine."


StacksCustomViews: disable Add New Credential button while new cred form is open

--- client/admin/lib/views/stacks/stackscustomviews.coffee
+++ client/admin/lib/views/stacks/stackscustomviews.coffee
@@ -11,14 +11,18 @@
 module.exports = class StacksCustomViews extends CustomViews

-  handleNewCredential = (views, provider) ->
+  handleNewCredential = (views, provider, button) ->

     {controller} = views.credentialList
     view = controller.getView()
+    button.disable()
     view.hide()

     form = controller.showAddCredentialFormFor provider
-    form.on 'Cancel', view.bound 'show'
+    form.on 'Cancel', view.bound   'show'
+    form.on 'Cancel', button.bound 'enable'
+
+    kd.utils.defer -> form.inputs.title?.focus()

     # After adding credential, we are sharing it with the current
     # group, so anyone in this group can use this credential ~ GG
@@ -26,6 +30,7 @@
       {slug} = kd.singletons.groupsController.getCurrentGroup()
       credential.shareWith {target: slug}, (err) ->
         console.warn 'Failed to share credential:', err  if err
+        button.disable()
         view.show()

@@ -119,9 +124,9 @@
                            to setup your stack for your team."
           button_addNew :
             title       : 'Add New Credential'
-            cssClass    : 'solid medium green'
+            cssClass    : 'solid compact green'
             callback    : ->
-              handleNewCredential views, provider
+              handleNewCredential views, provider, this
         credentialList  : provider
         button_cancel   :
           title         : '< Select another provider'


StacksCustomViews: stepDefineStack requirements completed, stack save is done

--- client/admin/lib/views/stacks/stackscustomviews.coffee
+++ client/admin/lib/views/stacks/stackscustomviews.coffee
@@ -8,6 +8,7 @@
 dateFormat                      = require 'dateformat'

 FSHelper                        = require 'app/util/fs/fshelper'
+showError                       = require 'app/util/showError'
 applyMarkdown                   = require 'app/util/applyMarkdown'

 CustomViews                     = require 'app/commonviews/customviews'
@@ -416,7 +417,19 @@
         button_save     :
           title         : 'Save & Test >'
           cssClass      : 'solid compact green nav next'
-          callback      : -> callback data
+          callback      : ->
+
+            {title}  = views.input_title.getData()
+            template = views.editorView.getValue()
+
+            updateStackTemplate {
+              title, template, credential, stackTemplate
+            }, (err, _stackTemplate) ->
+              return  if showError err
+
+              callback {
+                stackTemplate: _stackTemplate, credential, provider
+              }

       return container

@@ -424,7 +437,9 @@
     stepTestAndSave: (options) =>

       console.log options
+
       {callback, cancelCallback, data} = options
+
       container = @views.container 'step-creds'

       views     = @addTo container,


first app installed

first app installed

--- client/landing/app/MainApp/kodingappscontroller.coffee
+++ client/landing/app/MainApp/kodingappscontroller.coffee
@@ -138,9 +138,9 @@
       {nickname}  = KD.whoami().profile
       publishPath = FSHelper.escapeFilePath "/opt/Apps/#{nickname}/#{manifest.name}/#{manifest.version}"
       userAppPath = if /~\//.test manifest.path
-        manifest.path.replace("~/", "/Users/#{nickname}/") + "index.js"
+        manifest.path.replace("~/", "/Users/#{nickname}/")
       else
-        "#{manifest.path}/index.js"
+        "#{manifest.path}/"
       options     =
         toDo          : "publishApp"
         withArgs      :


If some other controller added a vehicle, we need to redraw

--- src/scripts/controllers/detailControllers.coffee
+++ src/scripts/controllers/detailControllers.coffee
@@ -31,10 +31,10 @@
   constructor: (@log, scope, routeParams, @service) ->
     super(scope, routeParams)

-  add_vehicle: () =>
-    @service.saveId(@routeParams.id + "/vehicles", {}).then (results) =>
-      # Refetch our vehicles (causing view updates)
+    @scope.$on('vehicleAdded', (event, args) =>
+      @log.debug('fetching due to add')
       @fetch_all_records()
+    )

 class VehicleDetailController extends DetailController
   @$inject: ['$upload', '$log', '$scope', '$routeParams', 'vehicleService']


Eval module

Eval module

--- modules/eval.coffee
+++ modules/eval.coffee
@@ -5,26 +5,26 @@
 formatSettings =
   type: 'space',
   size: 2
-T = {}
+G = {}

 class EvalModule extends BotModule
-  init: =>
-    evalOptions =
-      ownerOnly: true
-      allowDM: true
+  init: ->
+    # coffeelint: disable=max_line_length
+    owner = ownerOnly: true, allowDM: true
     # CoffeeScript Eval Command
-    @registerCommand 'eval', evalOptions, (msg, args, d, bot, engine)->
+    @registerCommand 'eval', owner, ({ msg, args, data, saveData, settings, locale, bot, discord })->
       p = (text)-> msg.channel.sendMessage text
-      j = (obj, length)->
+      j = (obj, length = 2)->
         pruned = prune obj, length
         p "\`\`\`json\n#{format(JSON.parse(pruned),formatSettings)}\n\`\`\`"
-      eval(CoffeeScript.compile("(=>\n  #{args.replace(/\n/g, '\n  ')}\n)()", bare: true))
+      eval(CoffeeScript.compile("(=>\n  #{args.replace(/^/gm, '  ')}\n)()", bare: true))
     # JavaScript Eval Command
-    @registerCommand 'jseval', evalOptions, (msg, args, d, bot, engine)->
+    @registerCommand 'jseval', owner, ({ msg, args, data, saveData, settings, locale, bot, discord })->
       p = (text)-> msg.channel.sendMessage text
-      j = (obj, length)->
+      j = (obj, length = 2)->
         pruned = prune obj, length
         p "\`\`\`json\n#{format(JSON.parse(pruned),formatSettings)}\n\`\`\`"
       eval "(async function () {#{args}})()"
+    # coffeelint: enable=max_line_length

 module.exports = EvalModule


disregard old requests, only show the results for the latest query

--- core/app/backbone/collections/search_collection.coffee
+++ core/app/backbone/collections/search_collection.coffee
@@ -9,13 +9,14 @@

   searchFor: (query) ->
     return if query == @query
+    @jqxhr?.abort()

     if query == ''
       @makeEmpty()
     else
       @query = query
       @reset []
-      @fetch()
+      @jqxhr = @fetch()

   encodedQuery: -> encodeURIComponent @query


Removed no longer needed navigate stuff.

This also fixes an issue. @markijbema was this an actual issue in
Github?

--- core/app/backbone/controllers/client_controller.coffee
+++ core/app/backbone/controllers/client_controller.coffee
@@ -27,8 +27,6 @@

     factsNewView.on 'factCreated', (fact) =>
       parent.$(parent.document).trigger("factlinkCreated", fact.id )
-      Backbone.history.navigate "facts/#{fact.id}", false
-      @showFact fact

     FactlinkApp.mainRegion.show factsNewView


Reload page instead of showing an alert

Reload page instead of showing an alert

--- core/app/backbone/initializers/automatic_logout.coffee
+++ core/app/backbone/initializers/automatic_logout.coffee
@@ -7,5 +7,4 @@
       height:'100%'
     )

-    alert "You have been signed out, please sign in."
-    window.location = Factlink.Global.path.sign_in+    window.location.reload()


link to reset password video

link to reset password video

--- @client/auth.coffee
+++ @client/auth.coffee
@@ -168,7 +168,22 @@
                    ["#{t('New password')}:", @inputBox('password', t("choose_password"), 'password')],

                   ], t('verification_sent')
-          @submitButton(t('Log in')) ]
+          @submitButton(t('Log in'))
+          DIV
+            style:
+              marginTop: 20
+            'Having trouble resetting your password? Watch this brief '
+
+            A
+              target: '_blank'
+              href: 'https://vimeo.com/198802322'
+              style:
+                textDecoration: 'underline'
+                fontWeight: 600
+              'video tutorial'
+            '.'
+
+        ]

       # The email VERIFICATION form
       when 'verify email'


Remove proxy-pac-url switch

Remove proxy-pac-url switch

--- app.coffee
+++ app.coffee
@@ -40,11 +40,6 @@
 disableHA = config.get 'poi.disableHA', false
 if disableHA
   app.commandLine.appendSwitch 'disable-gpu'
-
-# PAC setting
-proxy = config.get 'proxy', {}
-if proxy.pac
-  app.commandLine.appendSwitch 'proxy-pac-url', proxy.pacAddr

 # Proxy setting
 listenPort = config.get 'poi.port', 12450


launch the browser to show help

launch the browser to show help

--- app.coffee
+++ app.coffee
@@ -7,6 +7,10 @@
 routes = require("./routes/index")
 os = require("os")
 app = express()
+
+# For launching the view
+sys = require('sys')
+exec = require('child_process').exec

 global.app = app

@@ -67,6 +71,9 @@

 server = app.listen(app.get("port"), ->
   preferredIP = publicIPList().splice(0,1)[0][1]
-  console.log "PebblePoint server listening on " + preferredIP + ":" + server.address().port
+  serverUrl = "http://" + preferredIP + ":" + server.address().port
+  console.log "PebblePoint server listening on " + serverUrl
+  exec "open #{serverUrl}"
+
   return
 )


Add logging

Add logging

--- app.coffee
+++ app.coffee
@@ -1,9 +1,14 @@
 express = require 'express'
+logfmt = require 'logfmt'

 app = express()
+
+app.use logfmt.requestLogger()

 app.get '/', (req, res) ->
 	res.send 'hello'

-app.listen(process.env.PORT)
-console.log('Started server in port', process.env.PORT)+port = Number(process.env.PORT || 5000)
+
+app.listen port, ->
+	console.log('Started server in port', port)


Change err msg in the describe

Change err msg in the describe

--- library/membrane/describe.coffee
+++ library/membrane/describe.coffee
@@ -72,7 +72,8 @@
     @documentation: (boolean) ->
         isDocumentation = @$documentation is this
         return isDocumentation unless boolean?
-        assert _.isBoolean(boolean), "invalid flag"
+        invalidFlag = "the flag has to be boolean"
+        assert _.isBoolean(boolean), invalidFlag
         return @$documentation = this if boolean
         delete @$documentation; @$documentation is @


fixes #2037 #2038

fixes #2037 #2038

--- lms/static/coffee/src/staff_grading/staff_grading.coffee
+++ lms/static/coffee/src/staff_grading/staff_grading.coffee
@@ -376,8 +376,11 @@
       @render_problem()

   problem_link:(problem) ->
+    problem_name = #{problem.problem_name}
+      if problem_name.length < 1
+        problem_name = "<" + gettext("Problem without name") + ">"
     link = $('').attr('href', "javascript:void(0)").append(
-      "#{problem.problem_name}")
+      problem_name)
         .click =>
           @get_next_submission problem.location

@@ -391,10 +394,8 @@
   render_list: () ->
     for problem in @problems
       problem_row = $('
')
-      problem_name = @problem_link(problem)
-      if problem_name.length < 1
-        problem_name = "<" + gettext("Problem without name") + ">"
-      problem_row.append($('').append(problem_name))
+
+      problem_row.append($('').append(@problem_link(problem)))
       problem_row.append($('').append("#{problem.num_graded}"))
       problem_row.append($('').append("#{problem.num_pending}"))
       problem_row.append($('').append("#{problem.num_required}"))


BaseCollection: moving the call to 'super' lower in 'contructor'

--- shared/base/collection.coffee
+++ shared/base/collection.coffee
@@ -13,8 +13,6 @@
     # Store a reference to the app instance.
     @app = @options.app

-    super
-
     # Store a reference to the params that were used to
     # query for these models.
     @params = @options.params || {}
@@ -25,6 +23,8 @@
     if _.isObject(@options.meta)
       _.extend(@meta, @options.meta)
       delete @options.meta
+
+    super

   # Idempotent parse
   parse: (resp) ->


Ensure package stylesheets are loaded in autocomplete spec

--- spec/autocomplete-spec.coffee
+++ spec/autocomplete-spec.coffee
@@ -11,6 +11,7 @@

   beforeEach ->
     editor = new Editor(editSession: fixturesProject.buildEditSessionForPath('sample.js'))
+    atom.loadPackage('autocomplete')
     autocomplete = new Autocomplete(editor)
     miniEditor = autocomplete.miniEditor


Removed old comment

Removed old comment

--- spec/autocomplete-spec.coffee
+++ spec/autocomplete-spec.coffee
@@ -3,10 +3,6 @@
 _ = require "underscore-plus"
 AutocompleteView = require '../lib/autocomplete-view'
 Autocomplete = require '../lib/autocomplete'
-
-###
-  Needs to be updated to the new package
-###

 describe "Autocomplete", ->
   [activationPromise] = []


:lipstick: cleanup specs

:lipstick: cleanup specs

--- spec/results-view-spec.coffee
+++ spec/results-view-spec.coffee
@@ -141,34 +141,9 @@
         expect(resultsView.find("li").length).toBe resultsView.getPathCount() + resultsView.getMatchCount()

   describe "arrowing through the list", ->
-    describe "when nothing is selected", ->
-      beforeEach ->
-        projectFindView.findEditor.setText('items')
-        projectFindView.trigger 'core:confirm'
-
-      it "doesnt error when the user arrows down", ->
-        waitsForPromise ->
-          searchPromise
-
-        runs ->
-          resultsView = getResultsView()
-          resultsView.find('.selected').removeClass('selected')
-          expect(resultsView.find('.selected')).not.toExist()
-          resultsView.trigger 'core:move-down'
-          expect(resultsView.find('.selected')).toExist()
-
-      it "doesnt error when the user arrows up", ->
-        waitsForPromise ->
-          searchPromise
-
-        runs ->
-          resultsView = getResultsView()
-          resultsView.find('.selected').removeClass('selected')
-          expect(resultsView.find('.selected')).not.toExist()
-          resultsView.trigger 'core:move-up'
-          expect(resultsView.find('.selected')).toExist()
-
-    it "arrows through the list without selecting paths", ->
+    resultsView = null
+
+    it "opens the correct file containing the result when 'core:confirm' is called", ->
       openHandler = null

       waitsForPromise ->
@@ -211,7 +186,7 @@
         activePane = atom.workspaceView.getActivePaneView()
         expect(atom.workspace.getActivePaneItem().getPath()).toContain('sample.')

-    it "arrows through the list without selecting paths", ->
+    it "arrows through the entire list without selecting paths and overshooting the boundaries", ->
       waitsForPromise ->
         atom.workspace.open('sample.js')

@@ -274,81 +249,14 @@

           lastSelectedItem = selectedItem[0]

-    it "collapsed vs expanded transition next", ->
-      projectFindView.findEditor.setText('items')
-      projectFindView.trigger 'core:confirm'
-
-      waitsForPromise ->
-        searchPromise
-
-      runs ->
-        resultsView = getResultsView()
-
-        # collapsed case
-        resultsView.find('.selected').removeClass('selected')
-        resultsView.find('.path:eq(0) .search-result:last').addClass('selected')
-        resultsView.find('.path:eq(1)').view().expand(false)
-
-        resultsView.trigger 'core:move-down'
-
-        selectedItem = resultsView.find('.selected')
-        expect(selectedItem).toHaveClass('path')
-        expect(selectedItem[0]).toBe resultsView.find('.path:eq(1)')[0]
-
-        # expanded case
-        resultsView.find('.selected').removeClass('selected')
-        resultsView.find('.path:eq(0) .search-result:last').addClass('selected')
-        resultsView.find('.path:eq(1)').view().expand(true)
-
-        resultsView.trigger 'core:move-down'
-
-        selectedItem = resultsView.find('.selected')
-        expect(selectedItem).toHaveClass('search-result')
-        expect(selectedItem[0]).toBe resultsView.find('.path:eq(1) .search-result:first')[0]
-
-
-    it "collapsed vs expanded transition previous", ->
-      projectFindView.findEditor.setText('items')
-      projectFindView.trigger 'core:confirm'
-
-      waitsForPromise ->
-        searchPromise
-
-      runs ->
-        resultsView = getResultsView()
-
-        # collapsed case
-        resultsView.find('.selected').removeClass('selected')
-        resultsView.find('.path:eq(1) .search-result:first').addClass('selected')
-        resultsView.find('.path:eq(0)').view().expand(false)
-
-        resultsView.trigger 'core:move-up'
-
-        selectedItem = resultsView.find('.selected')
-        expect(selectedItem).toHaveClass('path')
-        expect(selectedItem[0]).toBe resultsView.find('.path:eq(0)')[0]
-
-        # expanded case
-        resultsView.find('.selected').removeClass('selected')
-        resultsView.find('.path:eq(1) .search-result:first').addClass('selected')
-        resultsView.find('.path:eq(0)').view().expand(true)
-
-        resultsView.trigger 'core:move-up'
-
-        selectedItem = resultsView.find('.selected')
-        expect(selectedItem).toHaveClass('search-result')
-        expect(selectedItem[0]).toBe resultsView.find('.path:eq(0) .search-result:last')[0]
-
-    it "collapse current list", ->
-      projectFindView.findEditor.setText('items')
-      projectFindView.trigger 'core:confirm'
-
-      waitsForPromise ->
-        searchPromise
-
-      runs ->
-        resultsView = getResultsView()
-
+    describe "when there are a list of items", ->
+      beforeEach ->
+        projectFindView.findEditor.setText('items')
+        projectFindView.trigger 'core:confirm'
+        waitsForPromise -> searchPromise
+        runs -> resultsView = getResultsView()
+
+      it "collapses the selected results view", ->
         # select item in first list
         resultsView.find('.selected').removeClass('selected')
         resultsView.find('.path:eq(0) .search-result:first').addClass('selected')
@@ -359,16 +267,7 @@
         expect(selectedItem).toHaveClass('collapsed')
         expect(selectedItem[0]).toBe resultsView.find('.path:eq(0)')[0]

-    it "expand current list", ->
-      projectFindView.findEditor.setText('items')
-      projectFindView.trigger 'core:confirm'
-
-      waitsForPromise ->
-        searchPromise
-
-      runs ->
-        resultsView = getResultsView()
-
+      it "expands the selected results view", ->
         # select item in first list
         resultsView.find('.selected').removeClass('selected')
         resultsView.find('.path:eq(0)').addClass('selected').addClass('collapsed')
@@ -379,6 +278,41 @@
         expect(selectedItem).toHaveClass('search-result')
         expect(selectedItem[0]).toBe resultsView.find('.path:eq(0) .search-result:first')[0]

+      describe "when nothing is selected", ->
+        it "doesnt error when the user arrows down", ->
+          resultsView.find('.selected').removeClass('selected')
+          expect(resultsView.find('.selected')).not.toExist()
+          resultsView.trigger 'core:move-down'
+          expect(resultsView.find('.selected')).toExist()
+
+        it "doesnt error when the user arrows up", ->
+          resultsView.find('.selected').removeClass('selected')
+          expect(resultsView.find('.selected')).not.toExist()
+          resultsView.trigger 'core:move-up'
+          expect(resultsView.find('.selected')).toExist()
+
+      describe "when there are collapsed results", ->
+        it "moves to the correct next result when a path is selected", ->
+          resultsView.find('.selected').removeClass('selected')
+          resultsView.find('.path:eq(0) .search-result:last').addClass('selected')
+          resultsView.find('.path:eq(1)').view().expand(false)
+
+          resultsView.trigger 'core:move-down'
+
+          selectedItem = resultsView.find('.selected')
+          expect(selectedItem).toHaveClass('path')
+          expect(selectedItem[0]).toBe resultsView.find('.path:eq(1)')[0]
+
+        it "moves to the correct previous result when a path is selected", ->
+          resultsView.find('.selected').removeClass('selected')
+          resultsView.find('.path:eq(1) .search-result:first').addClass('selected')
+          resultsView.find('.path:eq(0)').view().expand(false)
+
+          resultsView.trigger 'core:move-up'
+
+          selectedItem = resultsView.find('.selected')
+          expect(selectedItem).toHaveClass('path')
+          expect(selectedItem[0]).toBe resultsView.find('.path:eq(0)')[0]

   describe "when the results view is empty", ->
     it "ignores core:confirm events", ->


display error when unable load fails

display error when unable load fails

--- client/skr/api/EventCheckout.cjsx
+++ client/skr/api/EventCheckout.cjsx
@@ -4,6 +4,8 @@
 ##= require './OrderingComplete'
 ##= require './OrderingForm'

+ErrorFetching = (props) ->
+
Error loading event {props.eventCode}

 class Skr.Api.EventCheckout extends Skr.Api.Components.Base

@@ -23,7 +25,10 @@

     componentWillMount: ->
         @event.fetch(query: {code: @event.code}).then =>
-            @cart.addSku(@event.sku)
+            if @event.isNew()
+                @setState(errorFetching: true)
+            else
+                @cart.addSku(@event.sku)

     getSaleOptions: ->
         _.extend({},
@@ -33,6 +38,8 @@
         )

     render: ->
+        return <ErrorFetching {...@props} /> if @state.errorFetching
+

             <Lanes.Components.NetworkActivityOverlay model={@event} />
             <Skr.Api.SingleItemCheckout


Update trello.coffee

Update trello.coffee

--- src/trello.coffee
+++ src/trello.coffee
@@ -108,7 +108,7 @@
         members[member.fullName.toLowerCase().split " ", 1] = member

-  robot.respond /trello new ["']([^]+)["'](.*)/i, (msg) ->
+  robot.respond /trello new ["“'‘]([^]+)["”'’](.*)/i, (msg) ->
     ensureConfig msg.send
     card_name = msg.match[2]
     list_name = msg.match[1]
@@ -124,16 +124,16 @@

     createCard msg, list_name, card_name

-  robot.respond /trello list ["'](.+)["']/i, (msg) ->
+  robot.respond /trello list ["“'‘](.+)["”'’]/i, (msg) ->
     showCards msg, msg.match[1]

-  robot.respond /trello move (\w+) ["'](.+)["']/i, (msg) ->
+  robot.respond /trello move (\w+) ["“'‘](.+)["”'’]/i, (msg) ->
     moveCard msg, msg.match[1], msg.match[2]

-  robot.respond /trello comment (\w+) ["']((.+|\n)+)["']/i, (msg) ->
+  robot.respond /trello comment (\w+) ["“'‘]((.+|\n)+)["”'’]/i, (msg) ->
     addComment msg, msg.match[1], msg.match[2], msg.message.user.name

-  robot.respond /trello description (\w+) ["']((.+|\n)+)["']/i, (msg) ->
+  robot.respond /trello description (\w+) ["“'‘]((.+|\n)+)["”'’]/i, (msg) ->
     addDescription msg, msg.match[1], msg.match[2]

   robot.respond /trello list lists/i, (msg) ->


Plot multiple [numeric] axis

Plot multiple [numeric] axis

--- assets/js/app.coffee
+++ assets/js/app.coffee
@@ -79,11 +79,30 @@
 plotSensorData = (id) ->
   sense.sensorData id, (err, resp) ->

-    data = []
-    data.push {date: new Date(datum.date*1000), value: JSON.parse(datum.value)['x-axis']} for datum in resp.object.data
+    # data = []
+    # data.push {date: new Date(datum.date*1000), value: JSON.parse(datum.value)['x-axis']} for datum in resp.object.data

-    graph.draw [label: 'X-axis', data: data], {min: data[0].date, max: data[data.length-1].date}
+    datasets = []
+
+    # Check for single or multi-valued data points
+    first_object = JSON.parse(resp.object.data[0].value)

+    if typeof first_object.value is "object"
+      # Multiple values, assume numeric
+      for key of first_object.value
+
+        data = []
+        data.push {date: new Date(datum.date*1000), value: JSON.parse(datum.value)[key]} for datum in resp.object.data
+        datasets.push {label: key, data: data}
+
+    else
+      # Single value (assume numeric)
+      data = []
+      data.push {date: new Date(datum.date*1000), value: JSON.parse(datum.value)} for datum in resp.object.data
+      datasets.push {label: 'Sensor ' + id, data: data }
+
+    graph.draw datasets, {min: data[0].date, max: data[data.length-1].date}
+    graph.setValueRangeAuto()
     $('#actions').fadeIn()

 callSegmentation = (sensor, cb) ->


re-enabling /build json/

re-enabling /build json/

--- scripts/example.coffee
+++ scripts/example.coffee
@@ -20,19 +20,18 @@
       #console.log body.json_Pretty
       res.reply 'The current HEAD of TM Design is: ' + body.json_Parse().object.sha

-#   bug: for some reason this was being triggered on all calls of tbot
-#   robot.respond 'build json',(res) ->
-#     res.reply "About to trigger build of tm-4-0-json-data repository"
-#     options =
-#       url    : 'https://ci.appveyor.com/api/builds',
-#       headers:
-#         'Authorization': 'Bearer ' + process.env.auth_key
-#         'Content-Type': 'application/x-www-form-urlencoded'
-#       body   : 'accountName=DinisCruz&projectSlug=tm-4-0-json-data&branch=master'
-#
-#     request.post options, (error, response, data)->
-#       res.reply 'Request sent, take a look at https://ci.appveyor.com/project/DinisCruz/tm-4-0-json-data where a new build should have just started'
-#       res.reply 'In a bit, if there were changes to the Lib_Uno xml files, the updated Json files will be at https://github.com/tm-build/Lib_UNO-json'
+   robot.respond /build json/i,(res) ->
+     res.reply "About to trigger build of tm-4-0-json-data repository"
+     options =
+       url    : 'https://ci.appveyor.com/api/builds',
+       headers:
+         'Authorization': 'Bearer ' + process.env.auth_key
+         'Content-Type': 'application/x-www-form-urlencoded'
+       body   : 'accountName=DinisCruz&projectSlug=tm-4-0-json-data&branch=master'
+
+     request.post options, (error, response, data)->
+       res.reply 'Request sent, take a look at https://ci.appveyor.com/project/DinisCruz/tm-4-0-json-data where a new build should have just started'
+       res.reply 'In a bit, if there were changes to the Lib_Uno xml files, the updated Json files will be at https://github.com/tm-build/Lib_UNO-json'
        #res.reply data.json_Parse().json_Pretty()

@@ -61,10 +60,18 @@
    enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
    leaveReplies = ['Are you still there?', 'Target lost', 'Searching']
   #
-   robot.enter (res) ->
-     res.send res.random enterReplies
-   robot.leave (res) ->
-     res.send res.random leaveReplies
+  # robot.enter (res) ->
+  #   res.send res.random enterReplies
+  # robot.leave (res) ->
+  #   res.send res.random leaveReplies
+
+
+  # robot.router.get '/hubot/hi', (req, res) ->
+  #   res.send robot.router._router.stack
+
+  # robot.respond '/routes/i', (res) ->
+  #   res.reply robot.router._router.json_Str()
+
   #
   # answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
   #


Aggiunto buongiorno

Aggiunto buongiorno

--- scripts/example.coffee
+++ scripts/example.coffee
@@ -23,3 +23,6 @@

 	robot.hear /vado via/i, (res) ->
 		res.reply "Allora non hai capito il momento!"
+
+	robot.hear /buongiorno/i, (res) ->
+		res.reply "Buongiorno il cazzo!!"


improve storage activity monitor

improve storage activity monitor

--- src/smc-hub/storage.coffee
+++ src/smc-hub/storage.coffee
@@ -1492,7 +1492,7 @@
         return @_synctable.get(project_id).toJS()

     list: () =>
-        return @_synctable.get().valueSeq().toJS()
+        return (x for x in @_synctable.get().valueSeq().toJS() when x.storage_request?.requested >= misc.minutes_ago(60))

     # activity that was requested but not started -- this is BAD!
     ignored: () =>
@@ -1516,23 +1516,31 @@
                 wait  : (x.storage_request.started - x.storage_request.requested)/1000
                 work  : (x.storage_request.finished - x.storage_request.started)/1000
         v.sort (a,b) ->
-            return misc.cmp(a.start + a.work, b.start + b.work)
+            return misc.cmp(a.wait + a.work, b.wait + b.work)
         return v

     summary: () =>
         t = @times()
+        data =
+            times    : t.slice(t.length-10)
+            running  : @running().length
+            finished : @finished().length
+            ignored  : @ignored().length
+        s = misc.to_json(data)
+        if s == @_last_data
+            return
+        @_last_data = s
+        console.log('\n\n\n---------------------------------------------------\n\n')
         console.log "     worst times:                             wait    work   action"
-        for x in t.slice(t.length-10)
+        for x in data.times
             console.log "     #{x.project_id}    #{x.wait}   #{x.work}    #{x.action}"
-        console.log "     running  : #{@running().length}"
-        console.log "     done     : #{@finished().length}"
-        ign = @ignored().length
-        if ign > 0 then warn = '*************************' else warn=''
-        console.log "     pending  : #{ign}  #{warn}"
+        console.log "     running  : #{data.running}"
+        console.log "     finished : #{data.finished}"
+        if data.ignored > 0 then warn = '*************************' else warn=''
+        console.log "     pending  : #{data.ignored}  #{warn}"

     monitor: () =>
         @_synctable.on 'change', =>
-            console.log('\n\n\n---------------------------------------------------\n\n')
             @summary()
         @summary()
         return


delete Session code from smc-util/client.coffee, since it is NOT used anymore (all such functionality was replaced by direct websocket to project)

--- src/smc-util/client.coffee
+++ src/smc-util/client.coffee
@@ -67,135 +67,6 @@
 # if there is no response to an operation after this amount of time.
 DEFAULT_TIMEOUT = 30  # in seconds

-
-class Session extends EventEmitter
-    # events:
-    #    - 'open'   -- session is initialized, open and ready to be used
-    #    - 'close'  -- session's connection is closed/terminated
-    #    - 'execute_javascript' -- code that server wants client to run related to this session
-    constructor: (opts) ->
-        super()
-        opts = defaults opts,
-            conn         : required     # a Connection instance
-            project_id   : required
-            session_uuid : required
-            params       : undefined
-            data_channel : undefined    # optional extra channel that is used for raw data
-            init_history : undefined    # used for console
-
-        @start_time   = misc.walltime()
-        @conn         = opts.conn
-        @params       = opts.params
-        if @type() == 'console'
-            if not @params?.path? or not @params?.filename?
-                throw Error("params must be specified with path and filename")
-        @project_id   = opts.project_id
-        @session_uuid = opts.session_uuid
-        @data_channel = opts.data_channel
-        @init_history = opts.init_history
-        @emit("open")
-
-        ## This is no longer necessary; or rather, it's better to only
-        ## reset terminals, etc., when they are used, since it wastes
-        ## less resources.
-        # I'm going to leave this in for now -- it's only used for console sessions,
-        # and they aren't properly reconnecting in all cases.
-        if @reconnect?
-            @conn.on("connected", @reconnect)
-
-    close: () =>
-        @removeAllListeners()
-        if @reconnect?
-            @conn.removeListener("connected", @reconnect)
-
-    reconnect: (cb) =>
-        # Called when the connection gets dropped, then reconnects
-        if not @conn._signed_in
-            setTimeout((()=>@reconnect(cb)), 500)
-            return
-
-        if @_reconnect_lock
-            #console.warn('reconnect: lock')
-            cb?("reconnect: hit lock")
-            return
-
-        @emit "reconnecting"
-        @_reconnect_lock = true
-        #console.log("reconnect: #{@type()} session with id #{@session_uuid}...")
-        f = (cb) =>
-            @conn.call
-                allow_post : false
-                message : message.connect_to_session
-                    session_uuid : @session_uuid
-                    type         : @type()
-                    project_id   : @project_id
-                    params       : @params
-                timeout : 30
-                cb      : (err, reply) =>
-                    if err
-                        cb(err); return
-                    switch reply.event
-                        when 'error'
-                            cb(reply.error)
-                        when 'session_connected'
-                            #console.log("reconnect: #{@type()} session with id #{@session_uuid} -- SUCCESS", reply)
-                            if @data_channel != reply.data_channel
-                                @conn.change_data_channel
-                                    prev_channel : @data_channel
-                                    new_channel  : reply.data_channel
-                                    session      : @
-                            @data_channel = reply.data_channel
-                            @init_history = reply.history
-                            cb()
-                        else
-                            cb("bug in hub")
-        misc.retry_until_success
-            max_time : 15000
-            factor   : 1.3
-            f        : f
-            cb       : (err) =>
-                #console.log("reconnect('#{@session_uuid}'): finished #{err}")
-                delete @_reconnect_lock
-                if not err
-                    @emit("reconnect")
-                cb?(err)
-
-    terminate_session: (cb) =>
-        @conn.call
-            message :
-                message.terminate_session
-                    project_id   : @project_id
-                    session_uuid : @session_uuid
-            timeout : 30
-            cb      : cb
-
-    walltime: () =>
-        return misc.walltime() - @start_time
-
-    handle_data: (data) =>
-        @emit("data", data)
-
-    write_data: (data) =>
-        @conn.write_data(@data_channel, data)
-
-    restart: (cb) =>
-        @conn.call(message:message.restart_session(session_uuid:@session_uuid), timeout:10, cb:cb)
-
-
-###
-#
-# A Console session, which connects the client to a pty on a remote machine.
-#
-#   Client <-- (primus) ---> Hub  <--- (tcp) ---> console_server
-#
-###
-
-class ConsoleSession extends Session
-    type: () => "console"
-
-
-
-
 class exports.Connection extends EventEmitter
     # Connection events:
     #    - 'connecting' -- trying to establish a connection
@@ -237,8 +108,6 @@
             recv        : 0     # number of messages received from backend
             recv_length : 0     # total amount of data recv'd
         @_id_counter       = 0
-        @_sessions         = {}
-        @_new_sessions     = {}
         @_data_handlers    = {}
         @execute_callbacks = {}
         @call_callbacks    = {}
@@ -274,15 +143,7 @@
                 # character c (there are 65536 possibilities).  If
                 # that character is JSON_CHANNEL, the message is
                 # encoded as JSON and we handle it in the usual way.
-                # If the character is anything else, the raw data in
-                # the message is sent to an appropriate handler, if
-                # one has previously been registered.  The motivation
-                # is that we the ability to multiplex multiple
-                # sessions over a *single* WebSocket connection, and it
-                # is absolutely critical that there is minimal
-                # overhead regarding the amount of data transfered --
-                # 1 character is minimal!
-
+                # If the character is anything else, [DEPRECATED and NOT USED ANYMORE].
                 channel = data[0]
                 data    = data.slice(1)

@@ -483,32 +344,6 @@
         if DEBUG
             console.log("handle_json_data: #{data}")
         switch mesg.event
-            when "execute_javascript"
-                if mesg.session_uuid?
-                    @_sessions[mesg.session_uuid].emit("execute_javascript", mesg)
-                else
-                    @emit("execute_javascript", mesg)
-
-            when "output"
-                cb = @execute_callbacks[mesg.id]
-                if cb?
-                    cb(mesg)
-                    delete @execute_callbacks[mesg.id] if mesg.done
-                if mesg.session_uuid?  # executing in a persistent session
-                    @_sessions[mesg.session_uuid].emit("output", mesg)
-                else   # stateless exec
-                    @emit("output", mesg)
-
-            when "terminate_session"
-                session = @_sessions[mesg.session_uuid]
-                session?.emit("close")
-
-            when "session_reconnect"
-                if mesg.data_channel?
-                    @_sessions[mesg.data_channel]?.reconnect()
-                else if mesg.session_uuid?
-                    @_sessions[mesg.session_uuid]?.reconnect()
-
             when "cookies"
                 @_cookies?(mesg)

@@ -557,17 +392,6 @@
         @_signed_in = false
         @_redux?.getActions('account')?.set_user_type('public')

-    change_data_channel: (opts) =>
-        opts = defaults opts,
-            prev_channel : undefined
-            new_channel  : required
-            session      : required
-        if opts.prev_channel?
-            @unregister_data_handler(opts.prev_channel)
-            delete @_sessions[opts.prev_channel]
-        @_sessions[opts.new_channel] = opts.session
-        @register_data_handler(opts.new_channel, opts.session.handle_data)
-
     register_data_handler: (channel, h) ->
         @_data_handlers[channel] = h

@@ -581,106 +405,6 @@
             f(data)
         #else
         #    console.log("Error -- missing channel '#{channel}' for data '#{data}'.  @_data_handlers = #{misc.to_json(@_data_handlers)}")
-
-    connect_to_session: (opts) ->
-        opts = defaults opts,
-            type         : required
-            session_uuid : required
-            project_id   : required
-            timeout      : DEFAULT_TIMEOUT
-            params       : required  # must include {path:?, filename:?}
-            cb           : required
-        @call
-            allow_post : false
-
-            message : message.connect_to_session
-                session_uuid : opts.session_uuid
-                type         : opts.type
-                project_id   : opts.project_id
-                params       : opts.params
-
-            timeout : opts.timeout
-
-            cb      : (error, reply) =>
-                if error
-                    opts.cb(error); return
-                switch reply.event
-                    when 'error'
-                        opts.cb(reply.error)
-                    when 'session_connected'
-                        @_create_session_object
-                            type         : opts.type
-                            project_id   : opts.project_id
-                            session_uuid : opts.session_uuid
-                            data_channel : reply.data_channel
-                            init_history : reply.history
-                            params       : opts.params
-                            cb           : opts.cb
-                    else
-                        opts.cb("Unknown event (='#{reply.event}') in response to connect_to_session message.")
-
-    new_session: (opts) ->
-        opts = defaults opts,
-            timeout    : DEFAULT_TIMEOUT # how long until give up on getting a new session
-            type       : "console"   # only "console" supported
-            params     : required    # must include {path:?, filename:?}
-            project_id : required
-            cb         : required    # cb(error, session)  if error is defined it is a string
-
-        @call
-            allow_post : false
-            message : message.start_session
-                type       : opts.type
-                params     : opts.params
-                project_id : opts.project_id
-
-            timeout : opts.timeout
-
-            cb      : (error, reply) =>
-                if error
-                    opts.cb(error)
-                else
-                    if reply.event == 'error'
-                        opts.cb(reply.error)
-                    else if reply.event == "session_started" or reply.event == "session_connected"
-                        @_create_session_object
-                            type         : opts.type
-                            project_id   : opts.project_id
-                            session_uuid : reply.session_uuid
-                            data_channel : reply.data_channel
-                            params       : opts.params
-                            cb           : opts.cb
-                    else
-                        opts.cb("Unknown event (='#{reply.event}') in response to start_session message.")
-
-    _create_session_object: (opts) =>
-        opts = defaults opts,
-            type         : required   # 'console'
-            project_id   : required
-            session_uuid : required
-            data_channel : undefined
-            params       : undefined
-            init_history : undefined
-            cb           : required
-
-        session_opts =
-            conn         : @
-            project_id   : opts.project_id
-            session_uuid : opts.session_uuid
-            data_channel : opts.data_channel
-            init_history : opts.init_history
-            params       : opts.params
-
-        switch opts.type
-            when 'console'
-                session = new ConsoleSession(session_opts)
-            else
-                opts.cb("Unknown session type: '#{opts.type}'")
-        @_sessions[opts.session_uuid] = session
-        if opts.data_channel != JSON_CHANNEL
-            @_sessions[opts.data_channel] = session
-        @register_data_handler(opts.data_channel, session.handle_data)
-        opts.cb(false, session)

     _do_post_call: (opts, cb) =>
         opts = defaults opts,


make go away sorta work again

make go away sorta work again

--- scripts/go-away.coffee
+++ scripts/go-away.coffee
@@ -1,48 +1,3 @@
-# Description:
-#   Make the bot leave the room for a while
-#   Only works on XMPP adapter
-#
-# Dependencies:
-#   hubot-xmpp
-#
-# Commands:
-#   hubot go away - Make the bot leave this room 30 minutes
-#   hubot stay in here - Make the bot come back to this room on reboots
-#   hubot stop coming in here - Make her stop logging in
-#
-# Author:
-#   ivey
-
 module.exports = (robot) ->
-  robot.brain.on 'loaded', =>
-    console.log robot.brain.data.always_join
-    console.log "BLANKING always_join ROOMS FOR SLACK - SAVE THESE"
-    robot.brain.data.always_join = []
-    robot.adapter.connector.join room for room in robot.brain.data.always_join
-
-  robot.respond /stay in here/i, (msg) ->
-    room = msg.message.user.reply_to
-    console.log "Request to stay in #{room}"
-    unless room in robot.brain.data.always_join
-      robot.brain.data.always_join.push room
-      console.log robot.brain.data.always_join
-      robot.brain.save()
-      msg.send "OK. I'll come back here whenever I login."
-
-  robot.respond /stop coming in here/i, (msg) ->
-    room = msg.message.user.reply_to
-    console.log "Request to stop staying in #{room}"
-    if room in robot.brain.data.always_join
-      robot.brain.data.always_join = robot.brain.data.always_join.filter (e) -> e != room
-      console.log robot.brain.data.always_join
-      robot.brain.save()
-      msg.send "OK. I'll stop coming in here. Invite me back if you change your mind."
-      robot.adapter.connector.part room
-
-  robot.respond /(go away|gtfo)/i, (msg) ->
-    room = msg.message.user.reply_to
-    msg.reply "OK. I'll come back in around 30 minutes."
-    setTimeout (->
-      robot.adapter.connector.join room
-    ), 1800000
-    robot.adapter.connector.part room
+  robot.respond /(stop coming in here|go away|gtfo)/i, (msg) ->
+    msg.send "Ugh, sorry. Slack doesn't allow us bots to leave on our own. You can either ask a slack admin in #ask-slack to remove me, or just make me shut up with `@lefay mute`"


stop coming should == leave, too

stop coming should == leave, too

--- scripts/go-away.coffee
+++ scripts/go-away.coffee
@@ -24,8 +24,8 @@
     console.log "Request to stay in #{room}"
     unless room in robot.brain.data.always_join
       robot.brain.data.always_join.push room
+      console.log robot.brain.data.always_join
       robot.brain.save()
-      console.log robot.brain.data.always_join
       msg.send "OK. I'll come back here whenever I login."

   robot.respond /stop coming in here/i, (msg) ->
@@ -33,9 +33,10 @@
     console.log "Request to stop staying in #{room}"
     if room in robot.brain.data.always_join
       robot.brain.data.always_join = robot.brain.data.always_join.filter (e) -> e != room
+      console.log robot.brain.data.always_join
       robot.brain.save()
-      console.log robot.brain.data.always_join
-      msg.send "OK. I'll stop coming in here when I login."
+      msg.send "OK. I'll stop coming in here. Invite me back if you change your mind."
+      robot.adapter.connector.leave room

   robot.respond /go away/i, (msg) ->
     room = msg.message.user.reply_to
@@ -44,12 +45,3 @@
       robot.adapter.connector.join room
     ), 1800000
     robot.adapter.connector.leave room
-
-  robot.respond /go away forever/i, (msg) ->
-    room = msg.message.user.reply_to
-    room = msg.message.user.room
-    if room in robot.brain.data.always_join
-      robot.brain.data.always_join = robot.brain.data.always_join.filter (e) -> e != room
-      robot.brain.save()
-    msg.reply "Jeez. OK."
-    robot.adapter.connector.leave room


Fix grading when URL is redirected

Fix grading when URL is redirected

--- src/js/app/helpers/handlebars.coffee
+++ src/js/app/helpers/handlebars.coffee
@@ -38,8 +38,10 @@
   unsafe = tags.all.unsafe.length
   safe = tags.all.safe.length + tags.all.sameorigin.length
   rate = 100 - ((unsafe / (safe + unsafe)) * 100)
-  rateFixed = rate.toFixed(1)
-  html = "Rate: #{rateFixed}%"
+  rateFixed = switch
+    when isNaN(rate) then "not applicable"
+    else "#{rate.toFixed(1)}%"
+  html = "Rate: #{rateFixed}"
   return new HandleBars.SafeString html

 # Generate number of safe tags out of SRI protected and same-origin
@@ -52,13 +54,16 @@
   safe = tags.all.safe.length + tags.all.sameorigin.length
   count = safe + unsafe
   rate = 100 - ((unsafe / (safe + unsafe)) * 100)
-  rateFixed = rate.toFixed(1)
+  rateFixed = switch
+    when isNaN(rate) then "not applicable as we detected URL redirection"
+    else "#{rate.toFixed(1)}%"
   grade = switch
-    when rateFixed < 63 then 'F'
-    when rateFixed < 73 then 'D'
-    when rateFixed < 83 then 'C'
-    when rateFixed < 93 then 'B'
-    else 'A'
+      when rate >= 93 then "A"
+      when rate >= 83 then "B"
+      when rate >= 73 then "C"
+      when rate >= 63 then "D"
+      when isNaN(rate) is true then "R"
+      else "F"
   html = """

Grading

@@ -70,7 +75,7 @@

Your grade is based on the (1) number of unsafe assets and the (2) total number of assets

We found #{unsafe} unsafe assets out of the total of #{count} assets

-
The rate of these two numbers is #{rateFixed}%, which provides your grade: '#{grade}'

+
The rate of these two numbers is #{rateFixed}, which provides your grade: '#{grade}'

   """
   return html


Empty bought array when drawing.

Empty bought array when drawing.

--- scripts/tickets.coffee
+++ scripts/tickets.coffee
@@ -67,6 +67,7 @@
         if robot.auth.hasRole(msg.envelope.user, ['admin'])
             raffle = off
             entered[username] = false
+            bought.length = 0
             save(robot)
             msg.send "The raffle is now closed. A winner will be announced in this message once finished."
             return


weather script

weather script

--- scripts/weather.coffee
+++ scripts/weather.coffee
@@ -2,41 +2,35 @@
 # Commands:
 #   hubot weather <address> - Weather info for the specified address

-rp = require('request-promise');
-
-
+rp = require('request-promise')

 module.exports = (robot) ->
   robot.respond /weather (.*)/i, (msg) ->
     location = escape(msg.match[1]) or "Kansas City"
-    locationOptions = {
+    locationOptions =
       uri: 'http://maps.googleapis.com/maps/api/geocode/json',
-      qs: {
+      qs:
         address: location
-      },
-        headers: {
+
+      headers:
           'User-Agent': 'Request-Promise'
-    },
     json: true
-    }

     rp(locationOptions)
-      .then( fucntion(data) {
+      .then( (data) ->
         latitude = data.geometry.location.lat
         longitude = data.geometry.location.lng
         latlng = latitude + ',' + longitude
-        forecastOptions = {
+        forecastOptions =
           uri: 'https://api.forecast.io/forecast/' + process.env.FORECAST_KEY + '/' + latlng
-          qs: {
+          qs:
             units: 'us'
-          },
-          headers: {
+          headers:
             'User-Agent': 'Request-Promise'
-          },
           json: true
-      }
+
       rp(forecastOptions)
-        .then(function(data) {
+        .then((data) ->
           currentTemp = data.currently.temperature
           summary = data.currently.summary
           icon = data.currently.icon
@@ -47,13 +41,11 @@
           msg.send('Current Temp: ' + currentTemp + '\n' +
                   'Max Temp:      ' + maxTemp + '\n' +
                   'Min Temp:      ' + minTemp +)
-        })
-        .catch(function(err) {
+        )
+        .catch( (err) ->
           msg.send('Error in forecast call.\n' + err)
-        })
-
-  })
-  .catch(function(err){
+        )
+  )
+  .catch( (err) ->
     msg.send("Error in google geocode call.\n" +  err )
-  })
-  msg.send "Ryan is a gawd among programmers!"
+  )


client: groups bundle UI: add a debit button

- advanced mode
- initialize the sliders (even though their about to get axed)

--- client/landing/app/Applications/Groups.kdapplication/Views/groupsbundleview.coffee
+++ client/landing/app/Applications/Groups.kdapplication/Views/groupsbundleview.coffee
@@ -98,7 +98,9 @@
       title     : "Save bundle"
       callback  : =>
         overagePolicy = @overagePolicy.getValue()
-        group.updateBundle { overagePolicy }
+        quotas = {}
+        quotas[title] = +limit.quota  for own title, limit of @limitsData
+        group.updateBundle { overagePolicy, quotas }

     @overageLabel = new KDLabelView
       title     : 'Overage policy'
@@ -113,6 +115,42 @@
         { value : "allowed",        title : "Allowed for all members" }
       ]

+    @advancedModeLabel = new KDLabelView title: 'Advanced mode'
+
+    @advancedMode = new KDOnOffSwitch
+      label     : @advancedModeLabel
+      callback  : @bound 'toggleAdvancedMode'
+
+    @debitButton = new KDButtonView
+      cssClass  : "clean-grey"
+      title     : "Debit..."
+      callback  : =>
+        number = +prompt "How many VMs do you want to debit?", "1"
+        debits =
+          cpu   : number * computeUnitMap.cpu
+          ram   : number * computeUnitMap.ram
+          disk  : number * computeUnitMap.disk
+        bundle.debit debits, (err)->
+          console.error err  if err
+
+  toggleAdvancedMode: (state) ->
+    method = if state then 'show' else 'hide'
+    for own limitName, notHidden of computeUnitVisibility when not notHidden
+      @limits[limitName][method]()
+
+  initializeSlider: (limit) ->
+
+    slider = switch limit.title
+      when 'vms'    then @computeSlider
+      when 'users'  then @usersSlider
+
+    return  unless slider?
+
+    @utils.defer =>
+      {quota} = if limit.title is 'vms' then @limitsData.cpu else limit
+      slider.setValue quota
+      slider.emit 'change'
+
   renderLimits: (limits) ->
     @limits.destroySubViews()
     @limitsData = {}
@@ -121,7 +159,9 @@
       cssClass = 'group-bundle-limit'
       cssClass += ' hidden'  unless computeUnitVisibility[limit.title]
       limitView = new GroupsBundleLimitView {cssClass}, limit
+      @limits[limit.title] = limitView
       @addSubView limitView
+      @initializeSlider limit

   pistachio: ->
     """
@@ -131,9 +171,11 @@

{{> @usersLabel}} {{> @usersSlider}}

{{> @computeLabel}}  {{> @computeSlider}}

{{> @overageLabel}} {div#overage{> @overagePolicy}}

+
{{> @advancedModeLabel}} {{> @advancedMode}}

     {{> @limits}}

{{> @saveButton}} {{> @destroyButton}}

+
{{> @debitButton}}

     """

 # class GroupsBundleLimitWrapper extends KDObject


client: radios instead of sliders for group bundle editor

--- client/landing/app/Applications/Groups.kdapplication/Views/groupsbundleview.coffee
+++ client/landing/app/Applications/Groups.kdapplication/Views/groupsbundleview.coffee
@@ -36,12 +36,35 @@
     ram   : no
     disk  : no

+  mapPlansToRadios: (plan) ->
+    @plansByCode[plan.code] = plan
+    { category, resource, upperBound } = plan.usage
+    {
+      title       : @getPlanTitle category, resource, upperBound
+      value       : plan.code
+    }
+
+  getResourceTitle =(resource)->
+    switch resource
+      when 'cpu'    then 'virtual cores'
+      when 'user'   then 'group members'
+      else               '(not implemented)'
+
+
+  getPlanTitle: (category, resource, upperBound)=>
+    """
+    Up to #{upperBound} #{getResourceTitle resource}
+    """
+
+  upperBoundSortFn =(a, b)->
+    a.usage.upperBound - b.usage.upperBound
+
   constructor: (options, data) ->
     super

-    {group, bundle} = @getData()
-
-    {JLimit} = KD.remote.api
+    { group, bundle } = @getData()
+
+    { JLimit, JGroupBundle } = KD.remote.api

     computeLimit = new JLimit { usage: 0, quota: 0, unit: 'VM', title: 'vms' }

@@ -49,6 +72,31 @@
       return error err      if err?
       limits.splice 1, 0, computeLimit
       @renderLimits limits  if limits?
+
+    @plansByCode = {}
+
+    JGroupBundle.fetchPlans (err, plans) =>
+
+      @computePlans.addSubView computePlans = new KDInputRadioGroup
+        name    : 'computePlan'
+        radios  : plans.vm.cpu
+          .sort(upperBoundSortFn)
+          .map @bound 'mapPlansToRadios'
+        change  : =>
+          plan = @plansByCode[computePlans.getValue()]
+          @changePlan ['cpu','vms','ram','disk'], plan
+
+      @usersPlans.addSubView userPlans = new KDInputRadioGroup
+        name    : 'userPlan'
+        radios  : plans.group.user
+          .sort(upperBoundSortFn)
+          .map @bound 'mapPlansToRadios'
+        change  : =>
+          plan = @plansByCode[userPlans.getValue()]
+          @changePlan ['users'], plan
+
+    @usersPlans = new KDView cssClass: 'group-bundle-plan-chooser'
+    @computePlans = new KDView cssClass: 'group-bundle-plan-chooser'

     @usersLabel = new KDLabelView
       title       : 'Users'
@@ -151,6 +199,13 @@
       slider.setValue quota
       slider.emit 'change'

+  changePlan: (types, plan) ->
+    return  unless plan?
+    for type in types
+      limit = @limitsData[type]
+      limit.quota = plan.usage.upperBound * (computeUnitMap[type] ? 1)
+      limit.emit 'update'
+
   renderLimits: (limits) ->
     @limits.destroySubViews()
     @limitsData = {}
@@ -168,8 +223,8 @@

Bundle details

There will be some cool details here.

-
{{> @usersLabel}} {{> @usersSlider}}

-
{{> @computeLabel}}  {{> @computeSlider}}

+
{{> @usersLabel}} {{> @usersPlans}}

+
{{> @computeLabel}} {{> @computePlans}}

{{> @overageLabel}} {div#overage{> @overagePolicy}}

{{> @advancedModeLabel}} {{> @advancedMode}}


server 90% done

server 90% done

--- server/kcServer.coffee
+++ server/kcServer.coffee
@@ -5,7 +5,7 @@
 Modules =
 	fs: require('fs')
 	https: require('https')
-	wss: require('websocket').server
+	sio: require('socket.io')
 	sha: require('sha1')
 	aes: require('aes256')
 	readline: require('readline')
@@ -16,8 +16,7 @@
 	cert: fs.readFileSync ResourceDir + 'cert.pem'
 , (req, res) ->
 	res.end 'You were not supposed to visit here with a browser. Use a KentChat client!'
-WSServer = new Modules.wss
-	httpServer: WebServer
+SIOServer = sio(WebServer)

 # Declare options and variables
 Options =
@@ -30,6 +29,8 @@
 messageRegistry = []
 loggedUsers = []
 hostBlacklist = JSON.parse Modules.fs.readFileSync ResourceDir + "blacklist.json"
+getFileJSON = (fname) -> JSON.parse Modules.fs.readFileSync "#{ResourceDir}#{fname}.json"
+GlobalDebug = yes

 # Declare object types using classes
 UserPowerLevels =
@@ -41,72 +42,75 @@

 class KcIdentifier
 	constructor: (@id) ->
-
+
 class User extends KcIdentifier
 	constructor: (id, @powerLevel, @auth) ->
 		super id
-		@connection = null
+		@connID = null
 		@cipher = Modules.aes.createCipher @auth

 	isQualifiedFor: (powerLevel) -> powerLevel <= @powerLevel
 	toString: () -> "[User id=#{@id} powerLevel=#{@powerLevel}]"
-	onConnect: (webSocketConnection) ->
-		@connection = webSocketConnection
-		loggedUsers.push this
-	onDisconnect: () -> @connection = null if @connection?
+	onConnect: (client) ->
+		@connID = client.id
+		if not this in loggedUsers then loggedUsers.push this else KcCoreLogger.warn "user #{@id} onConnect called twice"
+	onDisconnect: () ->
+		@connID = null if @connID?
+		if this in loggedUsers then loggedUsers.pop this else KCCoreLogger.warn "user #{@id} onDisconnect called twice"
 	authenticate: (pwd) -> Modules.sha pwd is @auth

-	send: (msg) ->
+	send: (event, msg) ->
 		throw Error "#{@toString()} Message undefined!" if not msg?
-		@connection.send @cipher.encrypt msg
-
-	isConnected: () -> @connection isnt null
-
-	toJSON: () ->
+		(SIOServer.to @connID).emit event, @cipher.encrypt msg
+
+	decipher: (msg) -> @cipher.decrypt msg
+	isConnected: -> @connID isnt null
+
+	toJSON: ->
 		id: @id
 		powerLevel: @powerLevel
 		auth: @auth

+toID = (user) -> if typeof user is 'string' then user else user.id
+
 class Group extends KcIdentifier
-	constructor: (id, @members = [], passphrase) ->
+	constructor: (id, owner, members, @passphrase) ->
 		super id
-		@passphrase = if passphrase? \
-			then passphrase else \
+		@owner = owner.id
+		@members = if members? then (toID member for member in members) else [toID @owner]
+		@passphrase ?= \
 			(String.fromCharCode(Math.floor(Math.random() * \
 			(Options.phRand[1] - Options.phRand[0])) + Options.phRand[0]) for _ in [1..Options.phLength]).join ''

-	addUser: (user) -> @members.push user
-	removeUser: (user) -> @members.pop user
+	addUser: (user) -> @members.push user.id
+	removeUser: (user) -> @members.pop user.id
 	toString: () -> "[Group id=#{@id} length-of-members=#{@members.length}]"

-	send: (msg) ->
-		throw Error "#{@toString()} Message undefined!" if not msg?
-		@connection.send Modules.aes.encrypt @passphrase, msg
+	# send: (msg) ->
+	# 	throw Error "#{@toString()} Message undefined!" if not msg?
+	# 	@connection.send Modules.aes.encrypt @passphrase, msg

 	toJSON: () ->
 		id: @id
 		passphrase: @passphrase
 		members: x.id for x in @members
-
+
 # Set up common functions
 getUserById = (id) ->
-	for user in userRegistry
-		return user if user.id is id
-
+	return user for user in userRegistry when user.id is id
 getGroupById = (id) ->
-	for group in groupRegistry
-		return group if group.id is id
+	return group for group in groupRegistry when group.id is id

 class Message extends KcIdentifier
 	DIRECT_MESSAGE: 0
 	GROUP_MESSAGE: 1
-
+
 	constructor: (@type, @sourceId, @destId, @message, @read = no) ->
 		if not (@type in [@DIRECT_MESSAGE..@GROUP_MESSAGE])
 			throw Error "Unknown message type: #{@type}"
 		@id = (String.fromCharCode(Math.floor(Math.random() * \
-			(Options.phRand[1] - Options.phRand[0])) + Options.phRand[0]) for _ in [0..19]).join ''
-
+			(Options.phRand[1] - Options.phRand[0])) + Options.phRand[0]) for _ in [0..13]).join ''
+
 	toJSON: () ->
 		type: @type
 		sourceId: @sourceId
@@ -114,74 +118,193 @@
 		message: @message
 		read: @read

+getMessageById = (id) ->
+	return msg for msg in messageRegistry when msg.id is id
+
+class Logger
+	Colors:
+		reset: `'\033[0m'`
+		red: `'\033[0;31m'`
+		green: `'\033[0;32m'`
+		yellow: `'\033[0;33m'`
+		blue: `'\033[0;34m'`
+
+	constructor: (@name, @includeTimeStamp=yes) -> @levels = []
+	log: (message, prefix='', suffix='', type='log') ->
+		console.log "#{prefix}#{if @includeTimeStamp then ("[" + new Date().toTimeString().substr 0,8 + "]") else ''}#{@name} #{type} -> #{level+" -> " for level in @levels}#{message}#{suffix}"
+	error: (message) -> @log message, @Colors.red, @Colors.reset, 'ERROR'
+	warn: (message) -> @log message, @Colors.yellow, @Colors.reset, 'WARN'
+	info: (message) -> @log message, @Colors.blue, @Colors.reset, 'info'
+	debug: (message) -> if GlobalDebug then @log message, @Colors.green, @Colors.reset, 'debug'
+	enter: (level) -> if not (level in @levels) then @levels.push level else @warn "logger was asked to enter existent level #{level}"
+	exit: (level) -> if (level in @levels) then @levels.pop level else @warn "logger was asked to exit nonexistent level #{level}"

 User.fromJSON = (jsonExpression) -> new User(jsonExpression.id, jsonExpression.powerLevel, jsonExpression.auth)
 Group.fromJSON = (jsonExpression) -> new Group(jsonExpression.id, getUserById userExp for userExp in jsonExpression.members, jsonExpression.passphrase)
 Message.fromJSON = (je) -> new Message je.type, je.sourceId, je.destId, je.message, je.read

 # Set up variables and functions that use the defined classes
-userRegistry.push User.fromJSON json for json in JSON.parse Modules.fs.readFileSync ResourceDir + "users.json"
-groupRegistry.push Group.fromJSON json for json in JSON.parse Modules.fs.readFileSync ResourceDir + "groups.json"
-messageRegistry.push Message.fromJSON json for json in JSON.parse Modules.fs.readFileSync ResourceDir + "messages.json"
+userRegistry.push User.fromJSON json for json in getFileJSON 'users'
+groupRegistry.push Group.fromJSON json for json in getFileJSON 'groups'
+messageRegistry.push Message.fromJSON json for json in getFileJSON 'messages'
+
+KcCoreLogger = new Logger "KentChat"
+KcMessageLogger = new Logger "MessageLogger"

 containsUser = (id) -> (getUserById id)?
 containsGroup = (id) -> (getGroupById id)?
 messagesFrom = (id) -> (msg for msg in messageRegistry when msg.sourceId is id)
-messagesToUser = (id) -> (msg for msg in messageRegistry when msg.type is Message.DIRECT_MESSAGE and msg.destId is id)
+messagesToUser = (id) -> (msg for msg in messageRegistry when msg.type is Message.DIRECT_MESSAGE and msg.destId is id).concat \
+	(msg for msg in messageRegistry when msg.type is Message.GROUP_MESSAGE and id in (getGroupById msg.destId).members)
 groupIDs = () -> (group.id for group in groupRegistry)
 userIDs = () -> (user.id for group in groupRegistry)
+connectionIsLoggedIn = (conn) -> if user.connID is conn.id for user in userRegistry then yes else no
+getUserBySocketID = (socketID) ->
+	return user for user in userRegistry when user.connID is socketID
 User::unreadMessages = () -> (msg for msg in messagesToUser @id when not msg.read)
 User::sentMessages = () -> messagesFrom @id
 User::onRead = (message) -> (message.read = yes) if message.type is Message.DIRECT_MESSAGE and message.destId is @id and not message.read
-User::sendMessage = (destId, message, type) -> messageRegistry.push new Message type, @id, destId, message
-User::tell = (data) -> @connection.send data
+User::sendMessage = (destId, message, type) ->
+	msg = new Message type, @id, destId, message
+	messageRegistry.push msg
+	@send {type: "newMessage", id: msg.id}
 Group::getMessages = () -> (msg for msg in messageRegistry when msg.type is Message.GROUP_MESSAGE and msg.destId is @id)

+# Event handler common functions
+fail = (reason) -> ['failure', reason]
+succeed = (info) -> if info? then ['success', info] else ['success']
+data = (raw) -> ['data', (if raw instanceof Array then raw.join ', ' else raw)]
 # Event handler modules
 eventHandlers = [
 	{
 		expectedType: "msg"
+		expectedPerm: UserPowerLevels.guest
 		expectedArgs: ["dest", "message", "toUser"]
 		onMessage: (msg, source) ->
-			return "failure: target user or group does not exist" if (msg.toUser and not containsUser msg.dest) or (not msg.toUser and not containsGroup msg.dest)
+			(return fail "target user or group does not exist") if (msg.toUser and not containsUser msg.dest) or (not msg.toUser and not containsGroup msg.dest)
 			source.sendMessage msg.dest, msg.message, if msg.toUser then Message.DIRECT_MESSAGE else Message.GROUP_MESSAGE
-			return true
+			KcMessageLogger.log "message sent from #{source.toString()} to #{msg.dest}: #{msg.message}"
+			return succeed()
 	}
 	{
 		expectedType: "listUsers"
+		expectedPerm: UserPowerLevels.guest
 		expectedArgs: []
-		onMessage: (msg, source) ->
-			return userIDs().join ", "
+		onMessage: (msg, source) -> data userIDs()
 	}
 	{
 		expectedType: "read"
+		expectedPerm: UserPowerLevels.guest
 		expectedArgs: ["id"]
-		# todo
+		onMessage: (msg, source) ->
+			message = getMessageById msg.id
+			if message? and message.destId is source.id
+				source.onRead message
+				return data
+					read: yes
+					sender: message.sourceId
+					message: message.message
+			else return data
+				read: no
+				why: 'message does not exist or is unaccessible'
+	}
+	{
+		expectedType: "getUnread"
+		expectedPerm: UserPowerLevels.guest
+		expectedArgs: []
+		onMessage: (msg, source) -> data(message.id for message in source.unreadMessages())
+	}
+	{
+		expectedType: "createGroup"
+		expectedPerm: UserPowerLevels.admin
+		expectedArgs: ["id"]
+		onMessage: (msg, source) ->
+			if msg.id in groupIDs()
+				return fail 'this group already exists'
+			else
+				groupRegistry.push new Group msg.id, [source.id]
+				return succeed "group #{msg.id} created"
+	}
+	{
+		expectedType: "listGroups"
+		expectedPerm: UserPowerLevels.guest
+		expectedArgs: []
+		onMessage: (msg, source) -> data groupIDs()
+	}
+	{
+		expectedType: "inviteToGroup"
+		expectedPerm: UserPowerLevels.guest
+		expectedArgs: ['target', 'group']
+		onMessage: (msg, source) ->
+			(return fail "group #{msg.group} does not exist") if not msg.group in groupIDs()
+			source.sendMessage msg.target, "I invite you to join the group named #{msg.group}. \
+				Use \"kc.join('#{msg.group}')\" to join. (generated by KentChat)", Message.DIRECT_MESSAGE
+			return succeed "invitation sent"
+	}
+	{
+		expectedType: "joinGroup"
+		expectedPerm: UserPowerLevels.guest
+		expectedArgs: ['target']
+		onMessage: (msg, source) ->
+			(return fail "group #{msg.group} does not exist") if not msg.target in groupIDs()
+			group = getGroupById msg.target
+			(return fail "you are already in this group") if source.id in group.members
+			group.addUser source
+			return succeed "you have joined #{group.id}"
+	}
+	{
+		expectedType: "register"
+		expectedPerm: UserPowerLevels.admin
+		expectedArgs: ['id', 'auth', 'powerLevel']
+		onMessage: (msg, source) ->
+			(return fail "#{msg.id} exists") if containsUser source.id
+			userRegistry.push new User msg.id, msg.powerLevel, Modules.sha msg.auth
+			return succeed "#{msg.id} created"
 	}
 ]
-setupHandlersForUser = (conn, id) ->
-
+KcCoreLogger.debug "Declared #{eventHandlers.length} event handlers"

 # Login procedure
-WSServer.on 'request', (req) ->
-	if req.host in hostBlacklist
-		req.reject 200, 'You are on the KentChat blacklist!'
-	else
-		conn = req.accept null, req.origin
-		conn.on 'message', (message) ->
-			if message.type is 'binary'
-				conn.drop 1007, 'expected login data to be utf8, received binary'
-			else if not (message.id? and message.auth?)
-				conn.drop 1007, 'expected msg.id and msg.auth, missing'
-			else
-				user = getUserById message.id
-				if not user?
-					conn.drop 1003, "unknown userID: #{message.id}"
+SIOServer.on 'connection', (socket) ->
+	socket.on 'login', (data) ->
+		if not data.id? or not data.auth?
+			socket.emit 'failure', 'expected data.id and data.auth, missing'
+			return
+		if connectionIsLoggedIn socket
+			socket.emit 'failure', 'already logged in'
+			return
+		user = getUserById data.id
+		if not user?
+			socket.emit 'failure', "unknown userID: #{data.id}"
+			return
+		if user.isConnected()
+			socket.emit 'failure', "user #{data.id} is already logged in"
+			return
+		if not user.authenticate data.auth
+			socket.emit 'failure', 'bad password'
+			return
+		user.onConnect socket
+		socket.emit 'success', {}
+		for handler in eventHandlers
+			socket.on handler.expectedType, (data) ->
+				if not connectionIsLoggedIn socket
+					socket.emit 'failure', 'You are not logged in'
+					socket.disconnect()
 					return
-				if user.isConnected()
-					conn.drop 1003, "user #{message.id} is already logged in"
+				source = getUserBySocketID socket.id
+				if not source.isQualifiedFor handler.expectedPerm
+					source.send 'failure', "You are not qualified to '#{handler.expectedType}', \
+						required power level is #{handler.expectedPerm}, you have #{source.powerLevel}"
 					return
-				if not user.authenticate message.auth
-					conn.drop 1003, 'bad password'
+				if not data[arg]? for arg in handler.expectedArgs
+					source.send 'failure', "Missing arguments; expected #{handler.expectedArgs.join ', '}"
 					return
-				user.onConnect conn
+				result = handler.onMessage data, source
+				if not (result instanceof Array) or result.length isnt 2
+					KcCoreLogger.error "Unexpected result from #{handler.expectedType} eventHandler: #{result}, expected 2-element array"
+					source.send 'failure', 'Server internal error, please report to the owners'
+					return
+				source.send result[0], result[1]
+	socket.on 'disconnect', ->
+		source = getUserBySocketID socket.id
+		source.onDisconnect() if source?


'Updating for version 0.12.5dev3'

'Updating for version 0.12.5dev3'

--- bokehjs/src/coffee/version.coffee
+++ bokehjs/src/coffee/version.coffee
@@ -1 +1 @@
-export version = '0.12.5dev2'
+export version = '0.12.5dev3'


Fixed tests for preserving Item with parent order

--- tests/integration/src/styles/style.server.coffee
+++ tests/integration/src/styles/style.server.coffee
@@ -557,7 +557,7 @@
             test()
             return

-        it "is valid for fixed item parent", ->
+        it "is preserved for item with parent", ->
             doc = render
                 html: """<b n-style="styles:file:style">
                     <em n-style="renderer:Item" />
@@ -578,7 +578,7 @@
             emNode = doc.node.query 'em'
             iNode = doc.node.query 'i'

-            assert.is emNode.style.nextSibling, iNode.style
+            assert.is emNode.style.previousSibling, iNode.style

     it '', ->
         for view in testViews


TOUCH IT

TOUCH IT

--- lib/app-list.coffee
+++ lib/app-list.coffee
@@ -44,7 +44,7 @@
       onThreeTouch: @touch

   touch: (e) ->
-    @dom.css. 'right',"#{event.diff.right}px"
+    @dom.css 'right',"#{event.diff.right}px"

   remove: (app) ->


replacing deprecated .live() jquery method by document.on()

--- lib/assets/javascripts/edit_mode/show_only_in_edit_mode.js.coffee
+++ lib/assets/javascripts/edit_mode/show_only_in_edit_mode.js.coffee
@@ -7,16 +7,19 @@
     # When edit mode is left, the events 'save' or 'cancel' are triggered.
     # For  tags, these events are rather simple:
     # Just show or hide these spans!
-    this.find( ".show_only_in_edit_mode" ).live( "edit", ->
+
+    this.on( 'edit', '.show_only_in_edit_mode', ->
       $( this ).show()
-    ).live( "save cancel", ->
+    )
+    this.on( 'save cancel', '.show_only_in_edit_mode', ->
       $( this ).hide()
     )

     # And just the opposite for .
-    this.find( ".do_not_show_in_edit_mode" ).live( "edit", ->
+    this.on( 'edit', '.do_not_show_in_edit_mode', ->
       $( this ).hide()
-    ).live( "save cancel", ->
+    )
+    this.on( 'save cancel', '.do_not_show_in_edit_mode', ->
       $( this ).show()
     )


Removed dependency on validate

Removed dependency on validate

--- lib/elements.coffee
+++ lib/elements.coffee
@@ -2,7 +2,6 @@
 cx = require "react/lib/cx"

 inputs = require "./inputs"
-validate = require "./validate"

 keys = esc: 13


It is Uint8Array, not UInt8Array.

It is Uint8Array, not UInt8Array.

--- arraybuffer.coffee
+++ arraybuffer.coffee
@@ -4,10 +4,10 @@

 if not ArrayBuffer.prototype.slice
   ArrayBuffer.prototype.slice = (start, end) ->
-    that = new UInt8Array this
+    that = new Uint8Array this
     end = that.length if typeof end is 'undefined'
     result = new ArrayBuffer end - start
-    resultArray = new UInt8Array result
+    resultArray = new Uint8Array result
     for i in [0..resultArray.length]
       resultArray[i] = that[i + start]
     result


Speed up messageNew by avoiding _messageParent

* Initial insertion now includes root (and submessage data if root),
  which helps avoid jitter in since/live views
* _submessagesChanged called directly from mesageNew now
* Key operation needed by messageNew and _messageParent
  extracted to _messageAddChild

--- lib/messages.coffee
+++ lib/messages.coffee
@@ -522,6 +522,16 @@
   notifyMessageUpdate message if Meteor.isServer  ## client in simulation
   diffid

+_messageAddChild = (child, parent, position = null) ->
+  if position?
+    Messages.update parent,
+      $push: children:
+        $each: [child]
+        $position: position
+  else
+    Messages.update parent,
+      $push: children: child
+
 _messageParent = (child, parent, position = null, oldParent = true, importing = false) ->
   ## oldParent is an internal option for server only; true means "search for/
   ## deal with old parent", while null means we assert there is no old parent.
@@ -531,13 +541,18 @@
     throw new Meteor.Error 'messageParent.noChild',
       "Missing child message #{child} to reparent"
   if parent?
-    pmsg = Messages.findOne parent
-    unless pmsg?
-      ## This can happen in client simulation when parent isn't in
-      ## subscription.
-      return if @isSimulation
-      throw new Meteor.Error 'messageParent.noParent',
-        "Missing parent message #{parent} to reparent"
+    ## On server, can give a parent message instead of an ID, to save query.
+    if parent._id?
+      pmsg = parent
+      parent = pmsg._id
+    else
+      pmsg = Messages.findOne parent
+      unless pmsg?
+        ## This can happen in client simulation when parent isn't in
+        ## subscription.
+        return if Meteor.isClient
+        throw new Meteor.Error 'messageParent.noParent',
+          "Missing parent message #{parent} to reparent"
     group = pmsg.group
     root = pmsg.root ? parent
   else
@@ -579,14 +594,7 @@
       oldParent = null
   return if parent == oldParent == null  ## no-op, root case
   if parent?
-    if position?
-      Messages.update parent,
-        $push: children:
-          $each: [child]
-          $position: position
-    else
-      Messages.update parent,
-        $push: children: child
+    _messageAddChild child, parent, position
   if root != cmsg.root
     Messages.update child,
       $set: root: root
@@ -712,24 +720,33 @@
     if message.published == true
       message.published = now
     message.deleted = false unless message.deleted?
-    ## Now handled by _messageParent
-    #if parent?
-    #  pmsg = Messages.findOne parent
-    #  message.root = pmsg.root ? parent
+    ## Speed up _messageParent by presetting root
+    if parent?
+      message.root = root._id
+    else
+      message.root = null
+      message.submessageCount = 0
+      message.submessageLastUpdate = now
+    ## Actual insertion
     id = Messages.insert message
+    ## Parenting
+    if parent?
+      #_messageParent id, parent, position, null  ## there's no old parent
+      _messageAddChild id, parent, position
+      ## Because we preset root, even _messageParent won't call this for us.
+      ## We already simulated the effect when message is its own root.
+      _submessagesChanged message.root
     ## Prepare for MessagesDiff
     delete message.creator
     delete message.created
     delete message.authors
     delete message.children
     delete message.root
+    delete message.submessageCount
+    delete message.submessageLastUpdate
     message.id = id
     diffid = MessagesDiff.insert message
     message._id = diffid
-    if parent?
-      _messageParent id, parent, position, null  ## there's no old parent
-    else
-      _submessagesChanged message
     notifyMessageUpdate message, true if Meteor.isServer  ## created = true
     id


:bug: variable name typo

:bug: variable name typo

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -199,7 +199,7 @@
   displayName: 'Toggle ~'
   hover: icon: ':toggle-case:', emoji: ':clap:'
   toggleCase: (char) ->
-    charLower = cahr.toLowerCase()
+    charLower = char.toLowerCase()
     if charLower is char
       char.toUpperCase()
     else


Rename docstr snippet to docstring

Rename docstr snippet to docstring

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -263,7 +263,7 @@
     'body': 'from ${1:module} import ${2:name}'

   'docstring':
-    'prefix': 'docstr'
+    'prefix': 'docstring'
     'body': '\"\"\"$1\"\"\"$2'

   'if':


:fire: extraneous trim call

:fire: extraneous trim call

--- lib/renderer.coffee
+++ lib/renderer.coffee
@@ -119,7 +119,7 @@
     editor = editorElement.getModel()
     # remove the default selection of a line in each editor
     editor.getDecorations(class: 'cursor-line', type: 'line')[0].destroy()
-    editor.setText(codeBlock.textContent.trim())
+    editor.setText(codeBlock.textContent)
     if grammar = atom.grammars.grammarForScopeName(scopeForFenceName(fenceName))
       editor.setGrammar(grammar)


Make sure itemPath exists before passing it to path.dirname

--- lib/reporter.coffee
+++ lib/reporter.coffee
@@ -62,7 +62,7 @@
       name = item.getViewClass?().name ? item.constructor.name
       itemPath = item.getPath?()

-      return name unless path.dirname(itemPath) is atom.getConfigDirPath()
+      return name unless itemPath? and path.dirname(itemPath) is atom.getConfigDirPath()

       extension = path.extname(itemPath)
       switch path.basename(itemPath, extension)


fix typo

fix typo

--- lib/simulate.coffee
+++ lib/simulate.coffee
@@ -63,8 +63,8 @@
       dmg.push damage
       critical = []
       critical.push if kouku.api_ecl_flag[i] == 1 then HitType.Critical else if damage == 0 then HitType.Miss else HitType.Hit
+      enemyShip[i - 1].nowHP -= damage
       list.push new Attack AttackType[AttackTypeMap[0]], null, enemyShip[i - 1], enemyShip[i - 1].maxHP, enemyShip[i - 1].nowHP, dmg, critical
-      enemyShip[i - 1].nowHP -= damage
   if kouku.api_fdam?
     for damage, i in kouku.api_fdam
       continue if (kouku.api_fbak_flag[i] <= 0 && kouku.api_frai_flag[i] <= 0) || i == 0


:minibus: Message.linter -> Message.name

--- lib/validate.coffee
+++ lib/validate.coffee
@@ -60,6 +60,6 @@
         result.filePath = null
       result.range = Range.fromObject result.range if result.range?
       result.key = JSON.stringify(result)
-      result.linter = result.name or linter.name
+      result.name = result.name or linter.name
       Validate.messages(result.trace, linter) if result.trace and result.trace.length
     return


Fix layout when extending RouteController

- Not sure why this works

--- client/boot.coffee
+++ client/boot.coffee
@@ -12,7 +12,8 @@

   render: (router) ->
     router.render 'voltageBlank'
-    router.render 'voltagePage'
+    router.template = 'voltagePage'
+    router.render()

 @Voltage = Voltage


Extract common helpers.

Extract common helpers.

--- client/loot.coffee
+++ client/loot.coffee
@@ -57,11 +57,6 @@

 Template.item.helpers
   total_value: () -> this.value * this.quantity
-  is_magic: () -> this.type == "magic"
-  is_armor: () -> this.type == "armor"
-  is_weapon: () -> this.type == "weapon"
-  is_equipment: () -> this.type == "equipment"
-  is_treasure: () -> this.type == "treasure"

 Template.item.events
   "click .button_delete": (event) ->
@@ -72,10 +67,13 @@
     obj[property] = event.target.value
     Inventory.update this._id, $set: obj

-Template.item_short.helpers
+methods =
   is_magic: () -> this.type == "magic"
   is_armor: () -> this.type == "armor"
   is_weapon: () -> this.type == "weapon"
   is_equipment: () -> this.type == "equipment"
   is_treasure: () -> this.type == "treasure"

+for name, method of methods
+  Template.registerHelper name, method
+


routes in routes.coffee

routes in routes.coffee

--- client/main.coffee
+++ client/main.coffee
@@ -41,9 +41,3 @@
   #userEmail: (id) -> Meteor.users.findOne(id)?.emails?[0]?.address
   gravatar: () -> 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='

-
-Router.route '/', ->
-	this.render 'stream'
-
-Router.route '/profile', ->
-	this.render 'profile'


Fix Material Legend Position

Fix Material Legend Position

--- server/app/assets/javascript/material.coffee
+++ server/app/assets/javascript/material.coffee
@@ -5,11 +5,13 @@
   xaxis: { mode: 'time', timezone: 'browser' }
   yaxes: [{}, { alignTicksWithAxis: 1, position: 'right' }]
   selection: { mode: 'x' }
+  legend: { position: 'nw' }
 optionO =
   series: { lines: { show: true, lineWidth: 1 }, shadowSize: 0 }
   xaxis: { mode: 'time', timezone: 'browser' }
   yaxes: [{}, { alignTicksWithAxis: 1, position: 'right' }]
   selection: { mode: 'x' }
+  legend: { position: 'nw' }

 plot = null
 table = []


use webgl mode

use webgl mode

--- source/javascripts/all.coffee
+++ source/javascripts/all.coffee
@@ -13,6 +13,7 @@
     view = new google.maps.StreetViewPanorama this, {
       position: new google.maps.LatLng(lat, lon)
       pov: pov
+      mode: 'webgl'
       linksControl: false
       panControl: false
       zoomControlOptions: { style: google.maps.ZoomControlStyle.SMALL }


add subscribers method

add subscribers method

--- api/models/Group.coffee
+++ api/models/Group.coffee
@@ -59,6 +59,12 @@
 		isPrivate: ->
 			@type == 'Members-Only'

+		subscribers: ->
+			ret = _.uniq [@createdBy].concat(@moderators), 'id'
+			if @isPrivate()
+				ret = ret.concat @members
+			ret
+
 	afterCreate: (values, cb) ->
 		sails.services.roster.recipient null, values.jid
 			.then ->


fallback to include template from www/templates only

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -82,7 +82,7 @@
     .pipe gulp.dest './www/js/'

 gulp.task 'template', ->
-  gulp.src ['./lib/tagDirective/*.html', './www/templates/**/*.html']
+  gulp.src('./www/templates/**/*.html')
     .pipe(templateCache(root: 'templates', standalone: true))
     .pipe(gulp.dest('./www/js/'))


Fix media defines

Fix media defines

--- src/scripts/pages/media/media.coffee
+++ src/scripts/pages/media/media.coffee
@@ -6,8 +6,8 @@
   'cs!modules/header/header'
   'cs!modules/footer/footer'
   #'cs!modules/splash/splash'
-  'hbs!./content-template'
-  'less!./content'
+  'hbs!./media-template'
+  'less!./media'
 ], ($, _, Backbone, BaseView, HeaderView, FooterView, template) ->

   return class MediaView extends BaseView


Use node's require instead of internal require

--- spec/autocomplete-spec.coffee
+++ spec/autocomplete-spec.coffee
@@ -1,7 +1,7 @@
 $ = require 'jquery'
 AutocompleteView = require 'autocomplete/lib/autocomplete-view'
 Autocomplete = require 'autocomplete/lib/autocomplete'
-Buffer = require 'buffer'
+Buffer = require 'text-buffer'
 Editor = require 'editor'
 RootView = require 'root-view'


fix https://github.com/sagemathinc/smc/issues/1769 -- "sagews history syncstring: Cannot read property 'version' of undefined"

--- src/smc-webapp/editor_history.coffee
+++ src/smc-webapp/editor_history.coffee
@@ -250,6 +250,9 @@
         @process_view()

     set_doc_diff: (time0, time1) =>
+        if not @syncstring?
+            # nothing to do if syncstring isn't opened/initialized yet.
+            return
         # Set the doc to show a diff from time0 to time1
         v0 = @syncstring.version(time0).to_str()
         v1 = @syncstring.version(time1).to_str()


Pushing multiply_elements

Pushing multiply_elements

--- spec/multiply_elements.coffee
+++ spec/multiply_elements.coffee
@@ -1,28 +0,0 @@
-noflo = require 'noflo'
-
-exports.getComponent = ->
-  c = new noflo.Component
-
-  c.inPorts.add 'func', (event, payload) ->
-    return unless event is 'data'
-    # Do something with the packet, then
-    c.outPorts.out.send payload
-  c.inPorts.add 'type', (event, payload) ->
-    return unless event is 'data'
-    # Do something with the packet, then
-    c.outPorts.out.send payload
-  c.inPorts.add 'state', (event, payload) ->
-    return unless event is 'data'
-    # Do something with the packet, then
-    c.outPorts.out.send payload
-  c.inPorts.add 'multiplier', (event, payload) ->
-    return unless event is 'data'
-    # Do something with the packet, then
-    c.outPorts.out.send payload
-  c.inPorts.add 'in', (event, payload) ->
-    return unless event is 'data'
-    # Do something with the packet, then
-    c.outPorts.out.send payload
-  c.outPorts.add 'product'
-
-  c


Unfocus spec

Unfocus spec

--- spec/spellchecker-spec.coffee
+++ spec/spellchecker-spec.coffee
@@ -34,7 +34,7 @@
 spellIndex = null

 for testAlwaysUseHunspell in [true, false]
-  fdescribe 'SpellChecker', ->
+  describe 'SpellChecker', ->
     describe '.setDictionary', ->
       beforeEach ->
         @fixture = buildSpellChecker()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -458,5 +458,13 @@
   equalEnough piPink.b, 153
   equalEnough piPink.a, 1.0

+test "fromHue", ->
+  red = Color.fromHue(60)
+
+  equalEnough red.r, 255
+  equalEnough red.g, 0
+  equalEnough red.b, 0
+  equalEnough red.a, 1
+
 module()