feat(core): better checking for scrum activity

Ends scrum after 15 minutes of no answers. This commit also includes a
major bugfix where answers weren't being accessed/stored correctly.

Closes #6

--- src/model/scrum.coffee
+++ src/model/scrum.coffee
@@ -33,6 +33,8 @@
             3. Do you have any blocks
             4. Any tasks to add to the Sprint Backlog? (If applicable)
             5. Have you learned or decided anything new? (If applicable)"""
+
+        that._timeoutHandle = setTimeout(endScrum, 900000)

         # Add some extra info to the scrum log so its more identifiable
         now = new Date()
@@ -83,35 +85,29 @@
                 # match answer, plus overly cautious checking to make sure the bot isn't trying to infiltrate
                 if userid != process.env.HUBOT_NAME && displayname != process.env.HUBOT_NAME && message.match answerPattern
                     console.log '[hubot-scrumminator] Received answer from ' + userid
-                    that._recentMessage = true
+                    if that._timeoutHandle
+                        clearTimeout(that._timeoutHandle)
+                    that._timeoutHandle = setTimeout(endScrum, 900000)
                     num = answerPattern.exec(message)[1]
-                    that._scrumLog[userid].answers[num-1] = message
-                    # Check to see if all answers have been given by the user, thank them if they have
-                    if that._scrumLog[userid].answers.indexOf('') < 0
-                        that._robot.send
-                            room: that._room,
-                            'Thanks ' + displayname.split(' ')[0]
+                    for user in that._scrumLog.participants
+                        if user.name == userid
+                            user.answers[num-1] = message
+                            # Check to see if all answers have been given by the user, thank them if they have
+                            if user.answers.indexOf('') < 0
+                                that._robot.send
+                                    room: that._room,
+                                    'Thanks ' + displayname.split(' ')[0]

-        activityCheck = () ->
-            console.log '[hubot-scrumminator] Checking activity.....'
-            if !that._recentMessage
-                console.log '[hubot-scrumminator] Ending scrum in ' + that._room
-                that.checkCronJob.stop()
-                # Unsubscribe from the rooms message stream
-                gitter.rooms.join(that._room)
-                    .then (room) ->
-                        room.unsubscribe()
-                that._robot.brain.data._private.scrumminator.logs[that._id].push( that._scrumLog )
-                that._robot.brain.save
-                that._recentMessage = true
-                return
-            that._recentMessage = false
-            console.log '[hubot-scrumminator] Continuing scrum in ' + that._room
-
-        # Run activityCheck every 15 minutes
-        that.checkCronJob = new CronJob('0 */15 * * * *', activityCheck, null, true, null)
-
+        endScrum = () ->
+            console.log '[hubot-scrumminator] Ending scrum in ' + that._room
+            clearTimeout(that._timeoutHandle)
+            # Unsubscribe from the rooms message stream
+            gitter.rooms.join(that._room)
+                .then (room) ->
+                    room.unsubscribe()
+            that._robot.brain.data._private.scrumminator.logs[that._id].push( that._scrumLog )
+            that._robot.brain.save

     stopCronJob: ->
         this.cronJob.stop()
@@ -131,8 +127,4 @@
         this._id

-    getLog: ->
-        this._scrumLog
-
-
 module.exports = Scrum


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -2,22 +2,8 @@
 The TimedEvents module allows arbitrary code to be executed at set intervals.

-player = Core
-  x: 10
-  y: 50
-  width: 20
-  height: 20
-  other: "stuff"
-  more: "properties"
+player = GameObject

-player.position()
-# => Uncaught TypeError: Object has no method 'position'
-
-player.include(Bounded)
-
-# now player has all the methods provided by this module
-player.position()
-# => {x: 10, y: 50}

 @see GameObject


[Group Notification Email] added email notification flags

--- workers/social/lib/social/models/emailnotification.coffee
+++ workers/social/lib/social/models/emailnotification.coffee
@@ -72,6 +72,14 @@
       eventType          : ['Approved']
       contentTypes       : ['JGroup'],
       definition         : "when user's group membership has been approved"
+    groupJoined          :
+      eventType          : ['GroupJoined']
+      contentTypes       : ['JGroup'],
+      definition         : "when a member joins your group"
+    groupLeft            :
+      eventType          : ['GroupLeft']
+      contentTypes       : ['JGroup'],
+      definition         : "when a member leaves your group"

   @checkEmailChoice = (options, callback)->


fixed some indentations (no logic changed)

--- workers/social/lib/social/models/invitationrequest.coffee
+++ workers/social/lib/social/models/invitationrequest.coffee
@@ -135,37 +135,37 @@
         @approveInvitation client, callback

   approveRequest: (client, callback=->)->
-      JGroup = require './group'
-      JGroup.one { slug: @group }, (err, group)=>
-        if err then callback err
-        else unless group?
-          callback new KodingError "No group! #{@group}"
-        else
-          @fetchAccount (err, account)=>
+    JGroup = require './group'
+    JGroup.one { slug: @group }, (err, group)=>
+      if err then callback err
+      else unless group?
+        callback new KodingError "No group! #{@group}"
+      else
+        @fetchAccount (err, account)=>
+          return callback err if err
+          group.approveMember account, (err)=>
             return callback err if err
-            group.approveMember account, (err)=>
+            @update $set:{ status: 'approved' }, (err)=>
               return callback err if err
-              @update $set:{ status: 'approved' }, (err)=>
-                return callback err if err
-                @sendRequestApprovedNotification client, group, account, callback
+              @sendRequestApprovedNotification client, group, account, callback

   approveInvitation: (client, callback=->)->
-      JGroup      = require './group'
-      JInvitation = require './invitation'
-
-      JGroup.one { slug: @group }, (err, group)=>
-        if err then callback err
-        else unless group?
-          callback new KodingError "No group! #{@group}"
-        else
-          @update $set:{ status: 'sent' }, (err)=>
-            return callback err if err
-            if @koding?.username
-              @sendInviteMailToKodingUser client, @koding, group, callback
-            else
-              JInvitation.one {inviteeEmail: @email}, (err, invite)->
-                return callback err if err
-                JInvitation.sendEmailForInviteViaGroup client, invite, group, callback
+    JGroup      = require './group'
+    JInvitation = require './invitation'
+
+    JGroup.one { slug: @group }, (err, group)=>
+      if err then callback err
+      else unless group?
+        callback new KodingError "No group! #{@group}"
+      else
+        @update $set:{ status: 'sent' }, (err)=>
+          return callback err if err
+          if @koding?.username
+            @sendInviteMailToKodingUser client, @koding, group, callback
+          else
+            JInvitation.one {inviteeEmail: @email}, (err, invite)->
+              return callback err if err
+              JInvitation.sendEmailForInviteViaGroup client, invite, group, callback

   fetchDataForAcceptOrIgnore: (client, callback)->
     {delegate} = client.connection
@@ -273,34 +273,34 @@
       else
         cb = (actor, actorData)=>
           group.fetchAdmins (err, accounts)=>
-              if err then callback err
-
-              if invitationType is 'invitation'
-                event = 'InvitationRequested'
-              else
-                event = 'ApprovalRequested'
-
-              for account in accounts when account
-                data =
-                  actor             : actor
-                  receiver          : account
-                  event             : event
-                  contents          :
-                    subject         : ObjectRef(group).data
-                    actionType      : 'approvalRequest'
-                    actorType       : 'requester'
-                    approvalRequest : ObjectRef(@).data
-                    requester       : actorData
-
-                account.sendNotification 'GroupAccessRequested',
-                  actionType : 'groupAccessRequested'
-                  actorType  : 'requester'
-                  requester  : ObjectRef(actor).data
-                  subject    : ObjectRef(group).data
-
-                JMailNotification.create data, (err)->
-                  if err then callback new KodingError "Could not send"
-                  else callback null
+            if err then callback err
+
+            if invitationType is 'invitation'
+              event = 'InvitationRequested'
+            else
+              event = 'ApprovalRequested'
+
+            for account in accounts when account
+              data =
+                actor             : actor
+                receiver          : account
+                event             : event
+                contents          :
+                  subject         : ObjectRef(group).data
+                  actionType      : 'approvalRequest'
+                  actorType       : 'requester'
+                  approvalRequest : ObjectRef(@).data
+                  requester       : actorData
+
+              account.sendNotification 'GroupAccessRequested',
+                actionType : 'groupAccessRequested'
+                actorType  : 'requester'
+                requester  : ObjectRef(actor).data
+                subject    : ObjectRef(group).data
+
+              JMailNotification.create data, (err)->
+                if err then callback new KodingError "Could not send"
+                else callback null

         {delegate} = client.connection
         if delegate instanceof JAccount


guv-update-workerstats: Fix wrong key being used [major]

The tool was using the 'process' config key instead of 'processing'
This would have caused the default (or old value in file) being
used instead, leading to wrong scaling parameters.

--- src/updateconfig.coffee
+++ src/updateconfig.coffee
@@ -4,7 +4,7 @@
 updateRoleStats = (cfg, stats) ->
   if stats.average
     delete cfg.p if cfg.p
-    cfg.process = stats.average
+    cfg.processing = stats.average
     delete cfg.stddev if cfg.stddev # no longer valid
   if stats.stddev
     cfg.stddev = stats.stddev


changed mongodb collection name

changed mongodb collection name

* because I have to aggregate data newly.
* new data structure is not competible with old one.
* and the service are operating based on old code.

--- src/persistence.coffee
+++ src/persistence.coffee
@@ -15,7 +15,7 @@

 module.exports =
   open: (callback) ->
-    MongoClient.connect "mongodb://#{process.env['MONGODB_HOST']}:#{process.env['MONGODB_PORT']}/popular_convention", (err, db) ->
+    MongoClient.connect "mongodb://#{process.env['MONGODB_HOST']}:#{process.env['MONGODB_PORT']}/popular_convention2", (err, db) ->
       return callback(err) if err?
       dbserver = db
       worklogs = dbserver.collection 'worklogs'


Put cover for ghost clicks back.

Put cover for ghost clicks back.

--- app/client/js/react/gesture.coffee
+++ app/client/js/react/gesture.coffee
@@ -9,7 +9,7 @@
     TODO: Move to este-library. Check if tap can safely stop scroll momentum.

     Example:
-    {a,span,button} = touch.none 'a', 'span', 'button'
+    {a, span, button} = touch.none 'a', 'span', 'button'

     @param {este.events.RoutingClickHandler} routingClickHandler
     @constructor
@@ -17,11 +17,30 @@
   ###
   constructor: (@routingClickHandler) ->

+    ###*
+      Prevent "ghost click" phenomenon. Ghost clicks occur when an element
+      is removed before the 300ms delayed click event is fired, but the
+      click event is still sent. This click event will be sent to whatever
+      is now on top at that coordinate.
+      This PR is trying to fix it, but it does not work in iOS Chrome.
+      https://github.com/Polymer/polymer-gestures/pull/73
+      TODO: Wait for polymer-gestures fix.
+      @type {Element}
+      @private
+    ###
+    @cover_ = document.createElement 'div'
+    @cover_.style.cssText = 'position: fixed; top: 0; left: 0; background-color: #000; opacity: 0; width: 100%; height: 100%; z-index: 2147483647;'
+
   ###*
     Used for removing touch-hover before iOS callout is shown.
     @type {number}
   ###
   @CALLOUT_DELAY: 750
+
+  ###*
+    @type {number}
+  ###
+  @COVER_DELAY: 350

   ###*
     @param {...string} var_args
@@ -67,6 +86,7 @@
     @return {function(): React.ReactComponent}
   ###
   createTouchableComponent: (tag, touchAction) ->
+    cover = @cover_
     gestures = window['PolymerGestures']
     routingClickHandler = @routingClickHandler

@@ -116,8 +136,20 @@
       onTap: (e) ->
         @props.onTap e if @props.onTap
         if e.target.tagName == 'A'
+          # Routing can change whole UI, therefore we have to ignore
+          # subsequent click on any shown element to prevent accidental
+          # action like focus.
+          @showCoverTemporarily_()
           @delegateAnchorTapToRoutingClickHandler_ e
         return

+       showCoverTemporarily_: ->
+        # Seems to be more reliable then display block/none.
+        document.body.appendChild cover
+        clearTimeout @coverHideTimer
+        @coverHideTimer = setTimeout ->
+          document.body.removeChild cover
+        , Gesture.COVER_DELAY
+
       delegateAnchorTapToRoutingClickHandler_: (e) ->
         routingClickHandler.onCustomClick e


Cosmetic change.

Cosmetic change.

--- app/javascripts/controllers.coffee
+++ app/javascripts/controllers.coffee
@@ -1,6 +1,6 @@
 ctlrs = angular.module 'qiprofile.controllers', ['qiprofile.services']

-# The controller helper methods.
+# The local controller helper methods.
 ctlrs.factory 'ControllerHelper', ['$location', ($location) ->
   # Replaces the browser URL search parameters to include at most
   # a non-default project, since all other search parameters are
@@ -63,8 +63,7 @@
       else if $scope.modelingFormat == 'Table'
         $scope.modelingFormat ='Chart'
       else
-        throw "Modeling format is not recognized: " +
-          $scope.modelingFormat
+        throw "Modeling format is not recognized: #{ $scope.modelingFormat }"

     # The modeling format is 'Chart' if the subject has
     # more than one session, 'Table' otherwise.
@@ -114,7 +113,7 @@
     # Capture the current project.
     $rootScope.project = image.parent.session.subject.project
     # Place the image in the scope.
-    $scope.image = image
+    $scope.image = image
     # If the project is the default, then remove it from the URL.
     ControllerHelper.cleanBrowserUrl($rootScope.project)
 ]


Enlarge the help modal window.

Enlarge the help modal window.

--- app/javascripts/controllers.coffee
+++ app/javascripts/controllers.coffee
@@ -328,7 +328,7 @@
           $modal.open
             controller: 'RecurrenceScoreHelpModalCtrl'
             templateUrl: '/partials/recurrence-score-help.html'
-            size: 'sm'
+            size: 'med'
     ]


Start adding ordinal data support.

Start adding ordinal data support.

--- app/javascripts/correlation.coffee
+++ app/javascripts/correlation.coffee
@@ -1,4 +1,4 @@
-define ['angular', 'dc', 'lodash', 'crossfilter', 'd3', 'breast'], (ng, dc) ->
+define ['angular', 'dc', 'roman', 'lodash', 'crossfilter', 'd3', 'breast'], (ng, dc, roman) ->
   correlation = ng.module 'qiprofile.correlation', ['qiprofile.breast']

   correlation.factory 'Correlation', ['Breast', (Breast) ->
@@ -8,6 +8,8 @@
     #     * label - Appears in the dropdown picklists and as chart axis labels.
     #     * coll - The collection(s) for which the data type is valid. May be
     #         'all' or a list of specific collections.
+    #     * scale - If the data type has ordinal values (e.g. TNM stage) then
+    #         they are specified as an array, else null.
     #     * accessor - The data accessor.
     #
     #   The order of the data types and their handles must be exactly
@@ -72,11 +74,21 @@
           accessor: (tumor) ->
             return null unless tumor.extent?
             if tumor.extent.depth? then tumor.extent.depth else null
+      'breastTNMStage':
+          label: 'TNM Stage'
+          coll: [
+            'Breast'
+          ]
+          accessor: (tumor) ->
+            return null unless tumor.tnm?
+            stage = Breast.stage(tumor.tnm)
+            stage.replace(/^\d+/, roman.romanize)
       'recurrenceScore':
           label: 'Recurrence Score'
           coll: [
             'Breast'
           ]
+          scale: null
           accessor: (tumor) ->
             return null unless tumor.geneticExpression.normalizedAssay?
             Breast.recurrenceScore(tumor.geneticExpression.normalizedAssay)
@@ -186,12 +198,25 @@

     # The imaging data types.
     IMAGING_DATA_TYPES = [
-      "fxlKTrans"
-      "fxrKTrans"
-      "deltaKTrans"
-      "vE"
-      "tauI"
+      'fxlKTrans'
+      'fxrKTrans'
+      'deltaKTrans'
+      'vE'
+      'tauI'
     ]
+
+    # All possible values for ordinal data types.
+    ORDINAL_SCALES =
+      'breastTNMStage':
+        [
+         'IA'
+         'IIA'
+         'IIB'
+         'IIIA'
+         'IIIB'
+         'IIIC'
+         'IV'
+        ]

     # The chart layout parameters.
     CHART_LAYOUT_PARAMS =
@@ -231,7 +256,7 @@
           }
           {
             x: 'necrosisPercent'
-            y: 'vE'
+            y: 'vE'
           }
           {
             x: 'necrosisPercent'
@@ -339,31 +364,39 @@
       scales = new Object
       # Iterate over the valid data types.
       for key of choices
-        # Make a list of all values for the data type and obtain the max
-        #   and min values.
-        allValues = (dat[key] for dat in data)
-        max = _.max allValues
-        min = _.min allValues
-        diff = max - min
-        # If the values are not all the same, calculate a padding value based
-        #   the chart layout parameter setting, e.g. a setting of .2 will
-        #   give the chart 20% padding.
-        # If the values are all the same, calculate a padding value of an
-        #   appropriate resolution for that value. The initial "result" value
-        #   reflects the number of digits or decimal places of the scatterplot
-        #   values. Each chart tick mark above and below that value is then set
-        #   to 10 to the power of the result reduced by 1.
-        if diff != 0
-          pad = diff * CHART_LAYOUT_PARAMS.axisPadding
+        # If the data type has ordinal values, return the value array.
+        #   Otherwise, make a list of all values for the data type and obtain
+        #   the max and min values.
+        if key in Object.keys ORDINAL_SCALES
+          scales[key] =
+            range: ORDINAL_SCALES[key]
+            padding: null
+            isOrdinal: true
         else
-          max = Math.abs(max)
-          result = Math.ceil(Math.log(max) / Math.log(10))
-          if Math.abs(result) is Infinity then result = 0
-          pad = CHART_LAYOUT_PARAMS.ticks / 2 * Math.pow(10, result - 1)
-        # Add the range and padding for the data type to the scales object.
-        scales[key] =
-          range: [min, max]
-          padding: pad
+          allValues = (dat[key] for dat in data)
+          max = _.max allValues
+          min = _.min allValues
+          diff = max - min
+          # If the values are not all the same, calculate a padding value based
+          #   the chart layout parameter setting, e.g. a setting of .2 will
+          #   give the chart 20% padding.
+          # If the values are all the same, calculate a padding value of an
+          #   appropriate resolution for that value. The initial "result" value
+          #   reflects the number of digits or decimal places of the scatterplot
+          #   values. Each chart tick mark above and below that value is then set
+          #   to 10 to the power of the result reduced by 1.
+          if diff != 0
+            pad = diff * CHART_LAYOUT_PARAMS.axisPadding
+          else
+            max = Math.abs(max)
+            result = Math.ceil(Math.log(max) / Math.log(10))
+            if Math.abs(result) is Infinity then result = 0
+            pad = CHART_LAYOUT_PARAMS.ticks / 2 * Math.pow(10, result - 1)
+          # Add the range and padding for the data type to the scales object.
+          scales[key] =
+            range: [min, max]
+            padding: pad
+            isOrdinal: false
       # Return the scales object.
       scales

@@ -407,6 +440,7 @@
             d.tumorLength
             d.tumorWidth
             d.tumorDepth
+            d.breastTNMStage
             d.recurrenceScore
             d.ki67
             d.gstm1
@@ -428,14 +462,9 @@
         # The chart configuration.
         chart.width(CHART_LAYOUT_PARAMS.width)
           .height(CHART_LAYOUT_PARAMS.height)
-          .x(d3.scale.linear().domain(scales[xAxis].range))
-          .y(d3.scale.linear().domain(scales[yAxis].range))
-          .elasticX(true)
           .elasticY(true)
           .xAxisLabel(LABELS[xAxis])
           .yAxisLabel(LABELS[yAxis])
-          .xAxisPadding(scales[xAxis].padding)
-          .yAxisPadding(scales[yAxis].padding)
           .renderVerticalGridLines(true)
           .renderHorizontalGridLines(true)
           .symbolSize(CHART_LAYOUT_PARAMS.symbolSize)
@@ -447,6 +476,21 @@
         chart.yAxis().ticks(CHART_LAYOUT_PARAMS.ticks)
         chart.margins().left += CHART_LAYOUT_PARAMS.leftMargin

+        # Special configuration for X-axis ordinal scales.
+        #
+        # TODO - Ordinal scales are not displaying correctly, need to fix.
+        if scales[xAxis].isOrdinal
+          chart.x(d3.scale.ordinal().domain(scales[xAxis].range))
+            .xUnits(dc.units.ordinal())
+            .elasticX(false)
+        else
+          chart.x(d3.scale.linear().domain(scales[xAxis].range))
+            .xAxisPadding(scales[xAxis].padding)
+            .elasticX(true)
+
+        chart.y(d3.scale.linear().domain(scales[yAxis].range))
+        chart.yAxisPadding(scales[yAxis].padding)
+
       # Render all the charts.
       dc.renderAll()


fixes nm calculation

fixes nm calculation

--- app/js/features/track/track.coffee
+++ app/js/features/track/track.coffee
@@ -63,7 +63,7 @@
     calculateCurrentDistance: ->
       reference = _.last(@waypoints)
       @distanceKm = @totalDistanceKm + Track.calculateDistanceKm(reference.lat, reference.long,  @lat, @long)
-      @distanceNm = @totalDistanceNm + Track.convertKmtoNm(@distanceKm)
+      @distanceNm = Track.convertKmtoNm(@distanceKm)

     calculateTotalDistance: ->
       distance = 0


Update mini-course-content.coffee

Update mini-course-content.coffee

--- app/lib/mini-course-content.coffee
+++ app/lib/mini-course-content.coffee
@@ -43,7 +43,7 @@
       figure_credits: "Image credit: NASA"
   }
   {
-    course_number: 4
+    course_number: 5
     material:
       title: "What can we learn from a lightcurve?"
       text: """
