convert stuff to given

convert stuff to given

--- spec/jasmine-stealth-spec.coffee
+++ spec/jasmine-stealth-spec.coffee
@@ -69,33 +69,21 @@
         Then -> expect(@fake).toHaveBeenCalledWith("panda", "baby")

     context "default andReturn plus some conditional stubbing", ->
-      beforeEach ->
-        @spy.andReturn "football"
-        @spy.when("bored").thenReturn "baseball"
+      Given -> @spy.andReturn "football"
+      Given -> @spy.when("bored").thenReturn "baseball"

       describe "it doesn't  appear to invoke the spy", ->
-        it "hasn't been called yet", ->
-          expect(@spy).not.toHaveBeenCalled()
-
-        it "has a callCount of zero", ->
-          expect(@spy.callCount).toBe 0
-
-        it "has nothing in the calls array", ->
-          expect(@spy.calls.length).toBe 0
-
-        it "has no argsForCall entries", ->
-          expect(@spy.argsForCall.length).toBe 0
-
-        it "has no mostRecentCall", ->
-          expect(@spy.mostRecentCall).toEqual {}
+        Then -> expect(@spy).not.toHaveBeenCalled()
+        Then -> @spy.callCount == 0
+        Then -> @spy.calls.length == 0
+        Then -> @spy.argsForCall.length == 0
+        Then -> expect(@spy.mostRecentCall).toEqual({})

       context "stubbing is not satisfied", ->
-        it "returns the default stubbed value", ->
-          expect(@spy("anything at all")).toBe "football"
+        Then -> @spy("anything at all") == "football"

       context "stubbing is satisfied", ->
-        it "returns the specific stubbed value", ->
-          expect(@spy("bored")).toBe "baseball"
+        Then -> @spy("bored") == "baseball"

   describe "#whenContext", ->
     Given -> @ctx = "A"
@@ -110,33 +98,22 @@
       Then -> @result == undefined

   describe "#mostRecentCallThat", ->
-    spy = undefined
-    beforeEach ->
-      spy = jasmine.createSpy()
-      spy "foo"
-      spy "bar"
-      spy "baz"
+    Given -> @spy = jasmine.createSpy()
+    Given -> @spy("foo")
+    Given -> @spy("bar")
+    Given -> @spy("baz")

     context "when given a truth test", ->
-      result = undefined
-      beforeEach ->
-        result = spy.mostRecentCallThat((call) ->
-          call.args[0] is "bar"
-        )
-
-      it "returns the call we want", ->
-        expect(result).toBe spy.calls[1]
+      When -> @result = @spy.mostRecentCallThat (call) ->
+        call.args[0] is "bar"
+      Then -> @result == @spy.calls[1]

     context "when the context matters", ->
-      result = undefined
-      beforeEach ->
-        @panda = "baz"
-        result = spy.mostRecentCallThat((call) ->
-          call.args[0] is @panda
-        , this)
-
-      it "returns the call we want", ->
-        expect(result).toBe spy.calls[2]
+      Given -> @panda = "baz"
+      When -> @result = @spy.mostRecentCallThat((call) ->
+        call.args[0] is @panda
+      , this)
+      Then -> @result == @spy.calls[2]

   describe "jasmine.createStubObj", ->
     context "used just like createSpyObj", ->


:bug: Fix bubble marker leak

:bug: Fix bubble marker leak

--- lib/linter-views.coffee
+++ lib/linter-views.coffee
@@ -57,13 +57,11 @@
     point = point || activeEditor.getCursorBufferPosition()
     for message in @messagesLine
       continue unless message.range?.containsPoint point
-      @bubble = activeEditor.decorateMarker(
-        activeEditor.markBufferRange([point, point], {invalidate: 'never'})
-        {
-          type: 'overlay',
-          position: 'tail',
-          item: @renderBubbleContent(message)
-        }
+      @bubble = activeEditor.markBufferRange([point, point], {invalidate: 'inside'})
+      activeEditor.decorateMarker(@bubble,
+        type: 'overlay',
+        position: 'tail',
+        item: @renderBubbleContent(message)
       )
       break


hittesting edges before nodes removed need for hitTest calc on nodes; using nearest like before

--- public/coffeescripts/renderer.coffee
+++ public/coffeescripts/renderer.coffee
@@ -60,7 +60,6 @@

   drawNode: (node, point) ->
       MARGIN = 10
-
       nodeView = view node.data
       ctx.font = "10pt Times"
       {width, height, count} = util.textSize ctx, nodeView
@@ -69,11 +68,6 @@
       width = Math.min(width, maxWidth)
       height = Math.min(height, maxHeight)

-      # Store dims so we can use them in hit testing later.
-      # TODO sort out the MARGIN business (separate textSize from nodeSize)
-      node.data.width = width + MARGIN*2;
-      node.data.height = height;
-
       color = if node.data.first then "blue" else "green"
       util.roundRect(ctx, point, width+(MARGIN*2), height, color, 10)

@@ -81,7 +75,6 @@
       x = util.centerToEdge(point.x, width)
       y = util.centerToEdge(point.y, height) + MARGIN*2
       util.drawText(ctx, nodeView, x, y)
-

   drawEdge: (edge, fromPoint, toPoint) ->
     ctx.strokeStyle = 'rgba(0,0,0, .333)'
@@ -92,15 +85,6 @@
     ctx.font = "10pt Times"
     ctx.fillStyle ='red'
     util.drawText(ctx, [edge.data.rel_type], x, y)
-
-  nodeHitTest: (node, point) ->
-    p = particleSystem.toScreen(node.p)
-    distx = Math.sqrt(Math.pow(point.x - p.x, 2))
-    disty = Math.sqrt(Math.pow(point.y - p.y, 2))
-    width = node.data.width
-    height = node.data.height
-
-    (distx <= width/2) && (disty <= height/2)

   pointToLineDist: (point, lineP1, lineP2) ->
     # See http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html
@@ -146,17 +130,17 @@
       click: (e) =>
         pos = $(canvas).offset()
         _mouseP = arbor.Point(e.pageX-pos.left, e.pageY-pos.top)
-        object = particleSystem.nearest(_mouseP)

-        if @nodeHitTest(object.node, _mouseP)
+        hitEdge = false
+        particleSystem.eachEdge (edge, fromPoint, toPoint) =>
+          if (@edgeHitTest(edge, _mouseP, 10))
+            objectHandler.selectedEdge edge.data._neo_id
+            hitEdge = true
+            return false
+
+        if !hitEdge
+          object = particleSystem.nearest(_mouseP)
           objectHandler.selectedNode(object.node.name)
-        else
-          # click on an edge?
-          particleSystem.eachEdge (edge, fromPoint, toPoint) =>
-            if (@edgeHitTest(edge, _mouseP, 10))
-              objectHandler.selectedEdge edge.data._neo_id
-              return false
-
         false

     $(canvas)


Fix edit booking

Fix edit booking

--- src/admin-dashboard/javascripts/directives/resource_calendar.js.coffee
+++ src/admin-dashboard/javascripts/directives/resource_calendar.js.coffee
@@ -12,7 +12,6 @@
             end_date: end.format('YYYY-MM-DD')
           AdminBookingService.query(params).then (bookings) ->
             b.resourceId = b.person_id for b in bookings
-            console.log bookings
             callback(bookings)
     ]

@@ -20,6 +19,7 @@
       calendar:
         editable: true
         # timezone: 'local'
+        height: 800
         header:
           left: 'today,prev,next'
           center: 'title'
@@ -40,19 +40,21 @@
       $scope.getCompanyPromise().then (company) ->
         params = {company: company}
         AdminPersonService.query(params).then (people) ->
-          $scope.people = people
-          resources = for p in people
-            id: p.id
-            title: p.name
+          $scope.people = _.sortBy people, 'name'
+          p.title = p.name for p in $scope.people
           uiCalendarConfig.calendars.resourceCalendar.fullCalendar('refetchEvents')
-          callback(resources)
+          callback($scope.people)

     $scope.updateBooking = (booking, delta) ->
       booking.person_id = booking.resourceId
-      booking.start = booking.start.add(delta)
-      booking.end = booking.end.add(delta)
-      booking = booking.$update()
-      booking.resourceId = booking.person_id
+      booking.$put('self', {}, booking.getPostData()).then (response) =>
+        new_booking = new BBModel.Admin.Booking(response)
+        booking.person_id = new_booking.person_id
+        booking.resourceId = booking.person_id
+        booking.start = new_booking.start
+        booking.end = new_booking.end
+        uiCalendarConfig.calendars.resourceCalendar.fullCalendar('updateEvent', booking)
+

     $scope.editBooking = (booking) ->
       ModalForm.edit
@@ -64,7 +66,6 @@
           booking.resourceId = booking.person_id
           booking.start = new_booking.start
           booking.end = new_booking.end
-          console.log 'updated booking ', booking
           uiCalendarConfig.calendars.resourceCalendar.fullCalendar('updateEvent', booking)

   link = (scope, element, attrs) ->


Add score method to Reputation that takes the average of all scores.

--- src/models/reputation.coffee
+++ src/models/reputation.coffee
@@ -1,5 +1,5 @@
 {log, p, pjson} = require 'lightsaber'
-{ first, flatten, unique } = require 'lodash'
+{ first, flatten, unique, pluck, round, sum } = require 'lodash'
 Promise = require 'bluebird'
 trustExchange = require './trustExchange'

@@ -10,6 +10,12 @@
       adaptor.ratingsOf identity, options
     Promise.all ratingSets
       .then (ratingSets) -> unique flatten ratingSets
+
+  @score: (identity, options) ->
+    @ratingsOf identity, options
+    .then (ratings) ->
+      ratingValues = pluck(ratings, 'value')
+      round(sum(ratingValues) / ratingValues.length * 100) if ratingValues.length > 0

   @report: (identity, options) ->
     @ratingsOf identity, options


:fire: log

:fire: log

--- src/react/attachments/twitter.coffee
+++ src/react/attachments/twitter.coffee
@@ -2,8 +2,6 @@

 class TwitterAttachment extends AttachmentElement
   render: ->
-    console.log(@props.attachment)
-


Confirmation dialog added to web form.

Confirmation dialog added to web form.

--- web/coffee/account.coffee
+++ web/coffee/account.coffee
@@ -2,11 +2,11 @@
   e.preventDefault()
   apiCall "POST", "/api/user/update_password", $("#password-update-form").serializeObject()
   .done (data) ->
-    switch data['status']
+    switch data['status']
       when 1
