teams: admin: no members/mods/admins found view added

--- client/admin/lib/views/members/adminmembersview.coffee
+++ client/admin/lib/views/members/adminmembersview.coffee
@@ -1,5 +1,6 @@
 kd                    = require 'kd'
 KDView                = kd.View
+KDCustomHTMLView      = kd.CustomHTMLView
 isKoding              = require 'app/util/isKoding'
 KDTabView             = kd.TabView
 KDTabPaneView         = kd.TabPaneView
@@ -27,9 +28,27 @@
     tabView.addPane admins = new KDTabPaneView name: 'Admins'
     tabView.addPane mods   = new KDTabPaneView name: 'Moderators'

-    all.addSubView    @allView    = new TeamMembersCommonView { fetcherMethod: 'fetchMembersWithEmail'    }, data
-    admins.addSubView @adminsView = new TeamMembersCommonView { fetcherMethod: 'fetchAdminsWithEmail'     }, data
-    mods.addSubView   @modsView   = new TeamMembersCommonView { fetcherMethod: 'fetchModeratorsWithEmail' }, data
+    all.addSubView @allView = new TeamMembersCommonView
+      fetcherMethod     : 'fetchMembersWithEmail'
+      noItemFoundWidget : new KDCustomHTMLView
+        partial         : 'No members found!'
+        cssClass        : 'no-item-view'
+    , data
+
+    admins.addSubView @adminsView = new TeamMembersCommonView
+      fetcherMethod     : 'fetchAdminsWithEmail'
+      noItemFoundWidget : new KDCustomHTMLView
+        partial         : 'No admins found!'
+        cssClass        : 'no-item-view'
+    , data
+
+    mods.addSubView @modsView = new TeamMembersCommonView
+      fetcherMethod     : 'fetchModeratorsWithEmail'
+      noItemFoundWidget : new KDCustomHTMLView
+        partial         : 'No moderators found!'
+        cssClass        : 'no-item-view'
+    , data
+

     if isKoding()
       tabView.addPane blockedMembersPane = new KDTabPaneView name: 'Blocked'


#485 - fixed destructive translations by adding another > character

thanks, google translate!

--- app/assets/javascripts/search/cells/search_results.coffee
+++ app/assets/javascripts/search/cells/search_results.coffee
@@ -73,6 +73,6 @@

     for category, index in ancestors
       output += Handlebars.partials['_category_link'] name: category
-      output += ' > ' unless index is last_index
+      output += ' >> ' unless index is last_index

     output


StacksCustomViews: credentialList pass stackTemplate to credItems to check if in use or not

--- client/admin/lib/views/stacks/stackscustomviews.coffee
+++ client/admin/lib/views/stacks/stackscustomviews.coffee
@@ -352,15 +352,19 @@
       return container

-    credentialList: (provider) =>
+    credentialList: (options) =>
+
+      { provider, stackTemplate } = options

       listView   = new AccountCredentialList
-        itemClass  : CredentialListItem
+        itemClass   : CredentialListItem
+        itemOptions : { stackTemplate }
+
       controller = new AccountCredentialListController
-        view       : listView
-        wrapper    : no
-        scrollView : no
-        provider   : provider
+        view        : listView
+        wrapper     : no
+        scrollView  : no
+        provider    : provider

       __view = controller.getView()
       return { __view, controller }
@@ -368,11 +372,11 @@

     stepSetupCredentials: (options) =>

-      {data, callback, cancelCallback} = options
-      {provider} = data
-
-      container = @views.container 'step-creds'
-      views     = @addTo container,
+      { data, callback, cancelCallback } = options
+      { provider, stackTemplate } = data
+
+      container  = @views.container 'step-creds'
+      views      = @addTo container,
         stepsHeaderView : 2
         container_top   :
           text_intro    : "To be able to use this provider <strong>you need to
@@ -384,7 +388,7 @@
             cssClass    : 'solid compact green action'
             callback    : ->
               handleNewCredential views, provider, this
-        credentialList  : provider
+        credentialList  : { provider, stackTemplate }
         navCancelButton :
           title         : '< Select another provider'
           callback      : ->


Updates catalog service to match new AMS response

--- app/assets/javascripts/sprangular/services/catalog.coffee
+++ app/assets/javascripts/sprangular/services/catalog.coffee
@@ -13,7 +13,7 @@
     taxonomies: ->
       $http.get("/api/taxonomies", {cache: true})
         .then (response) ->
-          response.data
+          response.data.taxonomies

     taxonsByName: (name) ->
       @taxonomies().then (response)->


Workspace: Updates for ChatPane to make it floating.

--- client/app/CommonViews/workspace/panes/chatpane.coffee
+++ client/app/CommonViews/workspace/panes/chatpane.coffee
@@ -2,7 +2,8 @@

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

-    options.cssClass = "workspace-chat"
+    floatingCssClass = if options.floating then "floating" else ""
+    options.cssClass = KD.utils.curry "workspace-chat", floatingCssClass

     super options, data

@@ -43,7 +44,7 @@
     @dock.addSubView @title

     @chatRef.on "child_added", (snapshot) =>
-      unless @isVisible()
+      unless @isVisible() or @getOptions().floating
         @title.updatePartial "Chat (#{++@unreadCount})"
         @dock.setClass "pulsing"


sidebar: emit an event when machines updated

--- client/app/lib/activity/sidebar/activitysidebar.coffee
+++ client/app/lib/activity/sidebar/activitysidebar.coffee
@@ -681,7 +681,9 @@
   updateMachines: (callback = kd.noop) ->

     kd.singletons.mainController.ready =>
-      @fetchEnvironmentData @bound 'redrawMachineList'
+      @fetchEnvironmentData (data) =>
+        @redrawMachineList()
+        @emit 'MachinesUpdated'

   invalidateWorkspaces: (machine) ->


ParseTerraformOutput: support providers as argument

--- client/app/lib/util/stacks/parseterraformoutput.coffee
+++ client/app/lib/util/stacks/parseterraformoutput.coffee
@@ -1,6 +1,4 @@
-globals = require 'globals'
-
-module.exports = parseTerraformOutput = (response) ->
+module.exports = parseTerraformOutput = (response, supportedProviders) ->

   # An example of a valid stack template
   # ------------------------------------
@@ -19,6 +17,10 @@
   #   }
   # ],

+  unless supportedProviders
+    globals = require 'globals'
+    supportedProviders = globals.config.providers
+
   out = { machines: [] }

   { machines } = response
@@ -29,7 +31,7 @@

     machineBaseData = { label, provider }

-    provider = globals.config.providers[provider]
+    provider = supportedProviders[provider] ? {}

     if attrMap = provider.attributeMapping
       (Object.keys attrMap).forEach (attribute) ->
@@ -38,6 +40,4 @@

     out.machines.push machineBaseData

-  console.info '[parseTerraformOutput]', out.machines
-
   return out.machines


fix equation editor when opening multiple times

test plan:
* using the rich text editor, use the equation editor
to insert an equation
* click again to insert another equation (should start
with a blank equation editor)
* should be able to insert another equation instead
of receiving an "undefined" message

fixes #CNVS-10242

Change-Id: I7d07a751a8e4fa454eec616d9fadeac01ad8e760
Reviewed-on: https://gerrit.instructure.com/28172
Reviewed-by: Jon Jensen <44f878afe53efc66b76772bd845eb65944ed8232@instructure.com>
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
QA-Review: Nathan Rogowski <2e8aa918660411855c6d44d5bb2da677aa033255@instructure.com>
Product-Review: James Williams  <3a5f4ed991dbfa5c9ef76ad729d240ca12f62180@instructure.com>

--- app/coffeescripts/views/tinymce/EquationEditorView.coffee
+++ app/coffeescripts/views/tinymce/EquationEditorView.coffee
@@ -42,7 +42,6 @@
       '#mathjax-message': '$mathjaxMessage'

     initialize: (@editor) ->
-      super
       @$editor = $("##{@editor.id}")
       @prevSelection = @editor.selection.getBookmark()


guestcleanerworker: console doesn't have err method, it has error

--- workers/guestcleaner/lib/guestcleaner/guestcleanerwoker.coffee
+++ workers/guestcleaner/lib/guestcleaner/guestcleanerwoker.coffee
@@ -64,7 +64,7 @@
       accountIds = _.map accounts, (account)-> return account._id

       JAccount.update {_id: $in: accountIds}, {$set: status: 'tobedeleted'}, {multi: yes}, (err)=>
-        if err then console.err err
+        if err then console.error err

         accounts.forEach (account) =>
           queue = [


For the first time we need to check even if user is the creator

--- workers/social/lib/social/models/chatconversation/index.coffee
+++ workers/social/lib/social/models/chatconversation/index.coffee
@@ -121,9 +121,9 @@
       return callback new KodingError 'Access denied.'

     {nickname} = delegate.profile
-    delegateCanInvite = nickname in @invitees

-    return callback new KodingError "Access denied!"  unless delegateCanInvite
+    unless nickname in [@createdBy].concat @invitees
+      return callback? new KodingError "Access denied!"

     @update {$addToSet: invitees: invitee}, (err)=>
       return console.error err  if err?


we are hitting stripe too much with this.  Of course we will need to rewrite this code at some point, e.g., to be more intelligent...

--- src/smc-hub/stripe/sync.coffee
+++ src/smc-hub/stripe/sync.coffee
@@ -18,7 +18,7 @@
         logger    : {debug:console.log}
         database  : required
         target    : undefined
-        limit     : 3  # number at once
+        limit     : 1  # number at once -- stripe will kick us out due to exceeding rate limit thresh if this is bigger than 1...
         cb        : undefined

     dbg = (m) -> opts.logger?.debug("stripe_sync: #{m}")


bug fixing for create branch modal

bug fixing for create branch modal

--- src/smc-webapp/editor_git.cjsx
+++ src/smc-webapp/editor_git.cjsx
@@ -561,8 +561,8 @@
                     <MenuItem eventKey="{file}" onSelect={(e)=>@props.actions.setState(show_create_branch_modal:true)}>Create a branch and reset to upstream master</MenuItem>
                     {@render_branches()}
                 </DropdownButton>
-

-                    <Modal.Dialog show={@props.show_create_branch_modal}>
+

+                    <Modal.Dialog show={@props.show_create_branch_modal} onHide={=>@props.actions.setState(show_create_branch_modal:false)}>
                         <Modal.Header>
                             <Modal.Title>Create a banch</Modal.Title>
                         </Modal.Header>
@@ -574,7 +574,7 @@
                             value       = {@props.new_branch_name ? ''}
                             placeholder = {'Branch name'}
                             onChange    = {=>@props.actions.setState(new_branch_name:@refs.new_branch_name.getValue())}
-                            onKeyDown   = {=>@handle_keypress(e, 'new_branch_name', 'create_branch_and_reset_to_upstream_master')}
+                            onKeyDown   = {=>@handle_keypress('new_branch_name', 'create_branch_and_reset_to_upstream_master')}
                         />
                       </Modal.Body>


The most important commit to get Apps running.

--- client/landing/app/MainApp/kodingappscontroller.coffee
+++ client/landing/app/MainApp/kodingappscontroller.coffee
@@ -287,6 +287,7 @@
           try
             # security please!
             do (appView)->
+              appScript = "var appView = KD.instances[\"#{appView.getId()}\"];\n\n"+appScript
               eval appScript
           catch e
             warn "App caused some problems:", e


landing: replace spaces with plus in emails

--- client/landing/site.landing/coffee/teams/routes.coffee
+++ client/landing/site.landing/coffee/teams/routes.coffee
@@ -26,6 +26,8 @@
     { params, query } = routeInfo
     { email } = query

+    email = email.replace /\s/g, '+'
+
     # Remember already typed companyName when user is seeing "Create a team"
     # page with refresh twice or more
     if teamData = utils.getTeamData()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/number_extensions.coffee
+++ src/number_extensions.coffee
@@ -74,7 +74,7 @@
   Math.round(this)

 ###*
-@name circularPo
+@name circularPoint
 ###
 Number::circularPoints = (n, block) ->
   n.times (i) ->


Version 2014-10-28

Version 2014-10-28

--- coffee/hatemile/util/jqueryandvanilla/VanillaHTMLDOMElement.coffee
+++ coffee/hatemile/util/jqueryandvanilla/VanillaHTMLDOMElement.coffee
@@ -41,7 +41,7 @@
 	 * @classdesc The VanillaHTMLDOMElement class is official implementation of
 	 * HTMLDOMElement interface for the Javascript.
 	 * @extends hatemile.util.HTMLDOMElement
-	 * @version 2014-07-23
+	 * @version 2014-10-28
 	 * @memberof hatemile.util.jqueryandvanilla
 	###
 	constructor: (@data) ->
@@ -83,12 +83,12 @@
 		return text

 	insertBefore: (newElement) ->
-		parent = @data.parentElement
+		parent = @data.parentNode
 		parent.insertBefore(newElement.getData(), @data)
 		return newElement

 	insertAfter: (newElement) ->
-		parent = @data.parentElement
+		parent = @data.parentNode
 		childs = parent.childNodes
 		found = false
 		for child in childs
@@ -105,7 +105,7 @@
 		return this

 	replaceElement: (newElement) ->
-		parent = @data.parentElement
+		parent = @data.parentNode
 		parent.replaceChild(newElement.getData(), @data)
 		return newElement

@@ -128,9 +128,9 @@
 		return not isEmpty(@data.children)

 	getParentElement: () ->
-		if isEmpty(@data.parentElement)
+		if isEmpty(@data.parentNode)
 			return undefined
-		return new exports.hatemile.util.jqueryandvanilla.VanillaHTMLDOMElement(@data.parentElement)
+		return new exports.hatemile.util.jqueryandvanilla.VanillaHTMLDOMElement(@data.parentNode)

 	getInnerHTML: () ->
 		return @data.innerHTML


Add/modify karma specs

Add/modify karma specs

--- spec/javascripts/unit/darkswarm/services/map_spec.js.coffee
+++ spec/javascripts/unit/darkswarm/services/map_spec.js.coffee
@@ -9,6 +9,16 @@
       orders_close_at: new Date()
       type: "hub"
       visible: true
+      latitude: 50
+      longitude: 50
+    }
+    {
+      id: 3
+      active: false
+      orders_close_at: new Date()
+      type: "hub"
+      visible: true
+
     }
   ]