@@ -60,7 +60,7 @@
   }
   {

-    course_number: 5
+    course_number: 6
     material:
       title: "False-positives - Variable stars"
       text: """
@@ -74,7 +74,7 @@

   {

-    course_number: 6
+    course_number: 7
     material:
       title: "False-positives - Eclipsing binary stars"
       text: """
@@ -87,7 +87,7 @@

   {

-    course_number: 7
+    course_number: 8
     material:
       title: "False-positives - Eclipsing binary stars"
       text: """
@@ -100,7 +100,7 @@
   }

   {
-    course_number: 8
+    course_number: 9
     material:
       title: "Other features in the lightcurves - flares"
       text: """
@@ -112,7 +112,7 @@
   }

   {
-    course_number: 9
+    course_number: 10
     material:
       title: "A brief history of Exoplanets"
       text: """
@@ -124,7 +124,7 @@
   }

   {
-    course_number: 10
+    course_number: 11
     material:
       title: "The first Planet Hunters planet"
       text: """
@@ -136,7 +136,7 @@
   }

   {
-    course_number: 11
+    course_number: 12
     material:
       title: "Planet Hunters 2b"
       text: """
@@ -148,7 +148,7 @@
   }

   {
-    course_number: 12
+    course_number: 13
     material:
       title: "Kepler-90"
       text: """
@@ -160,7 +160,7 @@
   }

   {
-    course_number: 13
+    course_number: 14
     material:
       title: "Star vs Planet"
       text: """
@@ -172,7 +172,7 @@
   }

   {
-    course_number: 14
+    course_number: 15
     material:
       title: "Types of stars - main sequence"
       text: """
@@ -184,7 +184,7 @@
   }

   {
-    course_number: 15
+    course_number: 16
     material:
       title: "The Habitable Zone"
       text: """
@@ -196,7 +196,7 @@
   }

   {
-    course_number: 16
+    course_number: 17
     material:
       title: "How else can we discover planets? - Radial Velocity"
       text: """
@@ -210,7 +210,7 @@
   }

   {
-    course_number: 17
+    course_number: 18
     material:
       title: "How else can we discover planets? - Radial Velocity"
       text: """
@@ -222,7 +222,7 @@
   }

   {
-    course_number: 18
+    course_number: 19
     material:
       title: "How else can we discover planets? - Gravitational Microlensing"
       text: """
@@ -234,7 +234,7 @@
   }

   {
-    course_number: 19
+    course_number: 20
     material:
       title: "How else can we discover planets? - Pulsar Timing"
       text: """
@@ -246,7 +246,7 @@
   }

   {
-    course_number: 20
+    course_number: 21
     material:
       title: "How else can we discover planets? - Transit Timing Variations"
       text: """
@@ -260,7 +260,7 @@
   }

   {
-    course_number: 21
+    course_number: 22
     material:
       title: "How else can we discover planets? - Astrometry"
       text: """
@@ -272,7 +272,7 @@
   }

   {
-    course_number: 22
+    course_number: 23
     material:
       title: "How else can we discover planets? - Direct Imaging"
       text: """
@@ -285,7 +285,7 @@
   }

   {
-    course_number: 23
+    course_number: 24
     material:
       title: "Hot Jupiters"
       text: """
@@ -297,7 +297,7 @@
   }

   {
-    course_number: 24
+    course_number: 25
     material:
       title: "Hot Jupiters - Migration"
       text: """
@@ -309,7 +309,7 @@
   }

   {
-    course_number: 25
+    course_number: 26
     material:
       title: "Congratulations!"
       text: """


Add remove link button to project links editor

--- app/pages/lab/project-details.cjsx
+++ app/pages/lab/project-details.cjsx
@@ -33,6 +33,7 @@

               <input type="text" name="urls.#{i}.label" value={link.label} onChange={@handleChange}/>
               <input type="text" name="urls.#{i}.url" value={link.url} onChange={@handleChange}/>
+              <button type="button" onClick={@handleRemoveLink.bind this, link._key}></button>

}

@@ -45,6 +46,11 @@
     changes["urls.#{@props.project.urls.length}"] =
       label: 'Example'
       url: 'https://example.com/'
+    @props.project.update changes
+
+  handleRemoveLink: (linkKey) ->
+    changes = {}
+    changes['urls'] = @props.project.urls.filter((link) -> link._key != linkKey)
     @props.project.update changes

 module.exports = React.createClass


Add test cases: Marked bug involving single quotes & html block tags

--- test/blockquote.coffee
+++ test/blockquote.coffee
@@ -161,4 +161,63 @@
       A paragraph.
       """

+  # Bug reported by Rangi, 16 Dec 2015: the Marked engine is escaping HTML
+  # entities inside of HTML block tags. It uses numeric entities, which MeFi
+  # then strips out.
+  QUnit.test 'single quotes in a markdown blockquote', (test) ->
+    testOneMarkdownText test,
+      """
+      > I'm happy
+      """,
+      """
+

+      I'm happy

+      """
+
+  # Fails with marked
+  # QUnit.test 'single quotes in an html blockquote', (test) ->
+  #   testOneMarkdownText test,
+  #     """
+  #

+  #     I'm happy
+  #

+  #     """,
+  #     """
+  #

+  #     I'm happy
+  #

+  #     """
+
+  QUnit.test 'double quotes in an html blockquote', (test) ->
+    testOneMarkdownText test,
+      """
+

+      "I am," I said.
+

+      """,
+      """
+

+      "I am," I said.
+

+      """
+
+  QUnit.test 'double quotes in a single-line html blockquote', (test) ->
+    testOneMarkdownText test,
+      """
"I am," I said.
""",
+      """
"I am," I said.
"""
+
+  # Marked: Converts to named entity. OK, but I'd rather it give the
+  # raw ampersand, for cases where we fall back to plain HTML.
+  QUnit.test 'ampersand in an html blockquote', (test) ->
+    testOneMarkdownText test,
+      """
+

+      Pork & beans
+

+      """,
+      """
+

+      Pork & beans
+

+      """
 )()


interact: fix given more structured output area

--- salvus/page/syncdoc.coffee
+++ salvus/page/syncdoc.coffee
@@ -1440,7 +1440,9 @@
                                         output.removeClass('sagews-output-hide')
                                         try
                                             #t = misc.mswalltime()
-                                            @process_output_mesg(mesg:JSON.parse(s), element:output)
+                                            @process_output_mesg
+                                                mesg:JSON.parse(s)
+                                                element:output.find("span:first")
                                             #console.log("time to parse = ", misc.mswalltime(t))
                                             mark.processed += 1 + s.length
                                         catch e
@@ -1675,7 +1677,7 @@
             element : required
             mark     : undefined
         mesg = opts.mesg
-        output = opts.element.find("span:first")
+        output = opts.element
         # mesg = object
         # output = jQuery wrapped element

@@ -1780,7 +1782,7 @@
                                         delete mesg.done
                                         @process_output_mesg
                                             mesg    : mesg
-                                            element : output
+                                            element : output.find("span:first")

                             for event, function_name of mesg.events
                                 img.data("salvus-events-#{event}", function_name)


ComputeUtils: reviveCredential: return credential's publicKey instead itself

--- workers/social/lib/social/models/computeproviders/computeutils.coffee
+++ workers/social/lib/social/models/computeproviders/computeutils.coffee
@@ -37,7 +37,7 @@
     return callback null

   if credential.bongo_?.constructorName is 'JCredential'
-    callback null, credential
+    callback null, credential.publicKey
   else
     JCredential = require './credential'
     JCredential.fetchByPublicKey client, credential, callback


Update file names

Update file names

--- test/linkedlist.coffee
+++ test/linkedlist.coffee
@@ -1,4 +1,4 @@
-Llist = require('../src/main').LinkedList
+Llist = require('../kran').LinkedList

 describe 'linked list', ->
   list = null


Don't send heartbeat while reconnecting

Don't send heartbeat while reconnecting


Oscillator

Oscillator


:art: WIP preparation for visiting next/prev with keybind.

--- lib/motion.coffee
+++ lib/motion.coffee
@@ -613,51 +613,48 @@
   moveCursor: (cursor) ->
     @ranges = @scan(cursor)
     if @ranges.length is 0
-      # @restoreEditorState?()
       unless @isComplete()
         @destroyMarkers()
-        @vimState.flasher.reset()
-        @flashScreen()
+        @flash(@getVisibleBufferRange(), timeout: 100)
         @vimState.hoverSearchCounter.reset()
       atom.beep()
       return
-
-    @current = @ranges[@getCount() % @ranges.length]
+    @index = @getCount() % @ranges.length
+    @visit(index, cursor)
+
+  visit: (index, cursor) ->
+    @current = @ranges[index]
     point = @current.start
-    @editor.scrollToBufferPosition(point, center: true)
-    if @editor.isFoldedAtBufferRow(point.row)
-      @editor.unfoldBufferRow point
-
-    @flash(@current) if settings.get('flashOnSearch')
-
-    # dont do acutual move in temporal visit in inclemental search.
     if @isComplete()
       cursor.setBufferPosition(point, center: true)
       @vimState.searchHistory.save(@input)
     else
-      @destroyMarkers()
-      @decorateVisibleRanges(@ranges, {@current})
-
-    if settings.get('enableHoverSearchCounter')
-      counter = @getCounter(@current, @ranges)
-      timeout =
-        if @isComplete()
-          switch
-            when @vimState.hoverSearchCounter.isVisibleAtPoint(point)
-              0
-            else
-              settings.get('searchCounterHoverDuration')
-        else
-          null
-      @vimState.hoverSearchCounter.addWithTimeout counter, point, timeout
-
-  decorateVisibleRanges: (ranges, {current}) ->
+      # dont do acutual move in temporal visit in inclemental search.
+      @showMatched()
+      @editor.scrollToBufferPosition(point, center: true)
+      if @editor.isFoldedAtBufferRow(point.row)
+        @editor.unfoldBufferRow point
+
+    @flash(@current) if settings.get('flashOnSearch')
+    @showHover(piont, @getCounter()) if settings.get('enableHoverSearchCounter')
+
+  showHover: (point, content) ->
+    timeout = null
+    if @isComplete()
+      timeout = switch
+        when @vimState.hoverSearchCounter.isVisibleAtPoint(point) then 0
+        else settings.get('searchCounterHoverDuration')
+    @vimState.hoverSearchCounter.addWithTimeout content, point, timeout
+
+  showMatched: ->
+    @destroyMarkers()
+    @markers = []
     visibleRange = @getVisibleBufferRange()
-    rangesToRender = (r for r in ranges when visibleRange.containsRange(r))
+    rangesToRender = (r for r in @ranges when visibleRange.containsRange(r))
     for r in rangesToRender
       klass = 'vim-mode-search-match'
-      klass += ' current' if r.isEqual(current)
-      @decorateRange r, class: klass
+      klass += ' current' if r.isEqual(@current)
+      @markers.push(@decorate r, class: klass)

   getVisibleBufferRange: ->
     [startRow, endRow] = @editor.getVisibleRowRange().map (row) =>
@@ -668,19 +665,19 @@
     m.destroy() for m in @markers ? []
     @markers = null

-  decorateRange: (range, {class: klass}) ->
+  decorate: (range, {class: klass}) ->
     marker = @editor.markBufferRange range,
       invalidate: 'never'
       persistent: false
-    (@markers ?= []).push marker
     @editor.decorateMarker marker,
       type: 'highlight'
       class: klass
-
-  getCounter: (range, ranges) ->
-    rangeSorted = ranges.slice().sort (a, b) -> a.compare(b)
-    current = rangeSorted.indexOf(range) + 1
-    "#{current}/#{ranges.length}"
+    marker
+
+  getCounter: ->
+    rangeSorted = @ranges.slice().sort (a, b) -> a.compare(b)
+    currentIndex = rangeSorted.indexOf(@current) + 1
+    "#{currentIndex}/#{@ranges.length}"

   scan: (cursor) ->
     return [] if @input is ""
@@ -721,24 +718,17 @@
   @extend()
   constructor: ->
     super
-    isearchEnabled = settings.get('enableIncrementalSearch')
-
-    @vimState.search.readInput {@backwards},
-      onDidConfirm: @onDidConfirm.bind(this)
-      onDidCancel:  @onDidCancel.bind(this)
-      onDidChange:  if isearchEnabled then @onDidChange.bind(this) else null
-
-    if isearchEnabled
+
+    @vimState.search.readInput {@backwards}, {@onDidConfirm, @onDidCancel, @onDidChange}
+    if settings.get('enableIncrementalSearch')
       @restoreEditorState = saveEditorState(@editor)
       @subscriptions = new CompositeDisposable
-      @subscriptions.add @editor.onDidChangeScrollTop =>
-        @decorateVisibleRanges(@ranges, {@current})
-      @subscriptions.add @editor.onDidChangeScrollLeft =>
-        @decorateVisibleRanges(@ranges, {@current})
-
-  onDidConfirm: (input) =>
-    repeatChar = if @backwards then '?' else '/'
-    if (input is '') or (input is repeatChar)
+      @subscriptions.add @editor.onDidChangeScrollTop => @showMatched()
+      @subscriptions.add @editor.onDidChangeScrollLeft => @showMatched()
+
+  onDidConfirm: (input) => # fat-arrow
+    latestSeachChars = ['', (if @isBackwards() then '?' else '/')]
+    if input in latestSeachChars
       input = @vimState.searchHistory.get('prev')
       atom.beep() if input is ''
     @input = input
@@ -748,23 +738,21 @@
     @subscriptions = null
     @destroyMarkers()

-  onDidCancel: =>
+  onDidCancel: => # fat-arrow
     unless @vimState.isMode('visual') or @vimState.isMode('insert')
       @vimState.activate('reset')
     @destroyMarkers()
-    @vimState.flasher.reset()
+    # @vimState.flasher.reset()
     @vimState.hoverSearchCounter.reset()
     @vimState.reset()
     @subscriptions?.dispose()
     @subscriptions = null
     @restoreEditorState?()

-  onDidChange: (@input) =>
+  onDidChange: (@input) => # fat-arrow
+    return unless settings.get('enableIncrementalSearch')
     for c in @editor.getCursors()
       @moveCursor(c)
-
-  flashScreen: ->
-    @flash @getVisibleBufferRange(), timeout: 100

 class SearchBackwards extends Search
   @extend()


Troubleshoot: whenever al lmongo models are loaded "modelsReady" event is emitted

--- client/Main/mq.config.coffee
+++ client/Main/mq.config.coffee
@@ -53,7 +53,9 @@
                 models.push model
               queue.fin()

-        dash queue, -> callback err, models, name
+        dash queue, =>
+          @emit "modelsReady"
+          callback err, models, name

   mq: do ->
     {authExchange} = KD.config


Simplified french wording

Simplified french wording

--- client/app/locales/fr.coffee
+++ client/app/locales/fr.coffee
@@ -5,8 +5,8 @@
     "all tasks": "Toutes les tâches"
     "all archived tasks": "Toutes les tâches archivées"
     "untagged tasks": "Tâches sans étiquettes"
-    "tasks of": "Tâches correspondant à l'étiquette|||| Tâches correspondant aux étiquettes"
-    "archived tasks of": "Tâches archivées correspondant à l'étiquette %{tagsList} |||| Tâches archivées correspondant aux étiquettes %{tagsList}"
+    "tasks of": "Tâches de"
+    "archived tasks of": "Tâches archivées de %{tagsList}"
     "and": "et"
     "archived date format": "{dd}/{MM}/{yyyy} à {HH}h{mm}"
     "actions headline": "Actions"


improve preset email translate

improve preset email translate

--- client/app/locales/fr.coffee
+++ client/app/locales/fr.coffee
@@ -15,7 +15,7 @@
     'step email settings': "configurer votre email"

     'preset email':              "email"
-    'preset email tip':          "Cet email sera utilisé pour vous adresser un message lors d'une restauration de mot de passe."
+    'preset email tip':          "Cette adresse email sera utilisée pour vous envoyer un message lors d'une récupération de mot de passe."
     'preset name':               "nom"
     'preset name tip':           "Le nom sous lequel vous apparaitrez dans les messages de partages et les invitations calendrier."
     'preset password':           "mot de passe"


Routes: Minor styling.

Routes: Minor styling.

--- client/ide/lib/routes.coffee
+++ client/ide/lib/routes.coffee
@@ -11,8 +11,7 @@

 selectWorkspaceOnSidebar = (data) ->

-  mainView = kd.getSingleton 'mainView'
-  mainView.activitySidebar.selectWorkspace data
+  kd.getSingleton('mainView').activitySidebar.selectWorkspace data

 getLatestWorkspace = ->
@@ -89,7 +88,7 @@
   if obj.machine
     routeToMachineWorkspace obj.machine
   else
-    router.handleRoute "/IDE/koding-vm-0/my-workspace"
+    router.handleRoute '/IDE/koding-vm-0/my-workspace'

 routeToMachineWorkspace = (machine) ->


New methods

New methods

added .prev() and .next() methods

--- src/quo.element.coffee
+++ src/quo.element.coffee
@@ -43,3 +43,12 @@

     $$.fn.remove = ->
         @each -> @parentNode.removeChild this  if @parentNode?
+
+    $$.fn.next = -> _getSibling.call @, "nextSibling"
+
+    $$.fn.prev = -> _getSibling.call @, "previousSibling"
+
+    _getSibling = (command) ->
+      element = @[0][command]
+      element = element[command] while element and element.nodeType isnt 1
+      $$ element


New methods


Started to implement proper dispatch and conversions for number.coffee

--- lib/number.coffee
+++ lib/number.coffee
@@ -61,8 +61,67 @@
 asNum = (n) ->
   if Math.abs(n) < BASE then new CheckedInt n else LongInt.fromNative n

-digits = (m) ->
-  if m then seq.conj m % BASE, -> digits Math.floor(m / BASE) else null
+# ----
+
+# The `NumberBase` class implements dispatching on binary arithmetic
+# operators.
+
+class NumberBase
+  makeNum = (n) ->
+    if n instanceof NumberBase
+      n
+    else if typeof n == 'number'
+      if Math.floor(n) == n
+        if Math.abs(n) < BASE
+          new CheckedInt n
+        else
+          LongInt.fromNative n
+      else
+        throw new Error "expected an integer, got #{n}"
+    else
+      throw new Error "expected a number, got #{n}"
+
+  upcast = (a, b) ->
+    [op1, op2] = [makeNum(a), makeNum(b)]
+
+    out =
+      switch op1.constructor
+        when LongInt
+          switch op2.constructor
+            when CheckedInt
+              [op1, LongInt.fromNative(op2.val)]
+            when LongInt
+              [op1, op2]
+        when CheckedInt
+          switch op2.constructor
+            when CheckedInt
+              [op1, op2]
+            when LongInt
+              [LongInt.fromNative(op1.val), op2]
+
+    if out
+      out
+    else
+      [tp1, tp2] = [op1.constructor, op2.constructor]
+      throw new Error "operands of types #{tp1} and #{tp2} not supported"
+
+  downcast = (x) ->
+    if x instanceof LongInt and x.cmp(BASE) < 0
+      new CheckedInt x.digits.first() * x.sign
+    else
+      x
+
+  operator = (name, f) ->
+    NumberBase[name]   = (args...) -> f.call NumberBase, args...
+    NumberBase::[name] = (args...) -> NumberBase[name] this, args...
+
+  for name in ['cmp', 'plus', 'minus', 'times', 'div', 'mod', 'gcd']
+    do (name) ->
+      namex = "#{name}__"
+      operator name, (a, b) ->
+        [x, y] = upcast a, b
+        downcast x[namex] y
+

 # ----

@@ -70,8 +129,17 @@
 # other words, if the result cannot be represented with full precision as a
 # Javascript number, a `LongInt` is produced instead.

-class CheckedInt
-  getval = (x) -> if x instanceof CheckedInt then x.val else x
+class CheckedInt extends NumberBase
+  getval = (x) ->
+    if x instanceof CheckedInt
+      x.val
+    else if typeof x == 'number'
+      if Math.floor(x) == x
+        x
+      else
+        throw new Error "expected an integer, got #{n}"
+    else
+      throw new Error "expected a number, got #{n}"

   constructor: (@val = 0) ->

@@ -83,16 +151,15 @@

   sqrt: -> new CheckedInt Math.floor Math.sqrt @val

-  cmp: (other) ->
-    x = getval other
-    if @val < x then -1 else if @val > x then 1 else 0
-
-  plus: (other) -> asNum @val + getval other
-
-  minus: (other) -> asNum @val - getval other
-
-  times: (other) ->
-    x = @val * getval other
+  cmp__: (other) ->
+    if @val < other.val then -1 else if @val > other.val then 1 else 0
+
+  plus__: (other) -> asNum @val + other.val
+
+  minus__: (other) -> asNum @val - other.val
+
+  times__: (other) ->
+    x = @val * other.val
     if Math.abs(x) < BASE
       new CheckedInt x
     else
@@ -114,10 +181,96 @@

 # Here are the beginnings of the `LongInt` class.

-class LongInt
+class LongInt extends NumberBase
   constructor: (@sign = 0, @digits = null) ->

-  times: (other) -> throw new Error "not yet implemented"
+  neg: -> new LongInt -@sign, @digits
+
+  abs: -> new LongInt 1, @digits
+
+  sgn: -> @sign
+
+  cmp = (r, s) ->
+    seq.sub(r, s)?.reverse()?.dropWhile((x) -> x == 0)?.first() or 0
+
+  cmp__: (x) ->
+    if @sign == 0
+      -x.sign
+    else if x.sign == 0
+      @sign
+    else if @sign != x.sign
+      @sign
+    else
+      @sign * cmp @digits, x.digits
+
+  ZERO = seq [0]
+  ONE  = seq [1]
+  TWO  = seq [2]
+
+  add = (r, s, c = 0) ->
+    if c or (r and s)
+      [r_, s_] = [r or ZERO, s or ZERO]
+      x = r_.first() + s_.first() + c
+      [digit, carry] = if x >= BASE then [x - BASE, 1] else [x, 0]
+      seq.conj(digit, -> add(r_.rest(), s_.rest(), carry))
+    else
+      s or r
+
+  plus__: (x) ->
+    if @sign != x.sign
+      @minus x.neg()
+    else
+      new LongInt @sign, add @digits, x.digits
+
+  sub = (r, s) ->
+    step = (r, s, b = 0) ->
+      if b or (r and s)
+        [r_, s_] = [r or ZERO, s or ZERO]
+        x = r_.first() - s_.first() - b
+        [digit, borrow] = if x < 0 then [x + BASE, 1] else [x, 0]
+        seq.conj(digit, -> step(r_.rest(), s_.rest(), borrow))
+      else
+        s or r
+    step r, s
+
+  minus__: (x) ->
+    if @sign != x.sign
+      @plus x.neg()
+    else if cmp(@digits, x.digits) < 0
+      new LongInt -@sign, sub x.digits, @digits
+    else
+      new LongInt @sign, sub @digits, x.digits
+
+  split = (n) -> [n % HALFBASE, Math.floor n / HALFBASE]
+
+  digitTimesDigit = (a, b) ->
+    if b < BASE / a
+      [a * b, 0]
+    else
+      [a0, a1] = split a
+      [b0, b1] = split b
+      [m0, m1] = split a0 * b1 + b0 * a1
+
+      tmp = a0 * b0 + m0 * HALFBASE
+      [lo, carry] = if tmp < BASE then [tmp, 0] else [tmp - BASE, 1]
+      [lo, a1 * b1 + m1 + carry]
+
+  seqTimesDigit = (s, d, c = 0) ->
+    if c or s
+      s_ = s or ZERO
+      [lo, hi] = digitTimesDigit(d, s_.first())
+      seq.conj(lo + c, -> seqTimesDigit(s_.rest(), d, hi))
+
+  mul = (a, b) ->
+    step = (r, a, b) ->
+      if a
+        t = add(r, seqTimesDigit(b, a.first())) or ZERO
+        seq.conj(t.first(), -> step(t.rest(), a.rest(), b))
+      else
+        r
+    step null, a, b
+
+  times__: (x) -> new LongInt @sign * x.sign, mul @digits, x.digits

   zeroes = BASE.toString()[1..]

@@ -126,15 +279,18 @@
     if parts
       sign = if @sign < 0 then '-' else ''
       rest = parts.rest()?.map (t) -> "#{zeroes[t.length..]}#{t}"
-      sign + parts.first() + rest.join ''
+      sign + parts.first() + if rest? then rest.join '' else ''
     else
       '0'
+
+  makeDigits = (m) ->
+    if m then seq.conj m % BASE, -> makeDigits Math.floor(m / BASE) else null

   @fromNative: (n) ->
     if n < 0
-      new LongInt -1, digits -n
+      new LongInt -1, makeDigits -n
     else if n > 0
-      new LongInt 1, digits n
+      new LongInt 1, makeDigits n
     else
       new LongInt 0, null

@@ -148,6 +304,11 @@
       number.fromNative n
     when 'string'
       number.parse n
+    else
+      if n instanceof NumberBase
+        n
+      else
+        throw new Error "expected a number, got #{n}"

 number.fromNative = (n) ->
   throw new Error "expected an integer, got #{n}" unless n == Math.floor n
@@ -215,7 +376,9 @@

   log ''
   show -> a = number Math.pow 2, 13
+  show -> LongInt.fromNative a.val
   show -> a.plus 2
+  show -> a.times 1
   show -> a.times 2
   show -> a.plus 2000

@@ -223,3 +386,13 @@
   show -> number -123456789000000
   show -> number '-1234'
   show -> number '-123456789000000'
+
+  log ''
+  show -> number(123456789).plus  876543211
+  show -> number(123456789).minus 123450000
+  show -> number(123456789).minus 123456790
+  show -> number(123456789).minus 123456789
+  show -> number(123456789).plus -123450000
+
+  log ''
+  show -> number(12345).times 100001


Using class method for loading params

Using class method for loading params

--- lib/oauth1.coffee
+++ lib/oauth1.coffee
@@ -208,9 +208,8 @@
 				callback null, result

 	request: (provider, parameters, req, callback) ->
-		params = {}
-		params[k] = v for k,v of provider.parameters
-		params[k] = v for k,v of provider.oauth1.parameters
+		@_setParams(provider.parameters)
+		@_setParams(provider.oauth1.parameters)

 		if ! parameters.oauthio.oauth_token || ! parameters.oauthio.oauth_token_secret
 			return callback new check.Error "You must provide 'oauth_token' and 'oauth_token_secret' in 'oauthio' http header"
@@ -227,13 +226,13 @@
 			if options.url[0] != '/'
 				options.url = '/' + options.url
 			options.url = oauthrequest.url + options.url
-		options.url = @_replace_param options.url, params, parameters.oauthio, parameters
+		options.url = @_replace_param options.url, @_params, parameters.oauthio, parameters

 		# build query
 		options.qs = {}
 		options.qs[name] = value for name, value of req.query
 		for name, value of oauthrequest.query
-			param = @_replace_param value, params, parameters.oauthio, parameters
+			param = @_replace_param value, @_params, parameters.oauthio, parameters
 			options.qs[name] = param if param

 		options.oauth =
@@ -249,7 +248,7 @@
 			'accept-language':req.headers['accept-language']
 			'content-type':req.headers['content-type']
 		for name, value of oauthrequest.headers
-			param = @_replace_param value, params, parameters.oauthio, parameters
+			param = @_replace_param value, @_params, parameters.oauthio, parameters
 			options.headers[name] = param if param

 		# build body


mobile routing

mobile routing

--- lib/routes.coffee
+++ lib/routes.coffee
@@ -44,6 +44,12 @@
     path: '/'
     fastRender: true
     data: -> page: 'home'
+    onBeforeAction: ->
+      ua = parseUseragent(window.navigator.userAgent)
+      if /ios|android/.test(ua.os.family)
+        Router.go('homeMobile');
+      else
+        @next()
     onAfterAction: ->
       seo('')


better method names

better method names

--- lib/scroll.coffee
+++ lib/scroll.coffee
@@ -10,13 +10,18 @@
     super(@editorView, @editor)

   execute: (count=1) ->
+    @keepCursorOnScreen(count)
+    @scrollUp(count)
+
+  keepCursorOnScreen: (count) ->
     {row, column} = @editor.getCursorBufferPosition()
-    lastRow = @editorView.getLastVisibleScreenRow() - @scrolloff
-    firstRow = @editorView.getFirstVisibleScreenRow() + @scrolloff
+    firstScreenRow = @editorView.getFirstVisibleScreenRow() + @scrolloff
+    if row - count <= firstScreenRow
+      @editor.setCursorBufferPosition([firstScreenRow + count, column])

-    if row - count <= firstRow
-      @editor.setCursorBufferPosition([firstRow + count, column])
-    @editorView.scrollToBufferPosition([lastRow + count, column])
+  scrollUp: (count) ->
+    lastScreenRow = @editorView.getLastVisibleScreenRow() - @scrolloff
+    @editorView.scrollToBufferPosition([lastScreenRow + count, 0])

 class ScrollUp extends Scroll
@@ -24,12 +29,18 @@
     super(@editorView, @editor)

   execute: (count=1) ->
+    @keepCursorOnScreen(count)
+    @scrollDown(count)
+
+  keepCursorOnScreen: (count) ->
     {row, column} = @editor.getCursorBufferPosition()
-    lastRow = @editorView.getLastVisibleScreenRow() - @scrolloff
-    firstRow = @editorView.getFirstVisibleScreenRow() + @scrolloff
+    lastScreenRow = @editorView.getLastVisibleScreenRow() - @scrolloff
+    if row + count >= lastScreenRow
+        @editor.setCursorBufferPosition([lastScreenRow - count, column])

-    if row + count >= lastRow
-      @editor.setCursorBufferPosition([lastRow - count, column])
-    @editorView.scrollToBufferPosition([firstRow - count, column])
+  scrollDown: (count) ->
+    firstScreenRow = @editorView.getFirstVisibleScreenRow() + @scrolloff
+    @editorView.scrollToBufferPosition([firstScreenRow - count, 0])
+

 module.exports = { ScrollDown, ScrollUp }


Factor out runRunUpdatesUntil Player spec helper.

--- tests/player.coffee
+++ tests/player.coffee
@@ -48,6 +48,17 @@
       player.update()
       return if stopAt is 'landing'

+    runRunUpdatesUntil = (stopAt, options) ->
+      if options?.backwards then player.cursors.left.isDown = yes
+      else player.cursors.right.isDown = yes
+
+      player.update()
+      return if stopAt is 'start'
+
+      endAnimation()
+      player.update()
+      return if stopAt is 'running'
+
     testStartAnimation = ->
       it 'will play start animation', ->
         expect(player.nextAction).toBe 'start'
@@ -67,15 +78,15 @@
         expect(player.nextState).toBeNull()

     describe 'when x cursor key is down in same direction', ->
-      beforeEach ->
-        player.cursors.right.isDown = yes
-        player.update()
+      it 'can get and set the right direction from #_xDirectionInput', ->
+        runRunUpdatesUntil 'start'

-      it 'can get and set the right direction from #_xDirectionInput', ->
         expect(player._xDirectionInput()).toBe Player.Direction.Right
         expect(player.nextDirection).toBe Player.Direction.Right

       describe 'when still', ->
+        beforeEach -> runRunUpdatesUntil 'start'
+
         it 'will begin to run', ->
           expect(player.state).toBe 'running'
           expect(player._beginRun).toHaveBeenCalled()
@@ -83,9 +94,7 @@
         testStartAnimation()

       describe 'when start animation finishes', ->
-        beforeEach ->
-          endAnimation()
-          player.update()
+        beforeEach -> runRunUpdatesUntil 'running'

         it 'will be fully running', ->
           expect(player._isFullyRunning()).toBe yes
@@ -96,15 +105,15 @@
           expect(player._isAnimationInterruptible()).toBe yes

     describe 'when x cursor key is down in opposite direction', ->
-      beforeEach ->
-        player.cursors.left.isDown = yes
-        player.update()
+      it 'can get and set the right direction from #_xDirectionInput', ->
+        runRunUpdatesUntil 'start', backwards: yes

-      it 'can get and set the right direction from #_xDirectionInput', ->
         expect(player._xDirectionInput()).toBe Player.Direction.Left
         expect(player.nextDirection).toBe Player.Direction.Left

       describe 'when still', ->
+        beforeEach -> runRunUpdatesUntil 'start', backwards: yes
+
         it 'will immediately (end) turn and begin to run', ->
           expect(player._isTurning).toBe yes
           expect(player.direction).toBe Player.Direction.Left
@@ -162,7 +171,7 @@

         it 'will play fall animation', ->
           expect(player._playAnimation).toHaveBeenCalledWith 31, no
-          expect(player._isAnimationInterruptible()).toBe yes
+          expect(player._isAnimationInterruptible()).toBe no

       describe 'when touching another object below', ->
         beforeEach -> runJumpUpdatesUntil 'landing'


Fix undefined tabView's parent panel

Fix undefined tabView's parent panel

--- lib/viewer.coffee
+++ lib/viewer.coffee
@@ -48,7 +48,7 @@
     if !@getUrl()
       return

-    if @tabView?
+    if @tabView? and atom.workspace.paneForItem(@tabView)?
       atom.workspace.paneForItem(@tabView).destroyItem(@tabView)
       @tabView = undefined
     if !@window? or @window.isDestroyed()
@@ -73,7 +73,7 @@
     if !@getUrl()
       return

-    if @tabView?
+    if @tabView? and atom.workspace.paneForItem(@tabView)?
       atom.workspace.paneForItem(@tabView).activateItem(@tabView)
     else
       @tabView = new PDFView(@url)


Remove logs

Remove logs

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -1983,7 +1983,6 @@
   [usedNames] = unzip compiledResults
   lifting = map (findDeps ctx), usedNames
   jointlyUsed = intersectSets lifting
-  console.log lifting

   branches = for [used, res], i in compiledResults
     lifted = lifting[i]
@@ -4321,7 +4320,6 @@
   else if type.TypeApp
     (kind type.op).to
   else
-    console.log "invalid", type
     throw new Error "Invalid type in kind"

 kindsEq = (k1, k2) ->


Wip on type classes implementation

Wip on type classes implementation

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -240,10 +240,15 @@
 labelClasses = (ast) ->
   macro 'class', ast, (node, args) ->
     node.type = 'class'
-    {params, wheres} = classDefinition node
+    {params, context, wheres} = classDefinition node
+    labelContext context
     labelWhere wheres
     labelParams node, params if params?
     node
+
+labelContext = (node) ->
+  for parent in inside node
+    parent.label = 'operator'

 labelInstances = (ast) ->
   macro 'instance', ast, (node, args) ->
@@ -1323,13 +1328,31 @@
 inferWheres = (context, pairs, nameIndex) ->
   # 1st find all classes

+  # classEnv = {}
+  # for [name, def] in pairs when def.type is 'class'
+  #   if classEnv[name]
+  #     throw new TypeError "Redefining class #{name}"
+  #   {context} = classDefinition def
+  #   classEnv[name] = [(tokens inside context), []]
+
+  # 1,5th find all instances
+
+  # for [name, def] in pairs when def.type is 'instance'
+  #   {klass} = instanceDefinition def
+  #   if not classEnv[name]
+  #     throw new TypeError "Missing class #{klass} for instance #{name}"
+  #   [supers, instances] = classEnv[klass]
+  #   instances.push  #TODO finish
+
+
+
   # 2nd find all declarations
-  for [name, def] in pairs
+  for [name, def] in pairs when not def.type
     [nameIndex, type] = freshOrDeclared def, nameIndex
     addToMap context, name.token, type

   # 3rd type check
-  for [name, def] in pairs
+  for [name, def] in pairs when def.type not in ['class', 'instance']
     [s1, nameIndex, def] = infer context, def, nameIndex
     s2 = unify (subExp s1, (inSet context, name.token)), def.tea
     context = subContext (concatMaps s2, s1), context
@@ -1346,7 +1369,11 @@
     [nameIndex, type]

 readType = (nameIndex, node) ->
-  freshenFree nameIndex, (inside node)[1..].map (x) -> x.token
+  freshenFree nameIndex, tokens (inside node)[1..]
+
+tokens = (words) ->
+  words.map (x) -> x.token
+

 infer = (context, expression, nameIndex) ->
   switch expression.label
@@ -1480,6 +1507,26 @@
   suffix = if nameIndex > 25 then nameIndex // 25 else ''
   String.fromCharCode 97 + nameIndex % 25

+typeVariable = (name) ->
+  new TypeVariable name
+
+nullaryType = (name) ->
+  new NullaryType name
+
+naryType = (name, params) ->
+  new PolyType name, params
+
+class MonoType
+  constructor: (@name) ->
+
+class TypeVariable extends MonoType
+class NullaryType extends MonoType
+
+class PolyType
+  constructor: (@name, @params) ->
+
+class NaryType extends PolyType
+
 ignoreTypeErrors = (fn) ->
   try
     ret = fn()


Added option for including Sass sourcemaps

--- src/sass.plugin.coffee
+++ src/sass.plugin.coffee
@@ -16,6 +16,7 @@
 			scssPath: null
 			compass: null
 			debugInfo: false
+      sourcemap: false
 			outputStyle: 'compressed'
 			requireLibraries: null
 			renderUnderscoreStylesheets: false
@@ -97,6 +98,8 @@
 					command.push('--compass')
 				if config.debugInfo
 					command.push('--debug-info')
+        if config.sourcemap
+          command.push('--sourcemap')
 				if config.outputStyle
 					command.push('--style')
 					command.push(config.outputStyle)


Getting the tfl script working

Getting the tfl script working

--- src/scripts/tfl.coffee
+++ src/scripts/tfl.coffee
@@ -1,6 +1,7 @@
 # Allows Hubot to find out how the Tube is running.
 #
 # show tube status for <line> line -  Queries the endpoint for the status of that line
+# show tube is the <line> working -  Queries the endpoint for the status of that line

 _  = require("underscore")

@@ -14,32 +15,29 @@
   robot.respond /tube status/i, (msg) ->
     msg.http("http://service-disruption.herokuapp.com/network")
       .get() (err, res, body) ->
-      if res.statusCode == 200
-        response = JSON.parse(body)
-        return_text = ""
-        for line in response.network.lines
-          do (line) ->
-            return_text += "\nThe #{line.line.name} line is currently running with #{line.line.status.status_description}"
-        msg.send return_text
-      else
-        msg.send "NEIN, NEIN, NEIN, NEIN, NEIN!"
+        if res.statusCode == 200
+          response = JSON.parse(body)
+          return_text = ""
+          for line in response.network.lines
+            do (line) ->
+              return_text += "\nThe #{line.line.name} line is currently running with #{line.line.status.status_description}"
+          msg.send return_text
+        else
+          msg.send "NEIN, NEIN, NEIN, NEIN, NEIN!"

-
-  robot.respond /is the (.*)line? fucked\?/i, (msg) ->
-    console.log msg.match
+  robot.respond /tube is the (.*) line ok\?/i, (msg) ->
     query = msg.match[1].trim().replace(/\s+/g, '-').toLowerCase()
-    console.log query
     msg.http("http://service-disruption.herokuapp.com/network/#{query}")
       .get() (err, res, body) ->
-      if res.statusCode == 200
-        line = JSON.parse(body)
-        if _(good_service_statuses).include(line.line.status.status_description)
-          msg.send "Nope it's all good, no problems reported"
-        if _(average_statuses).include(line.line.status.status_description)
-          msg.send "Well it's semi-fucked, TfL says: '#{line.line.status.status_details}'"
-        if _(bad_statuses).include(line.line.status.status_description)
-          msg.send "Totally fuckeyed mate, best to avoid, Tfl says: '#{line.line.status.status_details}'"
-        if _(closed_statuses).include(line.line.status.status_description)
-          msg.send "No chance, it's a planned closure, you should really pay attention to the TfL website..."
-      else
-        msg.send "No line by that name sonny, you sure you even live in London?"+        if res.statusCode == 200
+          line = JSON.parse(body)
+          if _(good_service_statuses).include(line.line.status.status_description)
+            msg.send "Nope it's all good, no problems reported"
+          if _(average_statuses).include(line.line.status.status_description)
+            msg.send "Well it's semi-fucked, TfL says: '#{line.line.status.status_details}'"
+          if _(bad_statuses).include(line.line.status.status_description)
+            msg.send "Totally fuckeyed mate, best to avoid, Tfl says: '#{line.line.status.status_details}'"
+          if _(closed_statuses).include(line.line.status.status_description)
+            msg.send "No chance, it's a planned closure, you should really pay attention to the TfL website..."
+        else
+          msg.send "No line by that name sonny, you sure you even live in London?"


best_player_marker model removed from #37

--- src/server/sync.coffee
+++ src/server/sync.coffee
@@ -1,7 +1,6 @@
 require('./models/player')
 require('./models/game')
 require('./models/player_game')
-require('./models/best_player_marker')
 db = require('./models/db')

 db.sync({force: true}).complete((err) ->


hub: better loging of slow json-ifications

--- src/smc-hub/hub.coffee
+++ src/smc-hub/hub.coffee
@@ -1740,7 +1740,7 @@
         json = to_json(mesg)
         tm = new Date() - t
         if tm > 10
-            winston.debug("client=#{@id}, mesg.id=#{mesg.id}: time to json=#{tm}ms; length=#{json.length}")
+            winston.debug("client=#{@id}, mesg.id=#{mesg.id}: time to json=#{tm}ms; length=#{json.length}; value='#{misc.trunc(json, 500)}'")
         @push_data_to_client(JSON_CHANNEL, json)
         if not listen
             cb?()


Don't allow caching of coffee requires to be overridden in tasks

This is a follow-on to the previous commit which handled the index.html
case.

--- src/stdlib/task.coffee
+++ src/stdlib/task.coffee
@@ -16,6 +16,10 @@
     bootstrap = """
       require('coffee-script');
       require('coffee-cache').setCacheDir('/tmp/atom-coffee-cache');
+      Object.defineProperty(require.extensions, '.coffee', {
+        writable: false,
+        value: require.extensions['.coffee']
+      });
       require('task-bootstrap');
     """


Make serialization format backwards/forwards compatible

Also, introduce a serialization version so we can drop the compatibility
fallbacks in the future.

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -59,6 +59,8 @@
 # soft wraps and folds to ensure your code interacts with them correctly.
 module.exports =
 class TextEditor extends Model
+  serializationVersion: 1
+
   buffer: null
   languageMode: null
   cursors: null
@@ -80,6 +82,10 @@
     get: -> @getElement()

   @deserialize: (state, atomEnvironment) ->
+    # TODO: Return null on version mismatch when 1.8.0 has been out for a while
+    if state.version isnt @prototype.serializationVersion and state.displayBuffer?
+      state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer
+
     try
       state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
     catch error
@@ -89,7 +95,7 @@
         throw error

     state.buffer = state.tokenizedBuffer.buffer
-    state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId)
+    state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId) ? state.buffer.addDisplayLayer()
     state.selectionsMarkerLayer = state.displayLayer.getMarkerLayer(state.selectionsMarkerLayerId)
     state.config = atomEnvironment.config
     state.notificationManager = atomEnvironment.notifications