-        ga('send', 'event', 'Authentication', 'UpdatePassword', 'Success')
+        ga('send', 'event', 'Authentication', 'UpdatePassword', 'Success')
       when 0
-        ga('send', 'event', 'Authentication', 'UpdatePassword', 'Failure::' + data.message)
+        ga('send', 'event', 'Authentication', 'UpdatePassword', 'Failure::' + data.message)
     apiNotify data, "/account"

 resetPassword = (e) ->
@@ -17,6 +17,18 @@
   .done (data) ->
     ga('send', 'event', 'Authentication', 'ResetPassword', 'Success')
     apiNotify data, "/login"
+
+disableAccount = (e) ->
+  e.preventDefault()
+  confirmDialog("This will disable your account, drop you from your team, and prevent you from playing!", "Disable Account Confirmation", "Disable Account", "Cancel",
+  () ->
+    form = $("#disable-account-form").serializeObject()
+    apiCall "POST", "/api/user/disable_account", form
+    .done (data) ->
+      ga('send', 'event', 'Authentication', 'DisableAccount', 'Success')
+      apiNotify data, "/")
+
 $ ->
   $("#password-update-form").on "submit", updatePassword
   $("#password-reset-form").on "submit", resetPassword
+  $("#disable-account-form").on "submit", disableAccount


Story replies

Story replies

--- lib/designer-news/stories.coffee
+++ lib/designer-news/stories.coffee
@@ -4,4 +4,7 @@
   get: (id, cb = ->) -> @api.get "/stories/#{id}", {}, cb
   frontPage: (args = {}, cb = ->) -> @api.get '/stories', args, cb
   recent: (args = {}, cb = ->) -> @api.get '/stories/recent', args, cb
-  search: (query, cb = ->) -> @api.get '/stories/search', {query: query}, cb+  search: (query, cb = ->) -> @api.get '/stories/search', {query: query}, cb
+
+  upvote: (id, cb = ->) -> @api.post "/stories/#{id}/upvote", {}, cb
+  reply: (id, comment, cb = ->) -> @api.post "/stories/#{id}/reply", {'comment[body]': comment}, cb


Added Object.extend and tests

Added Object.extend and tests

--- src/object_extensions.coffee
+++ src/object_extensions.coffee
@@ -28,3 +28,20 @@

   return target

+###*
+Merges properties from sources into target with overiding.
+Last in covers earlier properties.
+@name extend
+@methodOf Object
+
+@param {Object} target The object to merge the properties into.
+@type Object
+@returns target
+###
+Object.extend = (target, sources...) ->
+  for source in sources
+    for name of source
+      target[name] = source[name]
+
+  return target
+


add an icon to SSH key help

add an icon to SSH key help

