bounce still shing up on reuest page sometimes

--- SingularityUI/app/models/Request.coffee
+++ SingularityUI/app/models/Request.coffee
@@ -32,7 +32,7 @@

         data.hasActiveDeploy = data.activeDeploy? or data.requestDeployState?.activeDeploy?
         data.canBeRunNow = data.state is 'ACTIVE' and data.type in ['SCHEDULED', 'ON_DEMAND'] and data.hasActiveDeploy
-        data.canBeBounced = data.state in ['ACTIVE', 'SYSTEM_COOLDOWN']
+        data.canBeBounced = data.state in ['ACTIVE', 'SYSTEM_COOLDOWN'] and data.type in ['WORKER', 'SERVICE']
         data.canBeScaled = data.state in ['ACTIVE', 'SYSTEM_COOLDOWN'] and data.hasActiveDeploy and data.type in ['WORKER', 'SERVICE']

         data


Unbind document scroll on page:fetch to disable endlessScroll on pages where it is not used

--- app/assets/javascripts/main.js.coffee
+++ app/assets/javascripts/main.js.coffee
@@ -50,7 +50,11 @@
 window.stopSpinner = ->
   $('.turbolink-spinner').fadeOut()

+window.stopEndlessScroll = ->
+  $(document).unbind('scroll')
+
 document.addEventListener("page:fetch", startSpinner)
+document.addEventListener("page:fetch", stopEndlessScroll)
 document.addEventListener("page:receive", stopSpinner)

 $ ->


new(landingPage): remove Heinrich Boell Stiftung as partner

--- src/components/LandingExplain.coffee
+++ src/components/LandingExplain.coffee
@@ -154,10 +154,9 @@
       hr null

       h2 null, t "landingExplain.chapter7.heading"
-        a href:"https://www.engagement-global.de/rueckkehrende.html",
-          img className: "partnerLogo", src: require "../img/EngagementGlobal.jpg"
-        a href:"http://www.bmz.de/",
-          img className: "partnerLogo", src: require "../img/BMZ.jpg"
-        a href:"https://www.boell.de/de",
-          img className: "partnerLogo", src: require "../img/Heinrich_Böll.jpg"
+        div null,
+          a href:"https://www.engagement-global.de/rueckkehrende.html",
+            img className: "partnerLogo", src: require "../img/EngagementGlobal.jpg"
+          a href:"http://www.bmz.de/",
+            img className: "partnerLogo", src: require "../img/BMZ.jpg"
 )


undefined string

undefined string

--- static/coffee/completions.coffee
+++ static/coffee/completions.coffee
@@ -174,7 +174,7 @@
   try
     template = tpl(map) unless typeof tpl is 'string'

-    if typeof map.example isnt undefined and map.name is contextQuery and action is "onDisplay"
+    if typeof map.example isnt "undefined" and map.name is contextQuery and action is "onDisplay"
       template = "

${name}

${example}

${hint}

BLABLABLABAL

"

     template.replace /\$\{([^\}]*)\}/g, (tag, key, pos) -> map[key]


inputview setValidation refactored

inputview setValidation refactored

--- client/Framework/components/inputs/KDInputView.coffee
+++ client/Framework/components/inputs/KDInputView.coffee
@@ -160,33 +160,40 @@
         @setValue val
         _prevVal = val

+
+
   setValidation:(ruleSet)->

-    @valid = no
+    @valid                 = no
+    @currentRuleset        = ruleSet
+    @validationCallbacks or= {}
     @createRuleChain ruleSet
+
+    for oldEventName, oldCallbacks of @validationCallbacks
+      for oldCallback in oldCallbacks
+        @off oldEventName, oldCallback
+
     @ruleChain.forEach (rule)=>
-      eventName = if ruleSet.events
-        if ruleSet.events[rule]
-          ruleSet.events[rule]
-        else if ruleSet.event
-          ruleSet.event
-      else if ruleSet.event
-        ruleSet.event
+      eventName = \
+        if ruleSet.events
+          if ruleSet.events[rule] then ruleSet.events[rule]
+          else if ruleSet.event   then ruleSet.event
+        else if ruleSet.event     then ruleSet.event

       if eventName
-        @listenTo
-          KDEventTypes       : eventName
-          listenedToInstance : @
-          callback           : (input, event)=>
-            if rule in @ruleChain
-              @validate rule, event
+        @validationCallbacks[eventName] or= []
+        @validationCallbacks[eventName].push cb = (event)=>
+          @validate rule, event  if rule in @ruleChain
+
+        @on eventName, cb
+

   validate:(rule, event = {})->

-    @ruleChain or= []
-    @validationResults or= {}
-    rulesToBeValidated = if rule then [rule] else @ruleChain
-    ruleSet = @getOptions().validate
+    @ruleChain          or= []
+    @validationResults  or= {}
+    rulesToBeValidated    = if rule then [rule] else @ruleChain
+    ruleSet               = @currentRuleset or @getOptions().validate

     if @ruleChain.length > 0
       rulesToBeValidated.forEach (rule)=>


Notification: Update mention notification messages

--- client/Main/notifications/notificationlistitem.coffee
+++ client/Main/notifications/notificationlistitem.coffee
@@ -8,7 +8,7 @@
     follow  : "started following you."
     join    : "your group"
     leave   : "your group"
-    mention : "in a post."
+    mention : "status."

   actionPhraseMap =
     comment  : "commented on"
@@ -20,7 +20,7 @@
     member   : "joined"
     join     : "joined"
     leave    : "left"
-    mention  : "mentioned you"
+    mention  : "mentioned you in"

   constructor:(options = {}, data)->
     options.tagName        or= "li"
@@ -114,7 +114,7 @@
       data = @getData()
       adjective = ""
       switch data.type
-        when "comment", "like"
+        when "comment", "like", "mention"
           KD.remote.api.SocialMessage.fetch {id: data.targetId}, (err, message)=>
             return reject err  if err or not message
             KD.remote.api.JAccount.one _id: message.accountOldId, (err, origin)=>


activity: update topic follow button state when participant status change

--- client/Social/Activity/sidebar/activitysidebar.coffee
+++ client/Social/Activity/sidebar/activitysidebar.coffee
@@ -40,6 +40,7 @@
     channel        = revive channel
     listController = @getListController channel.typeConstant
     listController.addItem channel
+    @updateTopicFollowButtons channel

   removeFromChannel: (channel) ->
@@ -48,6 +49,7 @@
     listController = @getListController channel.typeConstant
     item           = listController.itemForId channel.getId()
     listController.removeItem item
+    @updateTopicFollowButtons channel

   notificationHasArrived: (update) ->
@@ -70,6 +72,15 @@
       else {}

     return section.listController
+
+
+  updateTopicFollowButtons: (channel) ->
+
+    for name in ['hot', 'followedTopics']
+      item = @sections[name].listController.itemForId channel.getId()
+      continue  unless item
+      state = if channel.isParticipant then 'Following' else 'Follow'
+      item.followButton.setState state

   # fixme:


chat: de-structure array after error check

--- client/activity/lib/models/channelparticipants.coffee
+++ client/activity/lib/models/channelparticipants.coffee
@@ -90,8 +90,9 @@
   ###
   add: (origin, emitEvent = yes) ->

-    fetchAccounts [origin], (err, [account]) =>
+    fetchAccounts [origin], (err, accounts) =>
       return console.error err  if err
+      [account] = accounts
       @setParticipant account
       @emitChange()  if emitEvent


Now the assignedTo and createdBy filters are sorted by name

--- app/coffee/modules/issues/list.coffee
+++ app/coffee/modules/issues/list.coffee
@@ -170,6 +170,7 @@

                 obj.selected = true if isSelected("assignedTo", obj.id)
                 return obj