@@ -188,7 +194,10 @@
       @setGrammar(grammar)

   serialize: ->
+    tokenizedBufferState = @tokenizedBuffer.serialize()
+
     deserializer: 'TextEditor'
+    version: @serializationVersion
     id: @id
     softTabs: @softTabs
     firstVisibleScreenRow: @getFirstVisibleScreenRow()
@@ -196,7 +205,9 @@
     selectionsMarkerLayerId: @selectionsMarkerLayer.id
     softWrapped: @isSoftWrapped()
     editorWidthInChars: @editorWidthInChars
-    tokenizedBuffer: @tokenizedBuffer.serialize()
+    # TODO: Remove this forward-compatible fallback once 1.8 reaches stable.
+    displayBuffer: {tokenizedBuffer: tokenizedBufferState}
+    tokenizedBuffer: tokenizedBufferState
     largeFileMode: @largeFileMode
     displayLayerId: @displayLayer.id
     registered: atom.textEditors.editors.has this


:memo: Remove `gutterName` argument from TextEditor::decorateMarker

Decorations of `type: 'gutter'` is deprecated. Use `Gutter::decorateMarker` to
create decoration for custom gutter.

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -1322,8 +1322,6 @@
   #   * `position` (optional) Only applicable to decorations of type `overlay`,
   #     controls where the overlay view is positioned relative to the marker.
   #     Values can be `'head'` (the default), or `'tail'`.