--- src/smc-webapp/project_settings.cjsx
+++ src/smc-webapp/project_settings.cjsx
@@ -886,7 +886,7 @@
                     {# WARNING: previous use of <Input> here completely breaks copy on Firefox.}

{"#{misc.replace_all(project_id, '-', '')}@#{host}.sagemath.com"}

-                    How to create SSH keys
+                    <Icon name='life-ring'/> How to create SSH keys

             else
                 <Row>


config: Modify description of videoFrameRate

--- config.coffee
+++ config.coffee
@@ -10,7 +10,7 @@
   # RTSP and HTTP response headers
   serverName: 'node-rtsp-rtmp-server/0.2.0'

-  # Average frame rate of video
+  # Average frame rate of video (informative)
   videoFrameRate: 30

   # Video bitrate in Kbps (informative)


Memoize `Rule.prototype.getIncludedPatterns` to shave a cheap 2% off profile

--- src/app/text-mate-grammar.coffee
+++ src/app/text-mate-grammar.coffee
@@ -76,12 +76,14 @@

   getIncludedPatterns: (included=[]) ->
     return [] if _.include(included, this)
-    included.push(this)
-    allPatterns = []
-
+
+    return @allPatterns if @allPatterns
+
+    included = included.concat([this])
+    @allPatterns = []
     for pattern in @patterns
-      allPatterns.push(pattern.getIncludedPatterns(included)...)
-    allPatterns
+      @allPatterns.push(pattern.getIncludedPatterns(included)...)
+    @allPatterns

   getNextTokens: (stack, line, position) ->
     patterns = @getIncludedPatterns()


Fix editor links for paths with spaces

Fix editor links for paths with spaces

--- lib/assets/javascripts/utilities.js.coffee
+++ lib/assets/javascripts/utilities.js.coffee
@@ -120,7 +120,7 @@
 #
 root.shellEscape = (str) ->
   if str == '' then return "''"
-  str.replace(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1").replace(/\n/, "'\n'")
+  str.replace(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\$1").replace(/\n/, "'\n'")

 # Converts an array of strings into a sentence with an Oxford comma.
 #


Fix wrong alert style on startup

Fix wrong alert style on startup

--- views/components/info/alert.cjsx
+++ views/components/info/alert.cjsx
@@ -4,8 +4,32 @@
 __n = window.i18n.others.__n.bind(i18n.others)
 keyCount = 0
 alertStyle = document.createElement 'style'
-remote.getCurrentWindow().webContents.on 'dom-ready', (e) ->
-  document.body.appendChild alertStyle
+alertStyle.innerHTML = """
+  poi-alert {
+    height: 30px;
+  }
+  #alert-container {
+    height: 30px;
+  }
+  #alert-main {
+    height: 182px;
+    overflow: hidden;
+    position: relative;
+    bottom: 152px;
+  }
+  .alert-history-hidden {
+    top: 182px;
+  }
+"""
+try
+  remote.getCurrentWindow().webContents.on 'dom-ready', (e) ->
+    document.body.appendChild alertStyle
+catch error
+  console.log error
+  setTimeout (e) =>
+    remote.getCurrentWindow().webContents.on 'dom-ready', (e) ->
+      document.body.appendChild alertStyle
+  , 1000

 # Alert info
 PoiAlert = React.createClass
@@ -98,8 +122,8 @@
         alertHeight = $('poi-control').offsetHeight
         historyHeight = $('.alert-history').offsetHeight
       catch error
-        alertHeight = 28
-        historyHeight = 30
+        alertHeight = 30
+        historyHeight = 152
       alertStyle.innerHTML = """
         poi-alert {
           height: #{alertHeight}px;


states.animationOptions -> animationOptions

--- framer/Components/ShareComponent.coffee
+++ framer/Components/ShareComponent.coffee
@@ -62,7 +62,7 @@
 		@states =
 			hover: opacity: .8
 			full: opacity: 1
-		@states.animationOptions =
+		@animationOptions =
 			time: .3

 		@onMouseOver ->


:bug: Fix error raised on tab close

:bug: Fix error raised on tab close

--- lib/minimap-highlight-selected-view.coffee
+++ lib/minimap-highlight-selected-view.coffee
@@ -12,7 +12,7 @@
     getActiveTextEditor: -> @getActiveMinimap()?.getTextEditor()

     ['markBufferRange', 'scanInBufferRange', 'getEofBufferPosition', 'getSelections', 'getLastSelection', 'bufferRangeForBufferRow', 'getTextInBufferRange', 'onDidChangeSelectionRange'].forEach (key) ->
-      FakeEditor::[key] = -> @getActiveTextEditor()[key](arguments...)
+      FakeEditor::[key] = -> @getActiveTextEditor()?[key](arguments...)

     ['decorateMarker'].forEach (key) ->
       FakeEditor::[key] = -> @getActiveMinimap()[key](arguments...)


Ignore empty output from hh_format

Ignore empty output from hh_format

--- src/beautifiers/hh_format.coffee
+++ src/beautifiers/hh_format.coffee
@@ -15,4 +15,8 @@
   beautify: (text, language, options) ->
     @run("hh_format", [
       @tempFile("input", text)
-      ])
+      ]).then((output) =>
+        # hh_format can exit with status 0 and no output for some files which
+        # it doesn't format.  In that case we just return original text.
+        if output.trim() then output else text
+      )


Improved tree select feature - allow also to select node/parent of childs.

--- app/assets/javascripts/app/lib/app_post/searchable_select.coffee
+++ app/assets/javascripts/app/lib/app_post/searchable_select.coffee
@@ -215,6 +215,7 @@
     @invisiblePart.text('')

   selectItem: (event) ->
+    return if !event.currentTarget.textContent
     @input.val event.currentTarget.textContent.trim()
     @input.trigger('change')
     @shadowInput.val event.currentTarget.getAttribute('data-value')
@@ -222,6 +223,7 @@

   navigateIn: (event) ->
     event.stopPropagation()
+    @selectItem(event)
     @navigateDepth(1)

   navigateOut: (event) ->
@@ -305,9 +307,7 @@

   onEnter: (event) ->
     if @currentItem
-      if @currentItem.hasClass('js-enter')
-        return @navigateIn(event)
-      else if @currentItem.hasClass('js-back')
+      if @currentItem.hasClass('js-back')
         return @navigateOut(event)

     @clearAutocomplete()
@@ -326,10 +326,16 @@
       @input.val valueName
       @shadowInput.val value

-    @currentItem = null
-
     @input.trigger('change')
     @shadowInput.trigger('change')
+
+    if @currentItem
+      if @currentItem.hasClass('js-enter')
+        @navigateIn(event)
+        @currentItem = null
+        return
+    @currentItem = null
+
     @toggle()

   onBlur: ->


Increase spec waitsFor timeouts from 100 to 300


Restrict ReactEditorView::getPane implementation

--- src/react-editor-view.coffee
+++ src/react-editor-view.coffee
@@ -140,7 +140,7 @@
     pane?.splitDown(pane?.copyActiveItem()).activeView

   getPane: ->
-    @closest('.pane').view()
+    @parent('.item-views').parents('.pane').view()

   focus: ->
     if @component?


added switch tabs fucntion

added switch tabs fucntion

--- src/render/ui-control.coffee
+++ src/render/ui-control.coffee
@@ -16,36 +16,73 @@
 events =  require 'events'
 path = require 'path'
 fs =  require 'fs'
+uuid = require 'uuid'
+

 BaseDir =  path.resolve(__dirname)

+obj =  require BaseDir + '\\html.json'
+
+htmlFiles = obj.htmlFiles
+
 $shell_tabs =  $('.chrome-tabs-shell')

+tabsEmitter =  new events.EventEmitter()

+getTabHTML = (uuid) ->
+    for tab in htmlFiles
+        console.log tab
+        if uuid is tab.uuid
+            return tab.html
+    return 'Cloud not find tab data'
+removeTabHTML = (uuid) ->
+    for tab, index in htmlFiles
+        if uuid is tab.uuid
+            delete htmlFiles[index]
+            return 'tab deleted'
+    return 'Cloud not find tab data'

-tabsEmitter =  new events.EventEmitter()
+addTabHTML = (uuid, html) ->
+    htmlFiles.push({uuid:uuid, html:html})

 tabListener = () ->
     tabsEmitter.on 'tab-click', (data) ->
         console.log data
+        console.log data.data('uuid')
+        html = getTabHTML(data.data('uuid'))
+        $('div.viewer').html('')
+        $('div.viewer').html(html)

     tabsEmitter.on 'tab-close', (data) ->
-        console.log data
+        $('div.viewer').html('')
+        removeTabHTML(data.data('uuid'))

 # window first pops up
 initDisplay = () ->
     chromeTabs.init({$shell: $shell_tabs, tabEmitter: tabsEmitter, minWidth: 45, maxWidth: 160})
-
-    fs.readFile(BaseDir + '\\tabs\\welcome.html',{encoding:'utf-8'},(error, data) ->
-            if error
-                $('div.viewer').html(error)
-            else
-                $('div.viewer').html(data)
-
-         )
-
-    chromeTabs.addNewTab($shell_tabs, {title:"Welcome"})
+    welcome = "
+

Hello, Welcome to...

+
+
Chizel is a cloud storage integrator. It streamlines the process of collecting cloud artifacts on a box. Data stored on the local computer is analyzed, and presented in a clear manner.

+
Learn more »

+

+

+

+
Heading

+

+
View details »

+

+

Heading

+

+
View details »

+

+

+
Heading

View details »

+

© 2015 Chizel, Inc.
"
+    NewId =  uuid.v4()
+    addTabHTML(NewId, welcome)
+    chromeTabs.addNewTab($shell_tabs, {title:"Welcome"} , NewId)

 Create_New_Case = () ->
     console.log 'you want to create a new case'
@@ -83,12 +120,13 @@
     console.log 'analying one drive'
     #chromeTabs.addNewTab({title:"OneDrive",  })
     ipcRenderer.send 'one-drive', ''
-    ipcRenderer.on 'actionReply' , (event, treeView ) ->
-            console.log treeView
+    ipcRenderer.on 'actionReply' , (event, oneDriveData ) ->
+            console.log oneDriveData
             treelist = []
-            treelist.push treeView.tree
+            treelist.push oneDriveData.fs.tree
             $('#tree').treeview({data: treelist})
             chromeTabs.addNewTab($shell_tabs, {title:"OneDrive Activity"})
+            console.log oneDriveData.reg
             fs.readFile(BaseDir + '\\update_graphIndex.html',{encoding:'utf-8'},(error, data) ->
                       console.log data
                       if error
@@ -97,5 +135,8 @@

                           $('div.viewer').html(data))

+
 initDisplay()
 tabListener()
+
+$('div.viewer').html(getTabHTML($shell_tabs.find('chrome-tab-current').data('uuid')))


Wrap sendMessage in Q promise. (#197)

Wrap sendMessage in Q promise. (#197)

I found that `asteroid.call('sendMessage', message)` did not always return a promise, or possibly a "partially functional" then. This was breaking my promise chains in Hubot methods that invoked `sendMessageByRoomId`. So I wrapped it in `Q` as described in Q docs...

>When working with promises provided by other libraries, you should convert it to a Q promise. Not all promise libraries make the same guarantees as Q and certainly don’t provide all of the same methods. Most libraries only provide a partially functional then method. This thankfully is all we need to turn them into vibrant Q promises.

--- src/rocketchat_driver.coffee
+++ src/rocketchat_driver.coffee
@@ -83,7 +83,7 @@

 	sendMessageByRoomId: (content, roomid) =>
 		message = @prepareMessage content, roomid
-		@asteroid.call('sendMessage', message)
+		Q(@asteroid.call('sendMessage', message))
 		.then (result)->
 			@logger.debug('[sendMessage] Success:', result)
 		.catch (error) ->


Remove unnecessary logging

Remove unnecessary logging

--- annotran/static/scripts/annotator/plugin/sentenceselection.coffee
+++ annotran/static/scripts/annotator/plugin/sentenceselection.coffee
@@ -112,8 +112,6 @@

     # test if we have found a sentence marker or if we are forcing this through anyway
     if (match or force) and (offset_to_use < endIndex)
-      console.log("Start:" + offset_to_use)
-      console.log("End:" + endIndex)
       data = this.packageData(initialTarget, currentTarget, endIndex, offset_to_use)
       this.anchorToPage(data)
     else


Don't redirect if it's an inline form submit

--- app/assets/javascripts/effective_datatables/inline_crud.js.coffee
+++ app/assets/javascripts/effective_datatables/inline_crud.js.coffee
@@ -81,9 +81,8 @@
     $table.DataTable().flash(flash || 'Item updated', 'success')
     $tr.fadeOut('slow')

-  unless redirectDatatables($table)
-    $table.DataTable().draw()
-    refreshDatatables($table)
+  $table.DataTable().draw()
+  refreshDatatables($table)

 beforeNew = ($action) ->
   $table = $action.closest('table')


only set formSubmitted to false if all update promises have settled and resolved

--- app/components/profile-settings-page/profile-settings-page.coffee
+++ app/components/profile-settings-page/profile-settings-page.coffee
@@ -55,14 +55,16 @@
         promises.push($scope.updateProfile())
       if $scope.showPasswordFields
         promises.push($scope.savePassword())
-      $q.allSettled(promises).finally ->
-        resolvedPromises = _.filter promises, (promise) ->
-          promise.$$state.status == 1
-        updatedFields = _.map resolvedPromises, (promise) ->
-          promise.$$state.value
-        if updatedFields.length > 0
-          Toast.show("Your new #{listify(updatedFields)} #{if updatedFields.length > 1 then 'were' else 'was'} saved")
-          $scope.formSubmitted = false
+      $q.allSettled(promises)
+        .then ->
+          formSubmitted = false
+        .finally ->
+          resolvedPromises = _.filter promises, (promise) ->
+            promise.$$state.status == 1
+          updatedFields = _.map resolvedPromises, (promise) ->
+            promise.$$state.value
+          if updatedFields.length > 0
+            Toast.show("Your new #{listify(updatedFields)} #{if updatedFields.length > 1 then 'were' else 'was'} saved")

     $scope.updateProfile = ->
       deferred = $q.defer()


Table view repeat cell optimization.

Table view repeat cell optimization.

--- src/directives/table_view_components.coffee
+++ src/directives/table_view_components.coffee
@@ -80,24 +80,18 @@
     @directive   = "table-row"
     @repeatCells = (cells, rowElement, sectionController) ->
       # Clear out our existing cell-templates
-      rowElement.html ""
+      while child = rowElement[0].firstChild
+        child.remove()

       cellMarker = angular.element "<!-- cells: #{sectionController.section.name} -->"
-      rowElement.append cellMarker
+      rowElement[0].appendChild cellMarker[0]

-      linkerFactory = (cell) =>
-        templateName = cell.column.colName of sectionController.cellTemplates and cell.column.colName or "?"
+      linkerFactory = (cell) ->
+        templateName = if cell.column.colName of sectionController.cellTemplates then cell.column.colName else "?"
         return template[1] if template = sectionController.cellTemplates[templateName]
-
-      # Store current display and then set it to display none
-      currentDisplay = rowElement.css "display"
-      rowElement.css "display", "none"

       # Repeat our cells
       directiveHelpers.repeater cells, "cell", rowElement.scope(), cellMarker, linkerFactory
-
-      # Reset our display
-      rowElement.css "display", currentDisplay

     return
   compile: (element, attr) ->


added trigger 'focus' for accessibility

added trigger 'focus' for accessibility

--- src/components/task/details.cjsx
+++ src/components/task/details.cjsx
@@ -16,7 +16,7 @@

   getDefaultProps: ->
     title: 'Instructions'
-    trigger: 'hover'
+    trigger: ['hover', 'focus']
     placement: 'top'
     popverClassName: 'task-details-popover'


Sort entries

Sort entries

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -191,8 +191,11 @@
     pagerDutyGet msg, "/schedules/#{pagerDutyScheduleId}/#{thing}", query, (json) ->
       entries = json.entries || json.overrides
       if entries
+        sortedEntries = entries.sort (a, b) ->
+          moment(a.start).unix() - moment(b.start).unix()
+
         buffer = ""
-        for entry in entries
+        for entry in sortedEntries
           if entry.id
             buffer += "* (#{entry.id}) #{entry.start} - #{entry.end}: #{entry.user.name}\n"
           else


removed the unnecessary parenthesis and changed the API logic from GET to POST.

--- src/scripts/voicechat.coffee
+++ src/scripts/voicechat.coffee
@@ -1,8 +1,8 @@
 # Description:
-#	Voice Chat API is an open-source audio conferencing app exposed via an API.
-#	This plugin makes a GET request to fetch a URL where you can do a simple
-#	WebRTC-powered audio call in browsers. The room will last for the next 24
-#	hours. VoiceChatAPI Learn more on http://VoiceChatAPI.com
+#   Voice Chat API is an open-source audio conferencing app exposed via an API.
+#   This plugin makes a POST request to fetch the UR of a conference room where you can do a simple
+#   WebRTC-powered voice calls in browsers. The room will last for the next 24
+#   hours. VoiceChatAPI Learn more on http://VoiceChatAPI.com
 #
 # Dependencies:
 #   None
@@ -11,14 +11,14 @@
 #   None
 #
 # Commands:
-#   hubot conference - create an audio chat room.
+#   hubot conference - create an audio conference room.
 #
 # Author:
 #   DHfromKorea <dh@dhfromkorea.com>

 module.exports = (robot) ->
-  robot.respond /(conference)/i, (msg) ->
-    msg.http('http://www.voicechatapi.com/api/v1/bridge/')
-       .get() (err, res, body) ->
+  robot.respond /conference/i, (msg) ->
+    msg.http('http://www.voicechatapi.com/api/v1/conference/')
+       .post() (err, res, body) ->
          json = JSON.parse(body)
-         msg.send "Your conference room: #{json.conference_url} \n*Be careful when scheduling a call. This room will be automatically removed after 24 hours."
+         msg.send "Your conference room: #{json.conference_url} \n*This room will be automatically removed after 24 hours. More info on www.voicechatapi.com"


test deletion

test deletion

--- karma/test-services.coffee
+++ karma/test-services.coffee
@@ -432,6 +432,14 @@
         $httpBackend.flush()
       )
     )
+
+    describe('delete', () ->
+      it('posts to delete endpoint', () ->
+        $httpBackend.expectPOST('/language/delete/602/', null).respond("")
+        LanguageService.delete(test.language)
+        $httpBackend.flush()
+      )
+    )
   )

   #=======================================================================
@@ -496,6 +504,14 @@
         $httpBackend.flush()
       )
     )
+
+    describe('delete', () ->
+      it('posts to delete endpoint', () ->
+        $httpBackend.expectPOST('/faq/delete/702/', null).respond("")
+        FaqService.delete(test.faq1)
+        $httpBackend.flush()
+      )
+    )
   )

   #=======================================================================


disable esc for normal mode during autocomplete

Esc normally rejects the autocomplete suggestion, but vim-mode-plus overrides this behaviour - with vim-mode-plus on, the autocomplete suggestion is rejected and you are returned to Normal mode, which is unlikely to be what you want.

This change limits the vim-mode-plus keybinding of Esc to only work outside of autocomplete. Ctrl-c and ctrl-[ still work to exit autocomplete and return to Normal mode.

Fixes #1149

--- keymaps/vim-mode-plus.cson
+++ keymaps/vim-mode-plus.cson
@@ -11,15 +11,17 @@

 # all
 # -------------------------
+'atom-text-editor.vim-mode-plus:not(.autocomplete-active)':
+  'escape': 'vim-mode-plus:reset-normal-mode'
 'atom-text-editor.vim-mode-plus':
-  'escape': 'vim-mode-plus:reset-normal-mode'
   'ctrl-c': 'vim-mode-plus:reset-normal-mode'
   'ctrl-[': 'vim-mode-plus:reset-normal-mode'

 # all except normal
 # -------------------------
+'atom-text-editor.vim-mode-plus:not([.normal-mode, .autocomplete-active])':
+  'escape': 'vim-mode-plus:activate-normal-mode'
 'atom-text-editor.vim-mode-plus:not(.normal-mode)':
-  'escape': 'vim-mode-plus:activate-normal-mode'
   'ctrl-[': 'vim-mode-plus:activate-normal-mode'

 # all except normal


Add a way for toggle active to allow clicks on links within the toggle active element

--- app/assets/javascripts/lib/utils/toggle_active.js.coffee
+++ app/assets/javascripts/lib/utils/toggle_active.js.coffee
@@ -44,7 +44,7 @@

       @broadcast($el)

-      if event.target.nodeName.toUpperCase() is "A"
+      if event.target.nodeName.toUpperCase() is "A" and !$el.closest(".js-toggle-active").data("allowLinks")
         event.preventDefault()

     _addInitialState: ->


Add ability to specify context/scope for toggle active

--- app/assets/javascripts/lib/utils/toggle_active.js.coffee
+++ app/assets/javascripts/lib/utils/toggle_active.js.coffee
@@ -4,25 +4,19 @@

     LISTENER = '#js-row--content'

-    constructor: ->
+    # @args = {}
+    # context: {string} selector so that we can add a context/scope. Useful for dynamically added content.
+    constructor: (args) ->
+      this.context = args.context if args
       @listen()
       @_addInitialState()

     listen: ->
-      $('.js-toggle-active').on 'click', (event) =>
-        $el = $(event.target)
-        @_updateClasses($el)
-
-        # Prevent the click event bubbling so we can update this component
-        # by click elsewhere on the document
-        #
-        # Replace it with the toggleActive/click event
-        event.stopPropagation()
-        @broadcast($el)
-
-        if event.target.nodeName.toUpperCase() is 'A'
-          event.preventDefault()
+      if (this.context)
+        $(this.context).on 'click', '.js-toggle-active', this._handleToggle
+      else
+        $('.js-toggle-active').on 'click', this._handleToggle

       $(LISTENER).on ':toggleActive/update', (e, target) =>
         @_updateClasses($(target))
@@ -33,8 +27,24 @@

     # Private

+    _handleToggle: (event) =>
+      $el = $(event.target)
+      @_updateClasses($el)
+
+      # Prevent the click event bubbling so we can update this component
+      # by click elsewhere on the document
+      #
+      # Replace it with the toggleActive/click event
+      event.stopPropagation()
+      @broadcast($el)
+
+      if event.target.nodeName.toUpperCase() is 'A'
+        event.preventDefault()
+
     _addInitialState: ->
-      $('.js-toggle-active').each ->
+      toggles = if this.context then $(".js-toggle-active", this.context) else $(".js-toggle-active")
+
+      toggles.each ->
         $el = $(@)
         $($el.data('toggleTarget')).addClass('is-not-active')
         $el.addClass('is-not-active') if $el.data('toggleMe')


Block Charts: fixed tooltip text update. Closes #1234

--- app/assets/javascripts/lib/views/block_chart_view.coffee
+++ app/assets/javascripts/lib/views/block_chart_view.coffee
@@ -47,8 +47,8 @@
           $(this).attr('data-title')
         text: ->
           item = $(this)
-          cost =   Metric.autoscale_value item.parent().data('cost'), 'Eur/MWhe'
-          invest = Metric.autoscale_value item.parent().data('invest'), 'Eur/MWe'
+          cost =   Metric.autoscale_value +item.parent().attr('data-cost'), 'Eur/MWhe'
+          invest = Metric.autoscale_value +item.parent().attr('data-invest'), 'Eur/MWe'
           url = "/descriptions/#{item.attr('data-description_id')}"
           "#{item.attr('data-description')}


fixes a11y VO read error  issue for posting a topic w/ no sections

fixes COMMS-733

Test Plan
* In Safari navigate to the announcement edit page with VO on
* Delete all sections from the Post To combobox
* Press save
* check errors with VO

Change-Id: I8858f80660da49feea76fb0e86aa1462f704915d
Reviewed-on: https://gerrit.instructure.com/138958
Reviewed-by: Felix Milea-Ciobanu <9384aa226ac9d3237ed0bc52e18ffcb251ee0bf7@instructure.com>
Tested-by: Jenkins
Reviewed-by: Venk Natarajan <f9eab59b292314637a3c917fea4742741e36a19b@instructure.com>
QA-Review: Steven Burnett <a0b450117c5d20a22d5c4d52bb8ae13adee003d8@instructure.com>
Product-Review: Aaron Kc Hsu <089edc94045de3d9f5f372fb17706d21afcc259f@instructure.com>

--- app/coffeescripts/views/DiscussionTopics/EditView.coffee
+++ app/coffeescripts/views/DiscussionTopics/EditView.coffee
@@ -412,6 +412,9 @@
       if data.allow_todo_date == '1' && data.todo_date == null
         errors['todo_date'] = [{type: 'date_required_error', message: I18n.t('You must enter a date')}]

+      if ENV.SECTION_SPECIFIC_ANNOUNCEMENTS_ENABLED && !data.specific_sections
+        errors['specific_sections'] = [{type: 'specific_sections_required_error', message: I18n.t('You must input a section')}]
+
       if @isAnnouncement()
         unless data.message?.length > 0
           unless @lockedItems.content
@@ -462,6 +465,10 @@
           @conditionalReleaseEditor.focusOnError()
         else
           @$discussionEditView.tabs("option", "active", 0)
+
+      if ENV.SECTION_SPECIFIC_ANNOUNCEMENTS_ENABLED && errors['specific_sections']
+        $.flashError(I18n.t("You must input a section"))
+
       super(errors)

     toggleGradingDependentOptions: ->


only invoke every 2d minute

only invoke every 2d minute

--- src/mailButler.appsscript.coffee
+++ src/mailButler.appsscript.coffee
@@ -62,7 +62,7 @@
   @SUPPORT_EMAIL:       'support@mailfred.de'
   @LABEL_BASE:          'MailFred'
   @LABEL_OUTBOX:        MailButler.LABEL_BASE + '/' + 'Scheduled'
-  @FREQUENCY_MINUTES:   1
+  @FREQUENCY_MINUTES:   2
   @DB:                  MailButlerDBLibrary.Db

   prefix: null


add fair fabricators

add fair fabricators

--- node/fabricate.coffee
+++ node/fabricate.coffee
@@ -344,7 +344,6 @@
       updated_at: '2013-09-26T15:03:29Z'
       coordinates: null

-
     when 'page'
       content: 'This is an awesome page'
       id: _.uniqueId()
@@ -486,6 +485,28 @@
       organizer:
         profile_id: 'the-armory-show'
         name: 'Armory Show'
+
+    when 'fair_organizer'
+      default_fair_id: "armory-show-2013"
+      profile_id: "the-armory-show"
+      id: "armory-show-2013"
+      name: "Zona MACO 2014"
+
+    when 'fair_profile'
+      cover_image: null
+      posts_count: 0
+      reposts_count: 8
+      id: "the-armory-show"
+      subtype: null
+      website: "http://getty.edu/"
+      location: null
+      private: false
+      bio: "Awesome Fair"
+      default_icon_version: "square"
+      owner_type: "FairOrganizer"
+      follows_count:307
+      short_description: null
+      published: true

     when 'sale'
       id: "whtney-art-party"


Set author and public to false when duplicating projects. WIP fixing certain fields not being mapped.


updated methods

updated methods


Changed if statements within _onTouchEnd - previously they were strictly checking for GESTURE.angle_difference !== 0 and GESTURE.distance_difference !== 0 but because capture rotation and capture pinch return undefined if they are below the predetermined threshold it meant that any double touch that wasn't a pinch or a zoom (like a double tap) was still firing these events: 'pinch', 'pinchIn' 'rotate', 'rotateLeft' So just changed them to check for if angle_difference and distance_difference are truthy so a negative or positive number will pass through but undefined will not. Also added in a 'twoTap' event for when you do a double finger tap as I couldn't find any support for this anywhere. I think the coffee script is correct but not sure as I usually just do pure JS. Also couldn't find any instructions for if a build was required before committing (assume that's what Grunt is for?) but you can at least see the changes here and decide if they're worthwhile. :)

--- src/quo.gestures.coffee
+++ src/quo.gestures.coffee
@@ -8,7 +8,7 @@
     TOUCH_TIMEOUT = undefined
     HOLD_DELAY = 650
     GESTURES = ["touch",
-                "tap", "singleTap", "doubleTap", "hold",
+                "tap", "singleTap", "twoTap" "doubleTap", "hold",
                 "swipe", "swiping", "swipeLeft", "swipeRight", "swipeUp", "swipeDown",
                 "rotate", "rotating", "rotateLeft", "rotateRight",
                 "pinch", "pinching", "pinchIn", "pinchOut",
@@ -105,12 +105,12 @@
                     ), 100)
         else
             anyevent = false
-            if GESTURE.angle_difference isnt 0
+            if GESTURE.angle_difference
                 _trigger "rotate", angle: GESTURE.angle_difference
                 rotation_direction = if GESTURE.angle_difference > 0 then "rotateRight" else "rotateLeft"
                 _trigger rotation_direction, angle: GESTURE.angle_difference
                 anyevent = true
-            if GESTURE.distance_difference isnt 0
+            if GESTURE.distance_difference
                 _trigger "pinch", angle: GESTURE.distance_difference
                 pinch_direction = if GESTURE.distance_difference > 0 then "pinchOut" else "pinchIn"
                 _trigger pinch_direction, distance: GESTURE.distance_difference
@@ -120,6 +120,9 @@
                     _trigger "drag"
                     drag_direction = _swipeDirection(FIRST_TOUCH[0].x, CURRENT_TOUCH[0].x, FIRST_TOUCH[0].y, CURRENT_TOUCH[0].y)
                     _trigger "drag" + drag_direction
+                    anyevent = true
+            if not anyevent
+                _trigger "twoTap"
             _cleanGesture()
         EVENT = undefined


Fix normal reporter output: print the file name

It seems like the `unless` made sense for `options.argv.q`, but not for `hasError`.

--- src/commandline.coffee
+++ src/commandline.coffee
@@ -116,7 +116,7 @@
             [@warn, 'yellow']
         else
             [@ok, 'green']
-        unless options.argv.q or hasError
+        if not options.argv.q or hasError
             @print "  #{overall} #{@stylize(path, color, 'bold')}"
         for e in errors
             continue if options.argv.q and e.level != 'error'


AdminModal: Fetch pending invitations and list.

--- client/admin/lib/views/invitations/pendinginvitationsview.coffee
+++ client/admin/lib/views/invitations/pendinginvitationsview.coffee
@@ -1,5 +1,7 @@
 kd                    = require 'kd'
 KDView                = kd.View
+remote                = require('app/remote').getInstance()
+InvitedItemView       = require './inviteditemview'
 TeamMembersCommonView = require '../members/teammemberscommonview'

@@ -7,5 +9,25 @@

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

+    options.listViewItemClass = InvitedItemView
+
     super options, data

+
+  fetchMembers: ->
+
+    return if @isFetching
+
+    @isFetching = yes
+
+    selector = status: 'pending'
+    options  = { @skip }
+
+    remote.api.JInvitation.some selector, options, (err, invitations) =>
+
+      if err
+        @listController.lazyLoader.hide()
+        return kd.warn err
+
+      @listMembers invitations
+      @isFetching = no


chat: participant avatars have context menus

--- client/ide/lib/views/chat/idechatmessageparticipantavatar.coffee
+++ client/ide/lib/views/chat/idechatmessageparticipantavatar.coffee
@@ -1,5 +1,7 @@
-kd = require 'kd'
-AvatarView = require 'app/commonviews/avatarviews/avatarview'
+kd              = require 'kd'
+AvatarView      = require 'app/commonviews/avatarviews/avatarview'
+getNick         = require 'app/util/nick'
+ProfileTextView = require 'app/commonviews/linkviews/profiletextview'

 ###*
  * The purpose of this class is to prevent users
@@ -11,6 +13,23 @@
 ###
 module.exports = class IDEChatMessageParticipantAvatar extends AvatarView

+  INTENT_DELAY = 177
+  MENU         = null
+
+  constructor: (options = {}, data) ->
+
+    options.bind = kd.utils.curry 'mouseenter mouseleave', options.bind
+
+    super options, data
+
+    @intentTimer = null
+    @nickname    = @getOptions().origin
+
+    @define 'nickname', => @data.profile.nickname
+
+    @setClass 'is-hostParticipant'  if @nickname is getNick()
+
+
   click: (event) ->

     return  unless participant = @getOption 'origin'
@@ -20,3 +39,58 @@
     @emit 'ParticipantSelected', participant

+  mouseEnter: -> @intentTimer = kd.utils.wait INTENT_DELAY, @bound 'showMenu'
+
+
+  mouseLeave: -> kd.utils.killWait @intentTimer  if @intentTimer
+
+
+  showMenu: ->
+
+    return  if @nickname is getNick()
+    return  if MENU and MENU.getOptions().nickname is @nickname
+
+    MENU.destroy()  if MENU
+
+    { appManager } = kd.singletons
+    items          = {}
+
+    items['ProfileText'] =
+      type : 'customView'
+      view : new ProfileTextView {}, @getData()
+
+    appManager.tell 'IDE', 'getCollaborationData', (data) =>
+      appManager.tell 'IDE', 'hasParticipantWithAudio', @nickname, (hasAudio) =>
+        if hasAudio
+          items['Unmute'] =
+            title    : 'Unmute'
+            callback : =>
+              MENU?.destroy()
+              appManager.tell 'IDE', 'unmuteParticipant', @nickname
+
+        if data.amIHost
+          items['Kick'] =
+            title    : 'Kick'
+            callback : ->
+              MENU?.destroy()
+              appManager.tell 'IDE', 'kickParticipant', @getData()
+
+        if data.amIHost or hasAudio
+          items['ProfileText'].separator = yes
+
+        menuWidth = 172
+        MENU = new kd.ContextMenu
+          nickname : @nickname
+          cssClass : 'dark statusbar-files'
+          delegate : this
+          x        : @getX()
+          y        : @getY()
+          offset   : { top: 35, left: -78 }
+          arrow    : { placement: 'top', margin: menuWidth / 2 }
+        , items
+
+        MENU.once 'KDObjectWillBeDestroyed', => MENU = null
+
+
+
+


Call os kite from VM buttons.

Call os kite from VM buttons.

--- client/landing/app/MainApp/sidebar/virtualizationcontrols.coffee
+++ client/landing/app/MainApp/sidebar/virtualizationcontrols.coffee
@@ -5,10 +5,19 @@
       cssClass      : "virt-controls"
       buttons       :
         "Start"     :
-          callback  : -> log "Start machine"
+          callback  : ->
+            KD.singletons.kiteController.run
+              kiteName: 'os',
+              method: 'vm.start'
         "Stop"      :
-          callback  : -> log "Stop machine"
-        "Turn Off"  :
-          callback  : -> log "Turn off machine"
+          callback  : ->
+            KD.singletons.kiteController.run
+              kiteName: 'os',
+              method: 'vm.stop'
+        "Nuke"      :
+          callback  : ->
+            KD.singletons.kiteController.run
+              kiteName: 'os',
+              method: 'vm.nuke'

     super options


Tests: Environments: Domain: fixed deleteDomain test.

--- tests/src/environments/domain.coffee
+++ tests/src/environments/domain.coffee
@@ -43,7 +43,7 @@

     browser
       .moveToElement             domainItem, 10, 10
-      .click                     domainItem + ' span.remove-domain'
+      .click                     domainItem + ' span.remove'
       .waitForElementVisible     loader, 10000
       .waitForElementNotVisible  loader, 20000
       .getText                   domainItem, (result) =>


Reverted undo to fuzzier regex

Reverted undo to fuzzier regex

--- src/scripts/time.coffee
+++ src/scripts/time.coffee
@@ -266,7 +266,7 @@
     else
       user.directMessage strings.addfail, Logger

-  robot.respond /\bundo$/i, id: 'time.undo', userRequired: true, (res) ->
+  robot.respond /undo/i, id: 'time.undo', userRequired: true, (res) ->
     user = Organization.getUserBySlackName res.message.user.name
     if user.punches and user.punches.length > 0
       Logger.addReaction 'clock4', res.message


levels and default game states

levels and default game states

--- src/title_screen.coffee
+++ src/title_screen.coffee
@@ -1,5 +1,5 @@
 TitleScreen = (I={}) ->
-  self = TextScree(I)
+  self = TextScreen(I)

   self.bind 'update', ->
     engine.nextLevel() if justPressed.any


coordinator: Fix coffeelint

coordinator: Fix coffeelint

--- src/coordinator.coffee
+++ src/coordinator.coffee
@@ -148,10 +148,10 @@
           @connect e[0], e[1], e[2], e[3] if not isConnected e

     else if action == 'removed'
-      # TODO: implement
+      null # TODO: implement

     else
-      # ignored
+      null # ignored

   addInitial: (partId, portId, data) ->
     id = iipId partId, portId


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/controllable.coffee
+++ test/controllable.coffee
@@ -96,6 +96,6 @@

   equals player.I.y, 23

-test "should stay in place if two d"
+test "should stay in place if two different directions"

 module()


removed denounce for now

removed denounce for now

--- core/app/backbone/views/autocompleted_add_to_channel_view.coffee
+++ core/app/backbone/views/autocompleted_add_to_channel_view.coffee
@@ -143,7 +143,7 @@
     else
       @$el.removeClass "has-text"

-  autoComplete: _.debounce(->
+  autoComplete: ->
     searchValue = @$("input.typeahead").val()
     @updateText()

@@ -162,7 +162,6 @@
     @_auto_completes_view.search_collection.fetch success: =>
       @setActiveAutoComplete 0, true
       updateWindowHeight()
-  , 300)

   #cleaning/closing functions:


Add special case for Perl `$` `%` and `@` variables

Perl uses symbols to denote variable types. Adding them to the special cases on the regex search.

--- lib/highlighted-area-view.coffee
+++ lib/highlighted-area-view.coffee
@@ -124,8 +124,14 @@

     if atom.config.get('highlight-selected.onlyHighlightWholeWords')
       if regexSearch.indexOf("\$") isnt -1 \
-      and editor.getGrammar()?.name in ['PHP', 'HACK']
+      and editor.getGrammar()?.name in ['PHP', 'HACK', 'Perl']
         regexSearch = regexSearch.replace("\$", "\$\\b")
+      else if regexSearch.indexOf("\%") isnt -1 \
+      and editor.getGrammar()?.name in ['Perl']
+          regexSearch = regexSearch.replace("\%", "\%\\b")
+      else if regexSearch.indexOf("\@") isnt -1 \
+      and editor.getGrammar()?.name in ['Perl']
+          regexSearch = regexSearch.replace("\@", "\@\\b")
       else
         regexSearch =  "\\b" + regexSearch
       regexSearch = regexSearch + "\\b"


Use constructor instead of initialize

Use constructor instead of initialize

--- lib/markdown-preview-view.coffee
+++ lib/markdown-preview-view.coffee
@@ -13,7 +13,7 @@
   @content: ->
     @div class: 'markdown-preview native-key-bindings', tabindex: -1

-  initialize: (@filePath) ->
+  constructor: (@filePath) ->
     super
     atom.project.bufferForPath(filePath).done (buffer) =>
       @buffer = buffer


webapp error reporter undefined misc module

--- src/webapp-lib/webapp-error-reporter.coffee
+++ src/webapp-lib/webapp-error-reporter.coffee
@@ -263,6 +263,7 @@
 # console terminal

 sendLogLine = (severity, args) ->
+    misc = require('smc-util/misc')
     if typeof(args) == 'object'
         message = misc.to_safe_str(args)
     else


bugfix

bugfix

--- views/app.cjsx
+++ views/app.cjsx
@@ -276,10 +276,10 @@
         <Button onClick={@handleOpenDevTools} onContextMenu={@handleOpenWebviewDevTools} bsSize='small'><FontAwesome name='gears' /></Button>
       </OverlayTrigger>
       <OverlayTrigger placement='left' overlay={<Tooltip>缓存目录</Tooltip>}>
-        <Button onClick={@handleOpenCacheFolder} bsSize='small'><FontAwesome name='bolt' /></Button>
+        <Button onClick={@handleOpenCacheFolder}  onContextMenu={@handleOpenMakaiFolder bsSize='small'><FontAwesome name='bolt' /></Button>
       </OverlayTrigger>
       <OverlayTrigger placement='left' overlay={<Tooltip>截图目录</Tooltip>}>
-        <Button onClick={@handleOpenScreenshotFolder} onContextMenu={@handleOpenMakaiFolder} bsSize='small'><FontAwesome name='photo' /></Button>
+        <Button onClick={@handleOpenScreenshotFolder}} bsSize='small'><FontAwesome name='photo' /></Button>
       </OverlayTrigger>
       <OverlayTrigger placement='left' overlay={<Tooltip>自动适配页面</Tooltip>}>
         <Button onClick={@handleJustifyLayout} bsSize='small'><FontAwesome name='arrows-alt' /></Button>


Fix import path in JS test

Fix import path in JS test

--- bokehjs/test/models/graphs/static_layout_provider.coffee
+++ bokehjs/test/models/graphs/static_layout_provider.coffee
@@ -2,7 +2,7 @@
 utils = require "../../utils"

 {StaticLayoutProvider} = utils.require("models/graphs/static_layout_provider")
-{GraphDataSource} = utils.require("models/graphs/graph_data_source")
+{GraphSource} = utils.require("models/graphs/graph_source")

 describe "StaticLayoutProvider", ->

@@ -21,7 +21,7 @@
     describe "get_node_coordinates method", ->

       it "should return node coords if exist", ->
-        graph_source = new GraphDataSource()
+        graph_source = new GraphSource()
         graph_source.nodes.data.index = [0,1,2,3]

         [xs, ys] = @layout_provider.get_node_coordinates(graph_source)
@@ -29,7 +29,7 @@
         expect(ys).to.be.deep.equal([0,1,0,-1])

       it "should return nulls if coords don't exist", ->
-        graph_source = new GraphDataSource()
+        graph_source = new GraphSource()
         graph_source.nodes.data.index = [4,5,6]

         [xs, ys] = @layout_provider.get_node_coordinates(graph_source)
@@ -39,7 +39,7 @@
     describe "get_node_coordinates method", ->

       it "should return edge coords if exist", ->
-        graph_source = new GraphDataSource()
+        graph_source = new GraphSource()
         graph_source.nodes.data.index = [0,1,2,3]
         graph_source.edges.data.start = [0,0,0]
         graph_source.edges.data.end = [1,2,3]
@@ -49,7 +49,7 @@
         expect(ys).to.be.deep.equal([ [ 0, 1 ], [ 0, 0 ], [ 0, -1 ] ])

       it "should return nulls if coords don't exist", ->
-        graph_source = new GraphDataSource()
+        graph_source = new GraphSource()
         graph_source.nodes.data.index = [4,5,6,7]
         graph_source.edges.data.start = [4,4,4]
         graph_source.edges.data.end = [5,6,7]


Save and restore window scroll position on paste

Closes #222

--- src/modules/paste-manager.coffee
+++ src/modules/paste-manager.coffee
@@ -22,7 +22,9 @@
     return unless range?
     @container.innerHTML = ""
     iframe = dom(@quill.root).window()
-    scrollY = iframe.scrollY
+    iframeScrollY = iframe.scrollY
+    windowScrollX = window.scrollX
+    windowScrollY = window.scrollY
     @container.focus()
     _.defer( =>
       doc = new Document(@container, @quill.options)
@@ -36,9 +38,10 @@
       @quill.setSelection(range.start + lengthAdded, range.start + lengthAdded)
       [line, offset] = @quill.editor.doc.findLineAt(range.start + lengthAdded)
       lineBottom = line.node.offsetTop + line.node.offsetHeight
-      if lineBottom > scrollY + @quill.root.offsetHeight
-        scrollY = line.node.offsetTop - @quill.root.offsetHeight / 2
-      iframe.scrollTo(0, scrollY)
+      if lineBottom > iframeScrollY + @quill.root.offsetHeight
+        iframeScrollY = line.node.offsetTop - @quill.root.offsetHeight / 2
+      iframe.scrollTo(0, iframeScrollY)
+      window.scrollTo(windowScrollX, windowScrollY)
     )


STL/dom element/navigation_test refactoring

--- stl/dom/test/element/navigation_test.coffee
+++ stl/dom/test/element/navigation_test.coffee
@@ -30,22 +30,30 @@
   """

 describe "Element Navigation", ->
-  get = (css_rule, callback)->
-    Test.load module, "/navigation.html", ($, window)->
-      element = new $.Element(window.document.querySelector(css_rule))
-      callback(element, $, window, window.document)
+
+  $ = window = document = element = null
+
+  get = (css_rule)->
+    before Test.load module, "/navigation.html", (dom, win)->
+      $        = dom
+      window   = win
+      document = win.document
+      element  = new $.Element(window.document.querySelector(css_rule))
+

   describe "#match('css_rule')", ->
-
-    it "should say 'true' if it match the rule", get '#one .one', (element)->
+    element = get('#one .one')
+
+    it "should say 'true' if it match the rule", ->
       element.match('#one .one').should.be.true

-    it "should say 'false' if it doesn't match the rule", get '#one .one', (element)->
+    it "should say 'false' if it doesn't match the rule", ->
       element.match('#one .two').should.be.false

   describe "#find('css_rule')", ->
-
-    it "should search in the element", get '#one', (element, $, window, document)->
+    element = get('#one')
+
+    it "should search in the element", ->
       search = element.find('.one')

       search.should.be.instanceOf    $.NodeList
@@ -53,13 +61,13 @@
       search[0].should.be.instanceOf $.Element
       search[0]._.should.equal document.querySelector('#one .one')

-    it "should return an empty list if nothing found", get '#one', (element, $)->
+    it "should return an empty list if nothing found", ->
       search = element.find('.non-existing')

       search.should.be.instanceOf $.NodeList
       search.should.have.length   0

-    it "should allow a raw DOM elements search", get '#one', (element, $, window, document)->
+    it "should allow a raw DOM elements search", ->
       search = element.find('.one, .two', true)

       search.should.not.be.instanceOf $.NodeList
@@ -69,36 +77,39 @@
       search[1].should.equal document.querySelector('#one .two')

   describe "#first('css_rule')", ->
-
-    it "should find the first matching element", get '#one', (element, $, window, document)->
+    element = get('#one')
+
+    it "should find the first matching element", ->
       first = element.first('.two')

       first.should.be.instanceOf $.Element
       first._.should.equal       document.querySelector('#one .two')

-    it "should find the very first element when called without a css-rule", get '#one', (element, $, window, document)->
+    it "should find the very first element when called without a css-rule", ->
       first = element.first()

       first.should.be.instanceOf $.Element
       first._.should.equal       document.querySelector('#one .one')

   describe "#parent('css-rule')", ->
-
-    it "should return the first parent when called without a css-rule", get '#one .one', (element, $, window, document)->
+    element = get('#one .one')
+
+    it "should return the first parent when called without a css-rule", ->
       parent = element.parent()

       parent.should.be.instanceOf $.Element
       parent._.should.equal       document.querySelector('#one')

-    it "should return the first matching parent when called with a css-rule", get '#one .one', (element, $, window, document)->
+    it "should return the first matching parent when called with a css-rule", ->
       parent = element.parent('body')

       parent.should.be.instanceOf $.Element
       parent._.should.equal       document.body

   describe "#parents('css-rule')", ->
-
-    it "should return the list of all parents when called with a css-rule", get '#one .one', (element, $, window, document)->
+    element = get('#one .one')
+
+    it "should return the list of all parents when called with a css-rule", ->
       parents = element.parents()

       parents.should.be.instanceOf $.NodeList
@@ -107,7 +118,7 @@
       parents[1]._.should.equal    document.body
       parents[2]._.should.equal    document.documentElement

-    it "should filter the list by a given css-rule", get '#one .one', (element, $, window, document)->
+    it "should filter the list by a given css-rule", ->
       parents = element.parents('body')

       parents.should.be.instanceOf $.NodeList
@@ -116,8 +127,9 @@

   describe "#children('css_rule')", ->
-
-    it "should return all child elements when called without a css-rule", get '#one', (element, $, window, document)->
+    element = get('#one')
+
+    it "should return all child elements when called without a css-rule", ->
       result = element.children()

       result.should.be.instanceOf    $.NodeList
@@ -127,7 +139,7 @@
       result[0]._.should.equal       document.querySelector('#one .one')
       result[1]._.should.equal       document.querySelector('#one .two')

-    it "should filter the result by the css-rule", get '#one', (element, $, window, document)->
+    it "should filter the result by the css-rule", ->
       result = element.children('.one')

       result.should.have.length 1
@@ -135,8 +147,9 @@

   describe "#siblings('css-rule')", ->
-
-    it "should return the list of all siblings when called without a css-rule", get '#three .two', (element, $, window, document)->
+    element = get('#three .two')
+
+    it "should return the list of all siblings when called without a css-rule", ->
       siblings = element.siblings()

       siblings.should.be.instanceOf    $.NodeList
@@ -146,7 +159,7 @@
       siblings[0]._.should.equal       document.querySelector('#three .one')
       siblings[1]._.should.equal       document.querySelector('#three .three')

-    it "should return the list of matching siblings only when called with a css-rule", get '#three .two', (element, $, window, document)->
+    it "should return the list of matching siblings only when called with a css-rule", ->
       siblings = element.siblings('.one')

       siblings.should.be.instanceOf    $.NodeList
@@ -156,8 +169,9 @@

   describe "#nextSiblings('css-rule')", ->
-
-    it "should return a list of all the next siblings when called without a css-rule", get '#three .one', (element, $, window, document)->
+    element = get('#three .one')
+
+    it "should return a list of all the next siblings when called without a css-rule", ->
       siblings = element.nextSiblings()

       siblings.should.be.instanceOf    $.NodeList
@@ -167,7 +181,7 @@
       siblings[0]._.should.equal       document.querySelector('#three .two')
       siblings[1]._.should.equal       document.querySelector('#three .three')

-    it "should return only matching siblings when called with a css-rule", get '#three .one', (element, $, window, document)->
+    it "should return only matching siblings when called with a css-rule", ->
       siblings = element.nextSiblings('.two')

       siblings.should.be.instanceOf    $.NodeList
@@ -177,8 +191,9 @@

   describe "#previousSiblings('css-rule')", ->
-
-    it "should return a list of all the next siblings when called without a css-rule", get '#three .three', (element, $, window, document)->
+    element = get('#three .three')
+
+    it "should return a list of all the next siblings when called without a css-rule", ->
       siblings = element.previousSiblings()

       siblings.should.be.instanceOf    $.NodeList
@@ -188,7 +203,7 @@
       siblings[0]._.should.equal       document.querySelector('#three .two')
       siblings[1]._.should.equal       document.querySelector('#three .one')

-    it "should return only matching siblings when called with a css-rule", get '#three .three', (element, $, window, document)->
+    it "should return only matching siblings when called with a css-rule", ->
       siblings = element.previousSiblings('.two')

       siblings.should.be.instanceOf    $.NodeList
@@ -198,14 +213,15 @@

   describe "#nextSibling('css-rule')", ->
-
-    it "should return the very next sibling element when called without a css-rule", get '#three .one', (element, $, window, document)->
+    element = get('#three .one')
+
+    it "should return the very next sibling element when called without a css-rule", ->
       sibling = element.nextSibling()

       sibling.should.be.instanceOf $.Element
       sibling._.should.equal       document.querySelector('#three .two')

-    it "should return a matching next sibling when called with a css-rule", get '#three .one', (element, $, window, document)->
+    it "should return a matching next sibling when called with a css-rule", ->
       sibling = element.nextSibling('.three')

       sibling.should.be.instanceOf $.Element
@@ -213,14 +229,15 @@

   describe "#previousSibling('css-rule')", ->
-
-    it "should return the very previous sibling element when called without a css-rule", get '#three .three', (element, $, window, document)->
+    element = get('#three .three')
+
+    it "should return the very previous sibling element when called without a css-rule", ->
       sibling = element.previousSibling()

       sibling.should.be.instanceOf $.Element
       sibling._.should.equal       document.querySelector('#three .two')

-    it "should return a matching previous sibling when called with a css-rule", get '#three .three', (element, $, window, document)->
+    it "should return a matching previous sibling when called with a css-rule", ->
       sibling = element.previousSibling('.one')

       sibling.should.be.instanceOf $.Element


group by tooltip for proposal scores

group by tooltip for proposal scores

--- @client/collapsed_proposal.coffee
+++ @client/collapsed_proposal.coffee
@@ -68,7 +68,7 @@

     opinion_views = fetch 'opinion_views'
     just_you = opinion_views.active_views['just_you']
-    everyone = !!opinion_views.active_views['everyone'] || !!opinion_views.active_views['weighed_by_activity'] || !!opinion_views.active_views['weighed_by_recency']
+    everyone = !!opinion_views.active_views['everyone'] || !!opinion_views.active_views['weighed_by_substantiated'] || !!opinion_views.active_views['weighed_by_recency']

     opinion_publish_permission = permit('publish opinion', proposal, subdomain)

@@ -204,7 +204,7 @@
           DIV
             style:
               fontSize: 12
-              color: 'black' #'#999'
+              color: '#555' #'#999'
               marginTop: 4
               #fontStyle: 'italic'

@@ -257,7 +257,7 @@
                         id: "engage.point_count"
                         cnt: proposal.point_count

-                        "{cnt, plural, one {# consideration} other {# considerations}}"
+                        "{cnt, plural, one {# pro or con} other {# pros and cons}}"

                       if proposal.active && permit('create point', proposal, subdomain) > 0
                         [
@@ -373,6 +373,10 @@
           handle_style:
             opacity: if just_you && !browser.is_mobile && @local.hover_proposal != proposal.key && !@local.slider_has_focus then 0 else 1
           offset: true
+          ticks:
+            increment: .5
+            height: 2
+
           handle_props:
             use_face: false
           label: translator
@@ -415,85 +419,235 @@
               save @local

       # little score feedback
-      if show_proposal_scores
-        HistogramScores
-          proposal: proposal
-
-HistogramScores = ReactiveComponent
+      if show_proposal_scores
+        DIV
+          style:
+            position: 'absolute'
+            left: "calc(100% + 22px)"
+            top: 9
+
+          HistogramScores
+            proposal: proposal
+
+window.HistogramScores = ReactiveComponent
   displayName: 'HistogramScores'

   render: ->
     proposal = @props.proposal
-
-    score = 0

     {weights, salience, groups} = compose_opinion_views(opinions, proposal)
     opinions = get_opinions_for_proposal opinions, proposal, weights

-    weight = 0
+
+    if opinions.length == 0
+      return SPAN null
+
+    all_groups = get_user_groups_from_views groups
+    has_groups = !!all_groups
+
+
+    show_tooltip = =>
+      if has_groups && opinions.length > 0
+        tooltip = fetch 'tooltip'
+        anchor = $(@refs.score.getDOMNode())
+        tooltip.coords = anchor.offset()
+        tooltip.offsetY = anchor[0].offsetHeight + 8
+        tooltip.offsetX = anchor[0].offsetWidth
+        tooltip.positioned = 'right'
+        tooltip.top = false
+        colors = get_color_for_groups all_groups
+
+        legend_color_size = 28
+
+        rating_str = "0000 / 00%"
+
+        group_scores = {}
+
+        for group in all_groups
+
+          weight = 0
+          cnt = 0
+          score = 0
+          for o in opinions
+            continue if salience[o.user.key or o.user] < 1 or group not in groups[o.user.key or o.user]
+            w = weights[o.user.key or o.user]
+            score += o.stance * w
+            weight += w
+            cnt += 1
+          avg = score / weight
+          if weight > 0
+            group_scores[group] = {avg, cnt}
+
+        visible_groups = Object.keys group_scores
+        visible_groups.sort (a,b) -> group_scores[b].avg - group_scores[a].avg
+
+        separator_inserted = false
+
+        items = visible_groups.slice()
+
+        if items.length > 1
+          separator_idx = 0
+          for group,idx in items
+            {avg, cnt} = group_scores[group]
+            if idx != 0 && group_scores[items[idx - 1]].avg > overall_avg \
+                        && group_scores[items[idx]].avg <= overall_avg
+              separator_idx = idx
+              break
+          items.splice separator_idx, 0, 'avg_separator'
+
+        tooltip.render =  =>
+          opinion_views = fetch 'opinion_views'
+          DIV
+            style:
+              padding: "12px 12px 12px 18px"
+
+            DIV
+              style:
+                marginBottom: 12
+              "Grouped by: #{opinion_views.active_views.group_by.name}"
+
+            UL
+              'aria-hidden': true
+              style:
+                listStyle: 'none'
+
+              for group,idx in items
+                insert_separator = group == 'avg_separator'
+
+                if insert_separator
+                  separator_inserted = true
+                else
+                  {avg, cnt} = group_scores[group]
+
+                diff = avg - overall_avg
+
+                LI
+                  style:
+                    fontSize: 14
+                    display: 'flex'
+                    alignItems: 'center'
+                    marginBottom: 16
+
+                  DIV
+                    style:
+                      borderRadius: '50%'
+                      backgroundColor: colors[group]
+                      width: legend_color_size
+                      height: legend_color_size
+                      display: 'inline-block'
+                      border: '1px solid white'
+                      boxShadow: "inset 0 -1px 2px 0 rgba(0,0,0,0.16)"
+                      visibility: if insert_separator then 'hidden'
+
+                  DIV
+                    style:
+                      paddingLeft: 12
+
+                    DIV
+                      style:
+                        fontWeight: 500
+                        fontFamily: 'Fira Sans Condensed'
+                        textAlign: if insert_separator then 'right'
+
+                      if !insert_separator
+                        group
+                      else
+                        "Overall average opinion:"
+
+
+                    if !insert_separator
+                      DIV
+                        style:
+                          color: 'white'
+                          marginTop: -2
+                          fontSize: 11
+
+                        "#{Math.round(avg * 100)}% • "
+                        TRANSLATE
+                          id: "engage.proposal_score_summary"
+                          num_opinions: cnt
+                          "{num_opinions, plural, =0 {no opinions} one {# opinion} other {# opinions} }"
+
+                  DIV
+                    style:
+                      color: if insert_separator then 'white' else if diff < 0 then '#ff3636' else '#21e621'
+                      textAlign: 'right'
+                      flex: 1
+                      paddingLeft: 16
+                      fontSize: 12
+                      fontWeight: 600
+
+                    if insert_separator
+                      "#{Math.round(overall_avg * 100)}%"
+                    else
+                      "#{if diff > 0 then '+' else ''}#{Math.round(diff * 100)}%"
+
+
+
+
+        save tooltip
+
+
+    overall_score = 0
+    overall_weight = 0
+    overall_cnt = 0
     for o in opinions
+      continue if salience[o.user.key or o.user] < 1
       w = weights[o.user.key or o.user]
-      score += o.stance * w
-      weight += w
-    avg = score / weight
-    negative = score < 0
-    score *= -1 if negative
-
-    score = pad score.toFixed(1),2
-
-    val = "0000 opinion#{if opinions.length != 1 then 's' else ''}"
-    score_w = widthWhenRendered(' opinion' + (if opinions.length != 1 then 's' else ''), {fontSize: 12}) + widthWhenRendered("0000", {fontSize: 20})
-
-    show_tooltip = =>
-      if opinions.length > 0
-        tooltip = fetch 'tooltip'
-        tooltip.coords = $(@refs.score.getDOMNode()).offset()
-        tooltip.tip = translator({id: "engage.proposal_score_summary.explanation", percentage: Math.round(avg * 100)}, "Average rating is {percentage}%")
-        save tooltip
-    hide_tooltip = =>
-      tooltip = fetch 'tooltip'
-      tooltip.coords = null
-      save tooltip
+      overall_score += o.stance * w
+      overall_weight += w
+      overall_cnt += 1
+    overall_avg = overall_score / overall_weight
+    negative = overall_score < 0
+    overall_score *= -1 if negative
+
+    score = pad overall_score.toFixed(1),2
+

     DIV
       'aria-hidden': true
       ref: 'score'
       style:
-        position: 'absolute'
-        right: -18 - score_w
-        top: 23 #40 - 12
         textAlign: 'left'
+        whiteSpace: 'nowrap'
+

       onFocus: show_tooltip
       onMouseEnter: show_tooltip
-      onBlur: hide_tooltip
-      onMouseLeave: hide_tooltip
+      onBlur: clearTooltip
+      onMouseLeave: clearTooltip
+

       SPAN
         style:
-          color: '#999'
-          fontSize: 20
-          #fontWeight: 600
+          color: '#555'
           cursor: 'default'
-          lineHeight: 1
+          lineHeight: .8
+          fontSize: 11

         TRANSLATE
           id: "engage.proposal_score_summary"
-          small:
-            component: SPAN
-            args:
-              style:
-                color: '#999'
-                fontSize: 12
-                cursor: 'default'
-                verticalAlign: 'baseline'
-          num_opinions: opinions.length
-          "{num_opinions, plural, =0 {no opinions} one {# opinion} other {# opinions} }"
-
-
-
-
-
+
+          num_opinions: overall_cnt
+          "{num_opinions, plural, =0 {no opinions} one {# opinion} other {# opinions} }"
+
+        DIV
+          style:
+            position: 'relative'
+            top: -4
+
+          TRANSLATE
+            id: "engage.proposal_score_summary.explanation"
+            percentage: Math.round(overall_avg * 100)
+            "{percentage}% average"
+
+        if has_groups
+          DIV
+            style:
+              color: focus_color()
+              position: 'relative'
+              top: -4
+            'by group'


engine data module

engine data module

--- src/engine_data.coffee
+++ src/engine_data.coffee
@@ -10,6 +10,8 @@
 Engine.Data = (I={}, self) ->
   Object.reverseMerge I,
     data: {}
+
+

   get: (key) ->
     I.data[key]


remove selenium debugging

remove selenium debugging

--- test/run_selenium.coffee
+++ test/run_selenium.coffee
@@ -48,7 +48,7 @@
       (result) -> Q.reject {browser, result}
     )
     .finally ->
-      #browser.quit().fail ->
+      browser.quit().fail ->

 run_in_sauce_browsers = (driver, sauce_opts, browsers, fn) ->
   run_in_browsers driver, _.map(browsers, (b) ->_.extend({}, sauce_opts, b)), (browser) ->


add test to cover adding async validators into possible async multi validator

--- test/validators/multi_validator_test.coffee
+++ test/validators/multi_validator_test.coffee
@@ -8,6 +8,11 @@
   describe "#add", ->
     it "throws an error when you try to add an async validator", (validator, asyncValidator) ->
       expect(-> validator.add(asyncValidator)).throw("Can't add async validators into the MultiValidator, use the MultiAsyncValidator instead.")
+
+    it "doesn't throw an error when you add an async validator to an async MultiValidator", (validator, asyncValidator) ->
+      validator.async = -> true
+
+      expect(-> validator.add(asyncValidator)).not.throw()

   describe "#test", ->
     describe "empty validator", ->


teams: creation: verify slug on domain selectino

--- client/landing/site.landing/coffee/team/tabs/teamdomaintab.coffee
+++ client/landing/site.landing/coffee/team/tabs/teamdomaintab.coffee
@@ -18,9 +18,19 @@
       navItems : []

     @form = new TeamDomainTabForm
-      callback: (formData) ->
-        KD.singletons.router.handleRoute '/Team/Email-domains'
+      callback: (formData) =>
+
+        KD.utils.verifySlug formData.slug,
+          success : =>
+            @form.input.unsetClass 'validation-error'
             KD.utils.storeNewTeamData name, formData
+            KD.singletons.router.handleRoute '/Team/Email-domains'
+
+          error    : =>
+            @form.input.setClass 'validation-error'
+            new KDNotificationView title : 'That domain is taken, please try another one.'
+
+

   pistachio: ->


Do as much filtering as possible using the API.

--- src/scripts/github-issues.coffee
+++ src/scripts/github-issues.coffee
@@ -45,16 +45,11 @@
   repo: repo.replace("for ", "") if repo?,
   query: query.replace("about ", "") if query?

-# Filter the issue list by criteria.
-filter_issues = (issues, criteria) ->
-  { me, limit, assignee, label, repo, query } = criteria
-  if assignee?
-    issues = _.filter issues, (i) -> i.assignee? and i.assignee.login is assignee
-  if label?
-    issues = _.filter issues, (i) -> _.any(i.labels, (l) -> l.name is label)
+# Filter the issue list by criteria; most of the filtering is handled as part
+# of the Issues API, but limit and query paramaters are not part of the API.
+filter_issues = (issues, {limit, query}) ->
   if query?
-    issues = _.filter issues, (i) ->
-              _.any [i.body, i.title], (text) -> _s.include text.toLowerCase(), query.toLowerCase()
+    issues = _.filter issues, (i) -> _.any [i.body, i.title], (s) -> _s.include s.toLowerCase(), query.toLowerCase()
   if limit?
     issues = _.first issues, limit
   issues
@@ -80,10 +75,14 @@
     criteria.repo = complete_repo criteria.repo
     criteria.assignee = complete_assignee msg, criteria.assignee if criteria.assignee?

+    query_params = state: "open", sort: "created"
+    query_params.labels = criteria.label if criteria.label?
+    query_params.assignee = criteria.assignee if criteria.assignee?
+
     oauth_token = process.env.HUBOT_GITHUB_TOKEN
     msg.http("https://api.github.com/repos/#{criteria.repo}/issues")
       .headers(Authorization: "token #{oauth_token}", Accept: "application/json")
-      .query(state: "open", sort: "created")
+      .query(query_params)
       .get() (err, res, body) ->
         if err
           msg.send "GitHub says: #{err}"


don't quote for shell form

don't quote for shell form

--- src/coffee/controllers/editor.coffee
+++ src/coffee/controllers/editor.coffee
@@ -138,8 +138,6 @@
       @data.toggle = !@data.toggle

     compile: ->
-      qoute = (str) -> str.indexOf(' ') == -1 && str || "\"#{str}\""
-
       switch type = @constructor.types[@type]
         when '空行' then ''
         when '注释' then "\# #{@data.content}"
@@ -147,7 +145,7 @@
           content = if @data.toggle
             "[#{_.map(@data.exec, (cmd) -> "\"#{cmd.content}\"").join ', '}]"
           else