@@ -24,3 +34,6 @@

   it "builds MapMarkers from enterprises", ->
     expect(OfnMap.enterprises[0].id).toBe enterprises[0].id
+
+  it "excludes enterprises without latitude or longitude", ->
+    expect(OfnMap.enterprises.map (e) -> e.id).not.toContain enterprises[1].id


fix traversal return node

fix traversal return node

--- src/normalizer.coffee
+++ src/normalizer.coffee
@@ -178,12 +178,13 @@

   normalizeTags: (lineNode) ->
     ScribeUtils.traversePreorder(lineNode, 0, (node) =>
+      containerNode = node
       [nodeFormat, nodeValue] = @formatManager.getFormat(node)
       if _.isArray(nodeFormat)
         _.each(nodeFormat.slice(1), (format, i) =>
           if @formatManager.formats[format]?
             container = @formatManager.formats[format].createContainer(nodeValue[i+1])
-            ScribeDOM.wrap(container, node)
+            containerNode = ScribeDOM.wrap(container, node)
         )
         nodeFormat = nodeFormat[0]
         nodeValue = nodeValue[0]
@@ -191,7 +192,7 @@
         @formatManager.formats[nodeFormat].clean(node)
       else
         ScribeDOM.removeAttributes(node)
-      return node
+      return containerNode
     )

   removeRedundant: (lineNode) ->


Make the error handling more defensive.

Make the error handling more defensive.

--- src/server/db/amazon.coffee
+++ src/server/db/amazon.coffee
@@ -123,7 +123,7 @@
    # Returns the error object if it should be retried and an empty object
    # otherwise.
    _shouldRetry: (error) ->
-     if error and error.Body? and error.Body.message.match('The conditional request failed')
+     if error and error.Body? and error.Body.message? and error.Body.message.match('The conditional request failed')
        false
      else
        true


Whoops

Whoops

--- src/chaplin/lib/route.coffee
+++ src/chaplin/lib/route.coffee
@@ -8,8 +8,8 @@

 isEmpty = (object) ->
   for own key, value of object
-    return true
-  false
+    return false
+  true

 module.exports = class Route
   # Borrow the static extend method from Backbone.


Fixes #353. Fix support for extensions with getLanguages

--- src/beautifiers/index.coffee
+++ src/beautifiers/index.coffee
@@ -294,8 +294,10 @@

             # Get language
             fileExtension = path.extname(filePath)
-            languages = @languages.getLanguages({grammar, fileExtension})
-
+            # Remove prefix "." (period) in fileExtension
+            fileExtension = fileExtension.substr(1)
+            languages = @languages.getLanguages({grammar, extension: fileExtension})
+            logger.verbose(languages, grammar, fileExtension)

             # Check if unsupported language
             if languages.length < 1


Fix class name for block tools

Fix class name for block tools

--- src/trix/config/toolbar.coffee
+++ src/trix/config/toolbar.coffee
@@ -10,7 +10,7 @@
         <button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="#{lang.link}" tabindex="-1">#{lang.link}</button>

-
+
         <button type="button" class="trix-button trix-button--icon trix-button--icon-heading-1" data-trix-attribute="heading1" title="#{lang.heading1}" tabindex="-1">#{lang.heading1}</button>
         <button type="button" class="trix-button trix-button--icon trix-button--icon-quote" data-trix-attribute="quote" title="#{lang.quote}" tabindex="-1">#{lang.quote}</button>
         <button type="button" class="trix-button trix-button--icon trix-button--icon-code" data-trix-attribute="code" title="#{lang.code}" tabindex="-1">#{lang.code}</button>


remove debug

remove debug

--- app/app.coffee
+++ app/app.coffee
@@ -87,8 +87,6 @@
     userlikeData.user.name= user.login;
     userlikeData.user.email = user.email;

-    console.log(user)
-
     s = document.createElement('script')
     s.src = '//userlike-cdn-widgets.s3-eu-west-1.amazonaws.com/0327dbb23382ccbbb91b445b76e8a91d4b37d90ef9f2faf84e11177847ff7bb9.js'
     document.body.appendChild(s)


Change internal version

Change internal version

--- app/assets/javascripts/webrtc_rails.coffee
+++ app/assets/javascripts/webrtc_rails.coffee
@@ -5,7 +5,7 @@

 CAPTURE_EVENT_FRONTEND = 'screen_capture_frontend'
 CAPTURE_EVENT_BACKEND = 'screen_capture_backend'
-PLUGIN_VERSION_NEEDED = '1.0.1'
+PLUGIN_VERSION_NEEDED = '1.0.0'

 WebRTC =


app: don't set state if component isn't mounted

--- client/app/lib/components/profile/profilefetchermixin.coffee
+++ client/app/lib/components/profile/profilefetchermixin.coffee
@@ -18,12 +18,18 @@

   componentDidMount: ->

+    @_isMounted = yes
+
     if @props.origin
       if @props.origin.isIntegration
         @setState { account: @props.origin }
       else
         fetchAccount @props.origin, (err, account) =>
+          return  unless @_isMounted
           @setState { err, account }
+
+
+  componentWillUnmount: -> @_isMounted = no

   renderChildren: ->


Add coupon UI skeleton

Add coupon UI skeleton

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -926,6 +926,7 @@
                     {<ConfirmPaymentMethod
                         is_recurring = {@is_recurring()}
                     /> if @props.selected_plan isnt ''}
+                    {<CouponAdder />}
                     {@render_create_subscription_buttons()}
                 </Well>
                 <ExplainResources type='shared'/>
@@ -971,6 +972,65 @@
                 />
             </Well>
         </Alert>
+
+CouponAdder = rclass
+    displayName : 'CouponAdder'
+
+    propTypes:
+        applied_coupons : rtypes.immutable.List
+
+    render: ->
+        # TODO: Error when a coupon duplicate is added
+        # TODO: (Here or elsewhere) Your final cost is:
+        #       $2 for the first month
+        #       $7/mo after the first
+        <Well>
+
<Icon name='plus' /> Add a coupon?

+            {<CouponList applied_coupons={@props.appied_coupons} /> if @props.applied_coupons?.size > 0}
+            <FormGroup>
+                <InputGroup>
+                    <FormControl
+                        ref         = 'coupon_adder'
+                        type        = 'text'
+                        size        = '7'
+                        placeholder = ''
+                        onChange    = {=>console.log "TODO: Update coupon"}
+
+                    />
+                    <InputGroup.Button>
+                        <Button onClick={=>console.log "TODO: Submit coupon:"} >
+                            <Icon name='check' />
+                        </Button>
+                    </InputGroup.Button>
+                </InputGroup>
+            </FormGroup>
+        </Well>
+
+CouponList = rclass
+    displayName : 'CouponList'
+
+    propTypes:
+        applied_coupons : rtypes.immutable.List
+
+    render: ->
+        for coupon in @props.applied_coupons
+            <CouponInfo coupon={coupon} id={coupon.get('id')} />
+
+CouponInfo = rclass
+    displayName : 'CouponInfo'
+
+    propTypes:
+        coupon : rtypes.immutable.Map
+
+    render: ->
+        <Row>
+            <Col md=4>
+                Name
+            </Col>
+            <Col md=8>
+                Description. ie. 30% off all payments or $10 of credit or $2 off all payments
+            </Col>
+        </Row>

 exports.SubscriptionGrid = SubscriptionGrid = rclass


get rid of grow effect in subscriptions page

--- src/smc-webapp/billing.cjsx
+++ src/smc-webapp/billing.cjsx
@@ -825,7 +825,6 @@

         <Panel
             style     = {style}
-            className = 'smc-grow'
             header    = {@render_plan_name(plan_data)}
             bsStyle   = {if @props.selected then 'primary' else 'info'}
             onClick   = {=>@props.on_click?()}


Fix clearImmediate polyfill in IE10.

Fix clearImmediate polyfill in IE10.

--- src/utilities/utilities.coffee
+++ src/utilities/utilities.coffee
@@ -82,11 +82,11 @@

   if container.setImmediate
     Batman.setImmediate = container.setImmediate
-    Batman.clearImmediate = container.clearImmediate
   else if container.msSetImmediate
     Batman.setImmediate = msSetImmediate
-    Batman.clearImmediate = msClearImmediate
   else if canUsePostMessage()
+    usingTasks = true
+
     prefix = 'com.batman.'
     functions = new Batman.SimpleHash
     handler = (e) ->
@@ -103,8 +103,9 @@
       tasks.set(handle = getHandle(), f)
       container.postMessage(prefix+handle, "*")
       handle
-    Batman.clearImmediate = (handle) -> tasks.unset(handle)
   else if typeof document isnt 'undefined' && "onreadystatechange" in document.createElement("script")
+    usingTasks = true
+
     Batman.setImmediate = (f) ->
       handle = getHandle()
       script = document.createElement("script")
@@ -115,7 +116,6 @@
         script = null
       document.documentElement.appendChild(script)
       handle
-    Batman.clearImmediate = (handle) -> tasks.unset(handle)
   else if process?.nextTick
     functions = {}
     Batman.setImmediate = (f) ->
@@ -126,15 +126,20 @@
         delete functions[handle]
       handle

+  else
+    Batman.setImmediate = (f) -> setTimeout(f, 0)
+
+  if container.clearImmediate
+    Batman.clearImmediate = container.clearImmediate
+  else if container.msClearImmediate
+    Batman.clearImmediate = msClearImmediate
+  else if usingTasks
+    Batman.clearImmediate = (handle) -> tasks.unset(handle)
+  else if process?.nextTick
     Batman.clearImmediate = (handle) ->
       delete functions[handle]
-
-  else
-    Batman.setImmediate = (f) -> setTimeout(f, 0)
+  else
     Batman.clearImmediate = (handle) -> clearTimeout(handle)
-
-  Batman.setImmediate = Batman.setImmediate
-  Batman.clearImmediate = Batman.clearImmediate

 Batman.setImmediate = ->
   _implementImmediates(Batman.container)


add a new in the support page about how to sign out.

--- src/smc-webapp/support.cjsx
+++ src/smc-webapp/support.cjsx
@@ -387,8 +387,12 @@

             {what}

-                Looking for documentation and help? Go to
+                Looking for documentation and help? Go to
                 the CoCalc documentation.
+

+

+                Trying to sign out?  Click on your name at the top, then click the
+                "Sign out..." button in account settings.

                 After submitting a ticket, you{"'"}ll get a link, which you may


tasks: debounce the normal saves/updates; this will speed things up a lot regarding interactive use.

--- src/smc-webapp/tasks.coffee
+++ src/smc-webapp/tasks.coffee
@@ -62,7 +62,7 @@
 HEADINGS    = ['custom', 'description', 'due', 'last-edited']
 HEADING_MAP = {custom:'position', description:'desc', due:'due_date', 'last-edited':'last_edited'}

-SPECIAL_PROPS = {element:true, changed:true, last_desc:true, desc_timer:true, desc_last_sync:true}
+SPECIAL_PROPS = {element:true, changed:true, last_desc:true}

 MIN_TIME = 1000 # minimum time in ms between sync events