-  #   * `gutterName` (optional) Only applicable to the `gutter` type. If provided,
-  #     the decoration will be applied to the gutter with the specified name.
   #
   # Returns a {Decoration} object
   decorateMarker: (marker, decorationParams) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/text_effect.coffee
+++ src/text_effect.coffee
@@ -1,5 +1,5 @@
 ###*
-The Text Effect class provides a method to display a
+The Text Effect class provides a method to display a mov

 By default, images are loaded asynchronously. A proxy object is
 returned immediately. Even though it has a draw method it will not


levels and default game states

levels and default game states

--- src/text_screen.coffee
+++ src/text_screen.coffee
@@ -5,7 +5,7 @@

   self = GameState(I).extend
     drawTitleText: (canvas, titleText, subtitleText) ->
-      canvas.fillColor 'white'
+      canvas.fillColor
       canvas.font ''

       canvas.centerText


Fixed #7 - Handle scenario where body and content are null

--- src/thesimpsons.coffee
+++ src/thesimpsons.coffee
@@ -192,9 +192,12 @@
       msg.http("https://api.imgur.com/3/gallery/r/TheSimpsons.json")
        .headers(Authorization: 'Client-ID 8e4f0ec64cc27f6')
        .get() (err, res, body) ->
-         content = JSON.parse(body)
-         if content.data and content.data.length > 0
-           msg.send (msg.random content.data).link
+         if body?
+           content = JSON.parse(body)
+           if content?.data and content.data.length > 0
+             msg.send (msg.random content.data).link
+           else
+             msg.send "D'oh!  No response from Imgur."
          else
            msg.send "D'oh!  No response from Imgur."