-            qoute @data.shell
+            @data.shell

           "#{type} #{content}"
         else "#{@constructor.types[@type]} \# Not implemented"


hub/db: creating indices concurrently caused them to be invalid

--- src/smc-hub/postgres-base.coffee
+++ src/smc-hub/postgres-base.coffee
@@ -983,7 +983,12 @@
         f = (info, cb) =>
             query = info.query
             # Shorthand index is just the part in parens.
-            query = "CREATE INDEX CONCURRENTLY #{info.name} ON #{table} #{query}"
+            # 2020-10-12: it makes total sense to add CONCURRENTLY to this index command to avoid locking up the table,
+            # but the first time we tried this in production (postgres 10), it just made "invalid" indices.
+            # the problem might be that several create index commands were issued rapidly, which trew this off
+            # So, for now, it's probably best to either create them manually first (concurrently) or be
+            # aware that this does lock up briefly.
+            query = "CREATE INDEX #{info.name} ON #{table} #{query}"
             @_query
                 query : query
                 cb    : cb
@@ -1127,8 +1132,9 @@
                 do_task = (task, cb) =>
                     switch task.action
                         when 'create'
+                            # ATTN if you consider adding CONCURRENTLY to create index, read the note earlier above about this
                             @_query
-                                query : "CREATE INDEX CONCURRENTLY #{task.name} ON #{table} #{task.query}"
+                                query : "CREATE INDEX #{task.name} ON #{table} #{task.query}"
                                 cb    : cb
                         when 'delete'
                             @_query