@@ -939,6 +939,7 @@
     # save live state of editor to syncdb by going through all codemirror editors
     # of open in-edit-mode tasks, and saving them.
     save_live: () =>
+        #console.log("save_live")
         for task in @_visible_tasks
             e = task?.element
             if e?.hasClass('salvus-task-editing-desc')
@@ -1041,11 +1042,8 @@
         elt.find(".CodeMirror-hscrollbar").remove()
         elt.find(".CodeMirror-vscrollbar").remove()

-        task.desc_last_sync = undefined
-
         task.last_desc = task.desc  # initialize last_desc, in case we get an update before ever sync'ing.
         sync_desc = () =>
-            task.desc_last_sync = misc.mswalltime()
             desc           = cm.getValue()
             task.last_desc = desc  # update current description before syncing.
             task.desc      = desc
@@ -1056,24 +1054,12 @@
                 where : {task_id : task.task_id}
             @set_dirty()

-        task.desc_timer = undefined
-
         cm.sync_desc = sync_desc  # hack -- will go away with react rewrite of tasks...

-        cm.on 'changes', () =>
-            t = misc.mswalltime()
-            if not task.desc_last_sync?
-                sync_desc()
-            else
-                if t - task.desc_last_sync >= MIN_TIME
-                    sync_desc()
-                else
-                    if not task.desc_timer?
-                        f = () ->
-                            task.desc_timer = undefined
-                            if misc.mswalltime() - task.desc_last_sync >= MIN_TIME
-                                sync_desc()
-                        task.desc_timer = setTimeout(f, MIN_TIME - (t - task.desc_last_sync))
+        # Only typically sync save 2s after the user stops typing.  This ensures that
+        # the task list doesn't feel slow or waste a lot of cpu during bursts of typing
+        # (unless there are incoming sync updates to process).
+        cm.on 'changes', underscore.debounce(sync_desc, 2000)

         cm.on 'focus', () ->
             currently_focused_editor = cm


removed round robin

removed round robin

--- node_modules_koding/kite-amqp/lib/kite-amqp/kite.coffee
+++ node_modules_koding/kite-amqp/lib/kite-amqp/kite.coffee
@@ -74,9 +74,9 @@

   # Singleton Helpers

-  @worker: (options, functions, roundRobin)=>
+  @worker: (options, functions)=>
     options = @key options
-    @instance or= new @ options, functions, roundRobin
+    @instance or= new @ options, functions

   @require: (kiteName)=>
     run: (method, withArgs, callback)=>


make sure the query params are stripped from the updateUrl

--- atom/browser/api/lib/auto-updater/auto-updater-win.coffee
+++ atom/browser/api/lib/auto-updater/auto-updater-win.coffee
@@ -1,6 +1,7 @@
 {EventEmitter} = require 'events'
 SquirrelUpdate = require './auto-updater/squirrel-update-win'
 app            = require('app')
+url            = require('url')

 class AutoUpdater extends EventEmitter

@@ -11,7 +12,14 @@
   setFeedUrl: (updateUrl) ->
     # set feed URL only when it hasn't been set before
     unless @updateUrl
-      @updateUrl = updateUrl
+      # See https://github.com/Squirrel/Squirrel.Windows/issues/132
+      # This way the Mac and Windows Update URL can be the same, even when
+      # the Mac version is sending additional data in the query string.
+      parsedUrl = url.parse(updateUrl)
+      delete parsedUrl.search
+      delete parsedUrl.query
+
+      @updateUrl = url.format(parsedUrl)

   checkForUpdates: ->
     throw new Error('Update URL is not set') unless @updateUrl


Implemented undo action in ClientController

--- core/app/backbone/controllers/client_controller.coffee
+++ core/app/backbone/controllers/client_controller.coffee
@@ -1,17 +1,7 @@
 class window.ClientController
   constructor: (@annotatedSiteEnvoy) ->
   showFact: (fact_id) ->
-    fact = new Fact id: fact_id
-
-    fact.on 'destroy', => @annotatedSiteEnvoy 'closeModal_deleteFactlink', fact_id
-
-    fact.fetch
-      success: =>
-        newClientModal = new DiscussionModalContainer
-        FactlinkApp.discussionModalRegion.show newClientModal
-        view = new DiscussionView model: fact
-        view.on 'render', => @annotatedSiteEnvoy 'openModalOverlay'
-        newClientModal.mainRegion.show view
+    @_renderDiscussion new Fact id: fact_id

   showNewFact: (params={}) =>
     fact = new Fact
@@ -23,7 +13,11 @@
       fact.save {},
         success: =>
           @annotatedSiteEnvoy 'highlightNewFactlink', params.displaystring, fact.id
-          Backbone.history.navigate "/client/facts/#{fact.id}", trigger: true
+          FactlinkApp.NotificationCenter.success "Successfully created #{Factlink.Global.t.factlink}",
+            'Undo', -> fact.destroy()
+
+          @_renderDiscussion fact
+          Backbone.history.navigate "/client/facts/#{fact.id}", trigger: false
     else
       view = new NewFactLoginView model: fact
       view.on 'render', => @annotatedSiteEnvoy 'openModalOverlay'
@@ -31,3 +25,14 @@
       clientModal = new DiscussionModalContainer
       FactlinkApp.discussionModalRegion.show clientModal
       clientModal.mainRegion.show view
+
+  _renderDiscussion: (fact) ->
+    fact.on 'destroy', => @annotatedSiteEnvoy 'closeModal_deleteFactlink', fact.id
+
+    fact.fetch
+      success: =>
+        newClientModal = new DiscussionModalContainer
+        FactlinkApp.discussionModalRegion.show newClientModal
+        view = new DiscussionView model: fact
+        view.on 'render', => @annotatedSiteEnvoy 'openModalOverlay'
+        newClientModal.mainRegion.show view


Remove unused param for sendEmailValidation

--- src/collections/users.coffee
+++ src/collections/users.coffee
@@ -25,4 +25,4 @@

 exports.sendEmailValidation = (callback) ->
-  @collections.users.exec('sendEmailValidation', userId).pop().callback callback
+  @collections.users.exec('sendEmailValidation').pop().callback callback


Fixed issue with return link and scroll after effects.

--- common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
+++ common/lib/xmodule/xmodule/js/src/annotatable/display.coffee
@@ -6,10 +6,11 @@
     spanSelector:    '.annotatable-span'
     commentSelector: '.annotatable-comment'
     replySelector:   '.annotatable-reply'
+    helpSelector:    '.annotatable-help-icon'
     returnSelector:  '.annotatable-return'
-    helpSelector:    '.annotatable-help-icon'
+
     discussionXModuleSelector: '.xmodule_DiscussionModule'
-    discussionSelector: '.discussion-module'
+    discussionSelector:        '.discussion-module'

     constructor: (el) ->
         console.log 'loaded Annotatable' if @_debug
@@ -82,7 +83,8 @@

     onClickReturn: (e) =>
         e.preventDefault()
-        @scrollTo(@getSpan e.currentTarget, @afterScrollToSpan)
+        span_el = @getSpan e.currentTarget
+        @scrollTo(span_el, @afterScrollToSpan)

     getSpan: (el) ->
         discussion_id = @getDiscussionId(el)
@@ -96,34 +98,38 @@
         $(el).data('discussion-id')

     toggleAnnotations: () ->
-        @annotationsHidden = not @annotationsHidden
-        @toggleButtonText @annotationsHidden
-        @toggleSpans @annotationsHidden
-        @toggleTips @annotationsHidden
+        hide = (@annotationsHidden = not @annotationsHidden)
+        @toggleButtonText hide
+        @toggleSpans hide
+        @toggleReturnLinks hide
+        @toggleTips hide

     toggleTips: (hide) ->
         if hide then @closeAndSaveTips() else @openSavedTips()
+
+    toggleReturnLinks: (hide) ->
+        $(@returnSelector)[if hide then 'hide' else 'show']()

     toggleButtonText: (hide) ->
         buttonText = (if hide then 'Show' else 'Hide')+' Annotations'
         @$(@toggleSelector).text(buttonText)

     toggleSpans: (hide) ->
-        @$(@spanSelector).toggleClass 'hide', hide
+        @$(@spanSelector).toggleClass 'hide', hide, 250

-    scrollTo: (el, after) ->
+    scrollTo: (el, after = -> true) ->
         $('html,body').scrollTo(el, {
             duration: 500
-            #onAfter: @_once => after.call this, el
+            onAfter: @_once => after.call this, el
+            offset: -20
         })

-    afterScrollToDiscussion: () ->
-        (el) ->
-            btn = $('.discussion-show', el)
-            btn.click() if !btn.hasClass('shown')
+    afterScrollToDiscussion: (el) ->
+        btn = $('.discussion-show', el)
+        btn.click() if !btn.hasClass('shown')

     afterScrollToSpan: (el) ->
-        (el) -> el.effect('highlight', {}, 500)
+        el.effect 'highlight', {color: 'rgba(0,0,0,0.5)' }, 1000

     makeTipContent: (el) ->
         (api) =>
@@ -142,7 +148,7 @@
         $("See Full Discussion")

     createReturnLink: (discussion_id) ->
-        $("Return to annotation")
+        $("Return to annotation")

     openSavedTips: () ->
         @showTips @savedTips


fixes #72 Models and collections are differentiated based upon the type of the response

--- src/client/datasource.coffee
+++ src/client/datasource.coffee
@@ -1,4 +1,4 @@
-define ['lib/utils/promises', 'underscore'], (promises, _)->
+define ['lib/utils/promises', 'underscore', 'backbone'], (promises, _, Backbone)->
   #
   # Parses the URI to replace placeholders with actual values
   #
@@ -30,11 +30,9 @@
         ds =
           path: ds
           provider: 'hull'
-        @type = if (ds.path.lastIndexOf('/') in [-1, 0]) then 'model' else 'collection'
       else if _.isObject(ds) && !_.isFunction(ds)
         throw _errDefinition unless ds.path
         ds.provider = ds.provider || 'hull'
-        @type = ds.type || 'collection'
       @def = ds

     #
@@ -60,20 +58,17 @@
         else
           dfd.resolve ret
       else
-        dfd.resolve(false) if /undefined/.test(@def.path)
-        if @type == 'model'
-          data = @transport.model(@def)
-        else if @type == 'collection'
-          data = @transport.collection(@def)
-        else
-          dfd.reject new TypeError('Unknown type of datasource: ' + @type);
-        if data._fetched
-          dfd.resolve data
-        else
-          data.once 'sync', ->
-            dfd.resolve data
-          data.once 'error', (model, xhr)->
-            dfd.reject xhr
+        if /undefined/.test(@def.path)
+          dfd.resolve(false)
+          return dfd.promise()
+        transportDfd = @transport(@def)
+        transportDfd.then (obj)->
+          if _.isArray(obj)
+            dfd.resolve (new Backbone.Collection obj)
+          else
+            dfd.resolve (new Backbone.Model obj)
+        , (err)->
+          dfd.reject err
       dfd.promise()

   Datasource


Fix path collision.

Fix path collision.

--- server.coffee
+++ server.coffee
@@ -70,10 +70,10 @@
       @loadAllScenes()
     , 250)

-path = process.argv[2] || process.env.SCENE_PATH
+scenePath = process.argv[2] || process.env.SCENE_PATH

-if !path
+if !scenePath
   console.log "Usage: scenevr [scenedirectory]"
   process.exit()

-new Server(path, PORT)
+new Server(scenePath, PORT)


Use really arguments passed to publish.

Use really arguments passed to publish.

Otherwise argument checker thinks not all arguments were checked because
_params is not the same to args for at least nested arrays.

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

     self = @

-    Meteor.publish @options.name, ->
+    Meteor.publish @options.name, (args...) ->
       publish = @

       state = {}

       publish.params = ->
-        @_params
+        args

       publish.set = (key, value) ->
         state[key] = value


Prevent duplicate questions

Prevent duplicate questions

--- server.coffee
+++ server.coffee
@@ -30,8 +30,12 @@
 	adult = Db.shared.get 'adult'
 	previous = Db.shared.get 'round_no' || 0

+	previous_questions = []
+	Db.shared.ref('rounds').observeEach (round) !->
+		previous_questions.push round.get('question')
+
 	for s, a in questions
-		if a >= adult
+		if a >= adult and s not in previous_questions
 			eligable.push s

 	if eligable.length
@@ -49,7 +53,7 @@
 			finished: false

 		Timer.cancel()
-		Timer.set duration, 'newRound'
+		Timer.set duration, 'nextRound'

 		Db.shared.set 'next', time + duration


always force domain to base url

always force domain to base url