use parseChunk to return both parsed and payload

parse just returns the parsed object

--- src/tnetstrings.coffee
+++ src/tnetstrings.coffee
@@ -10,7 +10,7 @@
     isArray = (a) -> Object::toString.call(a) == '[object Array]'

   parsePayload = (data) ->
-    assert data, "invalid data to parse, it's empty."
+    assert data, "invalid data to parseChunk, it's empty."
     colonIndex = data.indexOf(':')
     assert colonIndex > 0, "invalid length spec"

@@ -29,15 +29,15 @@
   parseArray = (data) ->
     result = []
     while data.length > 0
-      [value, data] = parse(data)
+      [value, data] = parseChunk(data)
       result[result.length] = value

     result

   parsePair = (data) ->
-    [key, extra] = parse(data)
+    [key, extra] = parseChunk(data)
     assert extra.length, "Unbalanced dictionary"
-    [value, extra] = parse(extra)
+    [value, extra] = parseChunk(extra)

     return [key, value, extra]

@@ -59,7 +59,7 @@
     "#{payload.length}:#{payload}"

   # -*- public methods -*- #
-  parse = (data) ->
+  parseChunk = (data) ->
     [payload, payloadType, remain] = parsePayload(data)

     value = switch payloadType
@@ -75,6 +75,8 @@
         assert false, "invalid payload type: "+payloadType

     return [value, remain]
