readd base label to message on processing

--- src/mailButler.appsscript.coffee
+++ src/mailButler.appsscript.coffee
@@ -184,6 +184,9 @@

         # remove the outbox label from the message
         @removeLabel @LABEL_OUTBOX, thread
+
+        # add/readd the base label
+        @addLabel @LABEL_BASE, thread

         # By default we assume there was no answer - this is also correct if the user doesn't care about answers
         hasAnswer = false


Add spiking support to hipchat

Add spiking support to hipchat

--- plugins/hipchat/index.coffee
+++ plugins/hipchat/index.coffee
@@ -14,14 +14,14 @@
         stack_trace_line: event.error.stacktrace && @firstStacktraceLine(event.error.stacktrace)
         project: event.project
         error: event.error
+        spiking: event.trigger.type == 'projectSpiking'
+        rate: event.trigger.rate
+

     else

       details =
         title: event.trigger.message
-
-      # Non-error events
-      message =  "#{event.trigger.message} from #{event.project.name}"

     if config.authToken.length == 40
       apiVer = 2
@@ -58,11 +58,16 @@
       callback(res.error)

   @render: Handlebars.compile(
-    '{{title}} from {{project.name}}' +
-    '{{#if error}}' +
-      ' in {{error.context}} (details)' +
-      '
   {{error_string}}' +
-      '{{#if stack_trace_line}}
   {{stack_trace_line}}{{/if}}' +
+    '{{#if spiking}}' +
+      'Spike of {{rate}} exceptions/minute in {{project.name}}
' +
+      'Most recent error: {{error_string}} (details)' +
+    '{{else}}' +
+      '{{title}} from {{project.name}}' +
+      '{{#if error}}' +
+        ' in {{error.context}} (details)' +
+        '
   {{error_string}}' +
+        '{{#if stack_trace_line}}
   {{stack_trace_line}}{{/if}}' +
+      '{{/if}}' +
     '{{/if}}'
   )


Error message on call without

Error message on call without

--- lib/pages.coffee
+++ lib/pages.coffee
@@ -54,7 +54,7 @@
         continue  unless i?
         i.stop()
   constructor: (collection, settings) ->
-    if !@setCollection
+    unless @ instance of Meteor.Pagination
       throw "Please use the `new` constructor style " + (new Error).stack.split("\n")[2].trim()
     @setCollection collection
     @setDefaults()


Check only objects and not null/array for the host. Related to #1577

--- drivers/javascript/net.coffee
+++ drivers/javascript/net.coffee
@@ -400,7 +400,7 @@
 # The only exported function of this module
 module.exports.connect = ar (host, callback) ->
     # Host must be a string or an object
-    unless typeof(host) is 'string' or typeof(host) is 'object'
+    unless typeof(host) is 'string' or Object::toString.call(host) is '[object Object]'
         throw new err.RqlDriverError "First argument to `connect` must be a string giving the "+
                                      "host to `connect` to or an object giving `host` and `port`."


Testpie formula testing

Testpie formula testing

--- widgets/testpie/testpie.coffee
+++ widgets/testpie/testpie.coffee
@@ -52,7 +52,20 @@
     for val in data
       sum += val.value
     if !sum