--- server.coffee
+++ server.coffee
@@ -8,10 +8,8 @@
 appconfig = require('./api/config/globals')
 env = process.env.NODE_ENV || 'development'

-if (appconfig['base_url'].indexOf('https') > -1)
-  app.use( forceDomain(appconfig['base_url']) )
-  app.use(forceSSL.HTTPS({ trustProtoHeader: true }))
-
+app.use( forceDomain(appconfig['base_url']) )
+app.use(forceSSL.HTTPS({ trustProtoHeader: true })) if (appconfig['base_url'].indexOf('https') > -1)
 app.set('view engine', 'ejs')
 app.set('views', "#{__dirname}/api/views")
 app.use(express.static('./dist'))


/at/ redirects to /

/at/ redirects to /

--- server.coffee
+++ server.coffee
@@ -54,7 +54,7 @@
     return response.redirect('/') if err
     return response.redirect("/at/#{comic.time}/")

-app.get '/at/:stamp', (request, response) ->
+app.get '/at/:stamp?', (request, response) ->
   failHome = ->
     return response.redirect('/')
   return failHome() if isNaN(stamp = parseInt(request.params.stamp))


Disable artist representation links for everyone but admins for the time being

--- components/related_links/types/artist_representations.coffee
+++ components/related_links/types/artist_representations.coffee
@@ -3,12 +3,14 @@
 Backbone = require 'backbone'
 RelatedLinksView = require '../view.coffee'
 Partners = require '../../../collections/partners.coffee'
+CurrentUser = require '../../../models/current_user.coffee'

 module.exports = class RelatedRepresentationsLinksView extends RelatedLinksView
   headerTemplate: _.template '
Gallery Representation
'
   wrapperTemplate: _.template '
<%= links %>
'

   initialize: (options = {}) ->
+    return unless CurrentUser.orNull()?.isAdmin()
     @collection = new Partners
     @collection.url = "#{API_URL}/api/v1/artist/#{options.id}/partners"
     @collection.fetch data: size: 20, represented_by: true


Fix collision issue

Fix collision issue

--- src/components/player/PlayerControlledCube.coffee
+++ src/components/player/PlayerControlledCube.coffee
@@ -3,11 +3,11 @@
     @requires '2D, WebGL, Color, Listener, Collision, SunBlock, PlayerControlledShip, Acceleration, InventoryWeapons'
     @attr w: 40, h: 40

-    @bind 'Moved', (from) ->
+    @bind 'Move', (from) ->
       if @hit('Edge') or @hit('Solid') # Contain player within playfield
-        setBack = {}
-        setBack[from.axis] = from.oldValue
-        @attr setBack
+        delta = @motionDelta()
+        @shift(- delta.x, - delta.y)
+
     @primaryWeapon = undefined
     @primaryWeapons = []
     @secondaryWeapon = undefined


KDFramework: resetting validation fixed

KDFramework: resetting validation fixed

--- client/Framework/components/inputs/KDInputView.coffee
+++ client/Framework/components/inputs/KDInputView.coffee
@@ -43,11 +43,8 @@
     @enableTabKey() if options.enableTabKey
     @setCase options.forceCase if options.forceCase

-    if options.validate?
-      @setValidation options.validate
-      @on "ValidationError", @bound "giveValidationFeedback"
-      @on "ValidationPassed", @bound "giveValidationFeedback"
-      @on "focus", @bound "clearValidationFeedback"
+    @setValidation options.validate  if options.validate
+    @bindValidationEvents()

     if options.type is "select" and options.selectOptions
       @on "viewAppended", =>
@@ -72,6 +69,11 @@
       when "range"    then $ "<input #{name} type='range' class='kdinput range #{cssClass}'/>"
       else                 $ "<input #{name} type='#{@getType()}' class='kdinput #{@getType()} #{cssClass}'/>"

+  bindValidationEvents:->
+    @on "ValidationError", @bound "giveValidationFeedback"
+    @on "ValidationPassed", @bound "giveValidationFeedback"
+    @on "focus", @bound "clearValidationFeedback"
+
   setLabel:(label = @getOptions().label)->

     return  unless label
@@ -167,7 +169,11 @@
     @on "keyup", cb.bind this
     @on "blur",  cb.bind this

+  unsetValidation:-> @setValidation {}
+
   setValidation:(ruleSet)->
+
+    log "setValidation", ruleSet

     @valid                 = no
     @currentRuleset        = ruleSet


activity/flux: filter logged in user out from new channel participants

--- client/activity/lib/flux/createchannel/getters.coffee
+++ client/activity/lib/flux/createchannel/getters.coffee
@@ -21,7 +21,11 @@
   CreateNewChannelParticipantIdsStore
   ActivityFluxGetters.allUsers
   (participantIds, users) ->
-    participantIds.map (id) -> users.get id
+    participantIds
+      # filter me out
+      .filter (id) -> id isnt whoami()._id
+      # return instances.
+      .map (id) -> users.get id
 ]


activity: fix docs

activity: fix docs

--- client/activity/lib/models/channelparticipants.coffee
+++ client/activity/lib/models/channelparticipants.coffee
@@ -133,7 +133,7 @@
    * implement this method to modify how preview list should be populated.
    *
    * @param {Immutable.OrderedMap} participants - all participants
-   * @return {Immutable.List} computedPreviewParticipants
+   * @return {Immutable.List} computedHiddenParticipants
   ###
   computeHiddenParticipants: (participants) ->

@@ -146,7 +146,7 @@
    * implement this method to modify how preview list should be populated.
    *
    * @param {Immutable.OrderedMap} participants - all participants
-   * @return {Immutable.List} computedPreviewParticipants
+   * @return {Immutable.List} computedAllParticipants
   ###
   computeAllParticipants: (participants) ->


app: maincontroller: fixed coffelint errors

--- client/app/lib/maincontroller/groupscontroller.coffee
+++ client/app/lib/maincontroller/groupscontroller.coffee
@@ -57,7 +57,7 @@
         rest = remote.revive rest
         @emit event, rest...

-  openGroupChannel: (group, callback = ->) ->
+  openGroupChannel: (group, callback = -> ) ->
     @groupChannel = remote.subscribe "group.#{group.slug}",
       serviceType : 'group'
       group       : group.slug
@@ -75,7 +75,7 @@
     @groupChannel.once 'setSecretNames', callback

-  openSocialGroupChannel: (group, callback = ->) ->
+  openSocialGroupChannel: (group, callback = -> ) ->
     { realtime, socialapi } = kd.singletons

     socialapi.channel.byId { id: group.socialApiChannelId }, (err, channel) =>
@@ -133,7 +133,7 @@
     }

   joinGroup: (group, callback) ->
-    group.join (err, response) =>
+    group.join (err, response) ->
       return showError err  if err?
       callback err, response
       kd.getSingleton('mainController').emit 'JoinedGroup'
@@ -141,7 +141,7 @@
   acceptInvitation: (group, callback) ->
     whoami().acceptInvitation group, (err, res) =>
       mainController = kd.getSingleton 'mainController'
-      mainController.once "AccountChanged", callback.bind this, err, res
+      mainController.once 'AccountChanged', callback.bind this, err, res
       mainController.accountChanged whoami()

   ignoreInvitation: (group, callback) ->


Fix twitter signin

Fix twitter signin

--- core/app/backbone/views/popovers/react_signin_popover.coffee
+++ core/app/backbone/views/popovers/react_signin_popover.coffee
@@ -40,7 +40,8 @@
           _span ["signin-popover"],
             'Sign in with: ',
             _a ["button-twitter small-connect-button js-accounts-popup-link",
-              href: "/auth/twitter"
+              # The "?use_authorize=true&x_auth_access_type=write" part is for some weird twitter bug
+              href: "/auth/twitter?use_authorize=true&x_auth_access_type=write"
             ],
               _i ["icon-twitter"]
             ' ',


activity cache WIP

activity cache WIP

--- core/KD.coffee
+++ core/KD.coffee
@@ -23,8 +23,11 @@
 String.prototype.decapitalize = ()-> this.charAt(0).toLowerCase() + this.slice(1)
 String.prototype.trim         = ()-> this.replace(/^\s+|\s+$/g,"")

-# unless Array.prototype.last
-#   Array.prototype.__defineGetter__ "last", -> this[this.length-1]
+unless Array.prototype.last
+  Object.defineProperty Array.prototype, "last", get : -> this[this.length-1]
+
+unless Array.prototype.first
+  Object.defineProperty Array.prototype, "first", get : -> this[0]

 # KD Global
 KD = @KD or {}


update mvp position when get the result packet

--- index.cjsx
+++ index.cjsx
@@ -557,6 +557,10 @@
               getShip = body.api_get_ship
             if body.api_get_useitem?
               getItem = body.api_get_useitem
+          if body.api_mvp?
+            mvpPos[0] = if body.api_mvp >= 2 then body.api_mvp - 1 else 0
+          if body.api_mvp_combined?
+            mvpPos[1] = if body.api_mvp_combined >= 2 then body.api_mvp_combined - 1 else 0
           result = body.api_win_rank

         # Return to port


Added space on the two sides of x

Added space on the two sides of x

--- index.cjsx
+++ index.cjsx
@@ -250,7 +250,7 @@
                             for reward, i in @state.quest_selected.reward_other
                               name = __(reward.name)
                               name = __('「') + name + __('」') if reward.category?
-                              amount = if reward.amount? then '×' + reward.amount else ''
+                              amount = if reward.amount? then ' × ' + reward.amount else ''
                               category = __(reward.category || '')
                               <li key={"reward_other_#{i}"}>
                                 {category}{name}{amount}


fix bug

fix bug

--- index.cjsx
+++ index.cjsx
@@ -34,8 +34,9 @@
         info =
           teitokuLv: _teitokuLv
           teitokuId: _nickNameId
-          mapareaId: parseInt(postBody.api_maparea_id)
+          mapareaId: parseInt(postBody.api_maparea_id) * 10 + parseInt(postBody.api_map_no)
           rank: parseInt(postBody.api_rank)
+        mapLv[parseInt(postBody.api_maparea_id) * 10 + parseInt(postBody.api_map_no)] = parseInt(postBody.api_rank)
         try
           yield request.postAsync "http://#{SERVER_HOSTNAME}/api/report/v2/select_rank",
             form:


fix concatenation in locations.coffee

fix concatenation in locations.coffee

--- static/coffee/locations.coffee
+++ static/coffee/locations.coffee
@@ -334,7 +334,9 @@

       if displayOthers and results.set > 0
         html += "
"
-        html += "
" + window.string_Other_answers +":" + "

"
+        html += "
"
+        html += window.string_Other_answers
+        html += ":

"
         for label, count of results.others
           percentage = Math.round((100 * count) / results.set)
           html += "
"


Fire processing function on forwarding closeBrackets if buffer has data

--- src/lib/Component.coffee
+++ src/lib/Component.coffee
@@ -182,8 +182,9 @@
         # B. There are closeBrackets in queue after current packet
         # C. We've queued the results from all in-flight processes and
         #    new closeBracket arrives
-        if @outputQ.length >= @load
-          buf = port.getBuffer ip.scope
+        buf = port.getBuffer ip.scope
+        dataPackets = buf.filter (ip) -> ip.type is 'data'
+        if @outputQ.length >= @load and dataPackets.length is 0
           return unless buf[0] is ip
           # Remove from buffer
           port.get ip.scope
@@ -195,7 +196,9 @@
             __bracketClosingAfter: [context]
           @outputQ.push result
           do @processOutputQueue
-        return
+        # Check if buffer contains data IPs. If it does, we want to allow
+        # firing
+        return unless dataPackets.length

     # Prepare the input/output pair
     context = new ProcessContext ip, @, port, result


S3 data shouldn't be Base64 encoded.

S3 data shouldn't be Base64 encoded.

We don't need to Base64 encode S3 data therefore don't. This should provide about a 30% size decrease in storage overhead.

--- src/server/db/amazon.coffee
+++ src/server/db/amazon.coffee
@@ -121,7 +121,37 @@
       console.log(@name+'Blob:Decompressed['+data.length+','+result.length+','+elapsed+'ms]') if @logging
       callback?(error, result)