+            @scope.filters.assignedTo = _.sortBy(@scope.filters.assignedTo, "name")

             @scope.filters.createdBy = _.map data.created_by, (t) =>
                 obj = {
@@ -184,6 +185,7 @@

                 obj.selected = true if isSelected("createdBy", obj.id)
                 return obj
+            @scope.filters.createdBy = _.sortBy(@scope.filters.createdBy, "name")

             @scope.filters.tags = _.map data.tags, (t) =>
                 obj = {


fixed transition for delete and cancel

fixed transition for delete and cancel

--- src/components/task-plan/footer.cjsx
+++ src/components/task-plan/footer.cjsx
@@ -18,16 +18,16 @@
     @context.router.transitionTo('taskplans', {courseId})

   onDelete: ->
-    {id} = @props
+    {id, courseId} = @props
     if confirm('Are you sure you want to delete this?')
       TaskPlanActions.delete(id)
-      @context.router.transitionTo('dashboard')
+      @context.router.transitionTo('taskplans', {courseId})

   onCancel: ->
-    {id} = @props
+    {id, courseId} = @props
     if confirm('Are you sure you want to cancel?')
       TaskPlanActions.reset(id)
-      @context.router.transitionTo('dashboard')
+      @context.router.transitionTo('taskplans', {courseId})

   onViewStats: ->
     {id, courseId} = @props


config: remove old reference to jQuery UI

--- coffee/src/config.coffee
+++ coffee/src/config.coffee
@@ -27,7 +27,6 @@
 acorn.config.css = [
   '/build/css/acorn-player.css',
   '/lib/fontawesome/css/font-awesome.css',
-  '/lib/jquery/jquery-ui.custom.min.css',
 ]


Loading indicator can be present, but "off"

--- app/components/loading-indicator.cjsx
+++ app/components/loading-indicator.cjsx
@@ -3,8 +3,16 @@
 module.exports = React.createClass
   displayName: 'LoadingIndicator'

+  getDefaultProps: ->
+    off: false
+
   render: ->
-
+    visibility = if @props.off
+      'hidden'
+    else
+      ''
+
+
       {' '}
       {@props.children}


fixed discrete time axis

fixed discrete time axis

--- src/viz/types/helpers/graph/includes/buffer.coffee
+++ src/viz/types/helpers/graph/includes/buffer.coffee
@@ -68,6 +68,14 @@
             unless domainCompare
               domain[0] -= 1
               domain[1] += 1
+
+        else if vars[axis].value if vars.time.value
+          difference = Math.abs domain[1] - domain[0]
+          additional = difference / (vars[axis].ticks.values.length - 1)
+          additional = additional / 2
+
+          domain[0] = domain[0] - additional
+          domain[1] = domain[1] + additional

         else


Fix the scoped buffer in bracket forwarding

--- src/lib/Component.coffee
+++ src/lib/Component.coffee
@@ -120,7 +120,10 @@
       for outPort in @forwardBrackets[port.name]
         outputEntry[outPort] = [] unless outPort of outputEntry
         outputEntry[outPort].push ip
-      port.buffer.pop()
+      if ip.scope?
+        port.scopedBuffer[ip.scope].pop()
+      else
+        port.buffer.pop()
       @outputQ.push outputEntry
       @processOutputQueue()
       return


Handle Dates as primary key - Related to #1335

--- admin/static/coffee/models.coffee
+++ admin/static/coffee/models.coffee
@@ -30,18 +30,24 @@
     compute_shards: =>
         @.set 'computed_shards', new DataUtils.Shards [],@

+    regex:
+        string: /^S/
+        number: /^N.*%23(\-?[0-9]*(\.)?[0-9]*$)/
+        date: /^(PTIME).*%23\-?([0-9]*(\.)?[0-9]*$)/
     transform_key: (a) ->
         if a is null
             a_new = a
         else if a is ""
             a_new = -Infinity
-        else if typeof a is "string" and a[0]? and a[0] is 'N'
-            s = a.slice(a.indexOf("%23")+3)
-            if _.isNaN(parseFloat(s)) is false
-                a_new = parseFloat(s)
-        else if typeof a is "string" and a[0]? and a[0] is 'S'
-            a_new = a.slice(1)
-
+        else if typeof a is "string"
+            if @regex.string.test(a) is true
+                a_new = a.slice(1)
+            else if @regex.number.test(a) is true
+                s = a.slice(a.indexOf("%23")+3)
+                if _.isNaN(parseFloat(s)) is false
+                    a_new = parseFloat(s)
+            else if @regex.date.test(a) is true
+                a_new = new Date(parseFloat(a.slice(a.indexOf("%23")+3)))
         return a_new

@@ -52,7 +58,7 @@

         if typeof a_new is 'number' and typeof b_new is 'number'
             return a_new-b_new
-        else if typeof a_new is 'string' and typeof b_new is 'string'
+        else if (typeof a_new is 'string' and typeof b_new is 'string') or (a_new instanceof Date and b_new instanceof Date)
             if a_new > b_new
                 return 1
             else if a_new < b_new
@@ -62,8 +68,17 @@
         else if typeof a_new isnt typeof b_new
             if typeof a_new is 'number' and typeof b_new is 'string'
                 return -1
+            else if typeof a_new is 'number' and b_new instanceof Date
+                return -1
+            else if a_new instanceof Date and typeof b_new is 'string'
+                return -1
             else if typeof a_new is 'string' and typeof b_new is 'number'
                 return 1
+            else if a_new instanceof Date and typeof b_new is 'number'
+                return 1
+            else if typeof a_new is 'string' and b_new instanceof Date
+                return 1
+
             else if a_new is null and b_new isnt null
                 return 1
             else if b_new is null and a_new isnt null


specify what the copy task is for

specify what the copy task is for

--- config/application.coffee
+++ config/application.coffee
@@ -5,7 +5,7 @@
   pkg: lineman.grunt.file.readJSON("package.json")

   appTasks:
-    common: [ "coffee", "less", "jshint", "handlebars", "jst", "concat", "copy", "images", "webfonts", "pages" ]
+    common: [ "coffee", "less", "jshint", "handlebars", "jst", "concat", "copy:manifest", "images", "webfonts", "pages" ]
     dev: [ "exec", "watch" ]
     dist: []


Mimic javascriptinput displayClass

Mimic javascriptinput displayClass

--- common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -324,7 +324,9 @@
   inputtypeSetupMethods:

     textinputdynamath: (element) =>
-      @mathjax_preprocessor = window.mathjax_preprocessor
+      preprocessorClass = window['MathjaxPreprocessorFor6002x']
+      preprocessor = new preprocessorClass()
+      alert preprocessor.fn

     javascriptinput: (element) =>


Fixed js bug where select fields were not being selected.

--- admin/app/assets/javascripts/push_type/fields/select_field.js.coffee
+++ admin/app/assets/javascripts/push_type/fields/select_field.js.coffee
@@ -1,6 +1,6 @@
 # jQuery init
 $(document).on 'ready page:load', ->

-    $('select', '.select').selectize
+  $('select', '.select').selectize
     plugins:      ['remove_button']
     hideSelected: false


Fix problem by using parens?

Fix problem by using parens?

--- src/adaptors/firebase.coffee
+++ src/adaptors/firebase.coffee
@@ -30,9 +30,8 @@
             atomId

     ref = @fireproof.child key
-    ref.set trustAtom
-      .then =>
-        "#{@SHORT_NAME}: #{key}"
+    ref.set(trustAtom).then =>
+      "#{@SHORT_NAME}: #{key}"

   ratingsOf: (entity, options={}) ->
     @fireproof.child "#{options.firebase.path}/#{entity}/ratings"


wrap up quantities_question.cjsx

wrap up quantities_question.cjsx

--- app/assets/javascripts/components/questions/quantities_question.cjsx
+++ app/assets/javascripts/components/questions/quantities_question.cjsx
@@ -30,29 +30,20 @@
       </fieldset>
     </div>

-  handleLive: (e) =>
-    debugger
-    @update("live", e.target.value)
-  handleDead: (e) =>
-    @update("dead", e.target.value)
-  handleBodyParts: (e) =>
-    @update("body_parts", e.target.checked)
+  handleLive: (e) => @update("live", parseInt(e.target.value))
+  handleDead: (e) => @update("dead", parseInt(e.target.value))
+  handleBodyParts: (e) => @update("body_parts", e.target.checked)

-  valueFor: (type) =>
-    @props.data.selected?[type] || 0
-
-  increment: (type) =>
-    answer = parseInt(@props.data.selected?[type] || 0) + 1
-    @update(type, answer) if answer < 100
-
-  decrement: (type) =>
-    answer = parseInt(@props.data.selected?[type] || 0) - 1
-    @update(type, answer) if answer >= 0
+  increment: (type) => @update(type, @valueFor(type) + 1)
+  decrement: (type) => @update(type, @valueFor(type) - 1)

   update: (type, answer) =>
+    return unless 0 <= answer < 100
+
     @props.data.selected ||= {}
     @props.data.selected[type] = answer
     QuestionnaireStore.selectAnswer(@props.data.id, @props.data.selected)

+  valueFor: (type) => parseInt(@props.data.selected?[type] || 0)

 module.exports = QuantitiesQuestion


grunt-watch: spawn = false.

grunt-watch: spawn = false.

--- src/lib/gruntfile.coffee
+++ src/lib/gruntfile.coffee
@@ -52,7 +52,7 @@
 	if @hasSection 'watch'
 		# TODO: check duplicates
 		subsection = @toCoffee subsection, level=3
-		@gf = @gf.replace /^\t\twatch:$/m, "\t\twatch:\n\t\t\toptions:\n\t\t\t\tlivereload:true\n#{subsection}"
+		@gf = @gf.replace /^\t\twatch:$/m, "\t\twatch:\n\t\t\toptions:\n\t\t\t\tlivereload: true\n\t\t\t\tspawn: false\n#{subsection}"
 	else
 		@addSection 'watch', subsection
 	@writeln "watcher '#{name}' added"


[skip] Removed resolved tickets from Planning Poker view (1m)

--- app/assets/javascripts/houston-scheduler/views/planning_poker.coffee
+++ app/assets/javascripts/houston-scheduler/views/planning_poker.coffee
@@ -3,7 +3,7 @@

   initialize: ->
     @template = HandlebarsTemplates['houston-scheduler/tickets/planning_poker']
-    @tickets = @options.tickets
+    @tickets = @options.tickets.unresolved()
     @maintainers = @options.maintainers
     @myEstimateKey = "estimatedEffort[#{window.user.id}]"
     @allEstimateKeys = ("estimatedEffort[#{maintainer.id}]" for maintainer in @maintainers)


Fix Lint Issue

Fix Lint Issue

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -14,7 +14,7 @@
       advanceClock(10)
       setImmediate ->
         advanceClock(10)
-        done();
+        done()

 exports.buildIMECompositionEvent = (event, {data, target} = {}) ->
   event = new CustomEvent(event, {bubbles: true})


Stopping tracking analytics for admins

Stopping tracking analytics for admins

--- app/lib/Tracker.coffee
+++ app/lib/Tracker.coffee
@@ -12,7 +12,7 @@

   identify: (traits) ->
     console.log 'Would identify', traits if debugAnalytics
-    return unless me and @isProduction and analytics?
+    return unless me and @isProduction and analytics? and not me.isAdmin()
     # https://segment.io/docs/methods/identify
     traits ?= {}
     for userTrait in ['email', 'anonymous', 'dateCreated', 'name', 'wizardColor1', 'testGroupNumber', 'gender']
@@ -20,14 +20,14 @@
     analytics.identify me.id, traits

   trackPageView: ->
-    return unless @isProduction and analytics?
+    return unless @isProduction and analytics? and not me.isAdmin()
     url = Backbone.history.getFragment()
     console.log 'Going to track visit for', "/#{url}" if debugAnalytics
     analytics.pageview "/#{url}"

   trackEvent: (event, properties, includeProviders=null) =>
     console.log 'Would track analytics event:', event, properties if debugAnalytics
-    return unless me and @isProduction and analytics?
+    return unless me and @isProduction and analytics? and not me.isAdmin()
     console.log 'Going to track analytics event:', event, properties if debugAnalytics
     properties = properties or {}
     context = {}


:bug: Fix a typo

:bug: Fix a typo

--- lib/linter-plus.coffee
+++ lib/linter-plus.coffee
@@ -19,7 +19,7 @@
     @emitter = new Emitter
     @linters = new (require('./linter-registry'))()
     @editors = new (require('./editor-registry'))()
-    @messages = new (require('./messages-registry'))()
+    @messages = new (require('./message-registry'))()
     @views = new LinterViews(this)
     @commands = new Commands(this)


Correctly handle expired and forced margin orders and add them to the order book and depth chart

--- app/js/services/market_service.coffee
+++ app/js/services/market_service.coffee
@@ -390,6 +390,7 @@
         shorts_price = if inverted and market.shorts_price > 0 then 1.0 / market.shorts_price else market.shorts_price
         short_wall = new TradeData()
         short_wall.id = "short_wall"
+        short_wall.uid = "short_wall"
         short_wall.display_type = "Short Wall"
         short_wall.type = "short_wall"
         short_wall.price = shorts_price
@@ -494,49 +495,25 @@
                     else
                         short.price = feed_price
                         combined_bids.push short
+            ###
             for cover in @covers
-                # console.log cover
-                if new Date(cover.expiration.timestamp) < now
+                console.log cover
+                if new Date(cover.expiration.timestamp) > now or !(inverted and ask.price < feed_price) or (!inverted and ask.price > feed_price)
                     if not inverted
                         combined_asks.push cover
                     else
                         combined_bids.push cover
+            ###

         for ask in combined_asks
-            if ask.type == "margin_order"
-                ask.price = feed_price
             if inverted
                 if ask.type == "short" and ask.short_price_limit > feed_price
                     ask.price = ask.short_price_limit

         for bid in combined_bids
-            if bid.type == "margin_order"
-                bid.price = feed_price
             if not inverted
                 if bid.type == "short" and bid.short_price_limit < feed_price
                     bid.price = bid.short_price_limit
-
-        ###
-        combined_asks.sort (a,b) ->
-            if a.interest_rate and b.interest_rate
-                console.log a
-                if a.price - b.price == 0
-                    if b.interest_rate - a.interest_rate == 0 then a.index - b.index else b.interest_rate - a.interest_rate
-                else
-                    a.price - b.price
-            else if a.interest_rate and !b.interest_rate
-                if a.price - b.price == 0 then -1 else a.price - b.price
-            else if b.interest_rate and !a.interest_rate
-                if a.price - b.price == 0 then 1 else a.price - b.price
-            else
-                if a.price - b.price == 0 then a.index - b.index else a.price - b.price
-
-        for ask, index in combined_asks
-            ask.index = index
-
-        for bid, index in combined_bids
-            bid.index = index
-        ###

         @helper.update_array {
             target: @combined_asks,
@@ -585,9 +562,30 @@
     pull_covers: (market, inverted) ->
         covers = []
         feed_price =  if inverted then 1 / market.feed_price else market.feed_price
+        now  = Date.now()
+        expired = 0
+        margin_called = 0
+        margin_orders = new TradeData()
+        margin_orders.uid = "forced_covers"
+        margin_orders.type = "margin_order"
+        margin_orders.price = if inverted then feed_price * 1.1 else feed_price * (1/1.1)
+        margin_orders.cost = 0.0
+        margin_orders.quantity = 0.0
+        margin_orders.collateral = 0.0
+
+        expired_orders = new TradeData()
+        expired_orders.uid = "expired_covers"
+        expired_orders.type = "margin_order"
+        expired_orders.price = feed_price
+        expired_orders.cost = 0.0
+        expired_orders.quantity = 0.0
+        expired_orders.collateral = 0.0
+
+        margin_calls_dest = if inverted then @bids else @asks
+
         @blockchain_api.market_list_covers(market.asset_base_symbol, market.asset_quantity_symbol, 1000).then (results) =>
             results = if results then [].concat.apply(results) else [] # flattens array of results
-            for r in results
+            for r, i in results
                 continue unless r.type == "cover_order"
                 #r.type = "cover"
                 td = new TradeData()
@@ -600,11 +598,52 @@
                 td.price_dec = price_string[1]
                 td.quantity_filtered = @helper.filter_quantity(td.quantity,market, inverted)
                 #td.type = "cover"
-
+                if new Date(r.expiration) < now
+                    expired++
+                    expired_orders.collateral += td.collateral
+                    expired_orders.cost += td.cost
+                    expired_orders.quantity += td.quantity
+                else if (inverted and td.price < feed_price) or (!inverted and td.price > feed_price)
+                    margin_called++
+                    margin_orders.collateral += td.collateral
+                    margin_orders.cost += td.cost
+                    margin_orders.quantity += td.quantity
+
                 covers.push td
+
             @helper.update_array {target: @covers, data: covers }
             #console.log "------ pull_covers ------>", @covers
             #@helper.sort_array(@covers, "price", "quantity", !inverted)
+
+            if expired_orders.quantity > 0.0
+                price_string = @helper.split_price expired_orders.price, market, inverted
+                expired_orders.price_int = price_string[0]
+                expired_orders.price_dec = price_string[1]
+                expired_orders.quantity_filtered = @helper.filter_quantity(expired_orders.quantity, market, inverted)
+            if margin_orders.quantity > 0.0
+                price_string = @helper.split_price margin_orders.price, market, inverted
+                margin_orders.price_int = price_string[0]
+                margin_orders.price_dec = price_string[1]
+                margin_orders.quantity_filtered = @helper.filter_quantity(margin_orders.quantity, market, inverted)
+
+
+            expired_orders_array = if expired_orders.cost > 0.0 or expired_orders.quantity > 0.0 then [expired_orders] else []
+            margin_orders_array = if margin_orders.cost > 0.0 or margin_orders.quantity > 0.0 then [margin_orders] else []
+
+            @helper.update_array {
+                target: margin_calls_dest,
+                data: expired_orders_array,
+                can_remove: (target_el) ->
+                    target_el.uid == "expired_covers"
+                }
+
+            @helper.update_array {
+                target: margin_calls_dest,
+                data: margin_orders_array,
+                can_remove: (target_el) ->
+                    target_el.uid == "forced_covers"
+                }
+

     pull_orders: (market, inverted, account_name) ->
         orders = []


use req.systemId to filter data passed through crud controller

--- server/controllers/crud.coffee
+++ server/controllers/crud.coffee
@@ -1,5 +1,6 @@
 module.exports = (model) ->
   create = (req, res, next) ->
+    req.body.systemId = req.systemId
     model.create req.body, (err, obj) ->
       if err
         res.json 500, {error: err.toString()}
@@ -10,6 +11,7 @@
         else
           res.json 200, obj
   update = (req, res, next) ->
+    req.body.systemId = req.systemId
     if req.params.id

       # wierdly, mongoose doesn't work if you put an id
@@ -79,6 +81,10 @@
       else
         options.query[k] = v

+    #Only return date for the system in question
+    console.log 'sytemId filtering'
+    options.query.systemId = req.systemId
+
     model.find options
     , (err, result, pageCount) ->
       if err


ms -> mp

ms -> mp


Added a missing function arrow

Added a missing function arrow

--- server/models/recipient.coffee
+++ server/models/recipient.coffee
@@ -48,7 +48,7 @@
       .then -> _.extend(params, id: id)

   #Updates a recipient's info
-  self.update = (recipient, params)
+  self.update = (recipient, params) ->
     @config.output ?= 'recipient'

     query = squel.update()


Add some shortcuts to atom

Add some shortcuts to atom

--- keymaps.cson
+++ keymaps.cson
@@ -1,2 +1,4 @@
 'atom-text-editor':
   'ctrl-y': 'editor:delete-line'
+  'alt-j': 'find-and-replace:select-next'
+  'alt-shift-J': 'find-and-replace:select-all'


Fix embedded view popups and mouse listeners

Click bug introduced in b301da60ff1fcb581b9ef2a9587337777e1472e8.

--- src/map-base-view.coffee
+++ src/map-base-view.coffee
@@ -145,6 +145,7 @@
         postInitialize: ->
             @popups = L.layerGroup()
             @popups.addTo @map
+            @_addMouseoverListeners @allMarkers
             @setInitialView()
             @drawInitialState()

@@ -225,14 +226,10 @@

             if layer?
                 @[layer].clearLayers()
-                @[layer] = @getFeatureGroup()
                 @[layer].addLayers markers
-                @[layer].addTo @map
             else
                 @allMarkers.clearLayers()
-                @allMarkers = @getFeatureGroup()
                 @allMarkers.addLayers markers
-                @allMarkers.addTo @map

             @removeMarkerTabIndexes()

@@ -454,7 +451,6 @@
                 return new L.LatLngBounds(
                     new L.LatLng(sw.lat - latDiff, sw.lng - lngDiff, true),
                     new L.LatLng(ne.lat + latDiff, ne.lng + lngDiff, true))
-            @_addMouseoverListeners(featureGroup)
             featureGroup

         createMarker: (unit, markerOptions) ->


Bugfix.

Bugfix.

--- src/map-base-view.coffee
+++ src/map-base-view.coffee
@@ -110,6 +110,7 @@
                 bounds = _.reduce boundaries, iteratee, L.latLngBounds([])
                 bounds: bounds
             else
+                return defaults

         postInitialize: ->
             @_addMouseoverListeners @allMarkers


still trying...

still trying...

--- modules/player/search.coffee
+++ modules/player/search.coffee
@@ -11,8 +11,9 @@
     if search.results.partial
       await @updateResults(msg.id)
       search.results.on('video', => @updateResults(msg.id))
-      r = await search.rmsg.awaitReactions (r, u) =>
-        r.emoji.id in options and u.id is msg.author.id
+      r = await search.rmsg.awaitReactions((r, u) =>
+        r.emoji.name in options and u.id is msg.author.id
+      , time: 60000)
       try await search.rmsg.delete()
       delete @pending[msg.id]
       video = search.results.items[options.indexOf(r.emoji.id)]


:art: Respect max-line-length in editor-linter

--- lib/editor-linter.coffee
+++ lib/editor-linter.coffee
@@ -13,7 +13,8 @@
       @editor.onDidSave => @lint(false)
     )
     @subscriptions.add(
-      @editor.onDidChangeCursorPosition ({newBufferPosition}) => @linter.views.updateBubble(newBufferPosition)
+      @editor.onDidChangeCursorPosition ({newBufferPosition}) =>
+        @linter.views.updateBubble(newBufferPosition)
     )
     return unless @linter.lintOnFly
     @subscriptions.add(


remove deprecated api usage

remove deprecated api usage

--- lib/editor/result.coffee
+++ lib/editor/result.coffee
@@ -1,5 +1,5 @@
 # TODO: better scrolling behaviour
-{CompositeDisposable, TextEditor} = require 'atom'
+{CompositeDisposable} = require 'atom'

 # ## Result API
 # `Result`s are DOM elements which represent the result of some operation. They
@@ -114,7 +114,7 @@

   @all: -> # TODO: scope selector
     results = []
-    for item in atom.workspace.getPaneItems() when item instanceof TextEditor
+    for item in atom.workspace.getPaneItems() when atom.workspace.isTextEditor item
       item.findMarkers().filter((m) -> m.result?).forEach (m) ->
         results.push m.result
     results


Add kerning support for embedded fonts

Add kerning support for embedded fonts

--- lib/font/embedded.coffee
+++ lib/font/embedded.coffee
@@ -20,6 +20,7 @@
   encode: (text) ->
     features = ['ccmp', 'liga', 'dlig', 'kern']
     glyphs = @font.glyphsForString text, features
+    advances = @font.advancesForGlyphs glyphs, features

     res = []
     for glyph, i in glyphs
@@ -28,8 +29,9 @@

       @widths[gid] ?= @font.widthOfGlyph glyph.id
       @unicode[gid] ?= glyph.codePoints
+      advances[i] -= @widths[gid]

-    return res.join ''
+    return [res, advances]

   widthOfString: (string, size) ->
     features = ['ccmp', 'liga', 'dlig', 'kern']


:lipstick: Correct indentation

:lipstick: Correct indentation

--- lib/general-panel.coffee
+++ lib/general-panel.coffee
@@ -27,9 +27,9 @@
     return if _.isEmpty(settings)

     @append $$ ->
-        @h1 class: 'section-heading', "#{_.undasherize(_.uncamelcase(namespace))} Settings"
-        for name in _.keys(settings).sort()
-          appendSetting.call(this, namespace, name, settings[name])
+      @h1 class: 'section-heading', "#{_.undasherize(_.uncamelcase(namespace))} Settings"
+      for name in _.keys(settings).sort()
+        appendSetting.call(this, namespace, name, settings[name])

   bindFormFields: ->
     for input in @find('input[id]').toArray()


Use text note instead of alert

Use text note instead of alert

--- lib/general-panel.coffee
+++ lib/general-panel.coffee
@@ -13,11 +13,11 @@

     @subPanels = [
       new SettingsPanel('core', note: '''
-
These are Atom's core settings which affect behavior unrelated to text editing. Individual packages might have additional config settings of their own. Check individual package settings by clicking its package card in the Packages list.

+
These are Atom's core settings which affect behavior unrelated to text editing. Individual packages might have additional config settings of their own. Check individual package settings by clicking its package card in the Packages list.

       ''')

       new SettingsPanel('editor', note: '''
-
These config settings are related to text editing. Some of these settings can be overriden on a per-language basis. Check language settings by clicking the language's package card in the Packages list.

+
These config settings are related to text editing. Some of these settings can be overriden on a per-language basis. Check language settings by clicking the language's package card in the Packages list.

       ''')
     ]


added appendDots option, fixed appendArrows

--- app/scripts/app.coffee
+++ app/scripts/app.coffee
@@ -12,6 +12,7 @@
       arrows: "@"
       asNavFor: "@"
       appendArrows: "@"
+      appendDots: "@"
       autoplay: "@"
       autoplaySpeed: "@"
       centerMode: "@"
@@ -59,7 +60,8 @@
             adaptiveHeight: scope.adaptiveHeight is "true"
             arrows: scope.arrows isnt "false"
             asNavFor: if scope.asNavFor then scope.asNavFor else undefined
-            appendArrows: if scope.appendArrows then $(appendArrows) else $(element)
+            appendArrows: if scope.appendArrows then $(scope.appendArrows) else $(element)
+            appendDots: if scope.appendDots then $(scope.appendDots) else $(element)
             autoplay: scope.autoplay is "true"
             autoplaySpeed: if scope.autoplaySpeed? then parseInt(scope.autoplaySpeed, 10) else 3000
             centerMode: scope.centerMode is "true"


fix destroySlick() with new syntax

fix destroySlick() with new syntax

the new function-syntax was introduced with slick-carousel 1.4, and destroySlick() wasn't updated since

--- app/scripts/app.coffee
+++ app/scripts/app.coffee
@@ -54,7 +54,7 @@
       destroySlick = () ->
         $timeout(() ->
           slider = $(element)
-          slider.unslick()
+          slider.slick('unslick')
           slider.find('.slick-list').remove()
           slider
         )


use empty object for ranges if it doesn't exist

--- app/coffee/DocumentManager.coffee
+++ app/coffee/DocumentManager.coffee
@@ -30,7 +30,7 @@
 						return callback(error) if error?
 						RedisManager.setHistoryType doc_id, projectHistoryType, (error) ->
 							return callback(error) if error?
-							callback null, lines, version, ranges, pathname, projectHistoryId, null, false
+							callback null, lines, version, ranges || {}, pathname, projectHistoryId, null, false
 			else
 				callback null, lines, version, ranges, pathname, projectHistoryId, unflushedTime, true


Removed delay

Removed delay

--- test/cwebp.coffee
+++ test/cwebp.coffee
@@ -48,7 +48,7 @@
         filename.should.not.be.empty
         fs.existsSync(filename).should.be.true
         promise
-      .delay(1).then ->
+      .then ->
         throw new Error 'Should not be fulfilled'
       , (err) ->
         err.should.be.Error


Switched classification on, why not? #77

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -83,11 +83,11 @@
     # RECONFIGURATION:
     #
     # Normal operation:
-    # User.on 'change', @onUserChange
+    User.on 'change', @onUserChange
     #
     # Short circuit app logic until we load more data:
-    @el.find('.annotation').remove()
-    @el.find('.subjects').html require 'views/out_of_subjects'
+    # @el.find('.annotation').remove()
+    # @el.find('.subjects').html require 'views/out_of_subjects'
     #
     # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


test: DRY

test: DRY

--- test/index.coffee
+++ test/index.coffee
@@ -5,15 +5,18 @@
 _.mixin understream.exports()
 _.mixin require 'underscore.deep'

+BASE_LEFT = ({a: i, b: i + 1} for i in [1..3])
+BASE_RIGHT = ({a: i, b: i + 2} for i in [1..3])
+
 configs = [
   on: 'a'
-  left: ({a: i, b: i + 1} for i in [1..3])
-  right: ({a: i, b: i + 2} for i in [1..3])
-  expected: _.zip ({a: i, b: i + 1} for i in [1..3]), ({a: i, b: i + 2} for i in [1..3])
+  left: BASE_LEFT
+  right: BASE_RIGHT
+  expected: _.zip BASE_LEFT, BASE_RIGHT
 ,
   on: 'b'
-  left: ({a: i, b: i + 1} for i in [1..3])
-  right: ({a: i, b: i + 2} for i in [1..3])
+  left: BASE_LEFT
+  right: BASE_RIGHT
   expected: [
     [{a: 1, b: 2}, null]
     [{a: 2, b: 3}, {a: 1, b: 3}]
@@ -22,8 +25,8 @@
   ]
 ,
   on: {'a': 'b'}
-  left: ({a: i, b: i + 1} for i in [1..3])
-  right: ({a: i, b: i + 2} for i in [1..3])
+  left: BASE_LEFT
+  right: BASE_RIGHT
   expected: [
     [{a: 1, b: 2}, null]
     [{a: 2, b: 3}, null]
@@ -31,11 +34,10 @@
     [null, {a: 2, b: 4}]
     [null, {a: 3, b: 5}]
   ]
-  type: 'full'
 ,
   on: 'a'
-  left: ({a: i, b: i + 1} for i in [1..3]).concat {a: '', b: 5}
-  right: ({a: i, b: i + 2} for i in [1..3]).concat {a: '', b: 6}
+  left: BASE_LEFT.concat {a: '', b: 5}
+  right: BASE_RIGHT.concat {a: '', b: 6}
   expected: [
     [{a: 1, b: 2}, {a: 1, b: 3}]
     [{a: 2, b: 3}, {a: 2, b: 4}]
@@ -44,8 +46,8 @@
   ]
 ,
   on: 'a'
-  left: ({a: i, b: i + 1} for i in [1..3]).concat {a: undefined, b: 5}
-  right: ({a: i, b: i + 2} for i in [1..3]).concat {a: undefined, b: 6}
+  left: BASE_LEFT.concat {a: undefined, b: 5}
+  right: BASE_RIGHT.concat {a: undefined, b: 6}
   expected: [
     [{b: 5}, null]
     [null, {b: 6}]
@@ -55,8 +57,8 @@
   ]
 ,
   on: 'a'
-  left: ({a: i, b: i + 1} for i in [1..3]).concat {a: null, b: 5}
-  right: ({a: i, b: i + 2} for i in [1..3]).concat {a: null, b: 6}
+  left: BASE_LEFT.concat {a: null, b: 5}
+  right: BASE_RIGHT.concat {a: null, b: 6}
   expected: [
     [{a: null, b: 5}, null]
     [null, {a: null, b: 6}]
@@ -66,8 +68,8 @@
   ]
 ,
   on: 'a'
-  left: ({a: i, b: i + 1} for i in [1..3]).concat {b: 5}
-  right: ({a: i, b: i + 2} for i in [1..3]).concat {b: 6}
+  left: BASE_LEFT.concat {b: 5}
+  right: BASE_RIGHT.concat {b: 6}
   expected: [
     [{b: 5}, null]
     [null, {b: 6}]
@@ -76,9 +78,9 @@
     [{a: 3, b: 4}, {a: 3, b: 5}]
   ]
 ,
-  on: {'a': 'a'}
-  left: ({a: i, b: i + 1} for i in [1..3]).concat {a: null, b: 5}
-  right: ({a: i, b: i + 2} for i in [1..3]).concat {a: null, b: 6}, {a: null, b: 7}
+  on: 'a'
+  left: BASE_LEFT.concat {a: null, b: 5}
+  right: BASE_RIGHT.concat {a: null, b: 6}, {a: null, b: 7}
   expected: [
     [{a: null, b: 5}, null]
     [null, {a: null, b: 6}]
@@ -103,25 +105,16 @@
       mod_configs = _(mod_configs).chain()
         .map (mod_config) ->
           # If we don't specify type, generate an expected array for each type based on the original
-          left = _(mod_config).deepClone()
-          left.expected = _(left.expected).chain()
-            .deepClone()
-            .filter((expected_pair) -> expected_pair[0]?).value()
-          left.type = 'left'
-          right = _(mod_config).deepClone()
-          right.expected = _(right.expected).chain()
-            .deepClone()
-            .filter((expected_pair) -> expected_pair[1]?).value()
-          right.type = 'right'
-          inner = _(mod_config).deepClone()
-          inner.expected = _(inner.expected).chain()
-            .deepClone()
-            .filter((expected_pair) -> expected_pair[0]? and expected_pair[1]?).value()
-          inner.type = 'inner'
-          full = _(mod_config).deepClone()
-          full.expected = _(full.expected).deepClone()
-          full.type = 'full'
-          [left, right, inner, full]
+          config_spec =
+            left: (expected_pair) -> expected_pair[0]?
+            right: (expected_pair) -> expected_pair[1]?
+            inner: (expected_pair) -> expected_pair[0]? and expected_pair[1]?
+            full: -> true
+          _(config_spec).map (filter, type) ->
+            new_config = _(mod_config).deepClone()
+            new_config.expected = _(new_config.expected).filter filter
+            new_config.type = type
+            new_config
         .flatten()
         .value()
     _(mod_configs).each (mod_config, j) ->


browserSync reload on all php file changes

--- app/templates/gulp/tasks/watch.coffee
+++ app/templates/gulp/tasks/watch.coffee
@@ -8,4 +8,4 @@

   gulp.watch "#{conf.path.dev.scss}/**/*.scss", ['css']
   gulp.watch ["#{conf.path.dev.js}/**/!(*.bundle).js", "#{conf.path.dev.js}/**/*.jsx"], ['scripts:lint', 'scripts']
-  gulp.watch "#{conf.path.dev.views}/**/*.html.php", ['bs-reload']
+  gulp.watch "#{conf.path.dev.php}/**/*.php", ['bs-reload']


refactor(audio.coffee): throw an error when cannot load remote list

--- app/scripts/services/audio.coffee
+++ app/scripts/services/audio.coffee
@@ -19,6 +19,9 @@
 				data:
 					count: 5000
 				callback: ( result ) ->
+					if not result?.response?.items?
+						throw JSON.stringify result
+
 					rawRemoteList = result.response.items
 					remoteList = rawRemoteList.map ( item ) ->
 						id: item.id


Don't complete comments or strings. Fixes #5.

--- lib/jedi-provider.coffee
+++ lib/jedi-provider.coffee
@@ -127,9 +127,19 @@

 		resolve(suggestions)

+	isInString: (scopeDescriptor) ->
+		scopeArray = scopeDescriptor.getScopesArray()
+		(return true if scope.indexOf('string.') == 0) for scope in scopeArray
+		(return true if scope.indexOf('comment.') == 0) for scope in scopeArray
+		return false
+
 	getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
 		if not @isSetUp
 			@setUp()
+
+		if @isInString(scopeDescriptor)
+			return new Promise (resolve, reject) =>
+				resolve([])

 		if @halted
 			return new Promise (resolve, reject) =>


Added directories to application

Added directories to application

--- src/configuration.coffee
+++ src/configuration.coffee
@@ -1,4 +1,12 @@
+express = require 'express'
+path = require('path')
+
+public_dir = (filename) -> path.join __dirname, '../public', filename or ''
+
 exports.apply = (server) ->
+	server.use express.static public_dir()
+	server.set 'views', public_dir 'views'
+
 	server.configure 'production', ->
 		this.set 'listening-port', 80


change play key

change play key

--- public/src/subtitles-controls/subtitles-controls-view.coffee
+++ public/src/subtitles-controls/subtitles-controls-view.coffee
@@ -170,7 +170,7 @@
       if event.which is 39
         this.next()
         event.preventDefault()
-      if event.which is 40
+      if event.which is 32
         this.togglePlay()
         event.preventDefault()
   )


Issue #1714 ... broken 'in' after raw range 'for'

--- lexer.coffee
+++ lexer.coffee
@@ -241,6 +241,7 @@
     return 0 unless match = MULTI_DENT.exec @chunk
     indent = match[0]
     @line += count indent, '\n'
+    @seenFor = no
     prev = last @tokens, 1
     size = indent.length - 1 - indent.lastIndexOf '\n'
     noNewlines = @unfinished()


Remove extra spacing

Remove extra spacing

--- test/suite.coffee
+++ test/suite.coffee
@@ -157,7 +157,6 @@
             assert.equal image.meta.exif, undefined
             done()

-
         it 'should return exif data if returnExif is set to true', (done) ->
           image.config.returnExif = true
           image.getMeta (err) ->


Fix profile API route

Fix profile API route

--- apps/user/index.coffee
+++ apps/user/index.coffee
@@ -10,7 +10,7 @@
 app.set "views", __dirname + "/templates"
 app.set "view engine", "jade"

-app.get "/api/:username/channels", routes.channelsAPI
+app.get "/api/:username/profile", routes.fetchAuthor, routes.channelsAPI

 # All routes below need an author to render
 app.get "/:username/channels", routes.fetchAuthor, (req, res, next) ->


Fix virtual item height on mobile

Fix virtual item height on mobile

--- resources/assets/coffee/react/beatmaps/search-content.coffee
+++ resources/assets/coffee/react/beatmaps/search-content.coffee
@@ -18,7 +18,10 @@
 beatmapsetStore = core.dataStore.beatmapsetStore
 controller = core.beatmapsetSearchController

-ITEM_HEIGHT = 110 # needs to be known in advance to calculate size of virtual scrolling area.
+# needs to be known in advance to calculate size of virtual scrolling area.
+ITEM_HEIGHT =
+  1: 125
+  2: 110

 ListRender = ({ virtual, itemHeight }) ->
   div
@@ -100,7 +103,7 @@
                   el Observables.BeatmapList,
                     items: _.chunk(beatmapsetIds, Observables.numberOfColumns)
                     itemBuffer: 5
-                    itemHeight: ITEM_HEIGHT
+                    itemHeight: ITEM_HEIGHT[Observables.numberOfColumns]

                 else
                   div className: 'beatmapsets__empty',


Fix id

Fix id

--- resources/assets/coffee/react/profile-page/detail-bar.coffee
+++ resources/assets/coffee/react/profile-page/detail-bar.coffee
@@ -124,11 +124,11 @@
       button
         className: 'profile-page-toggle js-click-menu'
         title: osu.trans('common.buttons.show_more_options')
-        'data-click-menu-target': "profile-page-bar-#{@id}"
+        'data-click-menu-target': "profile-page-bar-#{@eventId}"
         span className: 'fas fa-ellipsis-v'
       div
         className: 'simple-menu simple-menu--profile-page-bar js-click-menu'
-        'data-click-menu-id': "profile-page-bar-#{@id}"
+        'data-click-menu-id': "profile-page-bar-#{@eventId}"
         'data-visibility': 'hidden'
         items


Pass data array even when empty

Pass data array even when empty

--- resources/assets/coffee/react/profile-page/rank-chart.coffee
+++ resources/assets/coffee/react/profile-page/rank-chart.coffee
@@ -71,20 +71,19 @@
       y: -rank
     .filter (point) -> point.y < 0

-    return unless data.length > 0
+    if data.length > 0
+      if data.length == 1
+        data.unshift
+          x: data[0].x - 1
+          y: data[0].y

-    if data.length == 1
-      data.unshift
-        x: data[0].x - 1
-        y: data[0].y
+      lastData = _.last(data)

-    lastData = _.last(data)
-
-    if lastData.x == 0
-      lastData.y = -@props.stats.rank.global
-    else
-      data.push
-        x: 0
-        y: -@props.stats.rank.global
+      if lastData.x == 0
+        lastData.y = -@props.stats.rank.global
+      else
+        data.push
+          x: 0
+          y: -@props.stats.rank.global

     @rankChart.loadData data


Extract InstalledPlugin and UninstalledPlugin

--- views/components/settings/parts/plugin-config.cjsx
+++ views/components/settings/parts/plugin-config.cjsx
@@ -19,6 +19,165 @@
     false
   render: ->
     React.createElement @props.plugin.settingsClass
+
+InstalledPlugin = React.createClass
+  render: ->
+    plugin = @props.plugin
+    index = @props.index
+    <Col xs={12} style={marginBottom: 8}>
+      <Col xs={12} className='div-row'>
+
+          {plugin.displayName}
+
+         @{plugin.author}
+

+

+
+              <FontAwesome name={
+                             if plugin.isUpdating
+                               "spinner"
+                             else if plugin.isOutdated
+                               "cloud-download"
+                             else
+                               "check"
+                           }
+                           pulse={plugin.isUpdating}/>
+              {
+                if plugin.isUpdating
+                   __ "Updating"
+                else if plugin.isOutdated
+                   "Version #{plugin.lastestVersion}"
+                else
+                   __ "Latest"
+              }
+
+

+

+            Version {plugin.version || '1.0.0'}
+

+

+      </Col>
+      <Col xs={12} style={marginTop: 4}>
+        <Col xs={5}>{plugin.description}</Col>
+        <Col xs={7} style={padding: 0}>
+

+            <ButtonGroup bsSize='small' style={width: '100%'}>
+              {
+                if plugin.settingsClass?
+                  <Button ref="setting-btn"
+                          bsStyle='primary' bsSize='xs' style={width: 'calc(100% / 3)'}
+                          onClick={_.partial @props.toggleSettingPop, plugin.name}>
+                    <FontAwesome name='gear' />
+                    {__ 'Settings'}
+                  </Button>
+              }
+              <Button bsStyle='info'
+                      disabled={PluginManager.getStatusOfPlugin(plugin) == PluginManager.NEEDUPDATE}
+                      onClick={_.partial @props.handleEnable, index}
+                      style={width: "#{if plugin.settingsClass? then 'calc(100% / 3)' else 'calc(100% / 2)'}"}
+                      className="plugin-control-button">
+                <FontAwesome name={
+                               switch PluginManager.getStatusOfPlugin plugin
+                                 when PluginManager.VALID
+                                   "pause"
+                                 when PluginManager.DISABLED
+                                   "play"
+                                 when PluginManager.NEEDUPDATE
+                                   "ban"
+                                 when PluginManager.BROKEN
+                                   "close"
+                             }/>
+                {
+                  switch PluginManager.getStatusOfPlugin plugin
+                    when PluginManager.VALID
+                      __ "Disable"
+                    when PluginManager.DISABLED
+                      __ "Enable"
+                    when PluginManager.NEEDUPDATE
+                      __ "Outdated"
+                    when PluginManager.BROKEN
+                      __ "Error"
+                }
+              </Button>
+              <Button bsStyle='danger'
+                      onClick={_.partial @props.handleRemove, index}
+                      disabled={not plugin.isInstalled}
+                      style={width: "#{if plugin.settingsClass? then 'calc(100% / 3)' else 'calc(100% / 2)'}"}
+                      className="plugin-control-button">
+                <FontAwesome name={if plugin.isInstalled then 'trash' else 'trash-o'} />
+                {
+                  if plugin.isUninstalling
+                    __ "Removing"
+                  else if plugin.isInstalled
+                    __ "Remove"
+                  else
+                    __ "Removed"
+                }
+              </Button>
+            </ButtonGroup>
+            {
+              if plugin.settingsClass?
+                <Overlay show={@props.settingPopOpen}
+                         onHide={_.partial @props.toggleSettingPop, plugin.name, false}
+                         rootClose={true}
+                         target={=> ReactDOM.findDOMNode @refs['setting-btn']}
+                         placement='top'>
+                  <Popover id="#{plugin.name}-setting-pop">
+                    <PluginSettingWrap plugin={plugin} />
+                  </Popover>
+                </Overlay>
+            }
+

+        </Col>
+      </Col>
+    </Col>
+
+UninstalledPlugin = React.createClass
+  render: ->
+    value = @props.record
+    <Col xs={12} style={marginBottom: 8}>
+      <Col xs={12} className='div-row'>
+
+          <FontAwesome name={value.icon} />
+            {value[window.language]}
+
+         @
+
+            {value.author}
+
+
+      </Col>
+      <Col xs={12} style={marginTop: 4}>
+        <Col xs={8}>{value["des#{window.language}"]}</Col>
+        <Col xs={4} style={padding: 0}>
+

+            <ButtonGroup bsSize='small' style={width: '100%'}>
+              <Button bsStyle='primary'
+                      disabled={@props.npmWorkding}
+                      onClick={_.partial @props.handleInstall, @props.name}
+                      style={width: "100%"}
+                      className="plugin-control-button">
+                <FontAwesome name={
+                               if @props.installing
+                                 'spinner'
+                               else
+                                 'download'
+                             }
+                             pulse={@props.installing}/>
+                {
+                  if @props.installing
+                    __ "Installing"
+                  else
+                    __ "Install"
+                }
+              </Button>
+            </ButtonGroup>
+

+        </Col>
+      </Col>
+    </Col>

 PluginConfig = React.createClass
   getInitialState: ->
@@ -78,8 +237,6 @@
     config = yield PluginManager.selectConfig(state ,null, null)
     @setState
       config: config
-  getDOMNodeByName: (node) ->
-    ReactDOM.findDOMNode(@refs[node])
   toggleSettingPop: (name, state) ->
     settingPopOpen = @state.settingPopOpen
     if state?
@@ -396,160 +553,30 @@
         </Col>
       {
         for plugin, index in @state.plugins
-          <Col key={index} xs={12} style={marginBottom: 8}>
-            <Col xs={12} className='div-row'>
-
-                {plugin.displayName}
-
-               @{plugin.author}
-

-

-
-                    <FontAwesome name={
-                                   if plugin.isUpdating
-                                     "spinner"
-                                   else if plugin.isOutdated
-                                     "cloud-download"
-                                   else
-                                     "check"
-                                 }
-                                 pulse={plugin.isUpdating}/>
-                    {
-                      if plugin.isUpdating
-                         __ "Updating"
-                      else if plugin.isOutdated
-                         "Version #{plugin.lastestVersion}"
-                      else
-                         __ "Latest"
-                    }
-
-

-

-                  Version {plugin.version || '1.0.0'}
-

-

-            </Col>
-            <Col xs={12} style={marginTop: 4}>
-              <Col xs={5}>{plugin.description}</Col>
-              <Col xs={7} style={padding: 0}>
-

-                  <ButtonGroup bsSize='small' style={width: '100%'}>
-                    {
-                      if plugin.settingsClass?
-                        <Button ref="#{plugin.name}-setting-btn"
-                                bsStyle='primary' bsSize='xs' style={width: 'calc(100% / 3)'}
-                                onClick={@toggleSettingPop.bind @, plugin.name}>
-                          <FontAwesome name='gear' />
-                          {__ 'Settings'}
-                        </Button>
-                    }
-                    <Button bsStyle='info'
-                            disabled={PluginManager.getStatusOfPlugin(plugin) == PluginManager.NEEDUPDATE}
-                            onClick={@handleEnable.bind @, index}
-                            style={width: "#{if plugin.settingsClass? then 'calc(100% / 3)' else 'calc(100% / 2)'}"}
-                            className="plugin-control-button">
-                      <FontAwesome name={
-                                     switch PluginManager.getStatusOfPlugin plugin
-                                       when PluginManager.VALID
-                                         "pause"
-                                       when PluginManager.DISABLED
-                                         "play"
-                                       when PluginManager.NEEDUPDATE
-                                         "ban"
-                                       when PluginManager.BROKEN
-                                         "close"
-                                   }/>
-                      {
-                        switch PluginManager.getStatusOfPlugin plugin
-                          when PluginManager.VALID
-                            __ "Disable"
-                          when PluginManager.DISABLED
-                            __ "Enable"
-                          when PluginManager.NEEDUPDATE
-                            __ "Outdated"
-                          when PluginManager.BROKEN
-                            __ "Error"
-                      }
-                    </Button>
-                    <Button bsStyle='danger'
-                            onClick={@handleRemove.bind @, index}
-                            disabled={not plugin.isInstalled}
-                            style={width: "#{if plugin.settingsClass? then 'calc(100% / 3)' else 'calc(100% / 2)'}"}
-                            className="plugin-control-button">
-                      <FontAwesome name={if plugin.isInstalled then 'trash' else 'trash-o'} />
-                      {
-                        if plugin.isUninstalling
-                          __ "Removing"
-                        else if plugin.isInstalled
-                          __ "Remove"
-                        else
-                          __ "Removed"
-                      }
-                    </Button>
-                  </ButtonGroup>
-                  {
-                    if plugin.settingsClass?
-                      <Overlay show={@state.settingPopOpen[plugin.name]}
-                               onHide={@toggleSettingPop.bind @, plugin.name, false}
-                               rootClose={true}
-                               target={@getDOMNodeByName.bind @, "#{plugin.name}-setting-btn"}
-                               placement='top'>
-                        <Popover id="#{plugin.name}-setting-pop">
-                          <PluginSettingWrap plugin={plugin} />
-                        </Popover>
-                      </Overlay>
-                  }
-

-              </Col>
-            </Col>
-          </Col>
+          <InstalledPlugin
+            key={plugin.name}
+            index={index}
+            plugin={plugin}
+            handleClickAuthorLink={@handleClickAuthorLink}
+            handleUpdate={@handleUpdate}
+            toggleSettingPop={@toggleSettingPop}
+            handleEnable={@handleEnable}
+            handleRemove={@handleRemove}
+            settingPopOpen={@state.settingPopOpen[plugin.name]}
+            />
       }
       {
         for name, index in Object.keys(@state.uninstalledPluginSettings)
           value = @state.uninstalledPluginSettings[name]
-          <Col key={index} xs={12} style={marginBottom: 8}>
-            <Col xs={12} className='div-row'>
-
-                <FontAwesome name={value.icon} />
-                  {value[window.language]}
-
-               @
-
-                  {value.author}
-
-
-            </Col>
-            <Col xs={12} style={marginTop: 4}>
-              <Col xs={8}>{value["des#{window.language}"]}</Col>
-              <Col xs={4} style={padding: 0}>
-

-                  <ButtonGroup bsSize='small' style={width: '100%'}>
-                    <Button bsStyle='primary'
-                            disabled={@state.npmWorkding}
-                            onClick={@handleInstall.bind @, name}
-                            style={width: "100%"}
-                            className="plugin-control-button">
-                      <FontAwesome name={
-                                     if name in @state.installingPluginNames
-                                       'spinner'
-                                     else
-                                       'download'
-                                   }
-                                   pulse={name in @state.installingPluginNames}/>
-                      {
-                        if name in @state.installingPluginNames
-                          __ "Installing"
-                        else
-                          __ "Install"
-                      }
-                    </Button>
-                  </ButtonGroup>
-

-              </Col>
-            </Col>
-          </Col>
+          <UninstalledPlugin
+            key={name}
+            name={name}
+            record={value}
+            npmWorkding={@state.npmWorkding}
+            installing={name in @state.installingPluginNames}
+            handleClickAuthorLink={@handleClickAuthorLink}
+            handleInstall={@handleInstall}
+            />
       }
       </Grid>
     </form>


Use notification instead of modal

Use notification instead of modal

--- views/components/settings/parts/plugin-config.cjsx
+++ views/components/settings/parts/plugin-config.cjsx
@@ -178,17 +178,17 @@
     for updateObject, index in updateData
       latest[updateObject[1]] = updateObject[4]
     if isfirst && updateData.length > 0
-      title = {__ 'Plugin update'}
-      content =
-

-          {
-            for plugin, index in plugins
-              if semver.lt(plugin.version, latest[plugin.packageName])
-                {plugin.displayName}
-          }
-          {__ "have newer version. Please update your plugins."}
-

-      toggleModal title, content
+      title = __ 'Plugin update'
+      content = ""
+      for plugin, index in plugins
+        if semver.lt(plugin.version, latest[plugin.packageName])
+          for child in plugin.displayName.props.children
+            if typeof child is "string"
+              content = "#{content} #{child}"
+      content = "#{content} #{__ "have newer version. Please update your plugins."}"
+      notify content,
+        type: title
+        icon: path.join(ROOT, 'assets', 'img', 'material', '7_big.png')
     @setState
       latest: latest
       checking: false


Add feature keeping image

Add feature keeping image

--- src/main/coffee/contentscript.coffee
+++ src/main/coffee/contentscript.coffee
@@ -53,11 +53,19 @@
       $that.data 'origin' , $that.html()

     embedMap = {}
+
+
+    # for embed gadgets
     $that.find('.sites-embed').each (index)->
       $embed = $ @
       key = "%%%embed#{index}%%%"
       $embed.before "<div>#{key}</div>"
       embedMap[key] = $embed
+
+    # for image
+    $that.find("img").each (index)->
+      $img = $ @
+      $img.before "![#{$img.attr('alt')}](#{$img.attr('src')})"

     result = marked $that[0].innerText
     list = []


stub trackFairVisit

stub trackFairVisit

--- apps/fair/test/client/info.coffee
+++ apps/fair/test/client/info.coffee
@@ -3,10 +3,10 @@
 Backbone = require 'backbone'
 sinon = require 'sinon'
 { resolve } = require 'path'
-rewire = require 'rewire'
 { fabricate } = require 'antigravity'
 Fair = require '../../../../models/fair.coffee'
 Profile = require '../../../../models/profile.coffee'
+FairInfo = require '../../client/info.coffee'

 describe 'FairInfoView', ->

@@ -32,7 +32,7 @@
     }, =>
       $('body').html '

'

-      sinon.stub(FairInfo, 'trackFairVisit')
+      sinon.stub(FairInfo::, 'trackFairVisit')

       @view = new FairInfo
         el: $('#fair')


Removed the fetch

Removed the fetch

--- client/events.coffee
+++ client/events.coffee
@@ -230,10 +230,10 @@
   $(".categories-chooser").chosen()

 Template.category.helpers
-  'events': -> getEvents(categories: Session.get "category").fetch()
+  'events': -> getEvents(categories: Session.get "category")

 Template.search.helpers
   'found_events': ->
     q = Session.get("q")
     re = new RegExp("#{q}.*", 'i')
-    Events.find($or: [{name: re}, {description: re}, {categories: re}, {location: re}]).fetch()
+    getEvents($or: [{name: re}, {description: re}, {categories: re}, {location: re}])


ipython file open change: check change time instead of modification time when deciding whether to re-open disk file

--- salvus/page/editor.coffee
+++ salvus/page/editor.coffee
@@ -4195,8 +4195,8 @@
                 salvus_client.exec
                     project_id : @editor.project_id
                     path       : @path
-                    command    : "stat"
-                    args       : ['--printf', '%Y ', @file, @syncdoc_filename]
+                    command    : "stat"   # %Z below = time of last change, seconds since Epoch; use this not %Y since often users put file in place, but with old time
+                    args       : ['--printf', '%Z ', @file, @syncdoc_filename]
                     timeout    : 15
                     err_on_exit: false
                     cb         : (err, output) =>


tweak build

tweak build

--- brunch-config.coffee
+++ brunch-config.coffee
@@ -1,9 +1,12 @@
 fs = require 'fs'
+
+LICENSES = 'LICENSES.js'
+OUT_FILE = 'sim-shim-bundle'

 module.exports =
   files:
     javascripts:
-      joinTo: 'sim-shim-bundle.js'
+      joinTo: OUT_FILE + '.js'

   paths:
     watched: ['src', 'vendor']
@@ -18,13 +21,12 @@
     production:
       files:
         javascripts:
-          joinTo: 'sim-shim-bundle.min.js'
+          joinTo: OUT_FILE + '.min.js'
       sourceMaps: true

   hooks:
     onCompile: (genFiles, changedAssets) ->
       # write licenses
-      f = genFiles[0] # only one output
-      lic = fs.readFileSync('LICENSES.js') # read existing contents into data
-      fs.appendFile f.path, lic, (err) =>
-        throw "Couldn't write Licenses!"
+      f = genFiles[0].path
+      lic = fs.readFileSync(LICENSES)
+      fs.appendFile f, lic, (err) => throw "Couldn't write Licenses!"


make sure plot pane is focused when getting size

--- lib/runtime/plots.coffee
+++ lib/runtime/plots.coffee
@@ -18,7 +18,6 @@
     @pane.open split: 'right'

   ensureVisible: ->
-    return Promise.resolve(@pane) if @pane.currentPane()
     @open()

   show: (view) ->


redact parts of the personal access token (#395)

Logging the full personal access token is a security-leak hazard, even
if it's useful for debugging. So let's redact parts of it, so we have
enough to reasonably validate the API-token. Copy/paste errors are more
likely in the beginning and end than the middle.

--- lib/sync-settings.coffee
+++ lib/sync-settings.coffee
@@ -262,7 +262,7 @@

   createClient: ->
     token = @getPersonalAccessToken()
-    console.debug "Creating GitHubApi client with token = #{token}"
+    console.debug "Creating GitHubApi client with token = #{token.substr(0, 4)}...#{token.substr(-4, 4)}"
     github = new GitHubApi
       version: '3.0.0'
       # debug: true


:non_potable_water: Plug instances leakage when spanning more than a screen in an update

--- lib/table-element.coffee
+++ lib/table-element.coffee
@@ -57,9 +57,9 @@
   @pool 'gutterCell', 'gutterCells'

   createdCallback: ->
-    @cells = []
-    @headerCells = []
-    @gutterCells = []
+    @cells = {}
+    @headerCells = {}
+    @gutterCells = {}

     @subscriptions = new CompositeDisposable

@@ -926,6 +926,8 @@
        not @hasChanged
       return

+    maxCellCount = ((lastVisibleRow - firstVisibleRow) + @getRowOverdraw() * 2) * ((lastVisibleColumn - firstVisibleColumn) + @getColumnOverdraw() * 2)
+
     rowOverdraw = @getRowOverdraw()
     firstRow = Math.max 0, firstVisibleRow - rowOverdraw
     lastRow = Math.min @tableEditor.getScreenRowCount(), lastVisibleRow + rowOverdraw
@@ -956,6 +958,34 @@

       @appendGutterCell(row) for row in visibleRows

+    # Whole table redraw, when the table suddenly jump from one edge to the
+    # other and the old and new visible range doesn't intersect.
+    else if lastRow < @firstRenderedRow or firstRow >= @lastRenderedRow or lastColumn < @firstRenderedColumn or firstColumn >= @lastRenderedColumn
+
+      @releaseCell(cell) for key,cell of @cells
+      @releaseGutterCell(cell) for row,cell of @gutterCells
+      @releaseHeaderCell(cell) for column,cell of @headerCells
+
+      @cells = {}
+      @headerCells = {}
+      @gutterCells = {}
+
+      for column in visibleColumns
+        @appendHeaderCell(columns[column], column)
+        @appendCell(row, column) for row in visibleRows
+
+      @appendGutterCell(row) for row in visibleRows
+
+      # For the moment we don't want to update the intact range so we return
+      # early
+      @firstRenderedRow = firstRow
+      @lastRenderedRow = lastRow
+      @firstRenderedColumn = firstColumn
+      @lastRenderedColumn = lastColumn
+      @hasChanged = false
+      return
+
+    # Classical scroll routine
     else if firstRow isnt @firstRenderedRow or lastRow isnt @lastRenderedRow or firstColumn isnt @firstRenderedColumn or lastColumn isnt @lastRenderedColumn
       disposed = 0
       created = 0
@@ -998,11 +1028,10 @@
           @appendHeaderCell(columns[column], column)
           @appendCell(row, column) for row in visibleRows

-
     for row in [intactFirstRow...intactLastRow]
       @gutterCells[row]?.setModel({row})
       for column in [intactFirstColumn...intactLastColumn]
-        @cells[column][row]?.setModel(@getCellObjectAtPosition([row, column]))
+        @cells[row + '-' + column]?.setModel(@getCellObjectAtPosition([row, column]))
     for column in [intactFirstColumn...intactLastColumn]
       @headerCells[column]?.setModel({column: columns[column], index: column})

@@ -1059,13 +1088,11 @@

   getScreenCellAtPosition: (position) ->
     position = Point.fromObject(position)
-    @cells[position.column][position.row]
+    @cells[position.row + '-' + position.column]

   appendCell: (row, column) ->
-    @cells[column] ?= []
-    return @cells[column][row] if @cells[column][row]?
-
-    @cells[column][row] = @requestCell(@getCellObjectAtPosition([row, column]))
+    key = row + '-' + column
+    @cells[key] ? @cells[key] = @requestCell(@getCellObjectAtPosition([row, column]))

   getCellObjectAtPosition: (position) ->
     {row, column} = Point.fromObject(position)
@@ -1079,15 +1106,14 @@
     }

   disposeCell: (row, column) ->
-    cell = @cells[column]?[row]
+    key = row + '-' + column
+    cell = @cells[key]
     return unless cell?
     @releaseCell(cell)
-    @cells[column][row] = undefined
+    delete @cells[key]

   appendHeaderCell: (column, index) ->
-    return @headerCells[index] if @headerCells[index]?
-
-    @headerCells[index] = @requestHeaderCell({column, index})
+    @headerCells[index] ? @headerCells[index] = @requestHeaderCell({column, index})

   disposeHeaderCell: (column) ->
     return unless cell = @headerCells[column]
@@ -1095,9 +1121,7 @@
     delete @headerCells[column]

   appendGutterCell: (row) ->
-    return @gutterCells[row] if @gutterCells[row]?
-
-    @gutterCells[row] = @requestGutterCell({row})
+    @gutterCells[row] ? @gutterCells[row] = @requestGutterCell({row})

   disposeGutterCell: (row) ->
     return unless cell = @gutterCells[row]


Author -> Autor

Author -> Autor

Author is in German: Autor
Plural: Autors -> Autoren

--- lib/translations/de.cson
+++ lib/translations/de.cson
@@ -12,7 +12,7 @@
     "tags": "Tags"
     "pages": "Pages"
     "posts": "Posts"
-    "authors": "Authors"
+    "authors": "Autoren"
     "search": "Suche"
     "bookmarks": "Lesezeichen"
     "socialNetworks": "Soziale Netzwerke"
@@ -48,9 +48,9 @@
 "sharing":
     "shared": "Geteilt!"
 "authors":
-    "title": "Authors"
+    "title": "Autoren"
 "author":
-    "title": "Author: {{name}}"
+    "title": "Autor: {{name}}"
 "pages":
     "title": "Pages"
 "posts":


Fixed compatibility issues with IE

Fixed compatibility issues with IE

--- src/ng-paste-from.coffee
+++ src/ng-paste-from.coffee
@@ -24,8 +24,11 @@
 				console.error "Missing required attribute ngPasteFromColumns."

 			$scope.pasteEvent = (event) ->
-				clipboardData = window.clipboardData || event.clipboardData || event.originalEvent && event.originalEvent.clipboardData
-				data = clipboardData.getData "text/plain"
+				if event.clipboardData? && event.clipboardData.getData? # Standard
+					data = event.clipboardData.getData "text/plain"
+				else if window.clipboardData? && window.clipboardData.getData? # Internet Explorer
+					data = window.clipboardData.getData "Text"
+
 				if typeof $scope.ngPasteFromBeforeParse is "function"
 					data = $scope.ngPasteFromBeforeParse data
 				$scope.processPasteData data


coffee-script 1.1.0

coffee-script 1.1.0

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -21,7 +21,7 @@
   require.registerExtension '.coffee', (content) -> compile content

 # The current CoffeeScript version number.
-exports.VERSION = '1.1.0-pre'
+exports.VERSION = '1.1.0'

 # Words that cannot be used as identifiers in CoffeeScript code
 exports.RESERVED = RESERVED


JUser::convert: instead provided locationData, use clientIP to guess region

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -1,4 +1,5 @@
 jraphical = require 'jraphical'
+Regions   = require 'koding-regions'

 Flaggable = require '../../traits/flaggable'

@@ -769,11 +770,11 @@
       guestsGroup.removeMember account, callback

   @convert = secure (client, userFormData, callback) ->
-    { connection, sessionToken : clientId } = client
+    { connection, sessionToken : clientId, clientIP } = client
     { delegate : account } = connection
     { nickname : oldUsername } = account.profile
-    { username, email, firstName, lastName, locationData
-      agree, inviteCode, referrer, password, passwordConfirm } = userFormData
+    { username, email, firstName, lastName, agree,
+      inviteCode, referrer, password, passwordConfirm } = userFormData

     if not firstName or firstName is "" then firstName = username
     if not lastName then lastName = ""
@@ -788,8 +789,8 @@
     if password isnt passwordConfirm
       return callback createKodingError "Passwords must match!"

-    if locationData?
-      try { ip, country, region } = JSON.parse locationData
+    if clientIP
+      { ip, country, region } = Regions.findLocation clientIP

     newToken       = null
     invite         = null


Alias guest user name to logged in user name

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -863,10 +863,13 @@
           $unset          :
             guestId       : 1

+        guestUsername = session.username
+
         session.update sessionUpdateOptions, (err) ->
           return callback err  if err

           Tracker.identify username, { lastLoginDate }
+          Tracker.alias guestUsername, username

           queue.next()


followable was complaining. i gave it a jtag. now it is happy.

--- workers/social/lib/social/traits/followable.coffee
+++ workers/social/lib/social/traits/followable.coffee
@@ -197,6 +197,7 @@
       if err then callback err
       else
         ids = (rel.sourceId for rel in docs)
+        JTag = require '../models/tag'
         JTag.all _id: $in: ids, (err, accounts)->
           callback err, accounts


generalize tf op vars

generalize tf op vars

--- src/tf.coffee
+++ src/tf.coffee
@@ -153,7 +153,7 @@
     brainloc = "hubot-tf_#{projname}"
     localstorage = JSON.parse(robot.brain.get brainloc) or {}
     ekvs = []
-    ekvs.push "TF_VAR_#{k}=#{v}" for k,v of localstorage
+    ekvs.push "#{k}=#{v}" for k,v of localstorage
     environment = ekvs.join " "

     cmdline = "cd #{basepath}/#{projname}; #{environment} terraform #{action} -input=false -no-color"
@@ -201,7 +201,7 @@
     robot.brain.set brainloc, JSON.stringify(localstorage)
     robot.brain.save()

-    return msg.send {room: msg.message.user.name}, "`#{projname}` env unset: `#{ekey}`"
+    return msg.send {room: msg.message.user.name}, "`#{projname}` env `#{ekey}` unset."

   robot.respond /tf env ([^\s]+)(?:\slist)?$/i, (msg) ->
     unless robot.auth.isAdmin(msg.envelope.user) or robot.auth.hasRole(msg.envelope.user,tfRole)


Adding events for start/end of loading.

Adding events for start/end of loading.

--- src/d3.timeslider.coffee
+++ src/d3.timeslider.coffee
@@ -532,6 +532,14 @@
         @svg.classed('loading', isLoading)
         d3.select('.reload-arrow').classed('arrowloading', isLoading)

+        if @isLoading != isLoading
+            if isLoading
+                @dispatch('loadStart')
+            else
+                @dispatch('loadEnd')
+
+            @isLoading = isLoading
+
     ###
     ## Public API
     ###


[IMPAC-890] Do not accept trends groups with the same name

--- src/components/common/trends-add/trends-add.component.coffee
+++ src/components/common/trends-add/trends-add.component.coffee
@@ -12,7 +12,7 @@
     boltPath: '<'
     companyId: '<'

-  controller: ($scope, HighchartsFactory, BoltResources) ->
+  controller: ($scope, HighchartsFactory, BoltResources, toastr) ->
     ctrl = this

     ctrl.$onInit = ->
@@ -59,7 +59,10 @@
       ctrl.dataIsValid()

     ctrl.createTrend = ->
-      return ctrl.createGroup() if ctrl.isAddingGroup
+      if ctrl.isAddingGroup
+        if _.find(ctrl.groups, (g) -> g.attributes.name == ctrl.trend.groupName)
+          return toastr.warning('A group with this name already exists')
+        return ctrl.createGroup()
       ctrl.onHide()
       ctrl.trend.untilDate = lastApplicationDate(ctrl.trend)
       ctrl.trend.period = ctrl.trend.period.toLowerCase()


all unit tests passing

all unit tests passing

--- src/gerber-parser.coffee
+++ src/gerber-parser.coffee
@@ -4,6 +4,8 @@

 # parse coordinate function
 parseCoord = require './coord-parser'
+# get integer function
+getInteger = require './get-integer'

 # constants
 # regular expression to match a coordinate
@@ -39,22 +41,45 @@
     switch shape
       # circle
       when 'C'
-        if mods.length > 2 then hole = { width: +mods[1], height: +mods[2] }
-        else if mods.length > 1 then hole = { dia: +mods[1] }
-        c.tool[code] = { dia: +mods[0] }
+        if mods.length > 2 then hole = {
+          width:  getInteger mods[1], { places: @format.places }
+          height: getInteger mods[2], { places: @format.places }
+        }
+        else if mods.length > 1 then hole = {
+          dia: getInteger mods[1], { places: @format.places }
+        }
+        c.tool[code] = {
+          dia: getInteger mods[0], { places: @format.places }
+        }
         if hole? then c.tool[code].hole = hole
       # rectangle, obround
       when 'R', 'O'
-        if mods.length > 3 then hole = { width: +mods[2], height: +mods[3] }
-        else if mods.length > 2 then hole = { dia: +mods[2] }
-        c.tool[code] = {width: +mods[0], height: +mods[1] }
+        if mods.length > 3 then hole = {
+          width:  getInteger mods[2], { places: @format.places }
+          height: getInteger mods[3], { places: @format.places }
+        }
+        else if mods.length > 2 then hole = {
+          dia: getInteger mods[2], { places: @format.places }
+        }
+        c.tool[code] = {
+          width:  getInteger mods[0], { places: @format.places }
+          height: getInteger mods[1], { places: @format.places }
+        }
         if shape is 'O' then c.tool[code].obround = true
         if hole? then c.tool[code].hole = hole
       # polygon
       when 'P'
-        if mods.length > 4 then hole = { width: +mods[3], height: +mods[4] }
-        else if mods.length > 3 then hole = { dia: +mods[3] }
-        c.tool[code] = { dia: +mods[0], verticies: +mods[1] }
+        if mods.length > 4 then hole = {
+          width:  getInteger mods[3], { places: @format.places }
+          height: getInteger mods[4], { places: @format.places }
+        }
+        else if mods.length > 3 then hole = {
+          dia: getInteger mods[3], { places: @format.places }
+        }
+        c.tool[code] = {
+          dia:       getInteger mods[0], { places: @format.places }
+          verticies: +mods[1]
+        }
         if mods.length > 2 then c.tool[code].degrees = +mods[2]
         if hole? then c.tool[code].hole = hole
       # else aperture macro


Allow Metric::track to pass options through to Trackers.

This allows them to cue the Tracker that an event should be tracked instantly. The actual behavior is still at the discretion of the Tracker.

--- client/Metric.coffee
+++ client/Metric.coffee
@@ -17,10 +17,14 @@
         @_last_tracked = null

     # Can take either a key/value pair, or an object.
-    track: (event_data_or_key, value=null) =>
+    # Options can be an Object or null, and is passed along to the Trackers.
+    # It can be anything, and it is up to the Trackers to decide what to do.
+    # Use-case: marking some events as `instant`, ie they should be tracked
+    # immediately instead of buffered.
+    track: (event_data_or_key, value=null, options=null) =>

         # Skip preparing the track if nothing is subscribed.
-        unless _subscriptions[@_name] or _subscriptions['*']
+        unless _subscriptions[@_name] or (_subscriptions['*'] and @_name[0] isnt '_')
             return

         if typeof event_data_or_key is 'object'
@@ -43,16 +47,16 @@
             @_last_tracked = event_data._date
             event_data._ms_since_last = null

-        @_fire(@_name, event_data)
+        @_fire(@_name, event_data, options)
         # Trackers must opt-in specifically to Metrics prefixed with `_`.
-        @_fire('*', event_data) unless @_name is '*' or @_name[0] is '_'
+        @_fire('*', event_data, options) unless @_name is '*' or @_name[0] is '_'

-    _fire: (metric_name, event_data) ->
+    _fire: (metric_name, event_data, options) ->
         _subscriptions[metric_name]?.forEach (tracker) =>
             if tracker.track?
-                tracker.track(@_name, event_data)
+                tracker.track(@_name, event_data, options)
             else
-                tracker(@_name, event_data)
+                tracker(@_name, event_data, options)

     # Trackers can subscribe to specific metrics, or any using
     # Metric.subscribe(tracker) or Metric.subscribe('*', tracker)


initialize pixi and add canvas element to page

--- client/client.coffee
+++ client/client.coffee
@@ -17,6 +17,16 @@
                 DomView.showLogin().then authenticated

     setupGame: ->
+        @setupPixi()
+        @setupConnection()
+        @startGameLoop()
+
+    setupPixi: ->
+        @stage = new PIXI.Stage 0xf5f5f5
+        @renderer = new PIXI.WebGLRenderer 1024, 576
+        @viewport.append @renderer.view
+
+    setupConnection: ->
         @socket = io.connect 'http://localhost:5000'
 #        window.setInterval ( =>
 #            console.log 'emitting'
@@ -24,3 +34,5 @@
 #        ), 500
 #        @socket.on 'server-event', (msg) ->
 #            console.log "message received", msg
+
+    startGameLoop: ->


Change cursor when dragging files. Fixes #215

--- client/import.coffee
+++ client/import.coffee
@@ -158,8 +158,12 @@

     if Meteor.personId()
       Session.set 'importOverlayActive', true
+      e.dataTransfer.effectAllowed = 'copy'
+      e.dataTransfer.dropEffect = 'copy'
     else
       Session.set 'loginOverlayActive', true
+      e.dataTransfer.effectAllowed = 'none'
+      e.dataTransfer.dropEffect = 'none'

 Template.loginOverlay.loginOverlayActive = ->
   Session.get 'loginOverlayActive'
@@ -167,6 +171,8 @@
 Template.loginOverlay.events =
   'dragover': (e, template) ->
     e.preventDefault()
+    e.dataTransfer.effectAllowed = 'none'
+    e.dataTransfer.dropEffect = 'none'

   'dragleave': (e, template) ->
     e.preventDefault()
@@ -182,6 +188,8 @@
 Template.importOverlay.events =
   'dragover': (e, template) ->
     e.preventDefault()
+    e.dataTransfer.effectAllowed = 'copy'
+    e.dataTransfer.dropEffect = 'copy'

   'dragleave': (e, template) ->
     e.preventDefault()


[Janky] Replace all spaces in user's name when requesting a new build.

Janky is returning a 400 (Bad Request) error at times when a request for a new
build is made. Those errors are due to whitespaces that are being used in user's
names for campfire and possibly another chat engines.
Replacement of all spaces for plus signs "+" in user's names makes the URL that
is being posted to from hubot's janky script compliant to the specification [1]
and solves the issue.

[1] http://www.w3.org/Addressing/URL/url-spec.txt

--- src/scripts/janky.coffee
+++ src/scripts/janky.coffee
@@ -77,7 +77,7 @@
     app     = msg.match[1]
     branch  = msg.match[3] || "master"
     room_id = msg.message.user.room
-    user    = msg.message.user.name
+    user    = msg.message.user.name.replace(/\ /g, "+")

     post "#{app}/#{branch}?room_id=#{room_id}&user=#{user}", {}, (err, statusCode, body) ->
       if statusCode == 201 or statusCode == 404


Fix non-capturing group syntax in stock.coffee

Corrected the non-capturing group regex syntax (changed from `:?` to `?:`)

--- src/scripts/stock.coffee
+++ src/scripts/stock.coffee
@@ -16,7 +16,7 @@
 #   johnwyles

 module.exports = (robot) ->
-  robot.respond /stock (:?info|price|quote)?\s?(:?for|me)?\s?@?([A-Za-z0-9.-_]+)\s?(\d+\w+)?/i, (msg) ->
+  robot.respond /stock (?:info|price|quote)?\s?(?:for|me)?\s?@?([A-Za-z0-9.-_]+)\s?(\d+\w+)?/i, (msg) ->
     ticker = escape(msg.match[1])
     time = msg.match[2] || '1d'
     msg.http('http://finance.google.com/finance/info?client=ig&q=' + ticker)


Removed second eventric

Removed second eventric

--- src/server/server.coffee
+++ src/server/server.coffee
@@ -2,7 +2,6 @@
 webserver               = express()
 http                    = require('http').Server(webserver)
 websocket               = require('socket.io')(http)
-eventric                = require 'eventric'
 eventric                = require 'eventric'
 socketIORemoteEndpoint  = require 'eventric-remote-socketio-endpoint'
 eventricMongoStore      = require 'eventric-store-mongodb'


Get creative.

Get creative.

--- base/assets/javascripts/graphs.coffee
+++ base/assets/javascripts/graphs.coffee
@@ -12,7 +12,7 @@
   console.log line

   area = d3.svg.area()
-    .interpolate('basis')
+    .interpolate('cardinal')
     .x((d) -> x(d.index))
     .y0(h)
     .y1((d) -> y(d.identifier))


Fix formatting of a coffee dict in plot.coffee

--- bokehjs/src/coffee/common/plot.coffee
+++ bokehjs/src/coffee/common/plot.coffee
@@ -560,8 +560,10 @@
       if box?
         box.set('solver', solver)
       else
-        box = new LayoutBox.Model({ name: name,
-        solver: solver })
+        box = new LayoutBox.Model({
+          name: name,
+          solver: solver
+        })
         list.push(box)
         @set(side, list)
       return box


Recall setup() during setup items

Recall setup() during setup items

--- src/sketch/sketch.coffee
+++ src/sketch/sketch.coffee
@@ -397,7 +397,9 @@
         (promise, item) =>
           if item.builder?
             for cls in builderClasses
-              return promise if item.builder instanceof cls
+              return promise.then(=>
+                return item.builder.setup()
+              ) if item.builder instanceof cls
             # Remove unsuported builder
             item.builder = null
           for cls in builderClasses


some corrections in 3d.coffee

some corrections in 3d.coffee

--- src/smc-webapp/3d.coffee
+++ src/smc-webapp/3d.coffee
@@ -464,9 +464,9 @@
                 geometry.faces.push(new THREE.Face3(a-1, b-1, c-1))
                 #geometry.faces.push(new THREE.Face3(b-1, a-1, c-1))   # both sides of faces, so material is visible from inside -- but makes some things like look really crappy; disable.  Better to just set a property of the material/light, which fixes the same problem.

-           push_face3_with_color = (a, b, c, d) =>
+           push_face3_with_color = (a, b, c, col) =>
                 face = new THREE.Face3(a-1, b-1, c-1)
-                face.color.setHex(d)
+                face.color.setHex(col)
                 geometry.faces.push(face)
                 #geometry.faces.push(new THREE.Face3(b-1, a-1, c-1))   # both sides of faces, so material is visible from inside -- but makes some things like look really crappy; disable.  Better to just set a property of the material/light, which fixes the same problem.

@@ -552,14 +552,13 @@
                         shininess   : "1"
                         wireframe   : false
                         transparent : m.opacity < 1
-                    material.color.setRGB(m.color[0],    m.color[1],    m.color[2])
+                        vertexColors: THREE.FaceColors
                 else
                     material =  new THREE.MeshPhongMaterial
                         shininess   : "1"
                         wireframe   : false
                         transparent : m.opacity < 1
-                        vertexColors: THREE.FaceColors
-
+                    material.color.setRGB(m.color[0],    m.color[1],    m.color[2])
                 material.specular.setRGB(m.specular[0], m.specular[1], m.specular[2])
                 material.opacity = m.opacity


Fixed incorrect selector

Fixed incorrect selector

--- src/scripts/basicNotification.coffee
+++ src/scripts/basicNotification.coffee
@@ -5,7 +5,7 @@
 	_dom: (id) ->

 		if not id? then return $('.basicNotification')
-		else return $(".basicModal[data-id='#{ id }']")
+		else return $(".basicNotification[data-id='#{ id }']")

 	_valid: (data) ->


Removed userId in favor of personId.

Removed userId in favor of personId.

--- server/publication.coffee
+++ server/publication.coffee
@@ -384,17 +384,15 @@
     removePublications removed

   handlePersons = Persons.find(
-    'user._id': @userId
+    _id: @personId
   ,
     fields:
       # id field is implicitly added
-      'user._id': 1
       library: 1
   ).observeChanges
     added: (id, fields) =>
       # There should be only one person with the id at every given moment
       assert.equal currentPersonId, null
-      assert.equal fields.user._id, @userId

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


check if all results are out

check if all results are out

--- src/test/template.coffee
+++ src/test/template.coffee
@@ -25,9 +25,10 @@
                                 li = @$li({'data-content':data}, data)
                         @end()

-
-        xml.ready æ.done
         xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag
+        xml.stream.on 'end', ->
+            æ.equal 0, results.length
+            æ.done()
         results = [
             '<!DOCTYPE html>'
             ''
@@ -50,6 +51,7 @@
             ''
         ]

+
     pretty: (æ) ->
         xml = streamify new Template schema:5, pretty:" ", doctype:on, ->
             @$html ->
@@ -57,8 +59,11 @@
                     @$title "holla"
                 @$body ->
                     @$p "hello world"
-        xml.ready æ.done
+#         xml.ready æ.done
         xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag
+        xml.stream.on 'end', ->
+            æ.equal 0, results.length
+            æ.done()
         results = [
             '<!DOCTYPE html>\n'
             '\n'
@@ -74,6 +79,7 @@
             ' \n'
             '\n'
         ]
+

     layout: (æ) ->

@@ -96,8 +102,10 @@
             title:'test'
             content:'..'

-        xml.ready æ.done
         xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag
+        xml.stream.on 'end', ->
+            æ.equal 0, results.length
+            æ.done()
         results = [
             ''
             ''
@@ -115,14 +123,17 @@
             ''
         ]

+
     'self closing': (æ) ->
         xml = streamify new Template schema:5, pretty:" ", doctype:on, ->
             @$html ->
                 @$body ->
                     @$div()
                     @$br()
-        xml.ready æ.done
         xml.stream.on 'data', (tag) -> æ.equal results.shift(), tag
+        xml.stream.on 'end', ->
+            æ.equal 0, results.length
+            æ.done()
         results = [
             '<!DOCTYPE html>\n'
             '\n'


add data handler back

add data handler back

--- src/smc-webapp/client_browser.coffee
+++ src/smc-webapp/client_browser.coffee
@@ -260,6 +260,7 @@
             @_num_attempts = 0

             conn.removeAllListeners('data')
+            conn.on("data", ondata)

             if auth_token?
                 @sign_in_using_auth_token


KDObject: app related method removed from framework

--- core/KDObject.coffee
+++ core/KDObject.coffee
@@ -53,8 +53,6 @@
   getInstance:(instanceId)->
     KD.getAllKDInstances()[instanceId] ? null

-  requireMembership:KD.requireMembership
-
   registerKDObjectInstance: -> KD.registerInstance @

   setData:(@data)->


Call .getElement if the model has it.

Call .getElement if the model has it.

--- src/view-registry.coffee
+++ src/view-registry.coffee
@@ -171,6 +171,11 @@
     if object instanceof HTMLElement
       return object

+    if typeof object?.getElement is 'function'
+      element = object.getElement()
+      if element instanceof HTMLElement
+        return element
+
     if object?.element instanceof HTMLElement
       return object.element


Export server for testing purposes

Export server for testing purposes

--- coffee/server.coffee
+++ coffee/server.coffee
@@ -34,4 +34,6 @@
 if not module.parent
   server.start () ->
     console.log 'Server is running...'
+else
+  module.exports = server


invitations: fix merge conflict

invitations: fix merge conflict

--- client/Login/AppController.coffee
+++ client/Login/AppController.coffee
@@ -1,11 +1,10 @@
 class LoginAppsController extends AppController

-  handler = (callback)->
-    ({params})->
-      unless KD.isLoggedIn()
-        KD.singleton('appManager').open 'Login', (app)-> callback app, params
-      else
-        KD.getSingleton('router').handleRoute "/Activity"
+  handler = (callback)->->
+    unless KD.isLoggedIn()
+      KD.singleton('appManager').open 'Login', (app)-> callback app
+    else
+      KD.getSingleton('router').handleRoute "/Activity"

   handleResetRoute = ({params:{token}})->
     KD.singleton('appManager').open 'Login', (app)=>


fixes comparator for all approved entries

--- client/collections/entries.coffee
+++ client/collections/entries.coffee
@@ -7,4 +7,6 @@

   url: "#{sd.API_URL}/entries"

-  model: Entry+  model: Entry
+
+  comparator: (model) -> - parseInt model.get('created_time')


Use parseLevel in TopPanel

Use parseLevel in TopPanel

--- client/components/TopPanel.coffee
+++ client/components/TopPanel.coffee
@@ -30,12 +30,14 @@
 import GROUPS from '../lib/groups'
 import isPaid, {unpaidBlocked} from '../lib/isPaid'
 import needDeactivatedWarning from '../lib/needDeactivatedWarning'
+import {parseLevel} from '../lib/level'
 import withLang from '../lib/withLang'

 import styles from './TopPanel.css'

 needCfWarning = (user) ->
-    (not user.cf?.login?) and (user.level.current >= "1В" || (user.level.current >= "1C" && user.level.current <= "1Z"))
+    level = parseLevel(user?.level?.current)
+    (not user.cf?.login?) and (level.major > 1 || level.minor >= 2)

 needUnpaidWarning = (user) ->
     (GROUPS[user?.userList]?.paid) and (user?.paidTill) && (not isPaid(user))


Display thumbnail images based on chosen view.

--- client/room_list/room_list.coffee
+++ client/room_list/room_list.coffee
@@ -8,6 +8,15 @@
   @$('[data-toggle="tooltip"]').tooltip()

 Template.room_summary.helpers
+  image: (room) ->
+    switch Session.get 'view'
+      when 'view-screen'
+        file = 'screen.jpg'
+      when 'view-camera'
+        file = 'presenter.jpg'
+      when 'view-galicaster'
+        file = 'presentation.jpg'
+    "http://localhost:3000/image/#{room}/#{file}"
   thumbnail: ->
     switch Session.get 'view'
       when 'view-screen'


Tests: IDE: Layout: Enabled undoSplitPanesNotShowOnScreen test.

--- client/test/lib/ide/layout.coffee
+++ client/test/lib/ide/layout.coffee
@@ -33,23 +33,23 @@
     browser.end()

-  # undoSplitPanesNotShowOnScreen: (browser) ->
+  undoSplitPanesNotShowOnScreen: (browser) ->

-  #   helpers.beginTest(browser)
-  #   helpers.waitForVMRunning(browser)
-  #   newPaneSelector = '.kdsplitcomboview .kdsplitview-panel.panel-1 .application-tab-handle-holder'
+    helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)
+    newPaneSelector = '.kdsplitcomboview .kdsplitview-panel.panel-1 .application-tab-handle-holder'

-  #   fn = ->
-  #     browser.elements 'css selector', newPaneSelector, (result) =>
-  #       if result.value.length is 1
-  #         browser
-  #           .waitForElementPresent '.panel-1 .general-handles .close-handle.hidden', 20000 # Assertion
-  #           .end()
-  #       else
-  #         layoutHelpers.undoSplit(browser, no)
-  #         fn()
+    fn = ->
+      browser.elements 'css selector', newPaneSelector, (result) =>
+        if result.value.length is 1
+          browser
+            .waitForElementPresent '.panel-1 .general-handles .close-handle.hidden', 20000 # Assertion
+            .end()
+        else
+          layoutHelpers.undoSplit(browser, no)
+          fn()

-  #   fn()
+    fn()

   openDrawingBoard: (browser) ->


remove duplicated registration switch clause

--- coach/src/concept-coach/base.cjsx
+++ coach/src/concept-coach/base.cjsx
@@ -179,8 +179,6 @@
         <Dashboard cnxUrl={@props.cnxUrl}/>
       when 'profile'
         <AccountsIframe type='profile' onComplete={@updateUser} />
-      when 'registration'
-        <CourseRegistration {...@props} />
       when 'student_id'
         <UpdateStudentIdentifier {...@props} course={course} />
       else


added option to language

added option to language

--- src/languages/php.coffee
+++ src/languages/php.coffee
@@ -44,6 +44,12 @@
       type: 'string'
       default: ""
       description: "Add rule(s). i.e. line_ending,-full_opening_tag,@PSR2 (PHP-CS-Fixer 2 only)"
+    allow_risky:
+      title: "Allow risky rules"
+      type: 'string'
+      default: "no"
+      enum: ["no", "yes"]
+      description: "allow risky rules to be applied!"
     phpcbf_path:
       title: "PHPCBF Path"
       type: 'string'


View: Fixed annotation for render method.

--- src/lib/core/View.coffee
+++ src/lib/core/View.coffee
@@ -285,7 +285,7 @@
     by default.

     @export
-    @param {spark.core.View|Element}
+    @param {spark.core.View|Element} target Target view or element to be rendered.
   ###
   render: (target) ->
     if target instanceof spark.core.View


registry: style - whitespace

registry: style - whitespace

--- coffee/src/shells/registry.coffee
+++ coffee/src/shells/registry.coffee
@@ -9,8 +9,8 @@
   @registerModule: (shellModule) =>

     # validate that shellModule contains required String properties
-    required_properties = [ 'id', 'title', 'description' ]
-    _.each required_properties, (property) ->
+    requiredProperties = [ 'id', 'title', 'description' ]
+    _.each requiredProperties, (property) ->
       unless shellModule[property]?
         MissingParameterError "shell registration", property

@@ -27,14 +27,14 @@
         class shellModule[key] extends value

     # validate shellModule's Model and View properties
-    class_properties = [ 'Model', 'ContentView', 'RemixView']
-    _.each class_properties, (property) ->
+    classProperties = [ 'Model', 'ContentView', 'RemixView']
+    _.each classProperties, (property) ->
       unless _.isFunction shellModule[property]
         TypeError property, 'class'

     # set the `shell` property of each class within shellModule to point
     # back to the shellModule namespace
-    _.each class_properties, (property) ->
+    _.each classProperties, (property) ->
       shellModule[property].shell = shellModule

     # ensure this module isn't already registered