+
+  parse = (data) -> parseChunk(data)[0]

   stringify = (obj) ->
     if !obj?
@@ -94,9 +96,7 @@
       dumpObject(obj)

-  return
-    parse: parse
-    stringify: stringify
+  return {parse, parseChunk, stringify}
 )()

-exports.TNETS = TNETS if typeof exports !== 'undefined'
+exports.TNETS = TNETS if typeof exports isnt 'undefined'


Strip example code.

Strip example code.

--- src/validateSSH.coffee
+++ src/validateSSH.coffee
@@ -55,11 +55,3 @@
 			return "invalid key type: #{key[0]}"
 	else
 		return "invalid key structure"
-
-input = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDItBls3aRTbVzfpBlRLdDCn8tErUuhBa
-fqfu6mWoBalyo5+DcpTIWKP29RXNJw6tmPPAkUbWP6+I6MD9+ki1TWizIw9e8if6yhuEkBuTE8Lwimy
-00NkzrUgXBicdbQL8lwusEdF+CSSQ7/SOrnkThVObUO0ZL9oVWDUNdzWX9IUu16Uwq9ZtdXcQqFCnYD
-vYgdFUlUMKfe9jNEexQRBgnU4BjX89CNjgMhQ1i637QiVPKLHnLTd8u2b5V9f+UV9NYSfn37vcUGeNk
-FXauvMmpv5CY2ZxuCN873UUmvCTGmyH3n7KvfVxcBlz8QgI9cW77SzAqnh5TEU7hkGTLlW8hP petrosagg@rachmaninoff"
-
-console.log validateOpenSSHKey(input)


Classroom: Added placeholder view.

Classroom: Added placeholder view.

--- client/landing/app/Applications/Classroom.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Classroom.kdapplication/AppView.coffee
@@ -5,5 +5,13 @@
     super options, data

     @addSubView @container = new KDView
-      partial  : "HELLLO"
-      cssClass : "classroom"+      cssClass : "classroom"
+      partial  : """
+

+

+
<classroom />

+
Start learning, teaching and sharing together.
 It's your classroom and it's online and it's free!

+
+

+

+      """


Workspace: Added showShareView method.

Workspace: Added showShareView method.

--- client/landing/app/CommonViews/workspace/collaborativeworkspace.coffee
+++ client/landing/app/CommonViews/workspace/collaborativeworkspace.coffee
@@ -219,6 +219,29 @@
     @joinSession sessionKey
     modal.destroy()

+  showShareView: (panel, workspace, event) ->
+    button   = KD.instances[event.currentTarget.id]
+    shareUrl = "#{location.origin}/Develop/#{@getOptions().name}?sessionKey=#{@sessionKey}"
+    new JContextMenu
+      cssClass    : "activity-share-popup"
+      type        : "activity-share"
+      delegate    : this
+      x           : button.getX() + 25
+      y           : button.getY() + 25
+      arrow       :
+        placement : "top"
+        margin    : -10
+      lazyLoad    : yes
+    , customView  : new SharePopup {
+        url       : shareUrl
+        shortenURL: false
+        twitter   :
+          text    : "Learn, code and deploy together to powerful VMs - @koding, the dev environment from the future! #{shareUrl}"
+        linkedin  :
+          title   : "Join me @koding!"
+          text    : "Learn, code and deploy together to powerful VMs - @koding, the dev environment from the future! #{shareUrl}"
+      }
+
   createUserListContainer: ->
     @container.addSubView @userListContainer = new KDView
       cssClass : "user-list"


protect from line deletion/addition

protect from line deletion/addition

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -130,6 +130,8 @@

   updateRealFile: ->
     return unless @provider.supportDirectEdit
+    return unless @ensureNarrowEditorIsValidState()
+
     states = []
     for lineText, row in @narrowEditor.buffer.getLines()
       continue if row is 0
@@ -190,7 +192,11 @@
     Promise.resolve(@provider.getItems()).then (items) =>
       @clearItemsText()
       @setItems(@provider.filterItems(items, regexps))
+      @narrowEditorLastRow = @narrowEditor.getLastBufferRow()
       @refreshing = false
+
+  ensureNarrowEditorIsValidState: ->
+    @narrowEditorLastRow is @narrowEditor.getLastBufferRow()

   observeInputChange: ->
     @narrowEditor.buffer.onDidChange ({newRange, oldRange}) =>


UI to Ui

UI to Ui

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -18,8 +18,8 @@
 ProviderPanel = require './provider-panel'

 module.exports =
-class UI
-  # UI static
+class Ui
+  # Ui static
   # -------------------------
   @uiByEditor: new Map()
   @unregister: (ui) ->