-class Compressor
+class TextCompressor
+  constructor: (@compression, logging) ->
+    @gzip = new BlobHandler 'Base64Gzip',
+      (data, callback) ->
+        zlib.gzip(new Buffer(data), callback)
+      (data, callback) ->
+        zlib.gunzip(data, callback)
+      logging
+
+    @base64 = new BlobHandler 'Base64',
+      (data, callback) ->
+        callback?(null, new Buffer(data, 'binary').toString('base64'))
+      (data, callback) ->
+        callback?(null, new Buffer(data, 'base64'))
+      logging
+
+  encode: (data, callback) =>
+    if @compression
+      @gzip.encode data, (error, result) =>
+        @base64.encode(result, callback)
+    else
+      callback?(null, data)
+
+  decode: (data, callback) =>
+    if @compression
+      @base64.decode data, (error, result) =>
+        @gzip.decode(result, callback)
+    else
+      callback?(null, data)
+
+class BinaryCompressor
   constructor: (@compression, logging) ->
     @gzip = new BlobHandler 'Gzip',
       (data, callback) ->
@@ -130,24 +160,15 @@
         zlib.gunzip(data, callback)
       logging

-    @base64 = new BlobHandler 'Base64',
-      (data, callback) ->
-        callback?(null, new Buffer(data, 'binary').toString('base64'))
-      (data, callback) ->
-        callback?(null, new Buffer(data, 'base64'))
-      logging
-
   encode: (data, callback) =>
     if @compression
-      @gzip.encode data, (error, result) =>
-        @base64.encode(result, callback)
+      @gzip.encode data, callback
     else
       callback?(null, data)

   decode: (data, callback) =>
     if @compression
-      @base64.decode data, (error, result) =>
-        @gzip.decode(result, callback)
+      @gzip.decode data, callback
     else
       callback?(null, data)

@@ -186,7 +207,8 @@
   # Calls callback(error) on failure.
   # Calls callback() on success.
   @create = (docName, docData, callback) ->
-    compressor = new Compressor(options['compress'], options['timing'])
+    compressor = new TextCompressor(options['compress'], options['timing'])
+    binaryCompressor = new BinaryCompressor(options['compress'], options['timing'])
     async.auto(
       compress_meta: (cb) ->
         compressor.encode(JSON.stringify(docData.meta), cb)
@@ -213,7 +235,7 @@
       ]

       compress_data: (cb) ->
-        compressor.encode(JSON.stringify(docData.snapshot), cb)
+        binaryCompressor.encode(JSON.stringify(docData.snapshot), cb)

       write_data: ['compress_data', (cb, results) ->
         path = snapshots_bucket+'/'+docName+'-'+docData.v+'.snapshot'
@@ -380,15 +402,18 @@
       ]

       compressor: ['get_snapshot', 'get_data', (cb, results) ->
-        cb(null, new Compressor(results.get_snapshot.Items[0].c, options['timing']))
+        cb(null,
+          text: new TextCompressor(results.get_snapshot.Items[0].c, options['timing'])
+          binary: new BinaryCompressor(results.get_snapshot.Items[0].c, options['timing'])
+        )
       ]

       snapshot: ['compressor', (cb, results) ->
-        results.compressor.decode(results.get_data.buffer.toString(), cb)
+        results.compressor.binary.decode(results.get_data.buffer, cb)
       ]

       meta: ['compressor', (cb, results) ->
-        results.compressor.decode(results.get_snapshot.Items[0].meta.S, cb)
+        results.compressor.text.decode(results.get_snapshot.Items[0].meta.S, cb)
       ]
     (error, results) ->
       if error?
@@ -437,7 +462,8 @@
   # Calls callback(error) on failure.
   # Calls callback() on success.
   @writeSnapshot = (docName, docData, dbMeta, callback) ->
-    compressor = new Compressor(options['compress'], options['timing'])
+    compressor = new TextCompressor(options['compress'], options['timing'])
+    binaryCompressor = new BinaryCompressor(options['compress'], options['timing'])
     async.auto(
       compress_meta: (cb) ->
         compressor.encode(JSON.stringify(docData.meta), cb)
@@ -464,7 +490,7 @@
       ]

       compress_data: (cb) ->
-        compressor.encode(JSON.stringify(docData.snapshot), cb)
+        binaryCompressor.encode(JSON.stringify(docData.snapshot), cb)

       write_data: ['compress_data', (cb, results) ->
         path = snapshots_bucket+'/'+docName+'-'+docData.v+'.snapshot'
@@ -526,7 +552,7 @@
       else
         data = []
         async.map(results.get_metadata.Items, (operation, cb) ->
-          compressor = new Compressor(operation.c?, options['timing'])
+          compressor = new TextCompressor(operation.c?, options['timing'])
           async.auto(
             snapshot: (c) ->
               compressor.decode(operation.op.S, c)
@@ -572,7 +598,7 @@
   # Calls callback(error) on failure.
   # Calls callback() on success.
   @writeOp = (docName, opData, callback) ->
-    compressor = new Compressor(options['compress'], options['timing'])
+    compressor = new TextCompressor(options['compress'], options['timing'])
     async.auto(
       compress_op: (cb) ->
         compressor.encode(JSON.stringify(opData.op), cb)


Only show poparrow in old subcommentview

--- core/app/backbone/views/sub_comments/sub_comment_view.coffee
+++ core/app/backbone/views/sub_comments/sub_comment_view.coffee
@@ -21,6 +21,12 @@
   initialize: ->
     @listenTo @model, 'change', @render

+
+class window.SubCommentView extends BaseSubCommentView
+  className: 'evidence-sub-comment'
+
+  templateHelpers: => creator: @model.creator().toJSON()
+
   onRender: ->
     @setPoparrow() if Factlink.Global.signed_in

@@ -32,11 +38,5 @@
       @poparrowRegion.show poparrowView

-class window.SubCommentView extends BaseSubCommentView
-  className: 'evidence-sub-comment'
-
-  templateHelpers: => creator: @model.creator().toJSON()
-
-
 class window.NDPSubCommentView extends BaseSubCommentView
   template: 'sub_comments/ndp_sub_comment'


Make error handling more verbose.

Make error handling more verbose.

--- src/server/db/amazon.coffee
+++ src/server/db/amazon.coffee
@@ -67,7 +67,6 @@
   @create = (docName, docData, callback) ->
     async.auto(
       write_metadata: (cb) ->
-
         request =
           TableName: snapshots_table,
           Item:
@@ -89,11 +88,15 @@

         s3.put(path, headers, data, cb)
     (error, results) ->
-      if error? and error.message.match 'The conditional request failed'
-        callback?('Document already exists')
-      else if error?
-        console.log('Failed to save Snapshot('+docName+'-'+docData.v+'): '+util.inspect(error))
-        callback?('Failure')
+      if error?
+        if error.message? and error.message.match 'The conditional request failed'
+          callback?('Document already exists')
+        else if results? and results.write_metadata?
+          console.log('Failed to save Snapshot('+docName+'-'+docData.v+') to S3: '+util.inspect(error))
+          callback?('Failed to save snapshot to S3')
+        else
+          console.log('Failed to save Snapshot('+docName+'-'+docData.v+'): '+util.inspect(error))
+          callback?('Failed to save snapshot')
       else
         callback?()
     )
@@ -189,11 +192,13 @@
       ]
     (error, results) ->
       if error?
-        if error.toString().match "The conditional request failed"
-          error = "Document does not exist"
-        else if error != 'Document does not exist'
+        if error.toString().match 'The conditional request failed'
+          callback?('Document does not exist')
+        else if error == 'Document does not exist'
+          callback?(error)
+        else
           console.log('Failed to delete snapshots or operations from Document('+docName+'): '+util.inspect(error))
-        callback?(error)
+          callback?('Failed to delete snapshots or operations')
       else
         callback?(null)
     )
@@ -227,11 +232,13 @@
       if error?
         if error == 'Document does not exist'
           callback?(error)
-        if results.get_snapshot?
+        else if results? and results.get_snapshot?
           item = results.get_snapshot.Items[0]
-          callback?('Failed to get snapshot data for Document('+item.doc.S+'-'+item.v.N+'): '+util.inspect(error))
+          console.log('Failed to get snapshot data for Document('+item.doc.S+'-'+item.v.N+'): '+util.inspect(error))
+          callback?('Failed to get snapshot data')
         else
-          callback?('Failed to get snapshot metadata for Document('+docName+')')
+          console.log('Failed to get snapshot metadata for Document('+docName+'): '+util.inspect(error))
+          callback?('Failed to get snapshot metadata')
       else
         item = results.get_snapshot.Items[0]
         data =
@@ -278,11 +285,15 @@

         s3.put(path, headers, data, cb)
     (error, results) ->
-      if error? and error.message.match 'The conditional request failed'
-        callback?('Document already exists')
-      else if error?
-        console.log('Failed to save Snapshot('+docName+'-'+docData.v+'): '+util.inspect(error))
-        callback?('Failure')
+      if error?
+        if error.message? and error.message.match 'The conditional request failed'
+          callback?('Document already exists')
+        else if results? and results.write_metadata?
+          console.log('Failed to save Snapshot('+docName+'-'+docData.v+') to S3: '+util.inspect(error))
+          callback?('Failed to save snapshot data')
+        else
+          console.log('Failed to save Snapshot('+docName+'-'+docData.v+'): '+util.inspect(error))
+          callback?('Failed to save snapshot metadata')
       else
         callback?()
     )
@@ -319,7 +330,7 @@
     (error, results) ->
       if error?
         console.log('Failed to fetch Operations('+docName+'-'+start+'..'+end+'): '+util.inspect(error))
-        callback?('Failure')
+        callback?('Failed to fetch operations')
       else
         data = []
         for op in results.get_metadata.Items


Initialize select value taking prompt into account.

--- core/htdocs_source/src/pods/oxifield-select/component.coffee
+++ core/htdocs_source/src/pods/oxifield-select/component.coffee
@@ -1,6 +1,12 @@
 `import Em from "vendor/ember"`

 Component = Em.Component.extend
+    sanitizeValue: Em.on "init", ->
+        value = @get "content.value"
+        if typeof value isnt "string"
+            options = @get "options"
+            @set "content.value", options[0]?.value or ""
+
     options: Em.computed "content.{options,prompt,is_optional}", ->
         prompt = @get "content.prompt"
         prompt = "" if not prompt and @get "content.is_optional"
@@ -25,7 +31,6 @@
     isCustom: Em.computed "options", "content.value", ->
         values = (o.value for o in @get "options")
         value = @get "content.value"
-        value = "" if value is null
         value not in values

     customize: Em.computed "isCustom", -> @get "isCustom"


Translate between 6.002x subscript convention and standard Latex

--- common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -299,8 +299,24 @@
     target = "display_#{element.id.replace(/^input_/, '')}"

     if jax = MathJax.Hub.getAllJax(target)[0]
-      MathJax.Hub.Queue ['Text', jax, $(element).val()],
+      eqn = @latexify($(element).val())
+      MathJax.Hub.Queue ['Text', jax, eqn],
         [@updateMathML, jax, element]
+
+  latexify: (eqn) ->
+    ###
+    Translate 6.002x conventions for subscripts to standard Latex, e.g.
+      'R3'  --> 'R_{3}'
+      'vGS' --> 'v_{GS}'
+      'K/2*(vIN-VT)^2' --> 'K/2*(v_{IN}-V_{T})^2'
+    ###
+    subscript_replace = (match) ->
+      # Default keywords are taken from capa/calc.py
+      default_keywords = ['sin', 'cos', 'tan', 'sqrt', 'log10', 'log2', 'ln', 'arccos', 'arcsin', 'arctan', 'abs', 'pi']
+      if match in default_keywords
+        return match
+      return match[0] + '_{' + match.substr(1) + '}'
+    return eqn.replace(/[A-Za-z]\w+/g, subscript_replace)

   updateMathML: (jax, element) =>
     try


update for MSUP

update for MSUP

--- common/lib/xmodule/xmodule/js/src/problem/edit.coffee
+++ common/lib/xmodule/xmodule/js/src/problem/edit.coffee
@@ -211,6 +211,38 @@
         return groupString;
       });