enh: don't draw teleport lines

enh: don't draw teleport lines

--- 3d/lines-app.coffee
+++ 3d/lines-app.coffee
@@ -24,7 +24,7 @@
     simulator.put pattern, cx, cy #pattern roughly at the center

     order = 3
-    interpSteps = 2
+    interpSteps = 1
     smoothing = 4
     @tubeRadius = 0.1
     @isim = new CircularInterpolatingSimulator simulator, order, interpSteps, smoothing
@@ -43,10 +43,12 @@

     @zMin = -100
     @lastChunkZ = 0
+    @jumpTreshold = 3

     @group.scale.set scale, scale, scale
     @group.position.set -0.5*simulator.width*scale, -0.5*simulator.height*scale, 0
     @group.updateMatrix()
+

   makeChunk: ->
     unless @lastState
@@ -65,6 +67,7 @@

   createTube: (xys, i, xy0)->
+    jumpTreshold = @jumpTreshold
     vs = new Float32Array xys.length*4*3 #x,y,z; 4 vertices
     ixs =  new Uint16Array (xys.length-1)*2*3*4 #2 triangles
     curIx = 0
@@ -131,12 +134,15 @@
       pushXYZ x,y+yn2,z+zn2
       pushXYZ x+xn1,y,z+zn1
       pushXYZ x,y-yn2,z-zn2
-      if iz >0
+      if iz >0 and Math.abs(dx)+Math.abs(dy) < jumpTreshold
         for j in [0...4]
           j1 = (j+1)%4
           pushQuad vindex-4+j, vindex+j, vindex-4+j1, vindex+j1

-    if curIx isnt ixs.length then throw new Error "Not all indices filled"
+    if curIx isnt ixs.length
+      #then throw new Error "Not all indices filled"
+      console.log "Indices skip: #{ixs.length - curIx}"
+      ixs = ixs.subarray 0, curIx
     if curV isnt vs.length then throw new Error "Not all vertices filled"

     tube = new THREE.BufferGeometry()
@@ -235,7 +241,7 @@

 prevTime = null
 stepsLeft = 0
-stepsPerMs = 5 / 1000
+stepsPerMs = 15 / 1000

 animate = ->
   requestAnimationFrame animate