@@ -42,7 +42,7 @@
       numbers.push(ui.titleNumber)
     Math.max(numbers...) + 1

-  # UI.prototype
+  # Ui.prototype
   # -------------------------
   selectedItem: null
   previouslySelectedItem: null


fix, now correctly move to prompt row by skipping header row

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -75,7 +75,7 @@
   onDidRefresh: (fn) -> @emitter.on('did-refresh', fn)
   emitDidRefresh: -> @emitter.emit('did-refresh')

-  # stopRefreshingTimeout is debounced, fired after stopRefreshingDelay
+  # 'did-stop-refreshing' event is debounced, fired after stopRefreshingDelay
   onDidStopRefreshing: (fn) -> @emitter.on('did-stop-refreshing', fn)
   emitDidStopRefreshing: ->
     clearTimeout(@stopRefreshingTimeout) if @stopRefreshingTimeout?
@@ -562,7 +562,9 @@
         newRow = @findRowForNormalOrPromptItem(newRow, direction)

       if @isPromptRow(newRow)
-        @emitDidMoveToPrompt()
+        @withIgnoreCursorMove =>
+          @editor.setCursorBufferPosition([newRow, newBufferPosition.column])
+          @emitDidMoveToPrompt()
       else
         @selectItemForRow(newRow)
         @moveToSelectedItem() if isHeaderRow


Add a loading spinner to the historical tasks table view

--- SingularityUI/app/views/requestHistoricalTasksTable.coffee
+++ SingularityUI/app/views/requestHistoricalTasksTable.coffee
@@ -5,6 +5,8 @@
 class RequestHistoricalTasksTableView extends View

     render: ->
+        @$el.html '

'
+
         @historicalTasks = new HistoricalTasks [],
             requestId: @options.requestId
             active: false


Attach flight goToPage click event on view.

--- js/views/views.coffee
+++ js/views/views.coffee
@@ -20,14 +20,17 @@

   el: $('#panel-2')

-  initialize: (options) ->
-
+  initialize: (options) ->
+
   render: (id) ->
     compiledTemplate = _.template( $('#panel-2-template').html(), {header:"panel " + id}  )
     @$el.html( compiledTemplate )

     # use flight for page load.
     flight.goToPage({targetPanel:'#panel-' + id})
+
+    $('.back').on 'click', ->
+      flight.goToPage({back:true});

     this


Changed inline binding css class

Changed inline binding css class

--- wzk/app/App.coffee
+++ wzk/app/App.coffee
@@ -126,7 +126,7 @@
       el = (`/** @type {HTMLSelectElement} */`) el
       wzk.ui.ac.buildExtSelectboxFromSelectNative el, dom

-    @on 'fieldset.inline', (el, dom) ->
+    @on 'fieldset.inline-js', (el, dom) ->
       wzk.ui.inlineform.buildDynamicButton el, dom

     @on '*[data-title]', (el, dom) ->


use shared scrollToColumnZero method

use shared scrollToColumnZero method

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -886,23 +886,22 @@
     if item?
       @items.selectItem(item)
       wasAtPrompt = @isAtPrompt()
-      @moveToSelectedItem(scrollToColumnZero: true)
+      @moveToSelectedItem()
+      @scrollToColumnZero()
       @emitDidMoveToItemArea() if wasAtPrompt

   isInSyncToProviderEditor: ->
     @boundToSingleFile or @items.getSelectedItem().filePath is @provider.editor.getPath()

-  moveToSelectedItem: ({scrollToColumnZero, ignoreCursorMove, column}={}) ->
+  moveToSelectedItem: ({ignoreCursorMove, column}={}) ->
     return if (row = @items.getRowForSelectedItem()) is -1

-    point = scrollPoint = [row, column ? @editor.getCursorBufferPosition().column]
-    scrollPoint = [row, 0] if scrollToColumnZero
-
+    point = [row, column ? @editor.getCursorBufferPosition().column]
     moveAndScroll = =>
       # Manually set cursor to center to avoid scrollTop drastically changes
       # when refresh and auto-sync.
       @editor.setCursorBufferPosition(point, autoscroll: false)
-      @editor.scrollToBufferPosition(scrollPoint, center: true)
+      @editor.scrollToBufferPosition(point, center: true)

     if ignoreCursorMove ? true
       @withIgnoreCursorMove(moveAndScroll)


Add (failing) test for recent items query

Implementation coming very soon.

--- test/post.coffee
+++ test/post.coffee
@@ -757,6 +757,83 @@

                     itemIds.should.eql ["test-H-7", "test-H-8"]
             ], done
+
+    it "must work for recent items requests", (done) ->
+        @timeout 4000
+        async.series [(cb) ->
+            publishEl = server.makePublishIq "riker@enterprise.sf", "buddycloud.example.org",
+                "publish-H-10", "/user/riker@enterprise.sf/posts",
+                content: "Test post H10", id: "test-H-10"
+            server.doTest publishEl, "got-iq-publish-H-10", cb, testPublishResultIq
+        , (cb) ->
+            setTimeout cb, 1200
+        , (cb) =>
+            @since = new Date().toISOString()
+            publishEl = server.makePublishIq "picard@enterprise.sf", "buddycloud.example.org",
+                "publish-H-11", "/user/picard@enterprise.sf/posts",
+                content: "Test post H11", id: "test-H-11"
+            server.doTest publishEl, "got-iq-publish-H-11", cb, testPublishResultIq
+        , (cb) ->
+            publishEl = server.makePublishIq "picard@enterprise.sf", "buddycloud.example.org",
+                "publish-H-12", "/user/picard@enterprise.sf/posts",
+                content: "Test post H12", id: "test-H-12"
+            server.doTest publishEl, "got-iq-publish-H-12", cb, testPublishResultIq
+        , (cb) ->
+            publishEl = server.makePublishIq "riker@enterprise.sf", "buddycloud.example.org",
+                "publish-H-13", "/user/riker@enterprise.sf/posts",
+                content: "Test post H13", id: "test-H-13"
+            server.doTest publishEl, "got-iq-publish-H-13", cb, testPublishResultIq
+        , (cb) ->
+            publishEl = server.makePublishIq "data@enterprise.sf", "buddycloud.example.org",
+                "publish-H-14", "/user/data@enterprise.sf/posts",
+                content: "Test post H14", id: "test-H-14"
+            server.doTest publishEl, "got-iq-publish-H-14", cb, testPublishResultIq
+        , (cb) ->
+            publishEl = server.makePublishIq "riker@enterprise.sf", "buddycloud.example.org",
+                "publish-H-15", "/user/riker@enterprise.sf/status",
+                content: "Test post H15", id: "test-H-15"
+            server.doTest publishEl, "got-iq-publish-H-15", cb, testPublishResultIq
+        , (cb) ->
+            publishEl = server.makePublishIq "picard@enterprise.sf", "buddycloud.example.org",
+                "publish-H-16", "/user/picard@enterprise.sf/posts",
+                content: "Test post H16", id: "test-H-16"
+            server.doTest publishEl, "got-iq-publish-H-16", cb, testPublishResultIq
+
+        , (cb) =>
+            # Query recent items
+            iq = server.makePubsubGetIq("picard@enterprise.sf", "buddycloud.example.org", "retrieve-H-17")
+                .c("recent-items", xmlns: NS.BUDDYCLOUD_V1, since: @since, max: 2)
+
+            server.doTest iq, "got-iq-retrieve-H-17", cb, (iq) ->
+                iq.attrs.should.have.property "type", "result"
+                pubsubEl = iq.getChild "pubsub", NS.PUBSUB
+                should.exist pubsubEl, "missing element: <pubsub/>"
+
+                gotItems = {}
+                for items in pubsubEl.getChildren("items")
+                    items.attrs.should.have.property "node"
+                    node = items.attrs.node
+                    unless node of gotItems
+                        gotItems[node] = []
+                    for item in items.children
+                        item.attrs.should.have.property "id"
+                        gotItems[node].push item.attrs.id
+
+                gotItems.should.have.property "/user/picard@enterprise.sf/posts"
+                gotItems.should.have.property "/user/riker@enterprise.sf/posts"
+                gotItems.should.not.have.property "/user/riker@enterprise.sf/status"
+                gotItems.should.not.have.property "/user/data@enterprise.sf/posts"
+
+                # Picard: there should be H-16 and H-12 (H-11 is too much).
+                # Riker: there should be H-13 but not H-10 (too old).
+                # H-14 (Data) and H-15 (/status) should not be there.
+                gotItems["/user/picard@enterprise.sf/posts"].should.include "test-H-16"
+                gotItems["/user/picard@enterprise.sf/posts"].should.include "test-H-12"
+                gotItems["/user/picard@enterprise.sf/posts"].should.not.include "test-H-11"
+
+                gotItems["/user/riker@enterprise.sf/posts"].should.include "test-H-13"
+                gotItems["/user/riker@enterprise.sf/posts"].should.not.include "test-H-10"
+        ], done
 # }}}
 # {{{ Retracting
 describe "Retracting", ->


Add permission change handling during update all

Now it returns the list of applications not updated because of permission
changes during an update all.

--- server/controllers/applications.coffee
+++ server/controllers/applications.coffee
@@ -34,12 +34,16 @@
     log.info "Sending error to client :"
     log.error err

-    res.send code,
+    error =
         error: true
         success: false
         message: err.message or err
         stack: err.stack

+    if err.permissionChanges?
+        error.permissionChanges = err.permissionChanges
+
+    res.send code, error

 baseIdController = new cozydb.SimpleController
     model: Application
@@ -63,6 +67,7 @@
                 req.application = apps[0]
                 next()

+
     applications: (req, res, next) ->
         Application.all (err, apps) ->
             if err then next err
@@ -130,6 +135,7 @@
     # Update application parameters like autostop or favorite.
     updateData: (req, res, next) ->
         app = req.application
+
         if req.body.isStoppable? and req.body.isStoppable isnt app.isStoppable
             Stoppable = req.body.isStoppable
             Stoppable = if Stoppable? then Stoppable else app.isStoppable
@@ -140,12 +146,14 @@
                 autostop.restartTimeout app.name
                 return sendError res, err if err
                 res.send app
+
         else if req.body.favorite? and req.body.favorite isnt app.favorite
             changes =
                 favorite: req.body.favorite
             app.updateAttributes changes, (err, app) ->
                 return next err if err
                 res.send app
+
         else
             res.send app

@@ -236,18 +244,55 @@
                 msg: localizationManager.t 'successfuly updated'

-    # Update all applications installed on the Cozy.
+    # Check for all applications installed on the Cozy if an update is
+    # required. In case of version changed, it updates the app.
+    #
+    # Other actions performed:
+    #
+    # * Build a list of failed updates and send it in the response body if it's
+    # not empty.
+    # * Build a list of permission changes. Adds it to the response (when an
+    # error occurs too).
     updateAll: (req, res, next) ->

         log.info 'Starting updating all apps...'
         Application.all (err, apps) ->
+            return sendError err if err

             updateFailures = {}
+            permissionChanges = {}
             async.forEachSeries apps, (app, done) ->

-                appHelpers.updateIfNeeded app, (err) ->
-                    updateFailures[app.name] = err
-                    done()
+                log.info "Check if update is required for #{app.name}."
+                appHelpers.isUpdateNeeded app, (err, result) ->
+
+                    if err
+                        updateFailures[app.name] = err
+                        log.error "Check update failed for #{app.name}."
+                        log.raw err
+                        done()
+
+                    else if result.isPermissionsChanged
+                        log.info "Permissions changed for #{app.name}."
+                        log.info "No update performed for #{app.name}."
+                        permissionChanges[app.name] = true
+                        done()
+
+                    else if result.isUpdateNeeded
+                        log.info "Updating #{app.name} (#{app.state})..."
+                        appHelpers.update app, (err) ->
+                            log.info "Update done for #{app.name}."
+                            if err
+                                updateFailures[app.name] = err
+                                log.error "Update failed for #{app.name}."
+                                log.raw err
+                            else
+                                log.info "Update done for #{app.name}."
+                            done()
+
+                    else
+                        log.info "No update required for #{app.name}.."
+                        done()

             , ->
                 log.info 'Updating all apps operation is done.'
@@ -255,17 +300,21 @@
                 if JSON.stringify(updateFailures).length > 2
                     log.error 'Errors occured for following apps:'
                     log.raw(JSON.stringify updateFailures, null, 2)
-                    sendError res, message: updateFailures
+                    sendError res,
+                        message: calendar: updateFailures
+                        permissionChanges: permissionChanges

                 else
                     log.info 'All updates succeeded.'
                     res.send
                         success: true