+
+      //intro for MSUP
+      xml = xml.replace(/(^\s*[I]:.*)+/gm, function(match, p) {
+        return "";
+      });
+      xml = xml.replace(/(^\s*[S]:.*)+/gm, function(match, p) {
+        var value = match.split(/^\s*[S]:\s*/)[1];
+        return value;
+      });
+
+
+      // group check answers for MSUP
+      xml = xml.replace(/(^\s*[+-]:.*?$\n*)+/gm, function(match, p) {
+        var groupString = '<choiceresponse>\n';
+        groupString += '  <checkboxgroup direction="vertical">\n';
+        var options = match.split('\n');
+        for(var i = 0; i < options.length; i++) {
+          if(options[i].length > 0) {
+            var value = options[i].split(/^\s*[+-]:\s*/)[1];
+            var correct = /^\s*\+:/i.test(options[i]);
+            groupString += '    <choice correct="' + correct + '">' + value + '</choice>\n';
+          }
+        }
+        groupString += '  </checkboxgroup>\n';
+        groupString += '</choiceresponse>\n\n';
+        return groupString;
+      });
+
+
+
+
+
       // group check answers
       xml = xml.replace(/(^\s*\[.?\].*?$\n*)+/gm, function(match, p) {
         var groupString = '<choiceresponse>\n';


implement getVersionList() & getVersionDetail()

--- src/service/identity.coffee
+++ src/service/identity.coffee
@@ -1,2 +1,34 @@
 module.exports = class IdentityService
+
   constructor: (@conoha) ->
+    @endpoint = @conoha.identityServiceEndpoint
+
+  # https://www.conoha.jp/docs/identity-get_version_list.html
+  getVersionList: (callback) ->
+    @conoha.request.get
+      url: @endpoint+'/'
+      headers: { 'Accept': 'application/json' }
+      (err, res, body) ->
+        versions = JSON.parse(body).versions
+        callback(err, versions)
+
+  # https://www.conoha.jp/docs/identity-get_version_detail.html
+  getVersionDetail: (callback) ->
+    @conoha.request.get
+      url: @endpoint+'/v2.0'
+      headers: { 'Accept': 'application/json' }
+      (err, res, body) ->
+        version = JSON.parse(body).version
+        callback(err, version)
+
+  # https://www.conoha.jp/docs/identity-post_tokens.html
+  ###
+  postTokens: (cache, callback) ->
+    @conoha.request.post
+      url: @endpoint+'/v2.0/tokens'
+      headers: { 'Accept': 'application/json' }
+      (err, res, body) ->
+        token = JSON.parse(body).access?.token
+        #TODO: cache to DB
+        @conoha.token = new ConoHa.AccessToken(token.id, token.expires) if cache
+  ###


Prettier text tables

Prettier text tables

--- src/core/modules/plot.coffee
+++ src/core/modules/plot.coffee
@@ -1,7 +1,10 @@
 Flow.Plot = (_config, go) ->

+  renderInterval = (config, go) ->
+
+
   renderTextTable = (config, go) ->
-    [ table, thead, tbody, tr, th, td ] = Flow.HTML.template 'table', '=thead', 'tbody', 'tr', '=th', '=td'
+    [ table, thead, tbody, tr, th, td, tdr ] = Flow.HTML.template 'table', '=thead', 'tbody', 'tr', '=th', '=td', '=td.rt'

     ths = for column in config.data.columns
       th column.name
@@ -9,7 +12,14 @@
     trs = for row in config.data.rows
       tds = for column in config.data.columns
         #XXX formatting
-        td row[column.name]
+        value = row[column.name]
+        switch column.type
+          when Flow.Data.StringEnum
+            td if value is null then '-' else column.domain[value]
+          when Flow.Data.Integer, Flow.Data.Real
+            tdr if value is null then '-' else value
+          else
+            td if value is null then '-' else value
       tr tds

     go null, Flow.HTML.render 'div',
@@ -20,6 +30,8 @@

   initialize = (config) ->
     switch config.type
+      when 'interval'
+        renderInterval config, go
       when 'text'
         renderTextTable config, go
       else


available_upgrades is api response, not a query

--- src/smc-util/message.coffee
+++ src/smc-util/message.coffee
@@ -2451,9 +2451,8 @@
 This request returns information on project upgrdes for the user
 whose API key appears in the request.
 Two objects are returned, total upgrades and available upgrades.
-Values for `mintime` are in units of seconds.
-Values for `cpu shares` correspond the number of CPU shares shown
-in the web UI in 'Account Settings / Upgrades' multiplied by 256.
+
+See https://github.com/sagemathinc/cocalc/blob/master/src/smc-util/upgrade-spec.coffee for units

 Example:
 ```
@@ -2485,7 +2484,7 @@

 # client <-- hub

-API message2
+API message
     event : 'available_upgrades'
     fields :
         id:


add markdown for desc for rest of API message2 defs

--- src/smc-util/message.coffee
+++ src/smc-util/message.coffee
@@ -701,6 +701,7 @@
 Examples:

 Simple built-in shell command.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -d command=pwd \\
     -d project_id=e49e86aa-192f-410b-8269-4b89fd934fba \\
@@ -708,8 +709,10 @@
   ==> {"event":"project_exec_output",
        "id":"8a78a37d-b2fb-4e29-94ae-d66acdeac949",
        "stdout":"/projects/e49e86aa-192f-410b-8269-4b89fd934fba\n","stderr":"","exit_code":0}
+```

 Shell command with different working directory.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -d command=pwd \\
     -d path=Private \\
@@ -718,22 +721,36 @@
   ==> {"event":"project_exec_output",
        "id":"8a78a37d-b2fb-4e29-94ae-d66acdeac949",
        "stdout":"/projects/e49e86aa-192f-410b-8269-4b89fd934fba/Private\n","stderr":"","exit_code":0}
+```

 Command line arguments specified by 'args' option. Note JSON format for request parameters.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -H 'Content-Type: application/json' \\
     -d '{"command":"echo","args":["xyz","abc"],"project_id":"e49e86aa-192f-410b-8269-4b89fd934fba"}' \\
     https://cocalc.com/api/v1/project_exec
-  ==> {"event":"project_exec_output","id":"39289ba7-0333-48ad-984e-b25c8b8ffa0e","stdout":"xyz abc\n","stderr":"","exit_code":0}
+  ==> {"event":"project_exec_output",
+       "id":"39289ba7-0333-48ad-984e-b25c8b8ffa0e",
+       "stdout":"xyz abc\n",
+       "stderr":"",
+       "exit_code":0}
+```

 Limiting output of the command to 3 characters.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -H 'Content-Type: application/json' \\
     -d '{"command":"echo","args":["xyz","abc"],"max_output":3,"project_id":"e49e86aa-192f-410b-8269-4b89fd934fba"}' \\
     https://cocalc.com/api/v1/project_exec
-  ==> {"event":"project_exec_output","id":"02feab6c-a743-411a-afca-8a23b58988a9","stdout":"xyz (truncated at 3 characters)","stderr":"","exit_code":0}+ echo
+  ==> {"event":"project_exec_output",
+       "id":"02feab6c-a743-411a-afca-8a23b58988a9",
+       "stdout":"xyz (truncated at 3 characters)",
+       "stderr":"",
+       "exit_code":0}
+```

 Setting a timeout for the command.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -H 'Content-Type: application/json' \\
     -d '{"command":"sleep 5","timeout":2,"project_id":"e49e86aa-192f-410b-8269-4b89fd934fba"}' \\
@@ -741,12 +758,13 @@
   ==>  {"event":"error",
         "id":"86fea3f0-6a90-495b-a541-9c14a25fbe58",
         "error":"Error executing command 'sleep 5' with args '' -- killed command 'bash /tmp/f-11757-1677-8ei2z0.t4fex0qkt9', , "}
+```

 Notes:
-- Argument 'command' may invoke an executable file or a built-in shell command. It may include
+- Argument `command` may invoke an executable file or a built-in shell command. It may include
   a path and command line arguments.
-- If option 'args' is provided, options must be sent as a JSON object.
-- Argument 'path' is optional. When provided, 'path' is relative to home directory in target project
+- If option `args` is provided, options must be sent as a JSON object.
+- Argument `path` is optional. When provided, `path` is relative to home directory in target project
   and specifies the working directory in which the command will be run.
 """

@@ -831,11 +849,15 @@
 Example:

 Read a text file.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -d project_id=e49e86aa-192f-410b-8269-4b89fd934fba \\
     -d path=Assignments/A1/h1.txt \\
     https://cocalc.com/api/v1/read_text_file_from_project
-  ==> {"event":"text_file_read_from_project","id":"481d6055-5609-450f-a229-480e518b2f84","content":"hello"}
+  ==> {"event":"text_file_read_from_project",
+       "id":"481d6055-5609-450f-a229-480e518b2f84",
+       "content":"hello"}
+```
 """

 # hub --> client
@@ -887,11 +909,13 @@
 Example:

 Create a text file.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -d project_id=e49e86aa-192f-410b-8269-4b89fd934fba \\
     -d "content=hello$'\n'world" \\
     -d path=Assignments/A1/h1.txt \\
     https://cocalc.com/api/v1/write_text_file_to_project
+```
 """

 # The file_written_to_project message is sent by a project_server to
@@ -923,8 +947,15 @@
             desc  : 'start running the moment the project is created -- uses more resources, but possibly better user experience'
     desc       : """
 Example:
-  curl -u sk_abcdefQWERTY090900000000: -d title='MY NEW PROJECT' -d description='sample project' https://cocalc.com/api/v1/create_project
-  == > {"event":"project_created","id":"0b4df293-d518-45d0-8a3c-4281e501b85e","project_id":"07897899-6bbb-4fbc-80a7-3586c43348d1"}
+```
+  curl -u sk_abcdefQWERTY090900000000: \\
+    -d title='MY NEW PROJECT' \\
+    -d description='sample project' \\
+    https://cocalc.com/api/v1/create_project
+  == > {"event":"project_created",
+        "id":"0b4df293-d518-45d0-8a3c-4281e501b85e",
+        "project_id":"07897899-6bbb-4fbc-80a7-3586c43348d1"}
+```
 """

 # hub --> client
@@ -959,7 +990,10 @@
 account id, first name, last name, and email address for the unique
 account with that email address, if there is one. A string query item
 will return account id, first name, and last name for all matching
-accounts. We do not reveal email addresses of users queried by name.
+accounts.
+
+We do not reveal email addresses of users queried by name.
+
 String query matches first and last names that start with the given string.
 If a string query item consists of two strings separated by space,
 the search will return accounts in which the first name begins with one
@@ -972,16 +1006,24 @@
 Examples:

 Search for account by email.
-  curl -u sk_abcdefQWERTY090900000000: -d query=jd@m.local https://cocalc.com/api/v1/user_search
+```
+  curl -u sk_abcdefQWERTY090900000000: \\
+    -d query=jd@m.local \\
+    https://cocalc.com/api/v1/user_search
   ==> {"event":"user_search_results",
        "id":"3818fa50-b892-4167-b9d9-d22d521b36af",
        "results":[{"account_id":"96c523b8-321e-41a3-9523-39fde95dc71d",
                    "first_name":"John",
                    "last_name":"Doe",
                    "email_address":"jd@m.local"}
+```

 Search for at most 3 accounts where first and last name begin with 'foo' or 'bar'.
-  curl -u sk_abcdefQWERTY090900000000: -d 'query=foo bar' -d limit=3 https://cocalc.com/api/v1/user_search
+```
+  curl -u sk_abcdefQWERTY090900000000: \\
+    -d 'query=foo bar'\\
+    -d limit=3 \\
+    https://cocalc.com/api/v1/user_search
   ==> {"event":"user_search_results",
        "id":"fd9b025b-25d0-4e27-97f4-2c080bb07155",
        "results":[{"account_id":"1a842a67-eed3-405d-a222-2f23a33f675e",
@@ -993,15 +1035,21 @@
                   {"account_id":"93f8131c-6c21-401a-897d-d4abd9c6c225",
                    "first_name":"Foo",
                    "last_name":"Bar"}]}
-  The same result would be returned with a search string of 'bar foo'.
-  A name of "Xfoo YBar" would not match.
-  Note that email addresses are not returned for string search items.
+```
+
+The same result as the last example above would be returned with a
+search string of 'bar foo'.
+A name of "Xfoo YBar" would not match.
+
+Note that email addresses are not returned for string search items.

 Email and string search types may be mixed in a single query:
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -d 'query=foo bar,jd@m.local' \\
     -d limit=4 \\
     https://cocalc.com/api/v1/user_search
+```
 """

 # hub --> client
@@ -1251,6 +1299,7 @@
 Get public directory listing. Directory "Public" is shared and
 contains one file "hello.txt" and one subdirectory "p2".

+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -d path=Public \\
     -d project_id=9a19cca3-c53d-4c7c-8c0f-e166aada7bb6 \\
@@ -1259,6 +1308,7 @@
        "id":"3e576b3b-b673-4d5c-9bce-780883f92958",
        "result":{"files":[{"size":41,"name":"hello.txt","mtime":1496430932},
                           {"isdir":true,"name":"p2","mtime":1496461616}]}
+```
 """

 message
@@ -1283,25 +1333,30 @@
 Read a public (shared) text file in the project whose id is supplied.
 User does not need to be owner or collaborator in the target project
 and does not need to be logged into CoCalc.
-Argument 'path' is relative to home directory in target project.
+Argument `path` is relative to home directory in target project.

 Examples

 Read a public file.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -d project_id=e49e86aa-192f-410b-8269-4b89fd934fba \\
     -d path=Public/hello.txt
     https://cocalc.com/api/v1/public_get_text_file
-  ==> {"event":"public_text_file_contents","id":"2d0e2faa-893a-44c1-9f64-59203bbbb017","data":"hello world\nToday is Friday\n"}
+  ==> {"event":"public_text_file_contents",
+       "id":"2d0e2faa-893a-44c1-9f64-59203bbbb017",
+       "data":"hello world\nToday is Friday\n"}
+```

 Attempt to read a file which is not public.
+```
   curl -u sk_abcdefQWERTY090900000000: \\
     -d project_id=e49e86aa-192f-410b-8269-4b89fd934fba \\
     -d path=Private/hello.txt
     https://cocalc.com/api/v1/public_get_text_file
   ==> {"event":"error","id":"0288b7d0-dda9-4895-87ba-aa71929b2bfb",
        "error":"path 'Private/hello.txt' of project with id 'e49e86aa-192f-410b-8269-4b89fd934fba' is not public"}+
-
+```
 """

 message
@@ -1528,12 +1583,13 @@
     desc           : """
 This queries directly the database (sort of Facebook's GraphQL)
 Options for the 'query' API message must be sent as JSON object.
-A query is either "get" (read from database), or "set" (write to database).
-A query is "get" if any query keys are null, otherwise the query is "set".
-
-Examples of 'get' query:
+A query is either _get_ (read from database), or _set_ (write to database).
+A query is _get_ if any query keys are null, otherwise the query is _set_.
+
+Examples of _get_ query:

 Get title and description for a project, given the project id.
+```
   curl -u sk_abcdefQWERTY090900000000: -H "Content-Type: application/json" \\
     -d '{"query":{"projects":{"project_id":"29163de6-b5b0-496f-b75d-24be9aa2aa1d","title":null,"description":null}}}' \\
     https://cocalc.com/api/v1/query
@@ -1543,8 +1599,10 @@
                             "title":"MY NEW PROJECT 2",
                             "description":"desc 2"}},
        "multi_response":false}
+```

 Get project id, given title and description.
+```
   curl -u sk_abcdefQWERTY090900000000: -H "Content-Type: application/json" \\
     -d '{"query":{"projects":{"project_id":null,"title":"MY NEW PROJECT 2","description":"desc 2"}}}' \\
     https://cocalc.com/api/v1/query
@@ -1554,9 +1612,12 @@
                             "description":"desc 2"}},
        "multi_response":false,
        "id":"2be22e08-f00c-4128-b112-fa8581c2d584"}