-      sum = 1
+#      sum = 1
+      arcs.append("svg:text")
+#        .attr("transform", (d, i) ->
+#        procent_val = Math.round(data[i].value/sum * 100)
+#        d.innerRadius = (radius * (100-procent_val)/100) - 45  #45=max text size/2
+#        d.outerRadius = radius
+#        "translate(" + arc.centroid(d) + ")")
+        .attr('fill', "#fff")
+        .attr("text-anchor", "middle").text("No Results").attr('font-size', '28px')
+#        .append('svg:tspan')
+#        .attr('x', 0)
+#        .attr('dy', 25)
+#        .attr('font-size', '90%')
+#        .text((d,i) -> data[i].value + ' (' + Math.round(data[i].value/sum * 100) + '%)')

     arcs.append("svg:text").attr("transform", (d, i) ->
       procent_val = Math.round(data[i].value/sum * 100)


hub/metrics: using prom_client.AggregatorRegistry to properly "merge" all collected metrics -- typo

--- src/smc-hub/metrics-recorder.coffee
+++ src/smc-hub/metrics-recorder.coffee
@@ -128,7 +128,7 @@
         @setup_monitoring()
         cb?(undefined, @)

-    client_metrics: =
+    client_metrics: =>
         ###
         exports.client_metrics is a mapping of client id to the json exported metric.
         The AggregatorRegistry is supposed to work with a list of metrics, and by default,


chat completely broken by the computed properties rewrite of project store

--- src/smc-webapp/project_store.coffee
+++ src/smc-webapp/project_store.coffee
@@ -386,7 +386,7 @@

     # Used by open/close chat below.
     _set_chat_state: (path, is_chat_open) =>
-        open_files = @get_store()?.get_open_files()  # store might not be initialized
+        open_files = @get_store()?.open_files  # store might not be initialized
         if open_files? and path?
             @setState
                 open_files : open_files.setIn([path, 'is_chat_open'], is_chat_open)
@@ -409,7 +409,7 @@
         opts = defaults opts,
             path  : required
             width : required     # between 0 and 1
-        open_files = @get_store()?.get_open_files()  # store might not be initialized
+        open_files = @get_store()?.open_files  # store might not be initialized
         if open_files?
             width = misc.ensure_bound(opts.width, 0.05, 0.95)
             require('./editor').local_storage?(@project_id, opts.path, 'chat_width', width)


Fix opening file in a different project

Fix opening file in a different project

--- src/smc-webapp/project_store.coffee
+++ src/smc-webapp/project_store.coffee
@@ -294,13 +294,12 @@

                         if open_files.has(opts.path) # Already opened
                             if opts.foreground
-                                @set_active_tab(misc.path_to_tab(opts.path))
+                                @foreground_opened_file(opts.path)
                             return

                         open_files_order = store.get_open_files_order()
                         # Intialize the file's store and actions
                         name = project_file.initialize(opts.path, @redux, @project_id)
-                        console.log("name? in open_file", name)

                         # Make the editor
                         editor = project_file.generate(opts.path, @redux, @project_id)
@@ -309,8 +308,12 @@
                         # Add it to open files
                         @setState(open_files: open_files.set(opts.path, editor), open_files_order:open_files_order.push(opts.path))
                         if opts.foreground
-                            @set_active_tab(misc.path_to_tab(opts.path))
+                            @foreground_opened_file(opts.path)
         return
+
+    foreground_opened_file : (path) =>
+        @redux.getActions('projects').foreground_project(@project_id)
+        @set_active_tab(misc.path_to_tab(path))

     convert_sagenb_worksheet: (filename, cb) =>
         async.series([


inline docs for rewriteURL()

inline docs for rewriteURL()

 On branch master
	modified:   lib/serverutil.coffee

--- lib/serverutil.coffee
+++ lib/serverutil.coffee
@@ -43,6 +43,41 @@
 exports.RewriteRule = RewriteRule

+# @param {String} path An URL path string like /foo/bar.html
+# @param {Array} rules A list of RewriteRule instance objects
+#
+# @returns {Object} A object with properties .path, .host, and .port
+#
+# An instance of RewriteRule my include .regex, .path, .host, and .port
+# definitions.
+#
+# If no rules are passed, the path is simply returned as is.
+#
+# If no rules are matched using the .regex property the path is simply returned
+# as is.
+#
+# The first .regex match is used and the rest are ignored.
+#
+# If no .path is given in a matched RewriteRule instance, the .path is simply returned
+# as is.
+#
+# The returned .path will be the .path of the RewriteRule if a match is made
+# using the .regex property of the rule.
+#
+# If there is a .host property on a matched RewriteRule, the returned object
+# will inherit the .host property.
+#
+# If there is a .port property on a matched RewriteRule, the returned object
+# will inherit the .port property.
+#
+# If there is a subgroup in the .regex of the RewriteRule and replacement
+# characters in the .path of the rewrite rule, then replacement will take
+# effect in the returned .path. Example:
+#
+#       rules = [{regex: /^\/foopath\/([a-z]*)\/uid\/(.*)/, path: '/barpath/$1/id/$2'}]
+#       newURL = serverutil.rewriteURL('/foopath/val/uid/123qwe', rules)
+#       expect(newURL.path).toBe '/barpath/val/id/123qwe'
+#
 rewriteURL = (path, rules) ->
     rv =
         path: path


Add support for `NC` pins in connector symbol

--- src/symbol/default/connector.coffee
+++ src/symbol/default/connector.coffee
@@ -57,6 +57,7 @@

   pins = element.pins
   for k, v of pins
-    pins[k].passive = true
+    if pins[k].nc isnt true
+      pins[k].passive = true

   enclosure symbol, element


On empty value as default first value in options.

--- lib/star_field.coffee
+++ lib/star_field.coffee
@@ -169,8 +169,11 @@
   constructor: (options) ->
     super(options)
     @toCallOnChange = []
+    @options = options.options # adding the select options
+    if not @value or @value==''
+      @value = @options[0]
     console.log "in Select constructor value:#{@value}"
-    @options = options.options # adding the select options
+

   editor: ->
     SelectEditor


Better images for damage analogy, partial kill / image clipping added

--- src/javascript/main.coffee
+++ src/javascript/main.coffee
@@ -970,26 +970,68 @@
       id: 32880
       ehp: 3770
       fit: 'https://o.smium.org/loadout/private/24585/6674104450400911360'
-    caracal:
-      pluralName: 'Caracals'
-      id: 621
-      ehp: 18000
-      fit: 'https://o.smium.org/loadout/private/24584/5861380819909607424'
-    proteus:
-      pluralName: 'Proteii'
-      id: 29988
-      ehp: 150000
-      fit: 'https://o.smium.org/loadout/private/23324/6303219387142766592'
+      image: 'images/ships/venture.png'
+      width: 43
+      height: 48
+    #caracal:
+    #  pluralName: 'Caracals'
+    #  id: 621
+    #  ehp: 18000
+    #  fit: 'https://o.smium.org/loadout/private/24584/5861380819909607424'
+    #  image: 'images/ships/caracal.png'
+    #  height: 32
+    vexor:
+      pluralName: 'Vexors'
+      ehp: 24000
+      fit: 'https://o.smium.org/loadout/private/24682/7359747123954319360'
+      image: 'images/ships/vexor.png'
+      width: 57
+      height: 48
+    #proteus:
+    #  pluralName: 'Proteii'
+    #  id: 29988
+    #  ehp: 150000
+    #  fit: 'https://o.smium.org/loadout/private/23324/6303219387142766592'
+    #  image: 'images/ships/proteus.png'
+    #  height: 32
+    #abaddon:
+    #  pluralName: 'Abaddons'
+    #  ehp: 140000
+    #  fit: ''
+    #  image: 'images/ships/abaddon.png'
+    #  height: 32
+    #  width: 90
+    apocalypse:
+      pluralName: 'Apocalypses Navy Issue'
+      ehp: 140000
+      fit: 'https://o.smium.org/loadout/private/24684/6057995995994652672'
+      image: 'images/ships/apocalypse.png'
+      height: 32
+      width: 92
+    #legion:
+    #  pluralName: 'Legions'
+    #  ehp: 140000
+    #  fit: 'https://o.smium.org/loadout/private/24683/477339014555238400'
+    #  image: 'images/ships/legion.png'
+    #  height: 32
     naglfar:
       pluralName: 'Naglfars'
       id: 19722
       ehp: 1500000
       fit: 'https://o.smium.org/loadout/private/24661/3437198039918313472'
+      image: 'images/ships/naglfar.png'
+      width: 32
+      height: 113
     avatar:
       pluralName: 'Avatars'
       id: 11567
       ehp: 22600000
       fit: 'https://o.smium.org/loadout/private/23322/7459269642280763392'
+      image: 'images/ships/avatar.png'
+      width: 96
+      height: 48
+  numShips: (shipKey) ->
+    return d3.round(@props.damage / @ships[shipKey].ehp, 1)
   render: ->
     if @props.damage == 0
       return null
@@ -998,14 +1040,28 @@
       numShips = 0
       for name, ship of @ships
         shipToUse = name
-        numShips = d3.round(@props.damage / ship.ehp, 1)
+        numShips = @numShips(name)
         if numShips < 100
           break

       ship = @ships[shipToUse]
+      numShips = @numShips(shipToUse)
+      wholeShips = Math.floor(numShips)
+      partial = numShips - wholeShips
       stamps = []
+      imgOpts = {src: ship.image, width: ship.width, height: ship.height, className: 'stamp'}
       for i in [0...Math.floor(numShips)]
-        stamps.push dom.img {key: i, src: eveIconUrl(ship.id), width: 32, height: 32, className: 'stamp pull-left'}
+        stamps.push dom.div({className: 'pull-left'}, dom.img(imgOpts))
+      # handle partial stamp by hiding overflow and explicitly setting width
+      if partial
+        width = Math.round(partial * ship.width)
+        divOpts =
+          className: 'pull-left'
+          style:
+            height: ship.height
+            width: width
+            overflow: 'hidden'
+        stamps.push dom.div(divOpts, dom.img(imgOpts))

       return dom.div {className: 'row'},
         dom.h4 {className: 'pull-left'},


Fix Models.Post::update()

Fix Models.Post::update()

--- lib/assets/javascripts/models/post.js.coffee
+++ lib/assets/javascripts/models/post.js.coffee
@@ -31,13 +31,15 @@
       unless xhr.status == 200
         post.trigger('update:failure', res, xhr)
         options.failure?(res, xhr)
+        options.complete?(res, xhr)
         return

       post.parseAttributes(res.post)
       post.trigger('update:success', post, xhr)
       options.success?(post, xhr)
+      options.complete?(res, xhr)

-    data.versions ?= { parents: [{ version: post.get('version.id') }] }
+    data.version ?= { parents: [{ version: post.get('version.id') }] }

     TentStatus.tent_client.post.update(
       params:


Don't replace the initial history page if the original page was requested with POST

Replacing and reloading would load the POST URL with a GET method.

--- lib/assets/javascripts/unpoly/history.coffee
+++ lib/assets/javascripts/unpoly/history.coffee
@@ -212,8 +212,13 @@
       # https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration
       window.history.scrollRestoration = 'manual'
       window.addEventListener('popstate', pop)
-      # Replace the vanilla state of the initial page load with an Unpoly-enabled state
-      replace(currentLocation(), event: false)
+
+      # Unpoly replaces the initial page state so it can later restore it when the user
+      # goes back to that initial URL. However, if the initial request was a POST,
+      # Unpoly will wrongly assume that it can restore the state by reloading with GET.
+      if up.protocol.initialRequestMethod() == 'GET'
+        # Replace the vanilla state of the initial page load with an Unpoly-enabled state
+        replace(currentLocation(), event: false)

     if jasmine?
       # Can't delay this in tests.


Fixed coffeescript bug: radio buttons in download submissions

--- lms/static/coffee/src/teacher_dashboard/data_download.coffee
+++ lms/static/coffee/src/teacher_dashboard/data_download.coffee
@@ -25,7 +25,6 @@
     # point to class_code selector
     @$class_code                      = @$section.find '.wrapper-member-select'
     @$class_code_select               = @$class_code.find '.member-lists-selector'
-    @$arrange_by_select               = @$section.find("input[name='download-class-submissions-arrange_by']")

     # response areas
     @$download                        = @$section.find '.data-download-container'
@@ -63,7 +62,7 @@
         type: 'GET'
         dataType: 'json'
         url: url
-        data: {class_code: @$class_code_select.val(),arrange_by: @$arrange_by_select.val()}
+        data: {class_code: @$class_code_select.val()}
         error: (std_ajax_err) =>
           @$reports_request_response_error.text errorMessage
           $(".msg-error").css({"display":"block"})
@@ -82,7 +81,7 @@
         dataType: 'json'
         url: url
         aync: false
-        data: {class_code: @$class_code_select.val(),arrange_by: @$arrange_by_select.val()}
+        data: {class_code: @$class_code_select.val(), arrange_by: @$section.find("input[name='download-class-submissions-arrange_by']:checked").val()}
         error: (std_ajax_err) =>
           @$reports_request_response_error.text errorMessage
           $(".msg-error").css({"display":"block"})


Fix wrong package name

Fix wrong package name

--- views/components/settings/parts/poi-config.cjsx
+++ views/components/settings/parts/poi-config.cjsx
@@ -10,7 +10,7 @@
 {config, toggleModal} = window
 {APPDATA_PATH} = window
 {showItemInFolder, openItem} = require 'shell'
-Mousetrap = require 'Mousetrap'
+mousetrap = require 'mousetrap'
 ipcRenderer = require("electron").ipcRenderer

 Divider = require './divider'
@@ -516,7 +516,7 @@
       </Row>
     </Col>

-Mousetrap.prototype.handleKey = (character, modifiers, e) ->
+mousetrap.prototype.handleKey = (character, modifiers, e) ->
   return if e.type != 'keydown'
   return if character in ['shift', 'alt', 'ctrl', 'meta']
   fn = ShortcutConfig.prototype.listener


CLEANUP NICER ERROR OUTPUT

CLEANUP
NICER ERROR OUTPUT

--- source/BabelBridge/Parser.coffee
+++ source/BabelBridge/Parser.coffee
@@ -126,7 +126,6 @@
   OUT: a Node with offset and matchLength
   ###
   subparse: (subsource, options = {}) ->
-    # log subparse: {subsource, options}

     subparser = new @class
     {originalMatchLength, parentNode, sourceMap, originalOffset} = options
@@ -175,12 +174,6 @@
       @parentParser.offsetInRootParserSource @offsetInParentParserSource suboffset
     else
       suboffset
-    # log offsetInRootParserSourceA: {suboffset}
-    # @_parentParserRootOffset ?= if @parentParser
-    #   @parentParser?.offsetInRootParserSource(0)
-    # else 0
-    # log offsetInRootParserSourceB: {suboffset, @_parentParserRootOffset, @source, offsetInParentParserSource: @offsetInParentParserSource suboffset}
-    # @_parentParserRootOffset + @offsetInParentParserSource suboffset

   @getter
     failureIndexInParentParser: -> @offsetInParentParserSource @_failureIndex
@@ -248,7 +241,7 @@

       out = compactFlatten [
         """
-        Parsing error at #{getLineColumnString @_source, @_failureIndex}
+        Parsing error at #{@options.sourceFile || ''}:#{getLineColumnString @_source, @_failureIndex}

         Source:
         ...
@@ -372,11 +365,9 @@
   _logParsingFailure: (parseIntoNode, patternElement) ->
     return unless @_matchingNegativeDepth == 0 && parseIntoNode.offset >= @_failureIndex && patternElement.isTokenPattern

-    # log _logParsingFailure: {parseIntoNode, offset: parseIntoNode.offset, source: parseIntoNode.source}
     @_addNonMatch parseIntoNode.offset, new NonMatch parseIntoNode, patternElement

   _addNonMatch: (offset, nonMatch) ->
-    # log _addNonMatch: {offset, nonMatch}
     if offset > @_failureIndex
       @_failureIndex = offset
       @_nonMatches = {}


get word position (and not word object)

get word position (and not word object)

--- lib/y-richtext.coffee
+++ lib/y-richtext.coffee
@@ -144,8 +144,9 @@
     @_model.getContent("cursors").observe (events)=> @locker.try ()=>
       for event in events
         author = event.changedBy
-        position = event.object.val(author)
-        if position?
+        word = event.object.val(author)
+        if word?
+          position = word.getPosition()
           params =
             id: author
             index: position


exported

exported

--- lib/setup.coffee
+++ lib/setup.coffee
@@ -7,7 +7,7 @@
 { ARTSY_URL, NODE_ENV, ARTSY_ID, ARTSY_SECRET, SESSION_SECRET, PORT, ASSET_PATH,
   FACEBOOK_APP_NAMESPACE, MOBILE_MEDIA_QUERY, MOBILE_URL, APP_URL, REDIS_URL, DEFAULT_CACHE_TIME,
   SECURE_APP_URL, CANONICAL_MOBILE_URL, IMAGES_URL_PREFIX, SECURE_IMAGES_URL,
-  GOOGLE_ANALYTICS_ID, MIXPANEL_ID } = config = require "../config"
+  GOOGLE_ANALYTICS_ID, MIXPANEL_ID, COOKIE_DOMAIN } = config = require "../config"
 { parse, format } = require 'url'
 _ = require 'underscore'
 express = require "express"


remove d3

remove d3

--- source/javascripts/vendor.coffee
+++ source/javascripts/vendor.coffee
@@ -1 +0,0 @@
-#= require components/d3/d3


unless _last?

unless _last?

Thx @soyjavi

--- source/quo.gestures.swipe.coffee
+++ source/quo.gestures.swipe.coffee
@@ -39,7 +39,7 @@
         _last = null

     cancel = end = (target, data) ->
-      if not _last? or _last is `undefined`
+     unless _last?
         if data.length >= 1
           delta =
             x: data[0].x - _start.x


remove redundant update functions in addie

--- coffee/main.coffee
+++ coffee/main.coffee
@@ -528,7 +528,7 @@
     addGuiElems(k, v) for k,v of dict

     $(@ve.datgui.domElement).focusout () =>
-      @ve.addie.update(x) for x in s
+      @ve.addie.update([x]) for x in s

     true

@@ -693,47 +693,7 @@
 class Addie
   constructor: (@ve) ->

-  #TODO: kill this and just use updates
-  update: (x) =>
-    console.log("updating object")
-    console.log(x)
-
-    msg = { Elements: [] }
-
-    if x.shp?
-      x.props.position = x.shp.obj3d.position
-
-    ido = { OID: x.id, Type: x.constructor.name, Element: x.props }
-    msg.Elements.push(ido)
-
-
-    if x.setEndpointData?
-      x.setEndpointData()
-      ep = x.endpoint[0]
-      msg.Elements.push(
-        { OID: ep.id, Type: ep.constructor.name, Element: ep.props }
-      )
-      ep = x.endpoint[1]
-      msg.Elements.push(
-        { OID: ep.id, Type: ep.constructor.name, Element: ep.props }
-      )
-
-    console.log(msg)
-    $.post "/addie/"+dsg+"/design/update", JSON.stringify(msg), (data) =>
-      x.id.name = x.props.name
-      x.id.sys = x.props.sys
-
-    updateLink = (l) =>
-      l.setEndpointData()
-      @update(l)
-
-
-    if x.links?
-      updateLink(l) for l in x.links
-
-    true
-
-  updates: (xs) =>
+  update: (xs) =>
     console.log("updating objects")
     console.log(xs)

@@ -878,7 +838,7 @@
       @mh.ve.container.onmouseup = (eve) => @handleUp(eve)

   handleUp: (event) ->
-    @mh.ve.addie.update(@mh.placingObject)
+    @mh.ve.addie.update([@mh.placingObject])

     @mh.ve.container.onmousemove = null
     @mh.ve.container.onmousedown = (eve) => @mh.baseDown(eve)
@@ -915,7 +875,7 @@
     @mh.ve.container.onmousemove = (eve) => @handleMove(eve)

   handleUp: (ixs) ->
-    @mh.ve.addie.update(@mh.placingObject)
+    @mh.ve.addie.update([@mh.placingObject])

     @mh.ve.container.onmousemove = null
     @mh.ve.container.onmousedown = (eve) => @mh.baseDown(eve)
@@ -952,7 +912,7 @@
       for k, v of @cprops
         for e in @elements
           e.props[k] = v
-      @ve.addie.updates(@elements)
+      @ve.addie.update(@elements)
       true

     true
@@ -1133,7 +1093,7 @@
       @mh.placingLink.ifInternetToWanLink()
       @mh.placingLink.setEndpointData()

-      @mh.ve.addie.update(@mh.placingLink)
+      @mh.ve.addie.update([@mh.placingLink])

       @mh.ve.container.onmousemove = null


Update task-panel.cjsx

Update task-panel.cjsx

--- views/components/main/parts/task-panel.cjsx
+++ views/components/main/parts/task-panel.cjsx
@@ -133,13 +133,17 @@
   return unless questRecord[id]?
   questRecord[id].active = false
   syncQuestRecord()
-resetQuestRecord = (types, resetInterval, id, questRecord) ->
+clearQuestRecordCount = (id, q) ->
+    q.count = 0
+    for own k, v of questGoals[id] when typeof v is 'object' and v isnt null
+      q[k].count = 0
+resetQuestRecord = (types, resetInterval, id, q) ->
   return unless questGoals[id]?
   if questGoals[id].type in types
-    questRecord.active = false
-    questRecord.count = 0
+    q.active = false
+    clearQuestRecordCount id, q
   else if questGoals[id].resetInterval is resetInterval
-    questRecord.count = 0
+    clearQuestRecordCount id, q
 resetQuestRecordDaily = resetQuestRecord.bind(null, [2, 4, 5], 1)
 resetQuestRecordWeekly = resetQuestRecord.bind(null, [3], 2)
 resetQuestRecordMonthly = resetQuestRecord.bind(null, [6], 3)


Fix @mention name escaping, more restrictive usernames (fix #516)

--- lib/users.coffee
+++ lib/users.coffee
@@ -21,7 +21,7 @@
   link = urlFor 'author',
     group: group
     author: username
-  link = """#{prefix or ''}#{displayUser user}"""
+  link = """#{prefix or ''}#{_.escape displayUser user}"""
   if Meteor.isClient and
      Router.current()?.route?.getName() == 'author' and
      Router.current()?.params?.author == username
@@ -39,7 +39,7 @@

 @validUsername = (username) ->
   validKey(username) and
-  not username.match(/[\s@]/) and  # bad characters for at-mentions
+  not /[\s@`&<>{}\\]/.test(username) and  # bad characters for at-mentions
   username.toLowerCase() != 'me'  # used in search as shorthand for self

 ## Need to escape dots in usernames.


Refactor prefix recognition, add about: prefix as well.

--- lib/utils.coffee
+++ lib/utils.coffee
@@ -30,9 +30,15 @@
     return -> id += 1
   )()

+  hasChromePrefix: (url) ->
+    chromePrefixes = [ 'about', 'view-source' ]
+    for prefix in chromePrefixes
+      return true if url.startsWith prefix
+    false
+
   # Completes a partial URL (without scheme)
   createFullUrl: (partialUrl) ->
-    if (!/^[a-z]{3,}:\/\//.test(partialUrl)) and not /^view-source:/.test partialUrl
+    if (!/^[a-z]{3,}:\/\//.test(partialUrl))
       "http://" + partialUrl
     else
       partialUrl
@@ -53,10 +59,6 @@

     # are there more?
     specialHostNames = [ 'localhost' ]
-
-    # special-case view-source:[url]
-    if /^view-source:/.test str
-      return true

     # it starts with a scheme, so it's definitely an URL
     if (/^[a-z]{3,}:\/\//.test(str))
@@ -106,7 +108,10 @@
   # We don't bother with escaping characters as Chrome will do that for us.
   convertToUrl: (string) ->
     string = string.trim()
-    if (Utils.isUrl(string)) then Utils.createFullUrl(string) else Utils.createSearchUrl(string)
+    # special-case about:[url] and view-source:[url]
+    if Utils.hasChromePrefix string then string
+    else
+      if (Utils.isUrl(string)) then Utils.createFullUrl(string) else Utils.createSearchUrl(string)

 # This creates a new function out of an existing function, where the new function takes fewer arguments.
 # This allows us to pass around functions instead of functions + a partial list of arguments.
@@ -116,6 +121,8 @@
   -> fn.apply(this, fixedArguments.concat(Array.copy(arguments)))

 Array.copy = (array) -> Array.prototype.slice.call(array, 0)
+
+String::startsWith = (str) -> @indexOf(str) == 0

 # A very simple method for defining a new class (constructor and methods) using a single hash.
 # No support for inheritance is included because we really shouldn't need it.


nitrogen6x.coffee: Discontinue board

nitrogen6x.coffee: Discontinue board

We will no longer be offering OS updates for this board

Changelog-entry: Discontinue the nitrogen6x board
Signed-off-by: Florin Sarbu <3a837a37a2ac9072a088641ec330e0e5d5b86f4e@balena.io>

--- nitrogen6x.coffee
+++ nitrogen6x.coffee
@@ -7,7 +7,7 @@
 	aliases: [ 'nitrogen6x' ]
 	name: 'Nitrogen 6X'
 	arch: 'armv7hf'
-	state: 'released'
+	state: 'discontinued'

 	instructions: commonImg.instructions
 	gettingStartedLink:


Fix a typo in _watchForDir

Fix a typo in _watchForDir

--- lifeguard.coffee
+++ lifeguard.coffee
@@ -101,7 +101,7 @@
   # longest part that does. watch that directory for the next step in the
   # tree to appear

-  _watchForDir: (dir,cb) ->
+  _watchForDir: (target,cb) ->
     # loop our way up until we find something that exists
     lFunc = (d,lcb) =>
       # test this
@@ -115,8 +115,8 @@
             d = path.resolve d, ".."
             lFunc(d,lcb)

-    lFunc dir, (existing) =>
-      if existing == dir
+    lFunc target, (existing) =>
+      if existing == target
         console.log "Starting up!"
         cb?()


Don't assume tokens match text nodes when measuring character widths

--- src/lines-component.coffee
+++ src/lines-component.coffee
@@ -72,23 +72,32 @@
     {editor} = @props
     rangeForMeasurement = null
     iterator = null
-    iteratorIndex = -1
+    charIndex = 0

     for {value, scopes}, tokenIndex in tokenizedLine.tokens
       charWidths = editor.getScopedCharWidths(scopes)
-      for char, i in value
+
+      for char in value
         unless charWidths[char]?
-          rangeForMeasurement ?= document.createRange()
-          iterator ?=  document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
+          unless textNode?
+            rangeForMeasurement ?= document.createRange()
+            iterator =  document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
+            textNode = iterator.nextNode()
+            textNodeIndex = 0
+            nextTextNodeIndex = textNode.textContent.length

-          while iteratorIndex < tokenIndex
+          while nextTextNodeIndex <= charIndex
             textNode = iterator.nextNode()
-            iteratorIndex++
+            textNodeIndex = nextTextNodeIndex
+            nextTextNodeIndex = textNodeIndex + textNode.textContent.length

+          i = charIndex - textNodeIndex
           rangeForMeasurement.setStart(textNode, i)
           rangeForMeasurement.setEnd(textNode, i + 1)
           charWidth = rangeForMeasurement.getBoundingClientRect().width
           editor.setScopedCharWidth(scopes, char, charWidth)
+
+        charIndex++

     @measuredLines.add(tokenizedLine)


Groups: admin is notified by invalid invitation emails

--- client/Dashboard/views/groupsinvitationview.coffee
+++ client/Dashboard/views/groupsinvitationview.coffee
@@ -146,7 +146,17 @@
       callback           : ({emails, message, saveMessage})=>
         KD.whoami().fetchFromUser "email", (err, userEmail)=>
           emails = emails.trim()
+          invalidEmails = []
           emailList = emails.split(/\n/).map (email)-> email.trim()
+          emailList = emailList.filter (email) ->
+            isValid = KD.utils.doesEmailValid email
+            invalidEmails.push email  unless isValid
+            isValid
+
+          if invalidEmails.length
+            @inviteByEmail.modalTabs.forms.invite.buttons.Send.hideLoader()
+            return KD.showError "Your invitations includes some invalid emails: #{invalidEmails}"
+
           if userEmail in emailList
             @inviteByEmail.modalTabs.forms.invite.buttons.Send.hideLoader()
             return new KDNotificationView


Pricing: workflow login/register section copy change

--- client/Main/payment/existingaccountworkflow.coffee
+++ client/Main/payment/existingaccountworkflow.coffee
@@ -21,7 +21,7 @@
           name             : "email"
           testPath         : "account-email-input"
       buttons              :
-        Save               :
+        'SIGN UP'          :
           type             : 'submit'
           style            : 'solid green fr'
       callback             : ({ email }) =>
@@ -34,7 +34,7 @@
   pistachio: ->
     """

-
Sign in or Signup to proceed with your checkout

+
Sign in or create an account to proceed with your checkout

       {{> @loginForm}}
       or
       {{> @emailCollectionForm}}


Use RootView::openSync in specs

Use RootView::openSync in specs

--- spec/command-palette-spec.coffee
+++ spec/command-palette-spec.coffee
@@ -6,7 +6,7 @@

   beforeEach ->
     window.rootView = new RootView
-    rootView.open('sample.js')
+    rootView.openSync('sample.js')
     atom.activatePackage("command-palette")
     rootView.attachToDom().focus()
     rootView.trigger 'command-palette:toggle'


:bug: Fix a typo

:bug: Fix a typo

--- spec/editor-registry-spec.coffee
+++ spec/editor-registry-spec.coffee
@@ -25,7 +25,7 @@
       expect(editorRegistry.editorLinters.size).toBe(0)

   describe '::has', ->
-    it 'returns the status of existance', ->
+    it 'returns the status of existence', ->
       editor = atom.workspace.getActiveTextEditor()
       expect(editorRegistry.has(1)).toBe(false)
       expect(editorRegistry.has(false)).toBe(false)


Reuse participant avatar view

Reuse participant avatar view

--- client/ide/lib/views/statusbar/idestatusbar.coffee
+++ client/ide/lib/views/statusbar/idestatusbar.coffee
@@ -78,7 +78,11 @@

   createParticipantAvatar: (nickname, isOnline) ->

-    return  if @participantAvatars[nickname] or nickname is nick()
+    return  if nickname is nick()
+
+    if view = @participantAvatars[nickname]
+      @updateParticipantAvatar view, isOnline
+      return @avatars.addSubView view

     view       = new IDEStatusBarAvatarView
       origin   : nickname
@@ -87,6 +91,16 @@

     @participantAvatars[nickname] = view
     @avatars.addSubView view
+
+
+  updateParticipantAvatar: (view, isOnline) ->
+
+    if isOnline
+      view.setClass 'online'
+      view.unsetClass 'offline'
+    else
+      view.setClass 'offline'
+      view.unsetClass 'online'

   showAvatars: (accounts, currentlyOnline) ->


add DPI for canvas tests

add DPI for canvas tests

--- spec/minimap-element-spec.coffee
+++ spec/minimap-element-spec.coffee
@@ -15,6 +15,8 @@
   transform = new WebKitCSSMatrix window.getComputedStyle(o).transform
   o.offsetLeft + transform.m41

+devicePixelRatio = window.devicePixelRatio || 1
+
 sleep = (duration) ->
   t = new Date
   waitsFor -> new Date - t > duration
@@ -119,8 +121,8 @@
       expect(minimapElement.offsetWidth).toBeCloseTo(editorElement.clientWidth / 11, 0)

     it 'resizes the canvas to fit the minimap', ->
-      expect(canvas.offsetHeight).toEqual(minimapElement.offsetHeight + minimap.getLineHeight())
-      expect(canvas.offsetWidth).toEqual(minimapElement.offsetWidth)
+      expect(canvas.offsetHeight / devicePixelRatio).toEqual(minimapElement.offsetHeight + minimap.getLineHeight())
+      expect(canvas.offsetWidth / devicePixelRatio).toEqual(minimapElement.offsetWidth)

     it 'requests an update', ->
       expect(minimapElement.frameRequested).toBeTruthy()
@@ -202,8 +204,8 @@
           expect(minimapElement.offsetWidth).toBeCloseTo(editorElement.offsetWidth / 11, 0)
           expect(minimapElement.offsetHeight).toEqual(editorElement.offsetHeight)

-          expect(canvas.offsetWidth).toEqual(minimapElement.offsetWidth)
-          expect(canvas.offsetHeight).toEqual(minimapElement.offsetHeight + minimap.getLineHeight())
+          expect(canvas.offsetWidth / devicePixelRatio).toEqual(minimapElement.offsetWidth)
+          expect(canvas.offsetHeight / devicePixelRatio).toEqual(minimapElement.offsetHeight + minimap.getLineHeight())

       describe 'when the editor visible content is changed', ->
         beforeEach ->
@@ -500,7 +502,7 @@
         nextAnimationFrame()

       it 'adjusts the width of the minimap canvas', ->
-        expect(minimapElement.canvas.width).toEqual(4)
+        expect(minimapElement.canvas.width / devicePixelRatio).toEqual(4)

       it 'offsets the minimap by the difference', ->
         expect(realOffsetLeft(minimapElement)).toBeCloseTo(editorElement.clientWidth - 4, -1)
@@ -511,7 +513,7 @@
           sleep(150)
           runs ->
             nextAnimationFrame()
-            expect(minimapElement.canvas.width).toEqual(4)
+            expect(minimapElement.canvas.width / devicePixelRatio).toEqual(4)

       describe 'when the editor is resized', ->
         beforeEach ->


Add another example TODO

Add another example TODO

--- specs/examples/todo.specs.coffee
+++ specs/examples/todo.specs.coffee
@@ -34,5 +34,8 @@
 describe 'Example: View types dependent on model property', ->
   it 'should be implemented'

+describe 'Reusing same view with different models', ->
+  it 'should be implemented'
+
 describe 'Example: Accepting or canceling changes with intermediate models', ->
   it 'should be implemented'


Write spec for directive.

Write spec for directive.

--- spec/javascripts/app/modules/spinner_spec.js.coffee
+++ spec/javascripts/app/modules/spinner_spec.js.coffee
@@ -101,4 +101,20 @@
           expect($scope.showSpinner()).toBeFalsy()

   describe "directive", ->
-    # TODO
+    element = null
+
+    beforeEach inject ($compile, $rootScope) ->
+      $scope = $rootScope
+      $scope.showSpinner = -> false
+
+      element = angular.element """
+      <spinner></spinner>
+      """
+      $compile(element)($scope)
+      $scope.$digest()
+
+    it "renders the spinner", ->
+      img = element.find("img")
+
+      expect(img.length).toBe(1)
+      expect(img.attr("src")).toEqual("/assets/ajax-loader.gif")


Use isEOF

Use isEOF

--- lisp.coffee
+++ lisp.coffee
@@ -52,10 +52,7 @@
   expects: (pattern, throwing = false) ->
     valid = @code[@pos] && (pattern instanceof RegExp and pattern.test @code[@pos]) || pattern == @code[@pos...@pos + pattern.length]
     if !valid && throwing
-      current = if @pos < @code.length
-          @code[@pos]
-        else
-          'EOF'
+      current = if @isEOF() then 'EOF' else @code[@pos]
       throw "unexpected \"#{current}\", expects \"#{pattern}\""

     return valid


add tab indent/unindent to console input

--- scripts/console.coffee
+++ scripts/console.coffee
@@ -21,6 +21,7 @@
   obj

 class Console
+  KEY_TAB    =  9
   KEY_RETURN = 13
   KEY_LEFT   = 37
   KEY_UP     = 38
@@ -64,8 +65,12 @@
       switch event.which
         when KEY_UP   then @selectHistory +1; event.preventDefault()
         when KEY_DOWN then @selectHistory -1; event.preventDefault()
-    else
+    else if event.shiftKey
       switch event.which
+        when KEY_TAB then @indentInput(-1); event.preventDefault()
+    else
+      switch event.which
+        when KEY_TAB    then @indentInput(+1); event.preventDefault()
         when KEY_RETURN then @processInput(); event.preventDefault()

   processInput: ->
@@ -84,6 +89,45 @@
   clearInput: ->
     @input.val null
     return
+
+  getLineAtPosition: (index) ->
+    input = @input[0]
+
+    index ?= input.selectionStart
+    text = @input.val()
+
+    begin = text.lastIndexOf '\n', index - 1
+    end = text.indexOf '\n', index
+
+    if begin < 0 then begin  = 0 \
+                 else begin += 1
+    if end   < 0 then end    = text.length
+
+    { begin, end, text: text[begin...end] }
+
+  indentInput: (levels = 1) ->
+    input = @input[0]
+    begin = input.selectionStart
+    end =input.selectionEnd
+
+    line = @getLineAtPosition()
+    oldText = @input.val()
+    newText = oldText[0...line.begin]
+
+    if levels >= 0
+      newText += '  ' for i in [0...levels]
+      newText += line.text
+    else
+      newText += line.text.replace ///^ [ ]{2} {0, #{Math.abs levels} } ///, ''
+
+    newText += oldText[line.end...]
+
+    @input.val newText
+    input.setSelectionRange(
+      Math.max(line.begin, begin + levels)
+      Math.max(line.begin, end   + levels) )
+
+    newText

   autosizeInput: ->
     input = @input[0]


state-ify selection sort

state-ify selection sort

--- main.coffee
+++ main.coffee
@@ -111,14 +111,32 @@
 # Selection sort
 # @author Bernhard Häussner (https://github.com/bxt)
 ssort = ->
-        min = index-1
-        for j in [index...colours.length]
-                min = j if colours[j].val < colours[min].val
-
-        swapRects(index-1, min)
-
-        index++
-        defer(ssort) if index < colours.length
+        sort_context ?=
+                i: 1
+                j: 1
+                min: 0
+                count: 0
+
+        { i, j, count, min } = sort_context
+
+        if j == colours.length
+                swapRects(i-1, min)
+
+                return if i == colours.length - 1
+
+                sort_context.i++
+                sort_context.min = sort_context.i - 1
+                sort_context.j = sort_context.i
+        else
+                sort_context.min = j if colours[j].val < colours[min].val
+                sort_context.j++
+
+        sort_context.count++
+
+        if (sort_context.count % UPDATE_INTERVAL) == 0
+                defer(ssort)
+        else
+                ssort()

 bsort = ->
         swapped = false


spec: Remove grabage console.log

spec: Remove grabage console.log

--- spec/api-cookies-spec.coffee
+++ spec/api-cookies-spec.coffee
@@ -14,7 +14,6 @@

   it 'should get cookies', (done) ->
     server = http.createServer (req, res) ->
-      console.log req
       res.setHeader('Set-Cookie', ['type=dummy'])
       res.end('finished')
       server.close()


Debounce mousemove after marking idle

Debounce mousemove after marking idle

Workaround for Chrome bug 103041

--- scripts/players.coffee
+++ scripts/players.coffee
@@ -102,17 +102,23 @@
         volumeClick.addEventListener('mousemove', adjustVolumeProgress, false)
         volumeClick.addEventListener('mouseleave', endAdjustVolume, false)

+    idleDebounce = false
     idleUI = ->
+        idleDebounce = true
         controls.classList.add('idle')
         video.classList.add('idle')
     timeout = null
     idleEvent = (e) ->
+        if idleDebounce
+            idleDebounce = false
+            return false
         clearTimeout(timeout)
         controls.classList.remove('idle')
         video.classList.remove('idle')
+        return true
     video.addEventListener('mousemove', (e) ->
-        idleEvent(e)
-        timeout = setTimeout(idleUI, 3000)
+        if idleEvent(e)
+            timeout = setTimeout(idleUI, 3000)
     , false)
     controls.addEventListener('mousemove', idleEvent, false)


Spec for custom keys for member, score, rank and member_data

--- spec/leaderboard_spec.coffee
+++ spec/leaderboard_spec.coffee
@@ -26,6 +26,29 @@
     @leaderboard.pageSize.should.equal(Leaderboard.DEFAULT_PAGE_SIZE)

     done()
+
+  it 'should allow you to set custom keys for member, score, rank and member_data', (done) ->
+    options =
+      'pageSize': Leaderboard.DEFAULT_PAGE_SIZE
+      'reverse': false
+      'memberKey': 'member_custom'
+      'rankKey': 'rank_custom'
+      'scoreKey': 'score_custom'
+      'memberDataKey': 'member_data_custom'
+
+
+    @leaderboard = new Leaderboard('highscores', options)
+
+    for index in [0...Leaderboard.DEFAULT_PAGE_SIZE + 1]
+      @leaderboard.rankMember("member_#{index}", index, "member_data_#{index}", (reply) -> )
+
+    @leaderboard.leaders(1, {'with_member_data': true}, (reply) ->
+      reply.length.should.equal(25)
+      reply[0]['member_custom'].should.equal('member_25')
+      reply[0]['score_custom'].should.equal(25)
+      reply[0]['rank_custom'].should.equal(1)
+      reply[0]['member_data_custom'].should.equal('member_data_25')
+      done())

   it 'should allow you to disconnect the Redis connection', (done) ->
     @leaderboard.disconnect()


Make sure a person document is not created if e-mail is already in use. Fixes #320

--- server/accounts.coffee
+++ server/accounts.coffee
@@ -27,9 +27,15 @@

     throw new Meteor.Error 400, "Username must contain only a-zA-Z0-9_- characters." unless USERNAME_REGEX.test user.username

-    throw new Meteor.Error 400, 'Username conflicts with existing slug.' if user.username in FORBIDDEN_USERNAMES
+    throw new Meteor.Error 400, "Username already exists." if user.username in FORBIDDEN_USERNAMES

     throw new Meteor.Error 400, "Invalid e-mail address." unless user.username is 'admin' or EMAIL_REGEX.test user.emails?[0]?.address
+
+    throw new Meteor.Error 400, "Invalid e-mail address." if user.emails?.length > 1
+
+    # A race condition, but better than nothing. Otherwise it fails later on when creating an
+    # user document, but person document has already been created and is not cleaned up.
+    throw new Meteor.Error 400, "Email already exists." if user.username isnt 'admin' and User.documents.findOne 'emails.address': user.emails?[0]?.address

     user.person =
       _id: personId
@@ -37,6 +43,7 @@
     person =
       _id: personId
       user:
+        # TODO: This sometimes throw a warning because we are creating a link before user document is really created, all this code should run after user document is created
         _id: user._id
         username: user.username
       slug: Person.Meta.fields.slug.generator(_id: personId, user: user)[1]
@@ -51,7 +58,7 @@
       throw error
     # TODO: Improve when https://jira.mongodb.org/browse/SERVER-3069
     if /E11000 duplicate key error index:.*Persons\.\$slug/.test error.err
-      throw new Meteor.Error 400, "Username conflicts with existing slug."
+      throw new Meteor.Error 400, "Username already exists."
     throw error

   user


rtmp: Fix parse error when receiving pauseRaw()

Fix a bug where server crashes with "Unknown AMF0 data type" message
after receiving pauseRaw() from client.

--- rtmp.coffee
+++ rtmp.coffee
@@ -2265,6 +2265,7 @@
         @respondPause commandMessage, callback
       when 'pauseRaw'
         logger.debug "[rtmp] ignoring pauseRaw"
+        callback null
 #        @respondPauseRaw commandMessage, callback
       # Methods used for publishing from the client
       when 'seek'


fixtures email addresses fix

fixtures email addresses fix

--- server/fixtures.coffee
+++ server/fixtures.coffee
@@ -48,7 +48,7 @@
         image: "/fixtures/" + _id + ".jpg"
       emails: [
         {
-          address: _id + "@meetings.me"
+          address: _id.toLowerCase() + "@meetings.me"
           verified: true
         }
       ]


Add download link of Github

Add download link of Github

--- views/services/update.cjsx
+++ views/services/update.cjsx
@@ -11,6 +11,9 @@

 doUpdate = ->
   shell.openExternal 'http://0u0.moe/poi'
+
+doUpdateGithub = ->
+  shell.openExternal 'https://github.com/poooi/poi/releases'

 checkUpdate = ->
   updateManager.checkUpdate (info) ->
@@ -30,8 +33,13 @@
           style: 'success'
         },
         {
-          name: __ 'Download latest version'
+          name: __('Download latest version') + ' (Baidu)'
           func: doUpdate
+          style: 'primary'
+        },
+        {
+          name: __('Download latest version') + ' (Github)'
+          func: doUpdateGithub
           style: 'primary'
         }
       ]


Rename `gutterComponent` to `gutterContainerComponent`.

Signed-off-by: Xiong Nandi <0249fc0987e64d1ff338d0d81ceeb72ec9f62923@xiaomi.com>

--- lib/debugger-view.coffee
+++ lib/debugger-view.coffee
@@ -176,7 +176,10 @@

   hackGutterDblClick: (editor) ->
     component = atom.views.getView(editor).component
-    gutter = component.gutterComponent
+    # gutterComponent has been renamed to gutterContainerComponent
+    gutter  = component.gutterComponent
+    gutter ?= component.gutterContainerComponent
+
     gutter.domNode.addEventListener 'dblclick', (event) =>
       position = component.screenPositionForMouseEvent(event)
       line = editor.bufferPositionForScreenPosition(position).row
@@ -247,7 +250,8 @@

     for editor in atom.workspace.getTextEditors()
       component = atom.views.getView(editor).component
-      gutter = component.gutterComponent
+      gutter  = component.gutterComponent
+      gutter ?= component.gutterContainerComponent
       gutter.domNode.removeEventListener 'dblclick'

     @panel.destroy()


Be more defensive when setting text in an editor

--- lib/general-panel.coffee
+++ lib/general-panel.coffee
@@ -73,7 +73,7 @@
         @observeConfig name, (value) =>
           stringValue = @valueToString(value)
           return if stringValue == editor.getText()
-          value ?= ""
+          stringValue ?= ""
           editor.setText(stringValue)

         editor.getBuffer().on 'contents-modified', =>
@@ -95,7 +95,7 @@
       floatValue = parseFloat(value)
       value = floatValue unless isNaN(floatValue)
     else if type == 'array'
-      arrayValue = value.split(',')
+      arrayValue = (value or '').split(',')
       value = (val.trim() for val in arrayValue when val)

     value


Fix bug

Fix bug

--- views/components/ship/ship.cjsx
+++ views/components/ship/ship.cjsx
@@ -130,7 +130,7 @@
               continue if shipId == -1
               ship = _ships[shipId]
               if ship.api_nowhp / ship.api_maxhp < 0.250001 and !goback[shipId]
-                toggleModal '进击注意！', "Lv. #{ship.api_lv} - #{ship.api_name} 大破，可能会被击沉，请到舰队面板确认是否有应急修理要员/女神携带！"
+                toggleModal '进击注意！', "Lv. #{ship.api_lv} - #{ship.api_name} 大破，请到舰队面板确认是否有应急修理要员/女神携带！"
         else
           flag = false
       return unless flag


rtmp: Use env var for dump dir

rtmp: Use env var for dump dir

--- rtmp.coffee
+++ rtmp.coffee
@@ -6,6 +6,7 @@
 fs            = require 'fs'
 net           = require 'net'
 crypto        = require 'crypto'
+path          = require 'path'
 Sequent       = require 'sequent'
 qs            = require 'querystring'

@@ -59,12 +60,18 @@

 streams = { }

-class Stream
+class Stream
   constructor: (name, fileName) ->
     @name = name
-    dumpName = (fileName || "dump-") + @stream.name + ".stream_dump";
-    console.log "Dumping to " + dumpName
-    @dump = fs.createWriteStream( dumpName )
+
+  startPublish: () ->
+    timestamp = new Date().getTime()
+    dumpFileName = "rtmp-dump-" + @name + "-" + timestamp + ".stream_dump"
+    dumpFileRoot = process.env.CAM_STREAM_DUMP_ROOT_PATH || "/tmp/"
+    dumpFilePath = path.join(dumpFileRoot, dumpFileName)
+    console.log "Dumping to " + dumpFilePath
+    if @dump then @dump.end()
+    @dump = fs.createWriteStream(dumpFilePath)

 getStreamByName = (name) ->
   if (name == 'broadcast')
@@ -558,7 +565,7 @@
         console.log "metadata re-sent"

       for rtmpMessage in msgs
-        session.lastTimestamp[rtmpMessage.avType] = rtmpMessage.timestamp = session.offsetTimestamp[rtmpMessage.avType] + session.getScaledTimestamp(rtmpMessage.originalTimestamp) % TIMESTAMP_ROUNDOFF
+        session.lastTimestamp[rtmpMessage.avType] = rtmpMessage.timestamp = session.offsetTimestamp[rtmpMessage.avType] + session.getScaledTimestamp(rtmpMessage.originalTimestamp) % TIMESTAMP_ROUNDOFF

       if msgs.length > 1
         buf = createRTMPAggregateMessage msgs, session.chunkSize
@@ -774,11 +781,11 @@
   len = commandBuf.length + transactionIDBuf.length
   for obj in params.objects
     len += obj.length
-
+
   amf0Bytes = Buffer.concat [commandBuf, transactionIDBuf, params.objects...], len

   return createRTMPMessage
-    chunkStreamID: params.chunkStreamID
+    chunkStreamID: params.chunkStreamID
     timestamp: params.timestamp
     messageTypeID: 0x14  # AMF0 Command
     messageStreamID: params.messageStreamID
@@ -1046,7 +1053,7 @@
     app = commandMessage.objects[0].value.app
     app = app.replace /\/$/, ''  # JW Player adds / at the end
     app = app.replace /\?.*/, '' # get rid of query string
-
+
     if app isnt config.rtmpApplicationName
       console.warn "[rtmp] requested invalid app name: #{app}"
       @rejectConnect commandMessage, callback
@@ -1110,7 +1117,7 @@
         })
       ]

-
+
     onBWDone = createAMF0CommandMessage
       chunkStreamID: 3
       timestamp: 0
@@ -1455,11 +1462,11 @@
     if publishingType isnt 'live'
       console.log "[rtmp] warn: publishing type other than 'live' is not supported: #{publishingType}; using 'live'"
     console.log "[rtmp] publish: stream=#{publishingName} publishingType=#{publishingType}"
-
+
     match = /^([^?]*)\??(.*)/.exec publishingName
     # parse query string from publishingName
     streamName = match[1]
-
+
     @stream = getStreamByName(streamName)
     @stream.query = qs.parse(match[2]);

@@ -1486,6 +1493,7 @@
       ]
     , @chunkSize

+    @stream.startPublish()
     @emit "stream_published", @stream

     callback null, publishStart
@@ -1599,9 +1607,9 @@
       callback null, @concatenate [
         setChunkSize, streamBegin1, playReset,
         playStart,
-        rtmpSampleAccess, metadata
-      ]
-
+        rtmpSampleAccess, metadata
+      ]
+
     # ready for playing
     @isWaitingForKeyFrame = true

@@ -1943,7 +1951,7 @@
               if audioData.adtsFrame?

                 fileHeader = new Buffer(4 * 4)
-
+
                 fileHeader.writeUInt32LE(8, 0)
                 fileHeader.writeUInt32LE(rtmpMessage.timestamp, 4)
                 fileHeader.writeUInt32LE(audioData.adtsFrame.length, 8)
@@ -1963,7 +1971,7 @@
               if videoData.nalUnitGlob?

                 fileHeader = new Buffer(4 * 4)
-
+
                 fileHeader.writeUInt32LE(9, 0)
                 fileHeader.writeUInt32LE(rtmpMessage.timestamp, 4)
                 fileHeader.writeUInt32LE(videoData.nalUnitGlob.length, 8)
@@ -2035,23 +2043,23 @@
     for clientID, session of sessions
       if session.stream == stream
         stream_ts = session.lastTimestamp
-
+
     console.log "setBroadcastStream", stream_ts
     @emit 'broadcast_actual_changed', name
     for clientID, session of sessions
       if session.streamName == "broadcast" && session.stream != stream
         session.offsetTimestamp[avType] = session.lastTimestamp[avType] - stream_ts[avType]
-
+
         if (avType == 'audio')
           session.audio_stream = stream
-
+
         if (avType == 'video')
           session.stream = stream
           session.isWaitingForKeyFrame = true
-
+
         session.stream_switched = true
         console.log "switched stream", avType, session.offsetTimestamp[avType]
-
+

   on: (event, listener) ->
     if @eventListeners[event]?
@@ -2230,7 +2238,7 @@
     stream.ppsPacket = null
     stream.isAudioStarted = false
     stream.isVideoStarted = false
-    stream.params =
+    stream.params =
       videoBitrateKbps: config.videoBitrateKbps
       videoFrameRate: config.videoFrameRate
       videoHeight: null
@@ -2246,7 +2254,7 @@
         avcprofile: config.flv_params.avcprofile
         videocodecid: config.flv_params.videocodecid
         audiocodecid: config.flv_params.audiocodecid
-
+
     # delete streams[stream]
     # spsPacket = null
     # ppsPacket = null
@@ -2485,4 +2493,3 @@

 module.exports = RTMPServer
 module.exports.streams = streams
-


:shirt:

:shirt:

--- lib/install-panel.coffee
+++ lib/install-panel.coffee
@@ -84,7 +84,6 @@
         @loadFeaturedPackages()
         @performSearch()

-
     @searchThemesButton.on 'click', =>
       unless @searchThemesButton.hasClass('selected')
         @searchType = 'themes'
@@ -126,7 +125,7 @@
       packageRow.append(new PackageCard(pack, @packageManager, back: 'Install'))

   filterPackages: (packages, themes) ->
-    packages.filter ({theme}) =>
+    packages.filter ({theme}) ->
       if themes
         theme
       else


workers: if somehow we fail with -1 id, fix it on the fly

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -413,7 +413,7 @@
     if @type is 'unregistered'
       return callback null, -1

-    return callback null, @socialApiId  if @socialApiId
+    return callback null, @socialApiId  if @socialApiId? and @socialApiId > 0
     { createAccount } = require './socialapi/requests'
     createAccount { id: @getId(), nickname: @profile.nickname }, (err, account) =>
       return callback new ApiError err  if err


Popup: Added support for open and close callbacks in options hash, added 'Reattach' mode as an alternative for 'Detach', hides closed popup contents and attaches them to page body to keep them loaded.

--- lib/Popup.coffee
+++ lib/Popup.coffee
@@ -30,7 +30,12 @@
         if @_view
           @_view.release()
           @_view = null
-        $(this).children().detach() if @_detach
+
+        if @_detach
+          $(this).children().detach()
+        else if @_reattach
+          $(document.body).append $(this).children().hide()
+
         $(this).remove()

   nextId: ->
@@ -42,6 +47,7 @@

     @_popupID = id
     @_detach = options.detach
+    @_reattach = options.reattach

     offset = element.offset()

@@ -103,13 +109,20 @@
   id = Popup.nextId()
   @mousedown (e) -> e.stopPropagation()

+  element = null
+
+  options = $.extend({}, options);
+  options._close = options.close
+  options.close = ->
+    close.call element, element if close
+    options._close.call element, element if options._close
+
   @click (e) ->
     e.preventDefault()
     element = this
-    options = $.extend({}, options);
-    options.close = -> close.call element if close
     if Popup._popupID == id
       Popup.close()
     else
       Popup.show $(this), id, (if items.call then items.call(element, e.altKey || e.shiftKey) else items), options
-      open.call element, element if open+      open.call element, element if open
+      options.open.call element, element if options.open


Attempt to fix #69 by setting cookie path, based on https://github.com/vsivsi/meteor-file-collection/pull/105

--- lib/files.coffee
+++ lib/files.coffee
@@ -56,7 +56,8 @@
 else
   Tracker.autorun ->
     Meteor.subscribe 'files', Meteor.userId()
-    $.cookie 'X-Auth-Token', Accounts._storedLoginToken()
+    $.cookie 'X-Auth-Token', Accounts._storedLoginToken(),
+      path: '/'

   Session.set 'uploading', {}
   updateUploading = (changer) =>


handle log upload fail

handle log upload fail

--- lib/index.coffee
+++ lib/index.coffee
@@ -67,6 +67,10 @@
  * Function to upload log file to server
 ###
 uploadLog = ->
+  onError = ->
+    $('#upload_log').attr('class','ui inverted red button')
+    $('#upload_log').text('Failed')
+
   log_server = 'http://clogger.dustinblackman.com'
   log_server = 'http://127.0.0.1:8080' if window.devEnabled
   fs.readFile error_log, 'utf8', (err, data) ->
@@ -74,14 +78,15 @@
     $('#upload_log').attr('class','ui inverted yellow button')
     $('#upload_log').text('Sending...')

-    # TODO Do something with error reading log file.
     if !err
-      $.post log_server + '/submit', data, (res) ->
-        $('#upload_log').attr('class', 'ui green button')
-        $('#upload_log').text('Sent!')
+      $.post(log_server + '/submit', data)
+        .done ->
+          $('#upload_log').attr('class', 'ui green button')
+          $('#upload_log').text('Sent!')
+        .fail ->
+          onError()
     else
-      $('#upload_log').attr('class','ui inverted red button')
-      $('#upload_log').text('Failed')
+      onError()

 ###*


SocialWorker: Tests: set random group slug if createGroup is truthy

--- workers/social/testhelper/index.coffee
+++ workers/social/testhelper/index.coffee
@@ -80,9 +80,14 @@

   [opts, callback] = [callback, opts]  unless callback
   opts ?= {}
-  { context, userFormData } = opts
-
-  context      ?= { group : 'koding' }
+  { context, userFormData, createGroup } = opts
+
+  # if createGroup parameter is truthy generate random group slug
+  groupSlug = if createGroup
+  then generateRandomString()
+  else 'koding'
+
+  context      ?= { group : groupSlug }
   userFormData ?= generateDummyUserFormData()

   withDummyClient context, ({ client }) ->


Tests: update default login info

Tests: update default login info

--- workers/social/testhelper/index.coffee
+++ workers/social/testhelper/index.coffee
@@ -150,8 +150,8 @@

   credentials =
     tfcode              : ''
-    username            : 'devrim'
-    password            : 'devrim'
+    username            : 'admin'
+    password            : 'admin'
     groupName           : 'koding'
     invitationToken     : undefined
     groupIsBeingCreated : no


fixed username bug

fixed username bug

--- lib/clientTransformManager.coffee
+++ lib/clientTransformManager.coffee
@@ -19,6 +19,7 @@
     username:     undefined
     color:        undefined

+    # Registers callbacks for events
     constructor: (@host, @username, @color) ->
       @net = new netClient @host

@@ -29,12 +30,15 @@
       @net.on types.end,              @handleEnd
       @net.on types.serverDown,       @handleServerDown

+    # Logically register as sharing client
     connectedToServer: =>
       @sendToServer
         type:     types.connecting
         username: @username
         color:    @color

+    # Send a message to the server and manage the state.
+    # If the connection is initialized, there is no state to update.
     sendToServer: (transform) =>
       @state.inc @id if @state? and transform.type isnt types.connect
       transform.id = @id
@@ -68,8 +72,13 @@
     handleEnd: =>
       @emit 'end'

-    handleServerDown: ->
+    handleServerDown: =>
+      @emit 'error', 'Server down'

+    # The server has send information for initialization.
+    # The state and the client id have been send and will be set.
+    # For each client already sharing a new cursor is created.
+    # Emit an event for signaling the connection is established.
     initialize: (transform) =>
       @emit types.clear
       @id       = transform.clientId
@@ -84,6 +93,7 @@
       initialized = true
       @emit types.initialized

+    # Manage acknowledges
     acknowledge: (ack) =>
       for i in [0...@notAcked.length]
         if @notAcked[i].state[@id] is ack[@id]
@@ -105,24 +115,30 @@
     selectionChanged: (transform) ->
       @sendToServer transform

+    # An textChange event has been sent by the server.
+    # The text needs to be changed now.
     changeText: (transform) ->
       @state.set transform.id, transform.state.get transform.id
       @emit types.textChange, transform

+    # Update the cursor of a client.
     updateCursor: (transform) ->
       @emit types.updateCursor, transform

+    # Update the selection of a client.
     changeSelection: (transform) ->
       @emit types.selectionChanged, transform

+    # A new client has connected
     newClient: (transform) ->
       @clients.push transform.client
       @state.add transform.client.id
       @emit types.newUser,
         clientId:   transform.client.id
-        username:   transform.username
+        username:   transform.client.username
         color:      transform.client.color

+    # A client left the session
     clientLeft: (transform) ->
       @emit types.userLeft,
         clientId: transform.client.id


something is using these

something is using these

--- app/assets/javascripts/main.coffee
+++ app/assets/javascripts/main.coffee
@@ -102,5 +102,5 @@

 slackReadyFunction(redraw)
 #TS.pri = "888"
-#window.$div = TS.client.ui.$msgs_div
-#window.$scroller_div = TS.client.ui.$msgs_scroller_div
+window.$div = TS.client.ui.$msgs_div
+window.$scroller_div = TS.client.ui.$msgs_scroller_div


BF-147 Extending Route to work with other planes

--- app/assets/javascripts/view.coffee
+++ app/assets/javascripts/view.coffee
@@ -372,23 +372,30 @@
   setActivePlane : (planeID) ->
     cam2d.setActivePlane planeID
     for i in [0..2]
-      $("canvas")[i].style.borderColor = if i==planeID then "#888800" else "#C7D1D8"
+      $("canvas")[i].style.borderColor = if i==planeID then "#f8f800" else "#C7D1D8"
     cam2d.hasChanged = true

   setWaypoint : (position) ->
     curGlobalPos = cam2d.getGlobalPos()
-    curZoomStep = cam2d.getZoomStep(cam2d.getActivePlane()) + 1
+    activePlane  = cam2d.getActivePlane()
+    zoomFactor   = cam2d.getPlaneScalingFactor activePlane
     # calculate the global position of the rightclick
-    position = [curGlobalPos[0] - 192/curZoomStep + position[0]/curZoomStep, curGlobalPos[1] - 192/curZoomStep + position[1]/curZoomStep, curGlobalPos[2]]
+    switch activePlane
+      when PLANE_XY then position = [curGlobalPos[0] - (VIEWPORT_WIDTH/2 - position[0])*zoomFactor, curGlobalPos[1] - (VIEWPORT_WIDTH/2 - position[1])*zoomFactor, curGlobalPos[2]]
+      when PLANE_YZ then position = [curGlobalPos[0], curGlobalPos[1] - (VIEWPORT_WIDTH/2 - position[1])*zoomFactor, curGlobalPos[2] - (VIEWPORT_WIDTH/2 - position[0])*zoomFactor]
+      when PLANE_XZ then position = [curGlobalPos[0] - (VIEWPORT_WIDTH/2 - position[0])*zoomFactor, curGlobalPos[1], curGlobalPos[2] - (VIEWPORT_WIDTH/2 - position[1])*zoomFactor]
     unless @curIndex
       @curIndex = 0
     # Translating ThreeJS' coordinate system to the preview's one
     if @curIndex < @maxRouteLen
       @route.geometry.vertices[@curIndex] = new THREE.Vector3(position[0], Game.dataSet.upperBoundary[1] - position[2], position[1])
-      @routeView.geometry.vertices[@curIndex] = new THREE.Vector3(position[0], -position[1], -position[2])
+      pos = [[position[0], -position[1], -position[2]], [position[2], -position[1], position[0]], [position[0], -position[2], position[1]]]
+      for i in [0..2]
+        @routeView[i].geometry.vertices[@curIndex] = new THREE.Vector3(pos[i][0], pos[i][1], pos[i][2])
+        @routeView[i].geometry.verticesNeedUpdate = true
       @particleSystem.geometry.vertices[@curIndex] = new THREE.Vector3(position[0], Game.dataSet.upperBoundary[1] - position[2], position[1])
+
       @route.geometry.verticesNeedUpdate = true
-      @routeView.geometry.verticesNeedUpdate = true
       @particleSystem.geometry.verticesNeedUpdate = true
       @curIndex += 1
       cam2d.hasChanged = true
@@ -396,15 +403,11 @@
   onPreviewClick : (position) ->
     vector = new THREE.Vector3((position[0] / 384 ) * 2 - 1, - (position[1] / 384) * 2 + 1, 0.5)
     projector = new THREE.Projector()
-    console.log vector.x, vector.y, vector.z
     projector.unprojectVector(vector, @camera[VIEW_3D])

     ray = new THREE.Ray(@camera[VIEW_3D].position, vector.subSelf(@camera[VIEW_3D].position).normalize())

-    console.log @camera[VIEW_3D].position
-
     intersects = ray.intersectObjects(@particles)
-    console.log vector, intersects.length

     if (intersects.length > 0)
       intersects[0].object.material.color.setHex(Math.random() * 0xffffff)
@@ -414,20 +417,25 @@
     # create route to show in previewBox and pre-allocate buffer
     @maxRouteLen = maxRouteLen
     routeGeometry = new THREE.Geometry()
-    routeGeometryView = new THREE.Geometry()
+    routeGeometryView = new Array(3)
+    for i in [0..2]
+       routeGeometryView[i] = new THREE.Geometry()
     particles = new THREE.Geometry()
     i = 0
     while i < maxRouteLen
       # workaround to hide the unused vertices
       routeGeometry.vertices.push(new THREE.Vector2(0, 0))
-      routeGeometryView.vertices.push(new THREE.Vector2(0, 0))
+      for g in routeGeometryView
+        g.vertices.push(new THREE.Vector2(0, 0))
       particles.vertices.push(new THREE.Vector2(0, 0))
       i += 1

     routeGeometry.dynamic = true
     routeGeometryView.dynamic = true
     route = new THREE.Line(routeGeometry, new THREE.LineBasicMaterial({color: 0xff0000, linewidth: 1}))
-    routeView = new THREE.Line(routeGeometryView, new THREE.LineBasicMaterial({color: 0xff0000, linewidth: 1}))
+    routeView = new Array(3)
+    for i in [PLANE_XY, PLANE_YZ, PLANE_XZ]
+      routeView[i] = new THREE.Line(routeGeometryView[i], new THREE.LineBasicMaterial({color: 0xff0000, linewidth: 1}))
     particleSystem = new THREE.ParticleSystem(particles, new THREE.ParticleBasicMaterial({color: 0xff0000, size: 3, sizeAttenuation: false}))
     @route = route
     @routeView = routeView
@@ -435,7 +443,9 @@

     # Initializing Position
     gPos = cam2d.getGlobalPos()
-    @routeView.position = new THREE.Vector3(-gPos[0], gPos[1], gPos[2]+1)
+    pos = [[-gPos[0], gPos[1], gPos[2]], [-gPos[2], gPos[1], -gPos[0]], [-gPos[0], gPos[2], -gPos[1]]]
+    for i in [0..2]
+      @routeView[i].position = new THREE.Vector3(pos[i][0], pos[i][1], pos[i][2]+1)

     particle = new THREE.Mesh(new THREE.CubeGeometry(300, 300, 300, 10, 10, 10), new THREE.MeshBasicMaterial({color: 0xff0000}))
     particle.position.x = 0
@@ -447,14 +457,15 @@

     @addGeometry VIEW_3D, route
     @addGeometry VIEW_3D, particleSystem
-    @addGeometry PLANE_XY, routeView
+    for i in [0..2]
+      @addGeometry i, routeView[i]

   updateRoute : ->
     gPos                = cam2d.getGlobalPos()
-    scale               = cam2d.getPlaneScalingFactor PLANE_XY
-    console.log "scale (alt): " + @routeView.scale
-    console.log "scale: " + scale
-    @routeView.scale    = new THREE.Vector3(1/scale, 1/scale, 1/scale)
-    @routeView.position = new THREE.Vector3(-gPos[0]/scale, gPos[1]/scale, gPos[2]/scale+1)
-    #@routeView.position = new THREE.Vector3(@routeView.position.x - p[0], @routeView.position.y + p[1], cam2d.getGlobalPos()[2]+1)
-    @routeView.geometry.verticesNeedUpdate = true
+    scale               = [cam2d.getPlaneScalingFactor(PLANE_XY), cam2d.getPlaneScalingFactor(PLANE_YZ), cam2d.getPlaneScalingFactor(PLANE_XZ)]
+
+    pos = [[-gPos[0], gPos[1], gPos[2]], [-gPos[2], gPos[1], -gPos[0]], [-gPos[0], gPos[2], -gPos[1]]]
+    for i in [0..2]
+      @routeView[i].scale    = new THREE.Vector3(1/scale[i], 1/scale[i], 1/scale[i])
+      @routeView[i].position = new THREE.Vector3(pos[i][0]/scale[i], pos[i][1]/scale[i], pos[i][2]/scale[i]+1)
+      @routeView[i].geometry.verticesNeedUpdate = true


Updating the cat allocations url

Updating the cat allocations url

--- src/scripts/elasticsearch.coffee
+++ src/scripts/elasticsearch.coffee
@@ -5,6 +5,7 @@
 #   hubot: elasticsearch cluster health [cluster]           - Gets the cluster health for the given server or alias
 #   hubot: elasticsearch cat nodes [cluster]                - Gets the information from the cat nodes endpoint for the given server or alias
 #   hubot: elasticsearch cat indexes [cluster]              - Gets the information from the cat indexes endpoint for the given server or alias
+#   hubot: elasticsearch cat allocation [cluster]           - Gets the information from the cat allocation endpoint for the given server or alias
 #   hubot: elasticsearch clear cache [cluster]              - Clears the cache for the specified cluster
 #   hubot: elasticsearch cluster settings [cluster]         - Gets a list of all of the settings stored for the cluster
 #   hubot: elasticsearch index settings [cluster] [index]   - Gets a list of all of the settings stored for a particular index
@@ -75,7 +76,7 @@
       msg.send("Do not recognise the cluster alias: #{alias}")
     else
       msg.send("Getting the cat allocation for the cluster: #{cluster_url}")
-      msg.http("#{cluster_url}/_cat/allocation/?v")
+      msg.http("#{cluster_url}/_cat/allocation/?h=disk.percent,node,shards,disk.used,disk.avail")
         .get() (err, res, body) ->
           lines = body.split("\n")
           header = lines.shift()


Make draft mode regex global

Make draft mode regex global

--- app/coffee/DraftModeManager.coffee
+++ app/coffee/DraftModeManager.coffee
@@ -16,6 +16,6 @@
 	_injectDraftOption: (content) ->
 		content
 			# With existing options (must be first, otherwise both are applied)
-			.replace(/\\documentclass\[/, "\\documentclass[draft,")
+			.replace(/\\documentclass\[/g, "\\documentclass[draft,")
 			# Without existing options
-			.replace(/\\documentclass\{/, "\\documentclass[draft]{")+			.replace(/\\documentclass\{/g, "\\documentclass[draft]{")


change the method for assuming s3

change the method for assuming s3

--- app/coffee/PersistorManager.coffee
+++ app/coffee/PersistorManager.coffee
@@ -2,9 +2,13 @@
 logger = require("logger-sharelatex")
 S3PersistorManager = require("./S3PersistorManager")

+# assume s3 if none specified
+settings.filestoreBackend ||= "s3"
+
+
 logger.log backend:settings.filestoreBackend, "Loading backend"
 module.exports = switch settings.filestoreBackend
-	when "s3",null
+	when "s3"
 		S3PersistorManager
 	else
 		throw new Error( "Unknown filestore backend: #{settings.filestoreBackend}" )


Restore new video element

Restore new video element

Integrate new video controls

--- app/components/subject-viewer.cjsx
+++ app/components/subject-viewer.cjsx
@@ -53,13 +53,15 @@
     playbackRate: 1
     frameDimensions: {}
     inFlipbookMode: @props.allowFlipbook
-    playbackRate: 1
+    videoStates: []

   componentDidMount: ->
-    @refs.videoScrubber?.value = 0
+    for frame of @props.subject.locations
+      @refs['videoScrubber'+frame]?.value = 0

   componentDidUpdate: ->
-    @refs.videoPlayer?.playbackRate = @state.playbackRate
+    for frame of @props.subject.locations
+      @refs['videoPlayer'+frame].playbackRate = @state.videoStates[frame]?.playbackRate || 1

   willReceiveProps: (nextProps) ->
     # The default state for subjects is flipbook if allowed
@@ -88,10 +90,11 @@

     tools = switch type
       when 'image'
-        if @props.allowFlipbook and @props.allowSeparateFrames
-          <button className="flipbook-toggle" onClick={@toggleInFlipbookMode}>
-
-          </button>
+        if not @state.inFlipbookMode or @props.subject?.locations.length < 2 or subjectHasMixedLocationTypes @props.subject
+          if @props.allowFlipbook and @props.allowSeparateFrames
+            <button className="flipbook-toggle" onClick={@toggleInFlipbookMode}>
+
+            </button>
         else

             {if @props.allowFlipbook and @props.allowSeparateFrames
@@ -99,6 +102,9 @@

               </button>}

+            {if not @state.inFlipbookMode or @props.subject?.locations.length < 2 or subjectHasMixedLocationTypes @props.subject
+              null
+            else

                 {if @state.playing
                   <button type="button" className="secret-button" onClick={@setPlaying.bind this, false}>
@@ -108,8 +114,8 @@
                   <button type="button" className="secret-button" onClick={@setPlaying.bind this, true}>

                   </button>}
-
-
+              }
+
       when 'video'

@@ -179,9 +185,36 @@
       when 'image'

       when 'video'
-        <video ref="videoPlayer" src={src} type={"#{type}/#{format}"} onCanPlayThrough={@handleLoad} onEnded={@endVideo} onTimeUpdate={@updateScrubber}>
-          Your browser does not support the video format. Please upgrade your browser.
-        </video>
+

+          <video ref={'videoPlayer'+frame} src={src} type={"#{type}/#{format}"} onCanPlayThrough={@handleLoad} onEnded={@endVideo.bind this, frame} onTimeUpdate={@updateScrubber.bind this, frame}>
+            Your browser does not support the video format. Please upgrade your browser.
+          </video>
+
+
+              {if @state.videoStates[frame]?.playing
+                <button type="button" className="secret-button" aria-label="Pause" onClick={@playVideo.bind this, frame, false}>
+
+                </button>
+              else
+                <button type="button" className="secret-button" aria-label="Play" onClick={@playVideo.bind this, frame, true}>
+
+                </button>}
+
+            <input type="range" className="video-scrubber" ref={'videoScrubber'+frame} min="0" step="any" onChange={@seekVideo.bind this, frame} />
+
+            Speed:
+              {for rate, i in [0.25, 0.5, 1]
+                checked = rate == @state.videoStates[frame]?.playbackRate or (not @state.videoStates[frame]?.playbackRate and rate is 1)
+
+                  <input type="radio" name={'playbackRate'+frame} value={rate} checked={checked} onChange={(e) => @setPlayRate e, frame } />
+
+                    {rate}×
+
+
+              }
+
+
+

     if FrameWrapper
       <FrameWrapper frame={frame} naturalWidth={@state.frameDimensions[src]?.width} naturalHeight={@state.frameDimensions[src]?.height} workflow={@props.workflow} subject={@props.subject} classification={@props.classification} annotation={@props.annotation}>
@@ -212,8 +245,7 @@
   setInFlipbookMode: (inFlipbookMode) ->
     @setState {inFlipbookMode}

-  setPlaying: (playing) ->
-    @setState {playing}
+  setPlaying: (frame, playing) ->
     if playing
       @nextFrame()
       @_playingInterval = setInterval @nextFrame, @props.playFrameDuration
@@ -247,32 +279,40 @@

-  playVideo: (playing) ->
-    player = @refs.videoPlayer
+  playVideo: (frame, playing) ->
+    player = @refs['videoPlayer'+frame]
     return unless player?
-    @setState {playing}
+    videoStates = @state.videoStates
+    videoStates[frame] = videoStates[frame] || {}
+    videoStates[frame].playing = playing
+    @setState {videoStates}
     if playing
       player.play()
     else
       player.pause()

-  setPlayRate: (e) ->
-    playbackRate = parseFloat e.currentTarget.value
-    @setState {playbackRate}
-
-  seekVideo: (e) ->
-    player = @refs.videoPlayer
-    scrubber = @refs.videoScrubber
+  setPlayRate: (e, frame) ->
+    # Yeah, updating arrays in @state is hard... this method is potentially open to race-conditions but currently in practice it's not a problem
+    videoStates = @state.videoStates
+    videoStates[frame] = videoStates[frame] || {}
+    videoStates[frame].playbackRate = parseFloat e.currentTarget.value
+    @setState {videoStates}
+
+  seekVideo: (frame) ->
+    player = @refs['videoPlayer'+frame]
+    scrubber = @refs['videoScrubber'+frame]
     time = scrubber.value
     player.currentTime = time

-  endVideo: (e) ->
-    playing = false
-    @setState {playing}
-
-  updateScrubber: (e) ->
-    player = @refs.videoPlayer
-    scrubber = @refs.videoScrubber
+  endVideo: (frame) ->
+    videoStates = @state.videoStates
+    videoStates[frame] = videoStates[frame] || {}
+    videoStates[frame].playing = false
+    @setState {videoStates}
+
+  updateScrubber: (frame) ->
+    player = @refs['videoPlayer'+frame]
+    scrubber = @refs['videoScrubber'+frame]
     scrubber.setAttribute 'max', player.duration unless scrubber.getAttribute 'max'
     scrubber.value = player.currentTime


refactor: revert back the changes

refactor: revert back the changes

--- app/components/team-details.coffee
+++ app/components/team-details.coffee
@@ -38,8 +38,8 @@
         identification: teamMember
       @get("ajax").post url, data: data
       .then (data)->
-        that.store.pushPayload data
         if data?.data?.type is "team"
+          that.store.pushPayload data
           that.get("notify").success tTeamMemberAdded
         else
           that.get("notify").success tTeamMemberInvited


fixed elias's weird infinite retries bug

--- lib/runnable/sync.coffee
+++ lib/runnable/sync.coffee
@@ -36,18 +36,26 @@
         serverPath = localPath.replace conf.local, ""
         simPath = "#{conf.remote}/#{serverPath}"

+        MAX_RETRIES = 3
+
         time = process.hrtime()
         if tryCount > 0
             console.log "retrying file " + localPath + ", " +
                 tryCount + " attempt" + (if tryCount > 1 then "s" else "")
+
+            if tryCount > MAX_RETRIES
+                die()
+
         uploader.uploadFileAPI conf.domain, localPath, simPath, token, (err, stdout, stderr) ->
             diff = process.hrtime time
             formattedDiff = ((diff[0] * 1e9 + diff[1]) / 1000000).toFixed(0)

             try
+                #Server sometimes doesn't return json for no reason, ignore and try again
                 response = JSON.parse stdout
             catch
                 return upload localPath, stats, tryCount + 1
+
             if +response.status_code is 201
                 console.log serverPath, (color "\u2192", "cyan"), "#{simPath}", "   #{formattedDiff}ms"

@@ -56,13 +64,10 @@
                 authenicateUser (newtoken)->
                     token = newtoken
                     upload localPath, stats, tryCount + 1
-
             else if response.message
-                console.error (color "#{response.status_code}:", "red"), response.message
-                upload localPath, stats, tryCount + 1
+                console.error (color "#{response.status_code}:", "red"), response.message, localPath
             else
                 console.error (color err, "red"), stderr, stdout
-                upload localPath, stats, tryCount + 1

     watcher.on "change", upload


test #23

test #23

--- lib/touch-gesture.coffee
+++ lib/touch-gesture.coffee
@@ -25,7 +25,7 @@
       console.log "move"
       console.log e.diff.left
   touchEnd: (e) ->
-    @threeActive=off if e.touches.length==2
+    @threeActive=off if e.touches.length==0

   addEvent: (name,callBack) ->


make free project banner clearer

make free project banner clearer

--- src/smc-webapp/project_page.cjsx
+++ src/smc-webapp/project_page.cjsx
@@ -252,6 +252,8 @@
             paddingRight : 7
             marginBottom : 0
             fontSize     : "#{font_size}pt"
+            color        : 'white'
+            background   : 'red'

         ###
         if @props.free_compute_slowdown? and @props.free_compute_slowdown > 0.0
@@ -264,7 +266,7 @@
         <Alert bsStyle='danger' style={styles}>
             <Icon name='exclamation-triangle' style={float:'right', marginTop: '3px'}/>
             <Icon name='exclamation-triangle' /> Upgrade this project, since it is {on a free server if host} {without internet access if internet}. —
-            @actions(project_id: @props.project_id).show_extra_free_warning()} style={cursor:'pointer'}> learn more...
+            @actions(project_id: @props.project_id).show_extra_free_warning()} style={color:'white', cursor:'pointer'}> learn more...
             {@render_dismiss()}
             {@extra(host, internet)}
         </Alert>


fix #3879 -- don't crash rendering due to an issue generating a tooltip.

--- src/smc-webapp/r_misc/index.cjsx
+++ src/smc-webapp/r_misc/index.cjsx
@@ -316,15 +316,22 @@

     render_timeago: (d) ->
         if @props.popover
-            s = d.toLocaleString()
-            <Tip title={s} tip={@props.tip} id={s} placement={@props.placement}>
+            try
+                s = d.toLocaleString()
+            catch err
+                s = "#{err}"
+            <Tip title={s} tip={@props.tip} placement={@props.placement}>
                 {@render_timeago_element(d)}
             </Tip>
         else
             @render_timeago_element(d)

     render_absolute: (d) ->
-        {d.toLocaleString()}
+        try
+            s = d.toLocaleString()
+        catch err
+            s = "#{err}"
+        {s}

     render: ->
         d = if misc.is_date(@props.date) then @props.date else new Date(@props.date)


added in help text for the discipline tags

added in help text for the discipline tags

--- app/pages/lab/project-details.cjsx
+++ app/pages/lab/project-details.cjsx
@@ -195,6 +195,7 @@
               options={DISCIPLINES}
               multi={true}
               onChange={@handleDisciplineTagChange} />
+              Select one or more discipline tags that best describe your project. These tags will be used to select the categories your project will appear under on the main Zooniverse projects page, if your project becomes a full Zooniverse project.

               </AutoSave>
               <AutoSave resource={@props.project}>
@@ -202,7 +203,7 @@

               <TagSearch name="tags" multi={true} value={@state.otherTagList} onChange={@handleOtherTagChange} />
             </AutoSave>
-            Enter a list of tags separated by commas to help users find your project.
+            Enter a list of additional tags to describe your project separated by commas to help users find your project.


Added Familiar, Moneylender, Ironworks. Fixed cost of Workshop (changed from 4 to 3). Relocated Mountebank to correct alphabetical spot.

--- cards.coffee
+++ cards.coffee
@@ -536,6 +536,17 @@
     vp
 }

+makeCard 'Familiar', action, {
+  cost: 3
+  costPotion: 1
+  actions: +1
+  cards: +1
+  isAttack: true
+  playEffect: (state) ->
+    state.attackOpponents (opp) ->
+      state.gainCard(opp, c.Curse)
+}
+
 makeCard "Followers", action, {
   cost: 0
   isAttack: true
@@ -614,6 +625,25 @@
       transferCard(c['Horse Traders'], player.hand, player.duration)
 }

+makeCard 'Ironworks', action, {
+  cost: 4
+  playEffect: (state) ->
+    choices = []
+    for cardName of state.supply
+      card = c[cardName]
+      [coins, potions] = card.getCost(state)
+      if potions == 0 and coins <= 4
+        choices.push(card)
+    gained = state.gainOneOf(state.current, choices)
+
+    if gained.isAction
+      state.current.actions += 1
+    if gained.isTreasure
+      state.current.coins += 1
+    if gained.isVictory
+      state.current.drawCards(1)
+}
+
 makeCard "Menagerie", action, {
   cost: 3
   actions: 1
@@ -635,20 +665,6 @@
       state.attackOpponents (opp) ->
         if opp.hand.length > 3
           state.requireDiscard(opp, opp.hand.length - 3)
-}
-
-makeCard "Mountebank", action, {
-  cost: 5
-  coins: 2
-  isAttack: true
-  playEffect: (state) ->
-    state.attackOpponents (opp) ->
-      if c.Curse in opp.hand
-        # Discarding a Curse against Mountebank is automatic.
-        opp.doDiscard(c.Curse)
-      else
-        state.gainCard(opp, c.Copper)
-        state.gainCard(opp, c.Curse)
 }

 makeCard "Goons", c.Militia, {
@@ -672,12 +688,35 @@
     (player) -> player.moatProtected = true
 }

+makeCard 'Moneylender', action {
+  cost: 4
+
+  playEffect: (state) ->
+    if c.Copper in state.current.hand
+      state.current.doTrash(c.Copper)
+      state.current.coins += 3
+}
+
 makeCard "Monument", action, {
   cost: 4
   coins: 2
   playEffect:
     (state) ->
       state.current.chips += 1
+}
+
+makeCard "Mountebank", action, {
+  cost: 5
+  coins: 2
+  isAttack: true
+  playEffect: (state) ->
+    state.attackOpponents (opp) ->
+      if c.Curse in opp.hand
+        # Discarding a Curse against Mountebank is automatic.
+        opp.doDiscard(c.Curse)
+      else
+        state.gainCard(opp, c.Copper)
+        state.gainCard(opp, c.Curse)
 }

 makeCard 'Nobles', action, {
@@ -850,7 +889,7 @@
 }

 makeCard 'Workshop', action, {
-  cost: 4
+  cost: 3
   playEffect: (state) ->
     choices = []
     for cardName of state.supply


Dynamic start position for level chooser

--- maze.coffee
+++ maze.coffee
@@ -118,10 +118,18 @@
             diameter: amplest.diameter}

 class LevelChooser extends Maze
-
-  constructor: ->
+  @waitingPosition = {
+      3: [1, 3, 0]
+      4: [1, 3, 0]
+      5: [0, 2, 0]
+      6: [3, 2, 0]
+      7: [1, 0, 0]
+      8: [1, 2, 0]
+  }
+
+  constructor: (@nextLevel=3) ->
     super([4, 4, 1])
-    @start = [1, 3, 0]
+    @start = LevelChooser.waitingPosition[@nextLevel]
     # row 0
     @passages[[[0, 0, 0], [1, 0, 0]]] = true
     @passages[[[1, 0, 0], [2, 0, 0]]] = true


Editable function can return false and nothing will happen.

--- ribs.coffee
+++ ribs.coffee
@@ -332,16 +332,20 @@
             # and swap out the cell with any passed
             # in element
             if @options.editable
+
                 # default to a text field
                 if @options.editable instanceof Function
-                    editField = $(@options.editable(@renderableValue(true), @model))
+                    editField = $(@options.editable.call(this, @renderableValue(true), @model))
                 else
                     editField = $($.el.input(type: 'text', value: @renderableValue(true)))
-                editField.addClass("editableField")
-                @$el.html(editField)
-                @delegateEvents()
-                editField.focus()
-                @model.editing = true
+
+                # A function may return null if it does its own thing
+                if editField
+                    editField.addClass("editableField")
+                    @$el.html(editField)
+                    @delegateEvents()
+                    editField.focus()
+                    @model.editing = true

             return false


Remove unnecessary ::mini subscription on model in TextEditorComponent

--- src/text-editor-component.coffee
+++ src/text-editor-component.coffee
@@ -218,7 +218,6 @@

   observeEditor: ->
     {editor} = @props
-    @subscribe editor.onDidChangeMini(@setMini)
     @subscribe editor.observeGrammar(@onGrammarChanged)
     @subscribe editor.observeCursors(@onCursorAdded)
     @subscribe editor.observeSelections(@onSelectionAdded)
@@ -778,9 +777,6 @@

   setShowIndentGuide: (showIndentGuide) ->
     atom.config.set("editor.showIndentGuide", showIndentGuide)
-
-  setMini: ->
-    @requestUpdate()

   # Deprecated
   setInvisibles: (invisibles={}) ->


Avoid full decoration query when line decorations’ markers change

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -555,13 +555,12 @@
       @didDestroyDecoration(decoration)
     @disposables.add(decorationDisposables)

-  decorationMarkerDidChange: (decoration) ->
-    if decoration.isType('line')
-      @updateDecorations()
-      @updateLinesState()
-    if decoration.isType('line-number')
-      @updateDecorations()
-      @updateLineNumbersState()
+  decorationMarkerDidChange: (decoration, change) ->
+    if decoration.isType('line') or decoration.isType('line-number')
+      @removeFromLineDecorationCaches(decoration, new Range(change.oldTailScreenPosition, change.oldHeadScreenPosition))
+      @addToLineDecorationCaches(decoration, new Range(change.newTailScreenPosition, change.newHeadScreenPosition))
+      @updateLinesState() if decoration.isType('line')
+      @updateLineNumbersState() if decoration.isType('line-number')
     if decoration.isType('highlight')
       @updateHighlightState(decoration)
     if decoration.isType('overlay')


getting initial hash

getting initial hash

--- src/theoricus/core/router.coffee
+++ src/theoricus/core/router.coffee
@@ -23,7 +23,11 @@
 	init:->
 		return if @initialized
 		@initialized = true
-		@run @the.boot.boot
+
+		url = window.location.pathname
+		url = @the.boot.boot if url == "/"
+
+		@run url

 	map:( route, to, at )->
 		@routes.push new theoricus.core.Route route, to, at


fix bugs

fix bugs

--- rock.coffee
+++ rock.coffee
@@ -87,6 +87,9 @@
 				return
 		err "Variable #{x} not found in any context!"
 	else
+		if x[0] != '$' and !$pragma['allow_override_declarations'] and _context[_context.length-1][x]
+			err "Cannot override variable #{x} declared in the same scope!"
+			return
 		_context[_context.length-1][x] = val
 		debug "Created new variable #{x} = #{val} in context ##{_context.length-1}", 2

@@ -97,8 +100,9 @@
 	_program = []
 	_programsrclines = []
 	_labels = {}
-	_context = [STD_CONTEXT]
+	_context = [Object.assign({}, STD_CONTEXT)]
 	_lineno = 1
+	$pragma = {}
 	debug "Program reset.", 3

 # reads the whole program in a table { lineno: line }
@@ -158,6 +162,9 @@
 				when 'undeclared_is_error'
 					debug "[meta] error on undeclared var enabled"
 					$pragma['undeclared_is_error'] = on
+				when 'allow_override_declarations'
+					debug "[meta] Enabling declaration overriding"
+					$pragma['allow_override_declarations'] = on
 				else
 					debug "[meta] unknown pragma: #{params[0]}"


Make it work on non-POSIX OSes

Make it work on non-POSIX OSes

--- rtsp.coffee
+++ rtsp.coffee
@@ -92,7 +92,6 @@
 # Return value is intended to be used as an SSRC identifier.
 generateRandom32 = ->
   str = "#{new Date().getTime()}#{process.pid}#{os.hostname()}" + \
-        "#{process.getuid()}#{process.getgid()}" + \
         (1 + Math.random() * 1000000000)

   md5sum = crypto.createHash 'md5'


Remove duplicate workspace view

Remove duplicate workspace view

--- spec/image-editor-view-spec.coffee
+++ spec/image-editor-view-spec.coffee
@@ -60,7 +60,6 @@
     [imageSizeStatus] = []

     beforeEach ->
-      atom.workspaceView = new WorkspaceView
       atom.workspaceView.attachToDom()
       filePath = atom.project.resolve('binary-file.png')


need to focus first for preformatting

need to focus first for preformatting

--- src/modules/toolbar.coffee
+++ src/modules/toolbar.coffee
@@ -23,8 +23,8 @@
         value = if input.tagName == 'SELECT' then input.options[input.selectedIndex].value else !ScribeDOM.hasClass(input, 'active')
         range = @editor.getSelection()
         if range
+          @editor.root.focus()
           range.format(format, value, { source: 'user' })
-          @editor.root.focus()
         activeFormats = {}
         activeFormats[format] = value
         this.updateActive(activeFormats)


Fix failing tests

Fix failing tests

--- src/coffee/bootstrap-tour.coffee
+++ src/coffee/bootstrap-tour.coffee
@@ -231,7 +231,7 @@
         .popover('destroy')
         .removeClass "tour-#{@_options.name}-element tour-#{@_options.name}-#{i}-element"
         $element
-        .data('bs.popover', null)
+        .removeData('bs.popover')
         if step.reflex
           $ step.reflexElement
           .removeClass('tour-step-element-reflex')


Optimized new device detection

Optimized new device detection

--- acs.coffee
+++ acs.coffee
@@ -70,8 +70,27 @@
       updates["#{path}_timestamp"] = now

   if Object.keys(updates).length > 0
-    devicesCollection.update({'_id' : deviceId}, {'$set' : updates}, {upsert: true, safe: true}, (err) ->
-      callback(err) if callback?
+    devicesCollection.update({'_id' : deviceId}, {'$set' : updates}, {safe: true}, (err, count) ->
+      if (err)
+        callback(err) if callback?
+        return
+
+      if count == 0
+        util.log("#{deviceId}: new device detected")
+        updates['_id'] = deviceId
+        devicesCollection.save(updates, {safe: true}, (err) ->
+          if err?
+            callback(err) if callback?
+            return
+
+          task = {device : deviceId, name : 'init', timestamp : mongo.Timestamp()}
+          tasksCollection.save(task, (err) ->
+            util.log("#{deviceId}: Added init task #{task._id}")
+            callback(err) if callback?
+          )
+        )
+      else
+        callback() if callback?
     )
   else
     callback() if callback?
@@ -189,18 +208,10 @@
         cookies.DeviceId = deviceId = common.getDeviceId(reqParams.deviceId)
         if config.DEBUG
           util.log("#{deviceId}: inform (#{reqParams.eventCodes}); retry count #{reqParams.retryCount}")
-        devicesCollection.count({'_id' : deviceId}, (err, count) ->
-          if not count
-            util.log("#{deviceId}: new device detected")
-            task = {device : deviceId, name : 'init', timestamp : mongo.Timestamp()}
-            tasksCollection.save(task, (err) ->
-              util.log("#{deviceId}: Added init task #{task._id}")
-            )
-
-          updateDevice(deviceId, {'inform' : true, 'parameterValues' : reqParams.informParameterValues}, (err) ->
-            res = tr069.response(reqParams.sessionId, resParams, cookies)
-            writeResponse response, res
-          )
+
+        updateDevice(deviceId, {'inform' : true, 'parameterValues' : reqParams.informParameterValues}, (err) ->
+          res = tr069.response(reqParams.sessionId, resParams, cookies)
+          writeResponse response, res
         )
         return


fix(console): rm console trace

fix(console): rm console trace

--- src/components/contenteditable/contenteditable.cjsx
+++ src/components/contenteditable/contenteditable.cjsx
@@ -493,7 +493,6 @@
   # you revert to a previous state, the selection updates as well.
   _saveSelection: =>
     selection = new ExtendedSelection(@_editableNode())
-    console.trace()
     return unless selection?.isInScope()

     @setInnerState


Bar graph support

Bar graph support

--- widgets/graph/graph.coffee
+++ widgets/graph/graph.coffee
@@ -13,11 +13,12 @@
     height = (Dashing.widget_base_dimensions[1] * container.data("sizey"))
     @graph = new Rickshaw.Graph(
       element: @node
+      renderer: $(@node).data("renderer") || 'area'
       width: width
       height: height
       series: [
         {
-        color: "#fff",
+        color: $(@node).data("renderer") || 'area'
         data: [{x:0, y:0}]
         }
       ]


always use regexString since parts all respond to it now

--- lib/route.coffee
+++ lib/route.coffee
@@ -140,7 +140,7 @@
     ret = ''
     # a route regex is a composite of its parts' regexe(s|n)
     for part in @parts
-      ret += if part.regexString then part.regexString() else regExpEscape part
+      ret += part.regexString()

     "(#{ret})#{if @optional then '?' else ''}"


Add menu items

Add menu items

--- menus/whitespace.cson
+++ menus/whitespace.cson
@@ -5,6 +5,8 @@
       'label': 'Whitespace'
       'submenu': [
         { 'label': 'Remove Trailing Whitespace', 'command': 'whitespace:remove-trailing-whitespace' }
+        { 'label': 'Save With Trailing Whitespace', 'command': 'whitespace:save-with-trailing-whitespace' }
+        { 'label': 'Save Without Trailing Whitespace', 'command': 'whitespace:save-without-trailing-whitespace' }
         { 'label': 'Convert Tabs To Spaces', 'command': 'whitespace:convert-tabs-to-spaces' }
         { 'label': 'Convert Spaces To Tabs', 'command': 'whitespace:convert-spaces-to-tabs' }
       ]


cleanup

cleanup

--- fme-sortable.coffee
+++ fme-sortable.coffee
@@ -6,7 +6,7 @@
     fmeIndex: '='
     fmeOnDrop: '&'
     fmeNotSortable: '&'
-  link: (scope, element, attrs) ->
+  link: (scope, element) ->
     unless scope.fmeNotSortable()
       element.attr('draggable', 'true')
       dragging_index = null
@@ -23,7 +23,7 @@
           element.addClass('dropzone')
           e.dataTransfer.dropEffect = 'move'

-      element.on 'dragleave', (event) ->
+      element.on 'dragleave', ->
         element.removeClass('dropzone')

       element.on 'drop', (event) ->
@@ -35,4 +35,3 @@
           scope.fmeList.splice(dropped_fmeIndex, 1)
           scope.fmeList.splice(scope.fmeIndex, 0, dropped_model)
           scope.fmeOnDrop()
-


More messy edits. Gawd this is an aweful set of toying.

--- lib/suite.coffee
+++ lib/suite.coffee
@@ -53,23 +53,21 @@
     @_run_afters meta, -> done()

   _on_child_complete: (meta, done) =>
-    console.log 'Child complete?'
     @_next()

+  _on_child_test: (meta, done) =>
+    #done()
+
   _complete: () =>
-    console.log 'Complete.'
-    @emit 'complete', =>
+    @emit 'complete'

   _run_afters: (meta, callback) =>
-    console.log "#{@description}: Emitting after"
     @emit 'after', [meta], =>
       @_run_runners @_after_eachs, =>
         callback()

   _run_befores: (meta, callback) =>
-    console.log "#{@description}: Emitting before"
     @emit 'before', [meta], =>
-      console.log "#{@description}: Emitting before done"
       if @session.ran_before_alls
         @_run_runners @_before_eachs, =>
           callback()
@@ -110,7 +108,8 @@
     # See: https://github.com/jashkenas/coffee-script/issues/2489
     suite.on 'before', @_on_child_before, callback: true
     suite.on 'after', @_on_child_after, callback: true
-    suite.on 'complete', @_on_child_complete
+    suite.on 'test', @_on_child_test, callback: true
+    suite.on 'complete', @_on_child_complete, callback: true
     suite._run()

   _run: =>


Update CampaignView width to fit volcano world

--- app/views/play/CampaignView.coffee
+++ app/views/play/CampaignView.coffee
@@ -927,7 +927,7 @@
     return if 0.2 < fraction < 0.8
     direction = if fraction < 0.5 then 1 else -1
     magnitude = 0.2 * bodyWidth * (if direction is -1 then fraction - 0.8 else 0.2 - fraction) / 0.2
-    portalsWidth = 2536  # TODO: if we add campaigns or change margins, this will get out of date...
+    portalsWidth = 2853  # TODO: if we add campaigns or change margins, this will get out of date...
     scrollTo = $portals.offset().left + direction * magnitude
     scrollTo = Math.max bodyWidth - portalsWidth, scrollTo
     scrollTo = Math.min 0, scrollTo


Initializes downloads dropdown on homepage

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -2,9 +2,10 @@
   'jquery', 'modules/search/bar', 'modules/map',
   'modules/downloads/base', 'modules/downloads/general',
   'modules/downloads/project', 'modules/downloads/search',
+  'modules/dropdown'
 ]

-require(DEPENDENCIES, ($, SearchBar, Map, DownloadsBase, DownloadsGeneral, DownloadsProject, DownloadsSearch)->
+require(DEPENDENCIES, ($, SearchBar, Map, DownloadsBase, DownloadsGeneral, DownloadsProject, DownloadsSearch, Dropdown)->
   initializeDownloadButtons = ->
     $downloadBtns = [
       $(".download-type-dropdown[data-download-type='general'] a")
@@ -37,4 +38,10 @@
   new SearchBar()
   new Map($('#map')).render()
   initializeDownloadButtons()
+
+  new Dropdown(
+    $('.btn-download'),
+    $(".download-type-dropdown[data-download-type='general']")
+  )
+
 )