+                        permissionChanges: permissionChanges
                         msg: localizationManager.t 'successfuly updated'

     # Start a stopped application.
     start: (req, res, next) ->
+
         # If controller is too slow, client receives a timeout
         # Below timeout allows to catch timeout error before client
         # If there is a timeout, application is consider like broken
@@ -427,6 +476,7 @@
             else
                 res.send 200, data

+
     # get token for static application access
     getToken: (req, res, next) ->
         Application.all key: req.params.name, (err, apps) ->


Cleanup controllers/bankaccesses

Cleanup controllers/bankaccesses

--- server/controllers/bankaccesses.coffee
+++ server/controllers/bankaccesses.coffee
@@ -5,6 +5,7 @@
 h = require './helpers'

+# Preloads a bank access (sets @access).
 module.exports.loadBankAccess = (req, res, next, bankAccessID) ->
     BankAccess.find bankAccessID, (err, access) =>
         if err?
@@ -20,6 +21,7 @@
         next()

+# Returns all bank accesses
 module.exports.index = (req, res) ->
     BankAccess.all (err, accesses) ->
         if err? or not accesses?
@@ -29,9 +31,14 @@
         res.send 200, accesses

+# Creates a new bank access (expecting at least (bank / login / password)), and
+# retrieves its accounts and operations.
 module.exports.create = (req, res) ->
-    # TODO check attributes
     access = req.body
+
+    if not access.bank? or not access.login? or not access.password?
+        h.sendErr res, "missing parameters", 400, "missing parameters"
+        return

     BankAccess.allLike access, (err, accesses) ->
         if err? or not accesses?
@@ -62,6 +69,7 @@
                     res.send 201, access

+# Deletes a bank access.
 module.exports.destroy = (req, res) ->
     @access.destroy (err) ->
         if err?
@@ -71,9 +79,16 @@
         res.send 204, success: true

+# Updates the bank access
 module.exports.update = (req, res) ->
-    # TODO check attributes
-    @access.updateAttributes req.body, (err, access) ->
+
+    access = req.body
+
+    if not access.bank? or not access.login? or not access.password?
+        h.sendErr res, "missing parameters", 400, "missing parameters"
+        return
+
+    @access.updateAttributes access, (err, access) ->
         if err?
             h.sendErr res, "couldn't update bank access: #{err}"
             return
@@ -81,10 +96,12 @@
         res.send 200, access

+# Returns the raw bank access
 module.exports.show = (req, res) ->
     res.send 200, @access

+# Retrieve accounts bounds to that bank access.
 module.exports.getAccounts = (req, res) ->
     BankAccount.allFromBankAccess @access, (err, accounts) ->
         if err


ignore pending state

ignore pending state

--- scripts/github-hook.coffee
+++ scripts/github-hook.coffee
@@ -127,6 +127,8 @@
     {repository, deployment} = event.data
     {state, creator, description} = event.data.deployment_status

+    return null if state is "pending"
+
     """
     Deployment of [#{repository.name}](https://github.com/#{repository.name}) to #{deployment.environment} by [#{creator.login}](https://github.com/#{creator.login}) has ended in #{state}
     """


Less sexual harrassment

Less sexual harrassment

--- scripts/personality.coffee
+++ scripts/personality.coffee
@@ -15,7 +15,7 @@

   robot.hear /(who|what)('s|’s| is) bebe/i, (res) ->
     adoration = res.random adorations
-    messages = ['your personal slack bot', 'the rocker of your world', 'the spoon to your pudding', 'the destroyer of worlds', 'a fan of the cookie monster']
+    messages = ['your personal slack bot', 'the rocker of worlds', 'the Luigi to your Mario', 'the destroyer of worlds', 'a fan of the cookie monster']

     res.send "I'm #{res.random messages}, #{res.random adorations}.  #{res.random emoticons_happy}"


[atom] add elixir package

[atom] add elixir package

--- .atom/packages.cson
+++ .atom/packages.cson
@@ -6,6 +6,7 @@
   "file-icons"
   "highlight-selected"
   "intentions"
+  "language-elixir"
   "language-ember"
   "linter-eslint"
   "linter-tslint"


In-code docs update

In-code docs update

--- logger.coffee
+++ logger.coffee
@@ -138,7 +138,7 @@

   ###
   @memberOf Logger
-  @name info; debug; error; fatal; warn; trace; _
+  @name info; debug; error; fatal; warn; trace; log; _
   @param message {String} - Any text message
   @param data    {Object} - [optional] Any additional info as object
   @param userId  {String} - [optional] Current user id


paste level 1 ok (no advanced test, known pb with non valid html pasted

--- app/views/CNeditor/CNeditor.coffee
+++ app/views/CNeditor/CNeditor.coffee
@@ -2134,21 +2134,11 @@
         # We assume that the structure of lines in frag and in the editor are :
         #
(TextNode)

         # what will be incorrect when styles will be taken into account.
+        #
         ###
-        targetNode   = currSel.range.startContainer
+        targetNode   = currSel.range.startContainer # a text node because of selection.normalize()
         startOffset  = currSel.range.startOffset
-        # if startContainer is a linediv, set targetNode to the inside text node
-        if targetNode.nodeName == 'DIV' and targetNode.id.substr(0,5)=='CNID_'
-            targetNode = targetNode.firstChild.firstChild
-            if startOffset > 0
-                startOffset = targetNode.length
-            else
-                startOffset = 0
-            endOffset = targetNode.length - startOffset
-        # if startContainer is a span, set targetNode to the inside text node
-        if targetNode.nodeName == 'SPAN'
-            targetNode = targetNode.firstChild
-            endOffset = targetNode.length - startOffset
+        endOffset = targetNode.length - startOffset
         # prepare lineElements
         if frag.childNodes.length > 0
             lineElements = frag.firstChild.childNodes
@@ -2187,8 +2177,9 @@
             parendDiv = targetNode
             while parendDiv.tagName != 'DIV'
                 parendDiv = parendDiv.parentElement
-
-        # remove the firstAddedLine from the fragment
+        ###*
+         * remove the firstAddedLine from the fragment
+        ###
         firstAddedLine = dummyLine.lineNext
         secondAddedLine = firstAddedLine?.lineNext
         if frag.firstChild?
@@ -2196,7 +2187,9 @@
         if firstAddedLine?
             delete this._lines[firstAddedLine.lineID]

-        # 7- updates nextLine and prevLines, insert frag in the editor
+        ###*
+         * 7- updates nextLine and prevLines, insert frag in the editor
+        ###
         if secondAddedLine?
             lineNextStartLine          = currSel.startLine.lineNext
             currSel.startLine.lineNext = secondAddedLine
@@ -2207,8 +2200,9 @@
                 domWalkContext.lastAddedLine.lineNext = lineNextStartLine
                 lineNextStartLine.linePrev = domWalkContext.lastAddedLine
                 @linesDiv.insertBefore(frag, lineNextStartLine.line$[0])
-
-        # 8- position caret
+        ###*
+         * 8- position caret
+        ###
         if secondAddedLine?
             # Assumption : last inserted line always has at least one  with only text inside
             caretTextNodeTarget = lineNextStartLine.linePrev.line$[0].childNodes[0].firstChild
@@ -2424,7 +2418,9 @@
         # if the line is empty, add an empty Span before the

         if context.currentLineFrag.childNodes.length == 0
             spanNode = document.createElement('span')
+            spanNode.appendChild(document.createTextNode(''))
             context.currentLineFrag.appendChild(spanNode)
+
         p =
             sourceLine         : context.lastAddedLine
             fragment           : context.currentLineFrag
@@ -2455,9 +2451,15 @@
             # context.absDepth += 1
         else
             # context.absDepth += deltaDepth
+        elemtFrag = document.createDocumentFragment()
+        n = elemt.childNodes.length
+        i = 0
+        while i < n
+            elemtFrag.appendChild(elemt.childNodes[0])
+            i++
         p =
             sourceLine         : context.lastAddedLine
-            innerHTML          : elemt.innerHTML
+            fragment           : elemtFrag
             targetLineType     : "Tu"
             targetLineDepthAbs : context.absDepth
             targetLineDepthRel : context.absDepth


Update CampaignView.coffee

Update CampaignView.coffee

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -369,8 +369,8 @@
     for session in @sessions.models
       levels = @getLevels()
       levelOriginal = session.get('level')?.original
+      continue unless levelOriginal
       level = levels[levelOriginal]
-      continue unless level
       topScore = _.first(LevelSession.getTopScores({session: session.toJSON(), level}))
       @levelScoreMap[levelOriginal] = topScore


Try light theme for Atom

Try light theme for Atom

--- apps/atom/atom.symlink/config.cson
+++ apps/atom/atom.symlink/config.cson
@@ -14,8 +14,8 @@
   'fontSize': 14
 'core':
   'themes': [
-    'monokai-tweaked-ui'
-    'monokai-tweaked-syntax'
+    'atom-light-ui'
+    'atom-light-syntax'
   ]
   'projectHome': '/Users/MoOx/Sync/Development'
 'autosave': {}


ClampBounds docs

ClampBounds docs

--- src/clamp_bounds.coffee
+++ src/clamp_bounds.coffee
@@ -24,7 +24,8 @@
   player.I.x
   # => 2.5 # half the player's width

-  player.I.
+  player.I.y
+  #


Refactor default options for Checkout.put

--- app/assets/javascripts/sprangular/services/checkout.coffee
+++ app/assets/javascripts/sprangular/services/checkout.coffee
@@ -107,13 +107,11 @@

       ga "ecommerce:send"

-    put: (params) ->
+    put: (params={}, config={}) ->
       url = "/spree/api/checkouts/#{Cart.current.number}"
-      params = params ||= {}

-      config =
-        headers:
-          'X-Spree-Order-Token': Cart.current.token
+      config.headers =
+        'X-Spree-Order-Token': Cart.current.token

       Cart.current.errors = null


Italic parsing.

Italic parsing.

--- app/assets/javascripts/views/clouds/chat/message.js.coffee
+++ app/assets/javascripts/views/clouds/chat/message.js.coffee
@@ -63,6 +63,9 @@
     # Redacted
     content = content.replace(/(\[REDACTED\])/ig,"[REDACTED]")

+    # Italic
+    content = content.replace(/\s\/([^\/]*)\/\s/ig,"$1")
+
     elem = $("

")

     if message.selfReference()


:art:

:art:

--- src/coffee-cache.coffee
+++ src/coffee-cache.coffee
@@ -61,15 +61,15 @@
     })

   addPathToCache: (filePath) ->
-    extension = path.extname(filePath)
-    if extension is '.coffee'
-      content = fs.readFileSync(filePath, 'utf8')
-      cachePath = getCachePath(coffee)
-      compileCoffeeScript(coffee, filePath, cachePath)
-    else if extension is '.cson'
-      CSON.readFileSync(filePath)
-    else if extension is '.js'
-      require('./6to5').addPathToCache(filePath)
+    switch path.extname(filePath)
+      when '.coffee'
+        content = fs.readFileSync(filePath, 'utf8')
+        cachePath = getCachePath(coffee)
+        compileCoffeeScript(coffee, filePath, cachePath)
+      when '.cson'
+        CSON.readFileSync(filePath)
+      when '.js'
+        require('./6to5').addPathToCache(filePath)

   getCacheMisses: -> stats.misses


Trying to figure out how to upload the pictures.

--- src/coffee/index.coffee
+++ src/coffee/index.coffee
@@ -6,6 +6,7 @@
     return true

   onSuccess = (imageData) ->
+    # Here we upload the picture to somewhere....
     console.log "wuuuu"
     image = document.getElementById 'foobar'
     image.src = "data:image/jpeg;base64," + imageData


DataTableView had el set to 'div' which is incorrect, the correct setting is tagName

--- src/coffee/views.coffee
+++ src/coffee/views.coffee
@@ -10,11 +10,7 @@
     super(options)
     @render()
     safebind(this, @model, 'change', @render)
-
-  el: 'div'
-
-  className: 'table table-striped table-bordered table-condensed'
-
+  className: 'table table-striped table-bordered table-condensed'
   render : () ->
     table_template = """

@@ -164,7 +160,7 @@
     super()
     @build_children()
     return null
-
+
 class InteractiveContextView extends DeferredParent
   # Interactive context keeps track of a bunch of components that we render
   # into dialogs
@@ -202,5 +198,3 @@
     super()
     @build_children()
     return null
-
-