+```

 Get users, given the project id.
-  curl -u sk_abcdefQWERTY090900000000: -H "Content-Type: application/json" \\
+```
+  curl -u sk_abcdefQWERTY090900000000: \\
+    -H "Content-Type: application/json" \\
     -d '{"query":{"projects":{"project_id":"29163de6-b5b0-496f-b75d-24be9aa2aa1d","users":null}}}' \\
     https://cocalc.com/api/v1/query
   ==> {"event":"query",
@@ -1565,11 +1626,14 @@
                                      "111634c0-7048-41e7-b2d0-f87129fd409e":{"group":"collaborator"}}}},
        "multi_response":false,
        "id":"9dd3ef3f-002b-4893-b31f-ff51440c855f"}
-
-Example of 'set' query.
+```
+
+Example of _set_ query.

 Set title and description for a project, given the project id.
-  curl -u sk_abcdefQWERTY090900000000: -H "Content-Type: application/json" \\
+```
+  curl -u sk_abcdefQWERTY090900000000: \\
+    -H "Content-Type: application/json" \\
     -d '{"query":{"projects":{"project_id":"29163de6-b5b0-496f-b75d-24be9aa2aa1d", \\
                               "title":"REVISED TITLE", \\
                               "description":"REVISED DESC"}}}' \\
@@ -1578,6 +1642,7 @@
          "query":{},
          "multi_response":false,
          "id":"ad7d6b17-f5a9-4c5c-abc3-3823b1e1773f"}
+```

 Information on which fields are gettable and settable in the database tables
 via API message is in file 'db-schema.coffee', in CoCalc sources on GitHub at
@@ -1585,14 +1650,14 @@

 Within file 'db-schema.coffee':

-for project fields you can get, see the definition of
+- for project fields you can get, see the definition of
 `schema.projects.user_query.get.fields`.
-for user account fields you can set, see the definition of
+- for user account fields you can set, see the definition of
 `schema.projects.user_query.set.fields`.

-for user account fields you can get, see the definition of
+- for user account fields you can get, see the definition of
 `schema.accounts.user_query.get.fields`.
-for user account fields you can set, see the definition of
+- for user account fields you can set, see the definition of
 `schema.accounts.user_query.set.fields`.
 """
     examples: [  # TODO: create real examples!  These are not done.


Fixed problem with recognising an instanceof LocalFileSystem

--- src/liquid/engine.coffee
+++ src/liquid/engine.coffee
@@ -16,7 +16,7 @@
         isSubclassOf klass.__super__?.constructor, ofKlass

     # Assign the passed FileSystem instance or create a new one
-    if isSubclassOf fileSystem, Liquid.LocalFileSystem
+    if fileSystem instanceof Liquid.LocalFileSystem
       @fileSystem = fileSystem
     else
       @fileSystem = new Liquid.LocalFileSystem "./"


no keyboard shortcuts if focused on a form

--- app/assets/javascripts/cambium/admin/views/keyboard_shortcuts.js.coffee
+++ app/assets/javascripts/cambium/admin/views/keyboard_shortcuts.js.coffee
@@ -6,6 +6,7 @@
     'keyup': 'shortcut'

   shortcut: (e) ->
+    return if $('input, select, textarea').is(':focus')
     switch e.keyCode
       when 27 # Esc
         $('.search-box input').blur()


Sidebar: Add tooltip if stack template title is long.

--- client/app/lib/activity/sidebar/sidebarmachinelist.coffee
+++ client/app/lib/activity/sidebar/sidebarmachinelist.coffee
@@ -41,6 +41,9 @@
         title    : ' '

     @addSubView @header
+
+    if title.length > 20 # css will apply ellipsis so show a tooltip
+      @header.setAttribute 'title', title

   headerClickHandler: -> new EnvironmentsModal selected: @getOption 'stack'


MachineSettingsCommonView: Human readable error handling.

--- client/app/lib/providers/machinesettingscommonview.coffee
+++ client/app/lib/providers/machinesettingscommonview.coffee
@@ -139,7 +139,13 @@
   showNotification: (err, type = 'error') ->

     view    = @notificationView
-    message = if _.isObject err then err.message else err
+    message = err
+
+    if _.isObject message
+      if err.code is '107'
+        message = 'The domain could not be added as the VM is locked by another update process. Please try again in a few minutes.'
+      else
+        message = err.message

     view.unsetClass 'success error warning'
     view.setClass type


Clear cursor blink interval when editor component unmounts

--- src/cursors-component.coffee
+++ src/cursors-component.coffee
@@ -29,6 +29,9 @@
     @subscribe editor, 'cursors-moved', @pauseCursorBlinking
     @startBlinkingCursors()

+  componentWillUnmount: ->
+    @stopBlinkingCursors()
+
   startBlinkingCursors: ->
     @cursorBlinkIntervalHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2)


We don't need to check if we're deleting from one of our own channels, because that's not possible any more

--- core/app/backbone/views/facts/fact_bottom_view.coffee
+++ core/app/backbone/views/facts/fact_bottom_view.coffee
@@ -41,9 +41,6 @@

     collection.on "remove", (channel) =>
       @model.removeFromChannel channel, {}
-      if @model.collection?.channel? and @model.collection.channel.get("id") is channel.get("id")
-        @model.collection.remove @model
-        FactlinkApp.Modal.close()

     FactlinkApp.Modal.show 'Repost Factlink',
       new AddToChannelModalView(collection: collection, model: @model)


allow overleaf.history.id to be blank

allow overleaf.history.id to be blank

--- services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
+++ services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
@@ -29,7 +29,7 @@
 			else
 				HistoryController.initializeProject (error, history) ->
 					return callback(error) if error?
-					ProjectCreationHandler._createBlankProject owner_id, projectName, history.overleaf_id, callback
+					ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, callback

 	_createBlankProject : (owner_id, projectName, projectHistoryId, callback = (error, project) ->)->
 		rootFolder = new Folder {'name':'rootFolder'}


updated methods

updated methods

--- src/drm-simple-slider.coffee
+++ src/drm-simple-slider.coffee
@@ -3,39 +3,40 @@
 ###############################################################################
 "use strict"

+$ = jQuery
 class @DrmSimpleSlider
     constructor: (@slider = $('div.drm-simple-slider'), @play = 10000, @speed = 300, @animate = no) ->
         self = @
         @slideHolder = self.slider.find('div').first()
         @slides = self.slideHolder.find 'div.drm-simple-slide'
         @slideList = self.createSlideList()
-        sliderControls = self.slider.find('div.drm-simple-slider-nav').find 'button'
+        _sliderControls = self.slider.find('div.drm-simple-slider-nav').find 'button'
         current = 0

         advanceImage = ->
-            last = self.slides.length - 1
+            _last = self.slides.length - 1
             current = self.getCurrent()
-            dir = $(@).data 'dir'
+            _dir = $(@).data 'dir'

-            nextImage = (current) ->
-                if current is last then 0 else current + 1
-            prevImage = (current) ->
-                if current is 0 then last else current - 1
+            _nextImage = (current) ->
+                if current is _last then 0 else current + 1
+            _prevImage = (current) ->
+                if current is 0 then _last else current - 1

-            next = if dir is 'prev' then prevImage(current) else nextImage(current)
+            next = if _dir is 'prev' then _prevImage(current) else _nextImage(current)

             self.replaceImage current, next

         ## Initialize

         if self.slides.length > 1
-            sliderControls.show()
+            _sliderControls.show()
             self.slideList.appendTo self.slideHolder
             self.slideList.find('button').first().addClass 'active'
             self.slides.hide()
             self.slides.first().show()
         else
-            sliderControls.hide()
+            _sliderControls.hide()
             self.slideList.hide()
             self.slides.first().show()

@@ -45,7 +46,7 @@
             $(window).on 'load', $.proxy begin
             self.slideHolder.on 'mouseenter', pause

-        sliderControls.on 'click', advanceImage
+        _sliderControls.on 'click', advanceImage
         self.slideList.on 'click', 'button', ->
             current = self.getCurrent()
             next = $(@).data 'item-num'


getBounds() -> _originalGetBounds()

getBounds() -> _originalGetBounds()

--- src/map-base-view.coffee
+++ src/map-base-view.coffee
@@ -87,7 +87,7 @@

         onMapClicked: (ev) -> # override

-        getMapBounds: -> @map.getBounds()
+        getMapBounds: -> @map._originalGetBounds()

         calculateInitialOptions: ->
             city = p13n.getCity()


Improve watch tasks declaration formatting

--- src/tasks/watch.cake.coffee
+++ src/tasks/watch.cake.coffee
@@ -49,8 +49,16 @@
         info green _('neat.tasks.watch.watching_plural', dirs: watches)

 watchTaskGen 'watch', 'compile', _('neat.tasks.watch.description'), 'src'
-watchTaskGen 'watch:test', 'test', _('neat.tasks.watch_test.description'), 'src', 'test/units', 'test/functionals'
-watchTaskGen 'watch:test:unit', 'test:unit', _('neat.tasks.watch_test_unit.description'), 'src', 'test/units'
-watchTaskGen 'watch:test:functional', 'test:functional', _('neat.tasks.watch_test_functional.description'), 'src', 'test/functionals'
-watchTaskGen 'watch:test:integration', 'test:integration', _('neat.tasks.watch_test_integration.description'), 'src', 'test/integrations'
+watchTaskGen 'watch:test', 'test',
+             _('neat.tasks.watch_test.description'),
+             'src', 'test/units', 'test/functionals'
+watchTaskGen 'watch:test:unit', 'test:unit',
+             _('neat.tasks.watch_test_unit.description'),
+             'src', 'test/units'
+watchTaskGen 'watch:test:functional', 'test:functional',
+             _('neat.tasks.watch_test_functional.description'),
+             'src', 'test/functionals'
+watchTaskGen 'watch:test:integration', 'test:integration',
+             _('neat.tasks.watch_test_integration.description'),
+             'src', 'test/integrations'


pass second-level header spec

pass second-level header spec

--- src/test/test_render.coffee
+++ src/test/test_render.coffee
@@ -53,7 +53,7 @@
     t.ok body.body, "header rows"
     console.log body.body
     t.ok child = body.body[0], "first child"
-    t.equal child.attr.class[0], header._LABEL, 'child label'
+    t.equal child.attr.class[0], 'game', 'child label'
     t.end()

   test "render children", (t) ->


Add icons and styling to access level, humanize access level

--- plugins/shared_filesystem_storage/app/assets/javascripts/shared_filesystem_storage/components/shares/access_item.coffee
+++ plugins/shared_filesystem_storage/app/assets/javascripts/shared_filesystem_storage/components/shares/access_item.coffee
@@ -1,33 +1,41 @@
 { tr,td,button,i } = React.DOM
-
-shared_filesystem_storage.AccessItem = React.createClass
+
+shared_filesystem_storage.AccessItem = React.createClass
   getInitialState: ->
     deleting: false
-
+
   handleDelete: (e) ->
     e.preventDefault()
-    shared_filesystem_storage.ConfirmDialog.ask 'Are you sure?',
+    shared_filesystem_storage.ConfirmDialog.ask 'Are you sure?',
       #validationTerm: @props.shared_network.name
       description: 'Would you like to delete this access?'
       confirmLabel: 'Yes, delete it!'
     .then => @delete()
     .fail -> null
-
+
   delete: ->
     @setState deleting: true
-
+
     @props.ajax.delete "shares/#{@props.shareId}/rules/#{@props.rule.id}",
       success: () =>
         @props.handleDeleteRule @props.rule
       error: ( jqXHR, textStatus, errorThrown ) =>
         @setState deleting: null
-
+
+  humanizeAccessLevel: () ->
+    switch @props.rule.access_level
+      when 'ro' then 'read only'
+      when 'rw' then 'read/write'
+      else @props.rule.access_level
+
   render: ->
     tr className: ('updating' if @state.deleting),
       td null, @props.rule.access_type
       td null, @props.rule.access_to
-      td null, @props.rule.access_level
+      td className: "#{if @props.rule.access_level == 'rw' then 'text-success' else 'text-info'}",
+        i className: "fa fa-fw fa-#{if @props.rule.access_level == 'rw' then 'pencil-square' else 'eye'}"
+        @humanizeAccessLevel()
       td null, @props.rule.state
-      td className: 'snug',
-        button className: 'btn btn-danger btn-sm', onClick: @handleDelete,
-          i className: 'fa fa-minus'+      td className: 'snug',
+        button className: 'btn btn-danger btn-sm', onClick: @handleDelete,
+          i className: 'fa fa-minus'


Remove _.isEmpty usage and optimise collection view filterer.

--- src/chaplin/views/collection_view.coffee
+++ src/chaplin/views/collection_view.coffee
@@ -244,7 +244,7 @@
     filterCallback ?= @filterCallback

     # Show/hide existing views.
-    unless _.isEmpty @getItemViews()
+    if @subviews.length
       for item, index in @collection.models

         # Apply filter to the item.


tests: added mutliple-instance tests back, minus state change updates

--- test/zorium.coffee
+++ test/zorium.coffee
@@ -1275,6 +1275,96 @@
             drawCnt.should.be 4
             done()

+  it 'when re-using components, all instances are updated', (done) ->
+    subject = new Rx.BehaviorSubject 'abc'
+    prefix = '1-'
+
+    class A
+      constructor: ->
+        @state = z.state
+          abc: subject
+      render: ({name}) =>
+        {abc} = @state.getValue()
+
+        z '.z-a',
+          z 'div', name
+          z 'div', abc
+
+    class B
+      constructor: ->
+        @state = z.state
+          $a: new A()
+      render: ->
+        {$a} = @state.getValue()
+
+        z 'div',
+          z '.a1',
+            z $a, {name: prefix + 'a1'}
+          z '.a2',
+            z $a, {name: prefix + 'a2'}
+
+    router = new Router()
+    router.add '/test-reuse', new B()
+
+    root = document.createElement 'div'
+
+    z.router.init {$$root: root}
+    z.router.use (req, res) ->
+      res.send z router, {path: req.path, query: req.query}
+
+    result1 = '

' +
+                '

' +
+                  '
1-a1
' +
+                  '
abc
' +
+                '

' +
+                '

' +
+                  '
1-a2
' +
+                  '
abc
' +
+                '

' +
+              '

'
+
+    result2 = '

' +
+                '

' +
+                  '
2-a1
' +
+                  '
xyz
' +
+                '

' +
+                '

' +
+                  '
2-a2
' +
+                  '
xyz
' +
+                '

' +
+              '

'
+
+    result3 = '

' +
+                '

' +
+                  '
2-a1
' +
+                  '
xxx
' +
+                '

' +
+                '

' +
+                  '
2-a2
' +
+                  '
xxx
' +
+                '

' +
+              '

'
+
+    z.router.go '/test-reuse'
+    root.isEqualNode(htmlToNode(result1)).should.be true
+
+    # change in props leads to both updating
+    prefix = '2-'
+    subject.onNext 'xyz'
+    setTimeout ->
+      root.isEqualNode(htmlToNode(result2)).should.be true
+      done()
+
+      # change in state currently does not lead to both updating
+      # TODO: see if this can reasonably be fixed
+      # subject.onNext 'xxx'
+      # setTimeout ->
+      #   root.isEqualNode(htmlToNode(result3)).should.be true
+      #   done()
+      # , 20
+    , 20
+
+
   it 'renders full page, setting title and #zorium-root content', ->
     class Root
       render: ->


Make visitRequestStarted/Finished adapter methods optional

--- src/turbolinks/visit.coffee
+++ src/turbolinks/visit.coffee
@@ -79,7 +79,7 @@
   # HTTP Request delegate

   requestStarted: ->
-    @adapter.visitRequestStarted(this)
+    @adapter.visitRequestStarted?(this)

   requestProgressed: (@progress) ->
     @adapter.visitRequestProgressed?(this)
@@ -91,7 +91,7 @@
     @adapter.visitRequestFailedWithStatusCode(this, statusCode)

   requestFinished: ->
-    @adapter.visitRequestFinished(this)
+    @adapter.visitRequestFinished?(this)

   # Private


Close #40. Sort stat array before computing median

--- src/ubret/statistics.coffee
+++ src/ubret/statistics.coffee
@@ -25,7 +25,7 @@
   displayStats: =>
     return unless @ul? and @opts.statKey? and !(_.isEmpty(@preparedData()))

-    @statData = _.pluck @preparedData(), @opts.statKey
+    @statData = _.chain(@preparedData()).pluck(@opts.statKey).sort().value()
     @count = @statData.length
     @sum = _.foldl @statData, ((memo, num) -> memo + num), 0

@@ -49,6 +49,7 @@
     @sum / @count

   median: =>
+    console.log(@statData)
     midpoint = (@count / 2) - 1
     if midpoint % 1 is 0
       return @statData[midpoint]


improve exception handler; in particular, set `process.exitCode` to ensure failure gets communicated to calling process

--- src/exception-handler.coffee
+++ src/exception-handler.coffee
@@ -7,7 +7,7 @@
 ############################################################################################################
 CND                       = require 'cnd'
 rpr                       = CND.rpr
-badge                     = 'exception-handler'
+badge                     = 'cndxh'
 log                       = CND.get_logger 'plain',     badge
 debug                     = CND.get_logger 'debug',     badge
 info                      = CND.get_logger 'info',      badge
@@ -21,9 +21,18 @@
 # require                   'longjohn'
 FS                        = require 'fs'
 PATH                      = require 'path'
-
-# error = new Error('Oops!')
-# debug '^4461^'
+{ isa_text
+  red
+  green
+  steel
+  grey
+  cyan
+  bold
+  gold
+  reverse
+  white
+  yellow
+  reverse }               = CND

 #-----------------------------------------------------------------------------------------------------------
@@ -34,11 +43,16 @@
     delta     = 1
     first_idx = Math.max 0, linenr - 1 - delta
     last_idx  = Math.min lines.length - 1, linenr - 1 + delta
-    R = ( "#{first_idx + idx + 1}  #{line}" for line, idx in lines[ first_idx .. last_idx ] )
-    R = R.join '\n'
+    R         = []
+    for line, idx in lines[ first_idx .. last_idx ]
+      this_linenr = first_idx + idx + 1
+      lnr = ( this_linenr.toString().padStart 4 ) + '│ '
+      if this_linenr is linenr  then  R.push  "#{grey lnr}#{cyan line}"
+      else                            R.push  "#{grey lnr}#{grey line}"
+    # R = R.join '\n'
   catch error
     throw error unless error.code is 'ENOENT'
-    return './.'
+    return [ ( grey './.' ), ]
   return R

 #-----------------------------------------------------------------------------------------------------------
@@ -46,58 +60,38 @@
   stackman.callsites error, ( error, callsites ) ->
     throw error if error?
     callsites.forEach ( callsite ) ->
-      path      = callsite.getFileName()
-      unless CND.isa_text path
-        debug '^3736^', rpr path
-      else
-        relpath   = PATH.relative process.cwd(), path
-        linenr    = callsite.getLineNumber()
-        echo CND.white "#{relpath} ##{linenr}:"
-        unless path.startsWith 'internal/'
-          source    = get_context path, linenr
-          echo CND.yellow source
+      unless isa_text ( path = callsite.getFileName() )
+        alert grey '—'.repeat 108
+        return null
+      relpath   = PATH.relative process.cwd(), path
+      linenr    = callsite.getLineNumber()
+      if path.startsWith 'internal/'
+        alert grey "#{relpath} ##{linenr}"
+        return null
+      alert()
+      # alert steel bold reverse ( "#{relpath} ##{linenr}:" ).padEnd 108
+      alert gold ( "#{bold relpath} ##{linenr}:" ).padEnd 108
+      source = get_context path, linenr
+      alert line for line in source
+      return null
     return null
   return null

 #-----------------------------------------------------------------------------------------------------------
 @exit_handler = ( exception ) ->
-  # debug '55567', rpr exception
   print               = alert
-  message             = 'ROGUE EXCEPTION: ' + ( exception?.message ? "an unrecoverable condition occurred" )
+  message             = ' EXCEPTION: ' + ( exception?.message ? "an unrecoverable condition occurred" )
   if exception?.where?
     message += '\n--------------------\n' + exception.where + '\n--------------------'
   [ head, tail..., ]  = message.split '\n'
-  print CND.reverse ' ' + head + ' '
+  print reverse ' ' + head + ' '
   warn line for line in tail
-  ### TAINT should have a way to set exit code explicitly ###
-  debug '^55663^', exception
   if exception?.stack?
     show_error_with_source_context exception
   else
     whisper exception?.stack ? "(exception undefined, no stack)"
-  # setImmediate -> process.exit 1
+  process.exitCode = 1
 @exit_handler = @exit_handler.bind @
-
-# debug 'µ55531', __filename
-# debug 'µ55531', "app:", typeof app
-# check for process.type:
-# if process.type is 'renderer'
-# # if typeof app is 'undefined'
-#   process.on 'uncaughtException',  @exit_handler
-#   process.on 'unhandledRejection', @exit_handler
-# else
-#   urge "µ55531 using electron-unhandled"
-# ( require 'electron-unhandled' ) { showDialog: true, logger: @exit_handler, }
-
-# if process.type is 'renderer'
-#   window.addEventListener 'error', ( event ) =>
-#     event.preventDefault()
-#     warn 'µ44333', "error:", ( k for k of event )
-
-#   window.addEventListener 'unhandledrejection', ( event ) =>
-#     event.preventDefault()
-#     # warn 'µ44333', "unhandled rejection:", ( k for k of event )
-#     warn 'µ44333', "unhandled rejection:", event.reason?.message ? "(no message)"

 ############################################################################################################


Format user count on the tooltip

Format user count on the tooltip

--- resources/assets/coffee/_classes/changelog-chart.coffee
+++ resources/assets/coffee/_classes/changelog-chart.coffee
@@ -156,7 +156,7 @@
     @tooltipName
       .attr 'class', "changelog-chart__text changelog-chart__text--name changelog-chart__text--#{labelModifier}"
       .text currentLabel
-    @tooltipUserCount.text @data[dataRow][pos].data[currentLabel].user_count
+    @tooltipUserCount.text @data[dataRow][pos].data[currentLabel].user_count.toLocaleString()
     @tooltipDate.html @data[dataRow][pos].data.date_formatted

     tooltipWidth = @tooltip.node().getBoundingClientRect().width


remove admin from display

remove admin from display

--- resources/assets/coffee/_classes/beatmap-discussion-helper.coffee
+++ resources/assets/coffee/_classes/beatmap-discussion-helper.coffee
@@ -122,7 +122,7 @@

   @moderationGroup: (user) =>
-    _.intersection(user.groups, ['admin', 'qat', 'bng'])[0]
+    _.intersection(user.groups, ['qat', 'bng'])[0]

   @previewMessage = (message) =>