code to backup blobs to a tarball (for offsite backup of blobs)

--- src/smc-hub/rethink.coffee
+++ src/smc-hub/rethink.coffee
@@ -2598,34 +2598,70 @@
                 @table('blobs').get(opts.uuid).update(gcloud:opts.bucket).run(cb)
         ], (err) => opts.cb?(err))

-    # Write the give blob to the file [path]/[sha1]; if write successful, sets
-    # the backup field in the database to true.
-    backup_blob: (opts) =>
-        opts = defaults opts,
-            uuid  : required  # uuid=sha1-based uuid of blob
-            path  : required  # target path
-            cb    : undefined # cb(err)
-        blob = undefined
+    # Backup limit blobs that previously haven't been dumped to blobs, and put them in
+    # a tarball in the given path.  The tarball's name is the time when the backup starts.
+    # The tarball is compressed using gzip compression.
+    backup_blobs_to_tarball: (opts) =>
+        opts = defaults opts,
+            limit : 10000     # number of blobs to backup
+            path  : required  # path where [timestamp].tar file is placed
+            cb    : undefined # cb(err, '[timestamp].tar')
+        dbg     = @dbg("backup_blobs_to_tarball(limit=#{opts.limit},path='#{opts.path}')")
+        join    = require('path').join
+        dir     = misc.date_to_snapshot_format(new Date())
+        target  = join(opts.path, dir)
+        tarball = target + '.tar.gz'
+        v       = undefined
+        to_remove = []
         async.series([
             (cb) =>
-                @get_blob
-                    uuid  : opts.uuid
-                    touch : false
-                    cb    : (err, x) =>
-                        blob = x
-                        cb(err)
-            (cb) =>
-                if blob?
-                    fs.writeFile(require('path').join(opts.path, opts.uuid), blob, cb)
-                else
-                    cb()
-            (cb) =>
-                if blob?
-                    # successful write to file
-                    @table('blobs').get(opts.uuid).update(backup:true).run(cb)
-                else
-                    cb()
-        ], (err) => opts.cb?(err))
+                dbg("make target='#{target}'")
+                fs.mkdir(target, cb)
+            (cb) =>
+                dbg("get blobs that we need to back up")
+                @table('blobs').filter(@r.row.hasFields('backup').not()).filter(@r.row.hasFields('expire').not()).limit(opts.limit).pluck('id').run (err, x) =>
+                    v = x; cb(err)
+            (cb) =>
+                dbg("backup #{v.length} blobs")
+                f = (x, cb) =>
+                    @get_blob
+                        uuid  : x.id
+                        touch : false
+                        cb    : (err, blob) =>
+                            if err
+                                dbg("ERROR! #{err}")
+                                cb(err)
+                            else if blob?
+                                dbg("got blob from db -- now write to disk")
+                                to_remove.push(x.id)
+                                fs.writeFile(join(target, x.id), blob, cb)
+                            else
+                                dbg("blob is expired, so nothing to be done, ever.")
+                                cb()
+                async.mapLimit(v, 3, f, cb)
+            (cb) =>
+                dbg("successfully wrote all blobs to files; now make tarball")
+                misc_node.execute_code
+                    command : 'tar'
+                    args    : ['zcvf', tarball, dir]
+                    path    : opts.path
+                    timeout : 3600
+                    cb      : cb
+            (cb) =>
+                dbg("remove temporary blobs")
+                f = (x, cb) =>
+                    fs.unlink(join(target, x), cb)
+                async.mapLimit(to_remove, 10, f, cb)
+            (cb) =>
+                dbg("remove temporary directory")
+                fs.rmdir(target, cb)
+            (cb) =>
+                dbg("backup succeeded completely -- mark all blobs as backed up")
+                @table('blobs').getAll((x.id for x in v)...).update(backup:true).run(cb)
+        ], (err) =>
+            dbg("done -- #{err}")
+            opts.cb?(err, if not err then tarball)
+        )

     # Copied all blobs that will never expire to a google cloud storage bucket.
     #
@@ -2636,7 +2672,7 @@
             bucket    : BLOB_GCLOUD_BUCKET # name of bucket
             limit     : undefined          # only copy this many to gcloud
             map_limit : 10                 # do this many in parallel
-            repeat_until_done_s : 0        # if nonzero, waits this many seconds keeps re-calling this function until nothing gets uploaded.
+            repeat_until_done_s : 0        # if nonzero, waits this many seconds, then recalls this function until nothing gets uploaded.
             errors    : {}                 # used to accumulate errors
             cb        : required
         dbg = @dbg("copy_all_blobs_to_gcloud()")


better loging

better loging

--- src/smc-hub/rethink.coffee
+++ src/smc-hub/rethink.coffee
@@ -3152,8 +3152,9 @@
             limit             : 1000 # do only this many per get query loop
             repeat_until_done : true
             delay             : 0    # artifical delay in ms between archiving.
+            total_limit       : 0    # only do this many **total**
             count             : 0    # used internally for logging
-            total_limit       : 0    # only do this many **total**
+            start_time        : new Date()   # used internally for loggin
             cb                : undefined
         dbg = @dbg("syncstring_maintenance")
         dbg(opts)
@@ -3176,7 +3177,7 @@
                 f = (string_id, cb) =>
                     i += 1
                     opts.count += 1
-                    console.log("\n***** #{opts.count} -- #{i}/#{syncstrings.length}: archiving string #{string_id} ***** \n")
+                    console.log("\n***** #{opts.count} (#{(new Date() - opts.start_time)/1000} seconds) -- #{i}/#{syncstrings.length}: archiving string #{string_id} ***** \n")
                     @archive_patches
                         string_id : string_id
                         cb        : (err) ->


add to blob store schema pointer to gcloud bucket and whether or not blob has been backed up offsite.

--- src/smc-util/schema.coffee
+++ src/smc-util/schema.coffee
@@ -276,6 +276,12 @@
         size :
             type : 'number'
             desc : 'The size in bytes of the blob.'
+        gcloud :
+            type : 'string'
+            desc : 'name of a bucket that contains the actual blob, if available.'
+        backed_up :
+            type : 'timestamp'
+            desc : 'if true, then this blob was saved to an offsite backup at the given time'
     indexes:
         expire : []


resolve #1314 and #1650 -- firefox fixed their flexbox performance issues

--- src/smc-webapp/last.coffee
+++ src/smc-webapp/last.coffee
@@ -118,13 +118,4 @@
         MathJax.Hub?.Queue([mathjax_finish_startup])
     document.getElementsByTagName("head")[0].appendChild(mjscript)

-    # hsy: showing firefox warning on all platforms (no idea why MacIntel was excluded)
-    if $.browser.firefox    # and window.navigator.platform != "MacIntel"
-        # See https://github.com/sagemathinc/smc/issues/1314
-        {alert_message} = require('./alerts')
-        alert_message
-            type    : 'info'
-            message : "There are major performance issues with Firefox and SageMathCloud due to bugs in Firefox.  We strongly recommend using Chrome, Safari, or Edge."
-            timeout : 120
-
     misc.wrap_log()


Updated generic-parser

Updated generic-parser

--- src/generic-parser.coffee
+++ src/generic-parser.coffee
@@ -9,6 +9,10 @@
     @inputArray = inputString.split ''
     @head = 0
     @headStack = []
+
+  ###
+    Basic Mechanism
+  ###

   backUp: ->
     @headStack.push @head
@@ -28,6 +32,16 @@
   moveForward: ->
     @head += 1
     return
+
+  ###
+    Utilities
+  ###
+
+
+
+  ###
+    Known Length Token Extraction
+  ###

   takeIfChar: (char)->
     if @current() is char
@@ -69,10 +83,14 @@
       return cur
     return false

+  ###
+    Variable Length Token Extraction
+  ###
+
   takeUntilChar: (char)->
     string = ''
-    while string = @takeUnlessChar char
-      'pass'
+    while takenChar = @takeUnlessChar char
+      string += takenChar
     return string

   takeUntilString: (string)->
@@ -84,6 +102,7 @@
         return returnString
       else
         returnString += @current()
+        @moveForward()

   takeUntilInCharArray: (charArray)->
     returnString = ''
@@ -92,6 +111,7 @@
         return returnString
       else
         returnString += @current()
+        @moveForward()

   takeUntilInStringArray: (stringArray)->
     returnString = ''
@@ -102,6 +122,7 @@
         return returnString
       else
         returnString += @current()
+        @moveForward()


ActivityTicker: No need for a different constructorNames local variable in filterSource

--- workers/social/lib/social/models/activityticker.coffee
+++ workers/social/lib/social/models/activityticker.coffee
@@ -16,8 +16,6 @@
   JAccount = require './account'

   filterSources = (filters) =>
-    constructorNames  = ["JAccount", "JNewApp", "JGroup", "JTag", "JNewStatusUpdate", "JComment"]
-
     relationshipMap =
       "follower" : ["JTag"]
       "like"     : ["JAccount", "JNewStatusUpdate"]


Use setting instead of hard-coding port

Use setting instead of hard-coding port

--- services/track-changes/config/settings.defaults.coffee
+++ services/track-changes/config/settings.defaults.coffee
@@ -14,7 +14,7 @@
 		docstore:
 			url: "http://#{process.env["DOCSTORE_HOST"] or "localhost"}:3016"
 		web:
-			url: "http://#{process.env["WEB_HOST"] or "localhost"}:3000"
+			url: "http://#{process.env["WEB_HOST"] or "localhost"}:#{process.env['WEB_PORT'] or 3000}"
 			user: "sharelatex"
 			pass: "password"
 	redis:


Optional socket for some tags without children

--- src/languages/html.coffee
+++ src/languages/html.coffee
@@ -98,7 +98,7 @@
   #Form
   form: {category: 'form'}
   input: {category: 'form'}
-  textarea: {category: 'form'}
+  textarea: {category: 'form', content: 'optional'}
   label: {category: 'form'}
   button: {category: 'form'}
   select: {category: 'form'}
@@ -107,14 +107,14 @@
   datalist: {category: 'form'}
   keygen: {category: 'form'}
   output: {category: 'form'}
-  progress: {category: 'form'}
-  meter: {category: 'form'}
+  progress: {category: 'form', content: 'optional'}
+  meter: {category: 'form', content: 'optional'}
   fieldset: {category: 'form'}
   legend: {category: 'form'}

   #Embedded
   img: {category: 'embedded'}
-  iframe: {category: 'embedded'}
+  iframe: {category: 'embedded', content: 'optional'}
   embed: {category: 'embedded'}
   object: {category: 'embedded'}
   param: {category: 'embedded'}
@@ -135,7 +135,7 @@
   dialog: {category: 'other'}
   noscript: {category: 'other'}
   template: {category: 'other'}
-  canvas: {category: 'other'}
+  canvas: {category: 'other', content: 'optional'}
   svg: {category: 'other'}
   frameset: {category: 'other'}
 }
@@ -466,8 +466,10 @@
           for child in node.childNodes
             @mark indentDepth, child, depth + 1, null
         else
-          if (node.nodeName isnt 'script' or not @hasAttribute node, 'src') and
-              node.__indentLocation.end isnt node.__location.end
+          unless TAGS[node.nodeName].content is 'optional' or
+              (node.nodeName is 'script' and @hasAttribute node, 'src') or
+              node.__indentLocation.end is node.__location.end
+            console.log  TAGS[node.nodeName]
             @htmlSocket node, depth + 1, null, indentBounds

       when 'text'


Fix spec helper still using up.util.all

Fix spec helper still using up.util.all

--- spec_app/spec/javascripts/helpers/to_match_list.coffee
+++ spec_app/spec/javascripts/helpers/to_match_list.coffee
@@ -11,4 +11,4 @@
           actualList &&
             expectedList &&
             actualList.length == expectedList.length &&
-            u.all(expectedList, (elem) -> u.contains(actualList, elem))
+            u.every(expectedList, (elem) -> u.contains(actualList, elem))


Remove input--focused class on input label on blur

--- _source/client/templates/components/forms/input.coffee
+++ _source/client/templates/components/forms/input.coffee
@@ -88,6 +88,10 @@
     isForm = parent?.find("form")
     data = self.data()

+    # switch to junction when ready
+    $(event.target.parentNode)
+      .removeClass "input--focused"
+
     if event.target.value

       valid = self.validate event.target.value
@@ -113,10 +117,6 @@
       else
         parent.children()[0]?.hasErrors?.set false

-    # switch to junction when ready
-    $(event.target.parentNode)
-      .removeClass "input--focused"
-
     # if the input is empty, remove the input--active class
     if not event.target.value


Set props to variable instead of using callback

--- app/packages/_api/survey.coffee
+++ app/packages/_api/survey.coffee
@@ -56,9 +56,8 @@
   addForm: (props) ->
     survey = @
     form = new Form()
-    @buildForm(props)
-      .then (formProps) ->
-        form.create(formProps, survey)
+    formProps = @buildForm(props)
+    form.create(formProps, survey)
       .then (form) ->
         relation = survey.relation 'forms'
         relation.add form


Allow extra SpriteCommand say moods that match Little Sophia

--- app/schemas/models/level.coffee
+++ app/schemas/models/level.coffee
@@ -86,7 +86,7 @@
   select: {title: 'Select', description: 'Select or deselect this Thang.', type: 'boolean'}
   say: c.object {title: 'Say', description: 'Make this Thang say a message.', required: ['text'], default: { mood: 'explain' }},
     blurb: c.shortString(title: 'Blurb', description: 'A very short message to display above this Thang\'s head. Plain text.', maxLength: 50)
-    mood: c.shortString(title: 'Mood', description: 'The mood with which the Thang speaks.', 'enum': ['explain', 'debrief', 'congrats', 'attack', 'joke', 'tip', 'alarm'])
+    mood: c.shortString(title: 'Mood', description: 'The mood with which the Thang speaks.', 'enum': ['explain', 'confused', 'curious', 'dance-happy', 'dance', 'idle', 'neutral', 'sad', 'debrief', 'congrats', 'attack', 'joke', 'tip', 'alarm'])
     text: {title: 'Text', description: 'A short message to display in the dialogue area. Markdown okay.', type: 'string', maxLength: 400}
     sound: c.object {title: 'Sound', description: 'A dialogue sound file to accompany the message.', required: ['mp3', 'ogg'] },
       mp3: c.shortString(title: 'MP3', format: 'sound-file')


Fix date formatting

Fix date formatting

Bug added when optimizing use of `Intl.DateTimeFormat`

--- client/lib/date.coffee
+++ client/lib/date.coffee
@@ -1,16 +1,20 @@
 import React from 'react'
 import {useTracker} from 'meteor/react-meteor-data'

-dateTimeFormat = new Intl.DateTimeFormat 'en-US'
+monthDayFormat = new Intl.DateTimeFormat 'en-US',
+  weekday: 'short'
+  month: 'short'
+  day: 'numeric'
+monthDayYearFormat = new Intl.DateTimeFormat 'en-US',
+  weekday: 'short'
+  month: 'short'
+  day: 'numeric'
+  year: 'numeric'

 export formatDateOnly = (date, prefix = '', absolute) ->
   return '???' unless date?
   return "?#{date}?" unless date instanceof Date  ## have seen this briefly, not sure when
   now = new Date
-  options =
-    weekday: 'short'
-    month: 'short'
-    day: 'numeric'
   if date.getFullYear() == now.getFullYear() and not absolute
     if date.getMonth() == now.getMonth() and date.getDate() == now.getDate()
       Session.get 'today'  # depend on the current date; see below
@@ -21,10 +25,9 @@
         Session.get 'today'  # depend on the current date; see below
         "yesterday"
       else
-        "#{prefix}#{dateTimeFormat.format date, options}"
+        "#{prefix}#{monthDayFormat.format date}"
   else
-    options.year = 'numeric'
-    "#{prefix}#{dateTimeFormat.format date, options}"
+    "#{prefix}#{monthDayYearFormat.format date}"

 export formatDate = (date, prefix, absolute) ->
   dateOnly = formatDateOnly date, prefix, absolute


Better comment for the success function

Better comment for the success function

--- client/lib/util.coffee
+++ client/lib/util.coffee
@@ -49,7 +49,7 @@
 # Navigate to page
 navto = (page, data) -> document.location = format_url page, data

-# returns a function that dispalys the given success message
+# success(message) returns a function that dispalys the message
 success = do (view = require '../views/dialog-success.jade') -> (message) -> ->
   dialog = $ view { message }
   dialog.on 'hidden', -> do dialog.remove


changed color

changed color

--- assets/js/chart.coffee
+++ assets/js/chart.coffee
@@ -11,12 +11,8 @@
   datasets: [
     {
       label: "Punkte",
-      fillColor: "rgba(11,20,220,0.2)",
-      strokeColor: "rgba(220,98,220,1)",
-      pointColor: "rgba(220,220,220,1)",
-      pointStrokeColor: "#fff",
-      pointHighlightFill: "#fff",
-      pointHighlightStroke: "rgba(220,220,220,1)",
+      strokeColor: "rgba(0,70,224,0.8)",
+      fillColor: "rgba(7,108,240,0.6)",
       data: _.map(classes, "points")
     }
   ]


Updated per lineman 0.7.1

Updated per lineman 0.7.1

--- config/application.coffee
+++ config/application.coffee
@@ -29,7 +29,7 @@
   #    }
   #  }

-  appTasks:
+  appendTasks:
     common: ["ember_handlebars"]

   loadNpmTasks: ['grunt-ember-handlebars']


working on test

working on test

--- src/test/bower-test.coffee
+++ src/test/bower-test.coffee
@@ -1,5 +1,7 @@
 join = require('path').join
+exists = require('fs').existsSync
 check = require '../check-bower-file'
+bowerFilename = join __dirname, 'bower-test/bower.json'

 gt.module 'running as parent tests',
   setup: ->
@@ -9,7 +11,8 @@

 gt.test 'check function', ->
   gt.func check, 'check is a function'
+  gt.ok exists(bowerFilename), 'bower file', bowerFilename, 'exists'

-# gt.test 'test itself via check', ->
-#  ok = check process.cwd(), true
-#  gt.ok ok, 'all deps are up to date'
+gt.test 'test itself via check', ->
+  ok = check bowerFilename, true
+  gt.ok ok, 'all deps are up to date'


minor js error when submitting completion-assessment task

--- app/assets/javascripts/lib/workflow-methods-mixin.cjsx
+++ app/assets/javascripts/lib/workflow-methods-mixin.cjsx
@@ -172,8 +172,6 @@
       for c, i in s.child_subjects
         if c.id == child_subject_id
           c[k] = v for k,v of props
-    else
-      console.warn "WorkflowMethodsMixin#appendChildSubject: couldn't find subject by ", subject_id

   # Add newly acquired child_subject to child_subjects array of relevant subject (i.e. after submitting a subject-generating classification)
   appendChildSubject: (subject_id, child_subject) ->
@@ -183,12 +181,14 @@
       # We've updated an internal object in @state.subjectSets, but framework doesn't notice, so tell it to update:
       @forceUpdate()

-    else
-      console.warn "WorkflowMethodsMixin#appendChildSubject: couldn't find subject by ", subject_id
-
   # Get a reference to the local copy of a subject by id regardless of whether viewing subject-sets or just subjects
   getSubjectById: (id) ->
     if @state.subjectSets?
+
+      # If current subject set has no subjects, we're likely in between one subject set
+      # and the next (for which we're currently fetching subjects), so return null:
+      return null if ! @getCurrentSubjectSet().subjects?
+
       for s in @getCurrentSubjectSet().subjects
         return s if s.id == id
     else


#269 ensure that the tracing view is completely unloaded when switching from tracing view to the dashboard

--- app/assets/javascripts/main/non_backbone_router.coffee
+++ app/assets/javascripts/main/non_backbone_router.coffee
@@ -22,6 +22,12 @@

   tracingTrace : ->
+
+    $('[href="/dashboard"]').click(->
+      # Temporary hack to circumvent backbones routing.
+      # This ensures that the tracing view is completely unloaded.
+      location.href = "/dashboard"
+    )

     require [
       "oxalis/controller"
@@ -58,6 +64,12 @@

   tracingView : ->
+
+    $('[href="/dashboard"]').click(->
+      # Temporary hack to circumvent backbones routing.
+      # This ensures that the tracing view is completely unloaded.
+      location.href = "/dashboard"
+    )

     require [
       "oxalis/controller"


console group for binding views report

console group for binding views report

--- app/assets/javascripts/ultimate/backbone/app.js.coffee
+++ app/assets/javascripts/ultimate/backbone/app.js.coffee
@@ -43,6 +43,7 @@
       cout 'info', "Processed #{bindersCount} custom element binder#{if bindersCount is 1 then '' else 's'} in #{Math.round((performanceBinders - performanceViews) * 1000)}\u00B5s"

   bindViews: (jRoot = $('html')) ->
+    console?.groupCollapsed? 'bindViews on', jRoot
     bindedViews = []
     for viewName, viewClass of @Views when viewClass::el
       #cout 'info', "Try bind #{viewName} [#{viewClass::el}]"
@@ -52,6 +53,7 @@
           cout 'info', "Binded view #{viewName}:", view
           @viewInstances.push view
           bindedViews.push view
+    console?.groupEnd?()
     bindedViews

   canBind: (element, viewClass) ->


remove unneeded console call

remove unneeded console call

--- app/scripts/controllers/fragments/movie-details.coffee
+++ app/scripts/controllers/fragments/movie-details.coffee
@@ -15,7 +15,6 @@
          # Movie model
          # @attribute movie
         ###
-        console.log $rootScope.selected
         $scope.movie = $rootScope.selected


Check if name is an email before submitting

--- app/views/core/CreateAccountModal/BasicInfoView.coffee
+++ app/views/core/CreateAccountModal/BasicInfoView.coffee
@@ -96,6 +96,16 @@

   onChangeName: (e) ->
     @updateAuthModalInitialValues { name: @$(e.currentTarget).val() }
+
+    # Go through the form library so this follows the same trimming rules
+    name = forms.formToObject(@$el.find('#basic-info-form')).name
+    # Carefully remove the error for just this field
+    @$el.find('[for="username-input"] ~ .help-block.error-help-block').remove()
+    @$el.find('[for="username-input"]').closest('.form-group').removeClass('has-error')
+    if name and forms.validateEmail(name)
+      forms.setErrorToProperty(@$el, 'name', $.i18n.t('signup.name_is_email'))
+      return
+
     @checkName()

   checkName: ->


Fix typo.

Fix typo.

--- src/util/referencer.coffee
+++ src/util/referencer.coffee
@@ -317,7 +317,7 @@

     see

-  # Resolve parameter references. This gies through all
+  # Resolve parameter references. This goes through all
   # method parameter and see if a param doc references another
   # method. If so, copy over the doc meta data.
   #


Fix error object when XHR failed

Fix error object when XHR failed

--- src/util/xhrpromise.coffee
+++ src/util/xhrpromise.coffee
@@ -41,7 +41,10 @@
     return new Promise((resolve, reject) ->
       xhr = new XMLHttpRequest()
       xhr.onload = -> resolve(xhr)
-      xhr.onerror = -> reject(xhr)
+      xhr.onerror = ->
+        error = Error(xhr.statusText)
+        error.xhr = xhr
+        reject(error)
       xhr.responseType = type
       xhr.timeout = options.timeout
       xhr.open(options.method, url, true, options.user, options.password)


Update documentation #29

Update documentation #29

--- app/assets/javascripts/lale/workgroup_dependent_select.coffee
+++ app/assets/javascripts/lale/workgroup_dependent_select.coffee
@@ -1,14 +1,17 @@
 #
-# The available projects and organizers of a task (and supply) depends on the
-# working group a task/supply is in. This class updates the availabel options for
-# a dependent field (projects/organizers) after when the working group changes.
+# The available projects and organizers of some entities (a task, supply or project) depend
+# on the working group it is in. This class updates the available options when the working
+# group changes.
 #
-# The code expects an optgroup HTML structure which was generated with Rails'
+# Example: when the workgroup is changed in the task form, the available organizers need
+# to change to all organizers of the new workgroup.
+#
+# In order to work, the class needs an optgroup HTML structure generated with
 # #grouped_collection_select in the dependent field.
 #
-# Example usages:
+# Example usage:
 # new Lale.WorkgroupDependentSelect('#task_working_group_id', '#task_project_id')
-# new Lale.WorkgroupDependentSelect('#supply_working_group_id', '#supply_organizer_id')
+#
 class Lale.WorkgroupDependentSelect
   constructor: (workgroup_field_selector, dependent_field_selector) ->
     this.workgroup_field = $(workgroup_field_selector)
@@ -22,7 +25,7 @@

   updateOptions: ->
     this.field.empty()
-    this.field.append(this.getEmptyOption()) # the empty option is only available sometimes
+    this.field.append(this.getEmptyOption()) # the empty option not always available
     this.field.append(this.getNewOptions())
     if this.field.children().length then this.field.parent().show() else this.field.parent().hide()


FIX: iframe detection

FIX: iframe detection

--- app/assets/javascripts/media_lib/jquery.nmdUploader.js.coffee
+++ app/assets/javascripts/media_lib/jquery.nmdUploader.js.coffee
@@ -56,8 +56,11 @@
           ".replace /\ /g, ''

         openUploader = ->
-          $uploaderIframes = $uploader.find('.js-uploader-iframe')
-          $uploaderIframes.add $("##{settings.open}").find('.js-uploader-iframe') if settings.open
+          if settings.open
+            $uploaderIframes = $("##{settings.open}").find('.js-uploader-iframe')
+          else
+            $uploaderIframes = $uploader.find('.js-uploader-iframe')
+
           if $uploaderIframes.length
             iframeTemplate = _.template '''
               <iframe width='<%= width %>' height='<%= height %>' src='<%= src %>' frameborder='0'></iframe>
@@ -71,15 +74,14 @@
                   src: getUrl()
                   width: iframeData.width or '100%'
                   height: iframeData.height or 500
-
-            # Для TinyMCE id модального окна передаётся в параметре
-            $("##{settings.open}").modal('show') if settings.open and $.fn.modal?
           else
             popupWindow = open getUrl(), 'NMD Media Lib', 'scrollbars=1, width=800, height=500'
             popupWindow.focus()

         if settings.open
-          openUploader()
+          openUploader()
+          # Для TinyMCE id модального окна передаётся в параметре
+          $("##{settings.open}").modal('show') if $.fn.modal?
         else
           $uploader.find('.js-uploader-open').on 'click', (e) ->
             e.preventDefault()


Don't check for presence before jquery methods

This is built into any jquery plugin, including select2

--- backend/app/assets/javascripts/spree/backend/taxons.js.coffee
+++ backend/app/assets/javascripts/spree/backend/taxons.js.coffee
@@ -9,32 +9,32 @@
         taxon_id: $('#taxon_id').val(),
         position: ui.item.index()

-  if $('#taxon_id').length > 0
-    $('#taxon_id').select2
-      dropdownCssClass: "taxon_select_box",
-      placeholder: Spree.translations.find_a_taxon,
-      ajax:
-        url: Spree.routes.taxons_search,
-        datatype: 'json',
-        params: { "headers": { "X-Spree-Token": Spree.api_key } },
-        data: (term, page) ->
-          per_page: 50,
-          page: page,
-          q:
-            name_cont: term
-        results: (data, page) ->
-          more = page < data.pages;
-          results: data['taxons'],
-          more: more
-      formatResult: (taxon) ->
-        taxon.pretty_name;
-      formatSelection: (taxon) ->
-        taxon.pretty_name;
   window.productTemplate = Handlebars.compile($('#product_template_sortable').text());

   productListTemplate = (products) ->
     products.map(productTemplate).join('') ||
     "<h4>#{Spree.translations.no_results}</h4>"
+
+  $('#taxon_id').select2
+    dropdownCssClass: "taxon_select_box",
+    placeholder: Spree.translations.find_a_taxon,
+    ajax:
+      url: Spree.routes.taxons_search,
+      datatype: 'json',
+      params: { "headers": { "X-Spree-Token": Spree.api_key } },
+      data: (term, page) ->
+        per_page: 50,
+        page: page,
+        q:
+          name_cont: term
+      results: (data, page) ->
+        more = page < data.pages;
+        results: data['taxons'],
+        more: more
+    formatResult: (taxon) ->
+      taxon.pretty_name;
+    formatSelection: (taxon) ->
+      taxon.pretty_name;

   $('#taxon_id').on "change", (e) ->
     el = $('#taxon_products')


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine.coffee
+++ test/engine.coffee
@@ -197,7 +197,7 @@

 test "Integration", ->
   engine = Engine
-    fps: 30
+    FPS: 30

   object = engine.add
     class: "GameObject"


fix faulty heroku progress tests

fix faulty heroku progress tests

--- test/heroku.coffee
+++ test/heroku.coffee
@@ -17,11 +17,7 @@

     project.deploy()
       .progress(progress_spy)
-      .tap ->
-        progress_spy.should.have.been.calledWith('-----> Node.js app detected\n')
-        progress_spy.should.have.been.calledWith('-----> Installing dependencies\n')
-        progress_spy.should.have.been.calledWith('-----> Discovering process types\n')
-        # progress_spy.should.have.been.calledWith('-----> Launching')
+      .tap -> progress_spy.should.have.been.called
       .tap (res) ->
         nodefn.call(request, res.url)
         .tap (r) -> r[0].body.should.match /look ma, it worked/


adding some more unimplemented tests for linode

--- test/linode.coffee
+++ test/linode.coffee
@@ -1,7 +1,15 @@
 should  = require 'should'

 describe 'Linode Instance', ->
-  describe 'when creating an instance', ->
+  #TODO: handle errors properly from linode
+  describe 'when creating an instance with the vanilla config', ->
     it 'calls avail.LinodePlans to find the closest plan to RAM & disk'
     it 'calls linode.create with the correct plan id, term & datacentre id'
     it 'calls linode.update to set the label'
+    # Disks should probably be specified in the config
+    it 'calls linode.disk.createfromdistribution to create a system disk'
+    it 'calls linode.disk.create to create a data disk'
+    it 'calls linode.config.create to set up a Linode config'
+    # Do we need to do this here, could be in a post_create hook
+    it 'calls linode.boot to start the instance'
+


CollaborativeEditorPane: Make it accessible from extended clasess too

--- client/CollaborativeWorkspace/panes/collaborativeeditorpane.coffee
+++ client/CollaborativeWorkspace/panes/collaborativeeditorpane.coffee
@@ -1,6 +1,6 @@
 class CollaborativeEditorPane extends CollaborativePane

-  cdnRoot = "https://koding-cdn.s3.amazonaws.com/codemirror/latest"
+  @cdnRoot = cdnRoot = "https://koding-cdn.s3.amazonaws.com/codemirror/latest"

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


sidebar: remove obselete default prop

sidebar: remove obselete default prop

--- client/activity/lib/components/sidebarchannelssection/index.coffee
+++ client/activity/lib/components/sidebarchannelssection/index.coffee
@@ -9,9 +9,8 @@
 module.exports = class SidebarChannelsSection extends React.Component

   @defaultProps =
-    threads       : immutable.Map()
-    selectedId    : null
-    itemComponent : React.createFactory 'div'
+    threads    : immutable.Map()
+    selectedId : null

   render: ->
     <SidebarSection title="Channels" className="SidebarChannelsSection">


use sentence case

use sentence case

--- client/app/Applications/Account.kdapplication/AppController.coffee
+++ client/app/Applications/Account.kdapplication/AppController.coffee
@@ -17,28 +17,28 @@
       title : "Personal"
       items : [
         { title : "Login & Email",        listHeader: "Email & username",           listType: "username",       id : 10,      parentId : null }
-        { title : "Password & Security",  listHeader: "Password & Security",        listType: "security",       id : 20,      parentId : null }
-        { title : "E-mail Notifications", listHeader: "E-mail Notifications",       listType: "emailNotifications", id : 22,  parentId : null }
-        { title : "Linked accounts",      listHeader: "Your Linked Accounts",       listType: "linkedAccounts", id : 30,      parentId : null }
+        { title : "Password & Security",  listHeader: "Password & security",        listType: "security",       id : 20,      parentId : null }
+        { title : "E-mail Notifications", listHeader: "E-mail notifications",       listType: "emailNotifications", id : 22,  parentId : null }
+        { title : "Linked accounts",      listHeader: "Your linked accounts",       listType: "linkedAccounts", id : 30,      parentId : null }
         { title : "Referrals",            listHeader: "Referrals ",                 listType: "referralSystem", id : 40,      parentId : null }
       ]
     billing :
       title : "Billing"
       items : [
-        { title : "Payment methods",      listHeader: "Your Payment Methods",       listType: "methods",        id : 10,      parentId : null }
-        { title : "Your subscriptions",   listHeader: "Your Active Subscriptions",  listType: "subscriptions",  id : 20,      parentId : null }
-        { title : "Billing history",      listHeader: "Billing History",            listType: "history",        id : 30,      parentId : null }
+        { title : "Payment methods",      listHeader: "Your payment methods",       listType: "methods",        id : 10,      parentId : null }
+        { title : "Your subscriptions",   listHeader: "Your active subscriptions",  listType: "subscriptions",  id : 20,      parentId : null }
+        { title : "Billing history",      listHeader: "Billing history",            listType: "history",        id : 30,      parentId : null }
       ]
     develop :
       title : "Develop"
       items : [
-        { title : "SSH keys",             listHeader: "Your SSH Keys",              listType: "keys",           id : 5,       parentId : null }
-        { title : "Koding Keys",          listHeader: "Your Koding Keys",           listType: "kodingKeys",     id : 10,      parentId : null }
+        { title : "SSH keys",             listHeader: "Your SSH keys",              listType: "keys",           id : 5,       parentId : null }
+        { title : "Koding keys",          listHeader: "Your Koding keys",           listType: "kodingKeys",     id : 10,      parentId : null }
       ]
     danger  :
       title : "Danger"
       items : [
-        { title : "Delete Account",       listHeader: "Danger Zone",                listType: "delete",         id : 5,       parentId : null }
+        { title : "Delete account",       listHeader: "Danger zone",                listType: "delete",         id : 5,       parentId : null }
       ]
       # kites :
       #   title : "Kites"


 Merge branch 'master.trollFix' into 'master'

Master.Troll Fix

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -925,8 +925,12 @@
     callback null, @isExempt

   # returns troll users ids
+  #
+  # Adding a temporary limit of 100. We currently've 24 trolls, by the
+  # time this limit runs out we'll have switched to a scalable model of
+  # filtering troll users. SA
   @getExemptUserIds: (callback)->
-    JAccount.someData {isExempt:true}, {_id:1}, (err, cursor)->
+    JAccount.someData {isExempt:true}, {_id:1, limit:100}, {sort:_id:-1}, (err, cursor)->
       return callback err, null if err
       ids = []
       cursor.each (err, account)->


Social: add function for checking permission

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -1008,6 +1008,8 @@
     else
       callback new KodingError 'Access denied'

+  canEditPost  : permit 'edit posts'
+
   fetchUserByAccountIdOrNickname:(accountIdOrNickname, callback)->

     kallback= (err, account)->


Define Tracker.page method

Define Tracker.page method

--- workers/social/lib/social/models/tracker.coffee
+++ workers/social/lib/social/models/tracker.coffee
@@ -111,6 +111,15 @@
     else mqClient.on 'ready', -> sendMessage()

+  @page = (userId, name, category, properties) ->
+
+    return  unless KONFIG.sendEventsToSegment
+
+    options = { userId, name, category, properties }
+    @addDefaults options
+    analytics.page options
+
+
   @addDefaults = (opts) ->
     opts['env']      = KONFIG.environment
     opts['hostname'] = KONFIG.hostname


EnvironmentMachineItem: Use a proper title when init script run requested

--- client/Environments/views/scene/environmentmachineitem.coffee
+++ client/Environments/views/scene/environmentmachineitem.coffee
@@ -333,6 +333,7 @@
             return

           modal = @openTerminal
+            title         : "Running init script for #{machine.getName()}..."
             command       : command
             readOnly      : yes
             destroyOnExit : no


EnvironmentMachineStateModal: do not hide the create new button ever for koding

--- client/app/lib/providers/environmentsmachinestatemodal.coffee
+++ client/app/lib/providers/environmentsmachinestatemodal.coffee
@@ -495,8 +495,9 @@
       title    = 'Create a new VM'
       callback = 'requestNewMachine'

-      {groupsController} = kd.singletons
-      return  unless groupsController.currentGroupHasStack()
+      if not isKoding()
+        {groupsController} = kd.singletons
+        return  unless groupsController.currentGroupHasStack()

     else if @isManaged
       title    = 'Search for Nodes'


Remove trailing spaces

Remove trailing spaces

--- client/landing/app/CommonViews/comments/newcommentform.coffee
+++ client/landing/app/CommonViews/comments/newcommentform.coffee
@@ -14,7 +14,7 @@
     host = "http://#{location.host}/"
     fallbackUrl = "url(http://www.gravatar.com/avatar/#{profile.hash}?size=30&d=#{encodeURIComponent(host + '/images/defaultavatar/default.avatar.30.png')})"

-    @addSubView commenterAvatar = new KDCustomHTMLView
+    @addSubView commenterAvatar = new KDCustomHTMLView
       tagName : "span"
       partial : ""

@@ -28,14 +28,14 @@
       # autogrow    : yes
       validate    :
         # event       : "keyup"
-        rules       :
-          required    : yes
+        rules       :
+          required    : yes
         messages    :
           required    : "Please type a comment..."
       callback    : @commentInputReceivedEnter

     @attachListeners()
-
+
   attachListeners:->
     @listenTo
       KDEventTypes:       "Focus"
@@ -56,7 +56,7 @@

   resetCommentField:()->
     @getDelegate().handleEvent type : "CommentViewShouldReset"
-
+
   otherCommentInputReceivedFocus:(instance)->
     if instance isnt @commentInput
       commentForm = @commentInput.getDelegate()
@@ -73,16 +73,16 @@
       fromUnixTime = Date.parse firstCommentTimestamp
     else
       fromUnixTime = Date.parse 1e7
-
+
     callback = (err,comments)=>
       @makeCommentFieldActive()
-
+
     list.propagateEvent KDEventType : "CommentInputReceivedFocus",{fromUnixTime,callback}
     no

   commentInputReceivedBlur:()->
     if @commentInput.getValue() is ""
-      @resetCommentField()
+      @resetCommentField()
     no

   commentInputReceivedEnter:(instance,event)=>


Actually always prevent the scroll propagation, as onRender is overridden in AutoCompleteSearchFactRelationsView

--- core/app/backbone/views/auto_complete/search_list_view.coffee
+++ core/app/backbone/views/auto_complete/search_list_view.coffee
@@ -7,7 +7,10 @@

   itemViewOptions: => query: @model.get('text')

-  onRender: -> @$el.preventScrollPropagation()
+  constructor: ->
+    super
+
+    @on 'render', -> @$el.preventScrollPropagation()

   showEmptyView: -> @$el.hide()
   closeEmptyView: -> @$el.show()


tweak

tweak

--- app/assets/javascripts/initialization.js.coffee
+++ app/assets/javascripts/initialization.js.coffee
@@ -13,8 +13,9 @@
   $(".alert").delay(5000).fadeOut("slow")

   selected = $(".radio-collection span").index($(".radio-collection input:checked").closest("span"))
-  for s in [0..selected]
-    $(".radio-collection span:eq("+s+") input").addClass("active")
+  if selected > -1
+    for s in [0..selected]
+      $(".radio-collection span:eq("+s+") input").addClass("active")
   $(".radio-collection input").on("change", -> (
     selected = $(".radio-collection span").index($(this).closest("span"))
     $(".radio-collection input.active").removeClass("active")


scrolling to Zepto object instead of el offset

--- app/assets/javascripts/mobile/_mobile.js.coffee
+++ app/assets/javascripts/mobile/_mobile.js.coffee
@@ -184,7 +184,7 @@
         .submit((e) ->
             if ($ @).find('.unhappy').length
                 e.preventDefault()
-                scrollTo(($ @).find('.unhappy').offset().top))
+                scrollTo(($ @).find('.unhappy').first()))

 if gon.page is 'orders_new' or gon.page is 'orders_edit' or gon.page is 'orders_create'
     $ ->
@@ -220,7 +220,7 @@
                         exp_year: $credit_card_expiration_year.val()
                     }, stripeResponseHandler)
                 else
-                    scrollTo(($ @).find('.unhappy').offset().top)
+                    scrollTo(($ @).find('.unhappy').first())

 render_nested_product_attribute_input = ($el) ->
@@ -272,5 +272,5 @@
             .submit((e) ->
                 if ($ @).find('.unhappy').length
                     e.preventDefault()
-                    scrollTo(($ @).find('.unhappy').offset().top))
-
+                    scrollTo(($ @).find('.unhappy').first()))
+


removed button for default map styles

removed button for default map styles

--- app/assets/javascripts/views/map_view.js.coffee
+++ app/assets/javascripts/views/map_view.js.coffee
@@ -62,7 +62,7 @@
       center: @currentAssignment.get('latLng')
       zoom: 14
       mapTypeControlOptions:
-        mapTypeIds: [google.maps.MapTypeId.ROADMAP, 'map_style']
+        mapTypeIds: ['map_style']
     }

     @map.mapTypes.set 'map_style', @styledMap()


Disable closing of modal on pressing escape key.

--- app/assets/javascripts/voluntary/base.js.coffee
+++ app/assets/javascripts/voluntary/base.js.coffee
@@ -97,7 +97,7 @@

     $.ajax(url: $this.attr('href'), type: "GET", dataType: "html").success (data) ->
       $('#bootstrap_modal').html(data)
-      $('#bootstrap_modal').modal('show')
+      $('#bootstrap_modal').modal(show: true, keyboard: false)

     event.preventDefault()


Update setup-boostrap.coffee

Update setup-boostrap.coffee

--- app/instance-initializers/setup-boostrap.coffee
+++ app/instance-initializers/setup-boostrap.coffee
@@ -3,7 +3,7 @@

 initialize = (application) ->
   try
-    container = Ember.__loader.require('container')
+    container = Ember.__loader.require('@ember/-internals/container')
     options = application.lookup(container.privatize(['template-compiler:main']))
     options.resolver.resolver.builtInModifiers['bs-bind-tooltip'] = {
       manager: new bsBindTooltip()


removed trace statements

removed trace statements

--- bokehjs/src/coffee/renderer/tile/tile_source.coffee
+++ bokehjs/src/coffee/renderer/tile/tile_source.coffee
@@ -13,7 +13,6 @@
     @normalize_case()

   defaults: =>
-    console.warn("TILE SOURCE DEFAULT CALLED")
     return _.extend {}, super(), {
       url : ''
       tile_size : 256
@@ -28,7 +27,6 @@
   }

   constructor: (options={}) ->
-    console.warn("TILE SOURCE CONSTRUCTOR CALLED")
     super
     @utils = new tile_utils.ProjectionUtils()
     @pool = new ImagePool()


Change 'array of type objects' spec to include all current types.

--- spec/hashparamsSpec.coffee
+++ spec/hashparamsSpec.coffee
@@ -54,15 +54,6 @@
                     params = new HashParams('foreground:scalar', 'background:scalar')
                 it 'has empty strings for values.foreground and values.background', ->
                     expect(params.values).toEqual {foreground: '', background: ''}
-            describe 'array of types.scalar', ->
-                params = null
-                beforeEach ->
-                    params = new HashParams([
-                        new HashParams.types.scalar('foreground'),
-                        new HashParams.types.scalar('background')
-                    ])
-                it 'has empty strings for values.foreground and values.background', ->
-                    expect(params.values).toEqual {foreground: '', background: ''}
         describe 'tags and authors as', ->
             describe 'types.set', ->
                 params = null
@@ -80,6 +71,15 @@
                     params = new HashParams('tags:set', 'authors:set')
                 it 'has empty sets for values.tags and values.authors', ->
                     expect(params.values).toEqual {tags: setOf(), authors: setOf()}
+        describe 'array of type objects', ->
+            params = null
+            beforeEach ->
+                params = new HashParams([
+                    new HashParams.types.scalar('foreground'),
+                    new HashParams.types.set('tags')
+                ])
+            it 'has empty string for values.foreground and empty set for values.tags', ->
+                expect(params.values).toEqual {foreground: '', tags: setOf()}
         describe 'invalid value', ->
             expectInvalidParameter = (value) ->
                 expect(-> new HashParams(value)).toThrowError /Invalid parameter definition/


Fix / clean up specs

Fix / clean up specs

--- spec/pane-view-spec.coffee
+++ spec/pane-view-spec.coffee
@@ -13,9 +13,14 @@
     @deserialize: ({id, text}) -> new TestView({id, text})
     @content: ({id, text}) -> @div class: 'test-view', id: id, tabindex: -1, text
     initialize: ({@id, @text}) ->
+      @emitter = new Emitter
     serialize: -> { deserializer: 'TestView', @id, @text }
     getUri: -> @id
     isEqual: (other) -> other? and @id == other.id and @text == other.text
+    changeTitle: ->
+      @emitter.emit 'did-change-title', 'title'
+    onDidChangeTitle: (callback) ->
+      @emitter.on 'did-change-title', callback

   beforeEach ->
     atom.deserializers.add(TestView)
@@ -146,39 +151,32 @@
       expect(view1.data('preservative')).toBe 1234

   describe "when the title of the active item changes", ->
-    it "emits pane:active-item-title-changed", ->
-      activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler")
-      pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler
-
-      expect(pane.getActiveItem()).toBe view1
-
-      view2.trigger 'title-changed'
-      expect(activeItemTitleChangedHandler).not.toHaveBeenCalled()
-
-      view1.trigger 'title-changed'
-      expect(activeItemTitleChangedHandler).toHaveBeenCalled()
-      activeItemTitleChangedHandler.reset()
-
-      pane.activateItem(view2)
-      view2.trigger 'title-changed'
-      expect(activeItemTitleChangedHandler).toHaveBeenCalled()
-
-    describe 'when there is a onDidChangeTitle method', ->
+    describe 'when there is no onDidChangeTitle method', ->
       beforeEach ->
-        TestView::changeTitle = ->
-          @emitter.emit 'did-change-title', 'title'
-        TestView::onDidChangeTitle = (callback) ->
-          @emitter.on 'did-change-title', callback
-
-        view1.emitter = new Emitter
-        view2.emitter = new Emitter
-
-        view1.id = 1
-        view2.id = 2
+        view1.onDidChangeTitle = null
+        view2.onDidChangeTitle = null

         pane.activateItem(view2)
         pane.activateItem(view1)

+      it "emits pane:active-item-title-changed", ->
+        activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler")
+        pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler
+
+        expect(pane.getActiveItem()).toBe view1
+
+        view2.trigger 'title-changed'
+        expect(activeItemTitleChangedHandler).not.toHaveBeenCalled()
+
+        view1.trigger 'title-changed'
+        expect(activeItemTitleChangedHandler).toHaveBeenCalled()
+        activeItemTitleChangedHandler.reset()
+
+        pane.activateItem(view2)
+        view2.trigger 'title-changed'
+        expect(activeItemTitleChangedHandler).toHaveBeenCalled()
+
+    describe 'when there is a onDidChangeTitle method', ->
       it "emits pane:active-item-title-changed", ->
         activeItemTitleChangedHandler = jasmine.createSpy("activeItemTitleChangedHandler")
         pane.on 'pane:active-item-title-changed', activeItemTitleChangedHandler


Reword specs

Reword specs

Items are shown 'in' a pane instead of 'on' the pane

--- spec/root-view-spec.coffee
+++ spec/root-view-spec.coffee
@@ -134,7 +134,7 @@

       window.keymap.bindKeys('*', 'x': 'foo-command')

-    describe "when a keydown event is triggered on the RootView", ->
+    describe "when a keydown event is triggered in the RootView", ->
       it "triggers matching keybindings for that event", ->
         event = keydownEvent 'x', target: rootView[0]

@@ -246,7 +246,7 @@
         initialItemCount = activePane.getItems().length

       describe "when called with no path", ->
-        it "opens an edit session with an empty buffer as an item on the active pane and focuses it", ->
+        it "opens an edit session with an empty buffer as an item in the active pane and focuses it", ->
           editSession = rootView.open()
           expect(activePane.getItems().length).toBe initialItemCount + 1
           expect(activePane.activeItem).toBe editSession
@@ -255,7 +255,7 @@

       describe "when called with a path", ->
         describe "when the active pane already has an edit session item for the path being opened", ->
-          it "shows the existing edit session on the pane", ->
+          it "shows the existing edit session in the pane", ->
             previousEditSession = activePane.activeItem

             editSession = rootView.open('b')
@@ -333,7 +333,7 @@
         activePane = rootView.getActivePane()

       describe "when called with no path", ->
-        it "opens an edit session with an empty buffer as an item on the active pane and focuses it", ->
+        it "opens an edit session with an empty buffer as an item in the active pane and focuses it", ->
           editSession = null

           waitsForPromise ->
@@ -347,7 +347,7 @@

       describe "when called with a path", ->
         describe "when the active pane already has an item for the given path", ->
-          it "shows the existing edit session on the pane", ->
+          it "shows the existing edit session in the pane", ->
             previousEditSession = activePane.activeItem

             editSession = null


Be more explicit about special case

Be more explicit about special case

--- apps/home_2/view_helpers.coffee
+++ apps/home_2/view_helpers.coffee
@@ -2,7 +2,7 @@

 module.exports =
   viewAllUrl: (module) ->
-    if module.context?.artist?
+    if module.key is 'related_artists'
       return module.context.artist.href

     return module.context.href if module.context


Only check panes in center

Only check panes in center

atom/atom#13977 adds multiple pane locations and updates the
`atom.workspace.getPanes()` API to return the panes from all of them.
This fixes the resultant test failures by only checking the panes in
the center.

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -29,6 +29,9 @@
   return dirPath

 getPaneFileName = (index) -> "test-file-#{index}.txt"
+
+# TODO: Remove this after atom/atom#13977 lands in favor of unguarded `getCenter()` calls
+getCenter = -> atom.workspace.getCenter?() ? atom.workspace

 describe "TreeView", ->
   [treeView, path1, path2, root1, root2, sampleJs, sampleTxt, workspaceElement] = []
@@ -1321,7 +1324,7 @@
           atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry-right')

     it "should have opened all windows", ->
-      expect(atom.workspace.getPanes().length).toBe 9
+      expect(getCenter().getPanes().length).toBe 9

     [0..8].forEach (index) ->
       paneNumber = index + 1
@@ -1335,7 +1338,7 @@
               atom.commands.dispatch treeView.element, command

           it "opens the file in pane #{paneNumber} and focuses it", ->
-            pane = atom.workspace.getPanes()[index]
+            pane = getCenter().getPanes()[index]
             item = atom.workspace.getActivePaneItem()
             expect(atom.views.getView(pane)).toHaveFocus()
             expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt')
@@ -1352,7 +1355,7 @@
           atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry-right')

     it "should have opened all windows", ->
-      expect(atom.workspace.getPanes().length).toBe 9
+      expect(getCenter().getPanes().length).toBe 9

     [0..8].forEach (index) ->
       paneNumber = index + 1
@@ -1368,7 +1371,7 @@
                 atom.commands.dispatch treeView.element, command

             it "opens the file in pane #{paneNumber} and focuses it", ->
-              pane = atom.workspace.getPanes()[index]
+              pane = getCenter().getPanes()[index]
               item = atom.workspace.getActivePaneItem()
               expect(atom.views.getView(pane)).toHaveFocus()
               expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve(fileName)
@@ -3353,21 +3356,21 @@
           atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry-right')

       it "should have opened both panes", ->
-        expect(atom.workspace.getPanes().length).toBe 2
+        expect(getCenter().getPanes().length).toBe 2

       describe "tree-view:open-selected-entry", ->
         beforeEach ->
           atom.config.set "tree-view.alwaysOpenExisting", true
         describe "when the first pane is focused, a file is opened that is already open in the second pane", ->
           beforeEach ->
-            firstPane = atom.workspace.getPanes()[0]
+            firstPane = getCenter().getPanes()[0]
             firstPane.activate()
             selectEntry 'tree-view.txt'
             waitsForFileToOpen ->
               atom.commands.dispatch treeView.element, "tree-view:open-selected-entry"

           it "opens the file in the second pane and focuses it", ->
-            pane = atom.workspace.getPanes()[1]
+            pane = getCenter().getPanes()[1]
             item = atom.workspace.getActivePaneItem()
             expect(atom.views.getView(pane)).toHaveFocus()
             expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt')
@@ -3380,7 +3383,7 @@
         describe "when the first pane is focused, a file is opened that is already open in the second pane", ->
           firstPane = null
           beforeEach ->
-            firstPane = atom.workspace.getPanes()[0]
+            firstPane = getCenter().getPanes()[0]
             firstPane.activate()
             selectEntry 'tree-view.txt'
             waitsForFileToOpen ->
@@ -3398,7 +3401,7 @@
         describe "when core.allowPendingPaneItems is set to true (default)", ->
           firstPane = activePaneItem = null
           beforeEach ->
-            firstPane = atom.workspace.getPanes()[0]
+            firstPane = getCenter().getPanes()[0]
             firstPane.activate()

             treeView.focus()
@@ -3423,7 +3426,7 @@
         activePaneItem = null

         beforeEach ->
-          firstPane = atom.workspace.getPanes()[0]
+          firstPane = getCenter().getPanes()[0]
           firstPane.activate()

           treeView.focus()
@@ -3440,7 +3443,7 @@
         it "opens the file and focuses it", ->

           expect(activePaneItem.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt')
-          expect(atom.views.getView(atom.workspace.getPanes()[1])).toHaveFocus()
+          expect(atom.views.getView(getCenter().getPanes()[1])).toHaveFocus()

   describe "Dragging and dropping root folders", ->
     [alphaDirPath, gammaDirPath, thetaDirPath, etaDirPath] = []


Correct indentation

Correct indentation

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -463,7 +463,7 @@
         treeView.trigger 'core:move-to-bottom'
         expect(treeView.root.find('.entry:last')).toHaveClass 'selected'

-   describe "core:page-up", ->
+    describe "core:page-up", ->
       it "scrolls up a page", ->
         treeView.height(5)
         treeView.attachToDom()


De-pending repeated typing test

De-pending repeated typing test

--- spec/vim-state-spec.coffee
+++ spec/vim-state-spec.coffee
@@ -173,19 +173,25 @@
       expect(editorView).not.toHaveClass 'insert-mode'
       expect(editorView).not.toHaveClass 'visual-mode'

-    xit "allows undoing an entire batch of typing", ->
-      type('a')
-      type('b')
-      type('c')
-      editorView.trigger('vim-mode:activate-command-mode')
-      editorView.trigger('vim-mode:insert-after')
-      type('d')
-      type('e')
-      type('f')
-      editorView.trigger('vim-mode:activate-command-mode')
+    it "allows undoing an entire batch of typing", ->
+      keydown 'i'
+      editor.setText("abc")
+      keydown 'escape'
+      keydown 'a'
+      editor.setText("abcdef")
+      keydown 'escape'
       expect(editor.getText()).toBe "abcdef"
-      editorView.trigger('core:undo')
+      keydown 'u'
       expect(editor.getText()).toBe "abc"
+      keydown 'u'
+      expect(editor.getText()).toBe ""
+
+    it "allows repeating typing", ->
+      keydown 'i'
+      editor.setText("abc")
+      keydown 'escape'
+      keydown '.'
+      editor.setText("ababcc")

   describe "visual-mode", ->
     beforeEach -> keydown('v')


fix: error discovered during refactor

fix: error discovered during refactor

--- coffee/observer.coffee
+++ coffee/observer.coffee
@@ -162,7 +162,7 @@
     bestDifference = null

     angleOffsets = [0.0]
-    angleOffsets.push Math.PI/2 if tessellation.group.n % 2 is 1
+    angleOffsets.push Math.PI/2 if @tessellation.group.n % 2 is 1
     for additionalAngle in angleOffsets
       for i in [0...2*@tessellation.group.n]
         angle = dAngle*i + additionalAngle


Change text in advanced greedy theory

Change text in advanced greedy theory

--- server/materials/data/topics/greedy_advanced.coffee
+++ server/materials/data/topics/greedy_advanced.coffee
@@ -8,7 +8,7 @@
 export default greedy_2 = () ->
     return {
         topic: topic("Жадные алгоритмы", "Задачи на жадность", [
-            label("
Довольно продвинутая теория (про коды Хаффмана можете не читать, можете прочитать \"для сведения\"). Еще вспомните теорию с уровня 2Б, и можете еще погуглить.
"),
+            label("
Очень продвинутая теория (вообще для задач ниже, да и в принципе по жизни, эта теория не особо нужна, можете прочитать \"для сведения\", но в принципе понимать про матроиды, а также про коды Хаффмана полезно). Еще вспомните теорию с уровня 2Б, и можете еще погуглить.
"),
             problem(3356),
             problem(3380),
             problem(3589),


Little refactoring

Little refactoring

--- cmd/list.coffee
+++ cmd/list.coffee
@@ -6,8 +6,9 @@
   storage.readAll ->
     series = storage.all()
     if series.length
-      storage.all().forEach (s) ->
-        printSeries(s['Data']['Series'][0])
+      storage.all()
+             .map((s) -> s['Data']['Series'][0])
+             .forEach(printSeries)
     else
       return error("Your database is empty")


specify that time is PM for 'Eat from all cooked foods'

--- source/javascripts/index/holidayTableFactory.coffee
+++ source/javascripts/index/holidayTableFactory.coffee
@@ -104,7 +104,7 @@
     if hebrewDate.hasHadlakatNerot() || hebrewDate.hasHadlakatNerotAfterSetHaKochabim()
       rows.push(description: "הַדְלַקָת נֵרוֹת", time: hadlakatNerotTime(hebrewDate, zmanim))
       if hebrewDate.yomYobThatWePrayAtPlag() && !hebrewDate.isErebShabbat() && !hebrewDate.isShabbat()
-        rows.push(description: "Eat from all cooked foods before #{zmanim.sunset().format('h:mm')}")
+        rows.push(description: "Eat from all cooked foods before #{zmanim.sunset().format('h:mm A')}")
     if hebrewDate.isErebHoshanaRaba()
       rows.push(description: "תִּקוּן לֵיל הוֹשַׁעֲנָא רַבָּא", time: moment(gregorianDate).hour(24).minute(0).format('h:mm A'), tomorrow: true)
     if hebrewDate.isErebShabuot()


Client: Activity: Added setChannelParticipantsSelectedIndex action to user.coffee

--- client/activity/lib/flux/chatinput/actions/user.coffee
+++ client/activity/lib/flux/chatinput/actions/user.coffee
@@ -128,6 +128,18 @@
   { RESET_CHANNEL_PARTICIPANTS_SELECTED_INDEX } = actionTypes
   dispatch RESET_CHANNEL_PARTICIPANTS_SELECTED_INDEX

+
+###*
+ * Action to set selected index of chat input users
+ *
+ * @param {number} index
+###
+setChannelParticipantsSelectedIndex = (index) ->
+
+  { SET_CHANNEL_PARTICIPANTS_SELECTED_INDEX } = actionTypes
+  dispatch SET_CHANNEL_PARTICIPANTS_SELECTED_INDEX, { index }
+
+
 ###*
  * Action to increment users selected index
 ###
@@ -157,6 +169,7 @@
   setChannelParticipantsInputQuery
   unsetChannelParticipantsInputQuery
   resetChannelParticipantsSelectedIndex
+  setChannelParticipantsSelectedIndex
   moveToNextChannelParticipantIndex
   moveToPrevChannelParticipantIndex
 }


Sidebar: Clear workspace list

Sidebar: Clear workspace list

--- client/app/lib/activity/sidebar/activitysidebar.coffee
+++ client/app/lib/activity/sidebar/activitysidebar.coffee
@@ -649,6 +649,8 @@

       return kd.warn err  if err

+      environmentDataProvider.clearWorkspaces machine
+

   removeMachineNode: (machine) ->


LoginModal: fix confirm email address message

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -336,7 +336,7 @@

   checkUserStatus = (user, account, callback)->
     if user.status is 'unconfirmed' and KONFIG.emailConfirmationCheckerWorker.enabled
-      error = createKodingError "CONFIRMATION_WAITING"
+      error = createKodingError "You should confirm your email address"
       error.code = 403
       error.data or= {}
       error.data.name = account.profile.firstName or account.profile.nickname


JUser: remove deprecated method finishRegistration

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -98,7 +98,6 @@
         getSSHKeys              : (signature Function)
         authenticateWithOauth   : (signature Object, Function)
         unregister              : (signature String, Function)
-        finishRegistration      : (signature Object, Function) # DEPRECATED ~ GG
         verifyPassword          : (signature Object, Function)
         verifyByPin             : (signature Object, Function)

@@ -1260,14 +1259,6 @@
     daisy queue

-  # DEPRECATED
-  @finishRegistration: secure (client, formData, callback) ->
-    { sessionToken: clientId, clientIP } = client
-    console.warn "DEPRECATED JUser::finishRegistration called from", \
-                 { clientIP, sessionToken }
-    callback null
-
-
   @removeUnsubscription:({email}, callback)->
     JUnsubscribedMail = require '../unsubscribedmail'
     JUnsubscribedMail.one {email}, (err, unsubscribed)->


Account Settings: Auto focus to first input when the modal is displayed

--- client/account/lib/views/accounteditusername.coffee
+++ client/account/lib/views/accounteditusername.coffee
@@ -30,6 +30,7 @@
   constructor: (options = {}, data) ->

     super options, data
+
     @initViews()

@@ -352,6 +353,9 @@
       @userProfileForm.fields.verifyEmail.show()
       @userProfileForm.inputs.verifyEmail.show()

+    @userProfileForm.inputs.firstName.setFocus()
+
+
   getAvatarOptions: ->
     tagName       : 'figure'
     cssClass      : 'AppModal--account-avatar'


payment controller: introducing #createUpgradeWorkflow

This workflow is for upgrading/downgrading a subscription to a given plan independently of a product purchase, e.g. from the account screen

--- client/app/MainApp/payment/paymentcontroller.coffee
+++ client/app/MainApp/payment/paymentcontroller.coffee
@@ -110,6 +110,17 @@

     return form

+  createUpgradeWorkflow: (tag, forceUpgrade = no) ->
+    upgradeForm = @createUpgradeForm tag, forceUpgrade
+
+    workflow = new PaymentWorkflow
+      productForm: upgradeForm
+      confirmForm: new KDView partial: 'howdy'
+
+    workflow.enter()
+
+    workflow
+
   debitSubscription: (subscription, pack, callback) ->
     subscription.debit pack, (err, nonce) =>
       return  if KD.showError err


Minor fix

Minor fix

--- demo/lib/files.collection.coffee
+++ demo/lib/files.collection.coffee
@@ -64,13 +64,16 @@
                   # Generate downloadable link
                   client.makeUrl stat.path, {long: true, downloadHack: true}, (error, xml) -> bound ->
                     # Store downloadable in file's meta object
-                    upd = $set: {}
-                    upd['$set']["versions.#{version}.meta.pipeFrom"] = xml.url
-                    upd['$set']["versions.#{version}.meta.pipePath"] = stat.path
-                    Collections.files.collection.update {_id: fileRef._id}, upd, (error) ->
-                      if error
-                        console.error error
-                      return
+                    if error
+                      console.error error
+                    else if xml
+                      upd = $set: {}
+                      upd['$set']["versions.#{version}.meta.pipeFrom"] = xml.url
+                      upd['$set']["versions.#{version}.meta.pipePath"] = stat.path
+                      Collections.files.collection.update {_id: fileRef._id}, upd, (error) ->
+                        if error
+                          console.error error
+                        return
                     return
                 return
             return


forgot to turn off broker in dev

forgot to turn off broker in dev

--- config/main.dev.coffee
+++ config/main.dev.coffee
@@ -32,8 +32,8 @@
     queueName   : socialQueueName+'web'
     watch       : yes
   mongo         : mongo
-  runGoBroker   : yes
-  watchGoBroker : yes
+  runGoBroker   : no
+  watchGoBroker : no
   compileGo     : no
   buildClient   : yes
   misc          :


add a missing group description

add a missing group description

--- content/groups.coffee
+++ content/groups.coffee
@@ -43,6 +43,10 @@
     image: 'groups/geological-society.jpg'
     alt: "The Quarterly Journal of the Geological Society of London: illustrated frontispiece"
     description: 'Running between 1845-1971, this journal is structured in a similar fashion to the Wiltshire Archaeological and Natural History Magazine and the Midland Naturalist, acting as the scholarly journal for the Geological Society of London.'
+  mobot:
+    image: 'groups/gardeners-chronicle.jpg'
+    alt: "Gardeners’ Chronicle: illustrated frontispiece"
+    description: 'One of the most influential and well known periodicals for gardeners and naturalists, which was regularly used by Charles Darwin as a place to collect new facts, and to ask questions of fellow naturalists. Running between 1841-1967, this was a periodical designed to incorporate the middle class fashion for maintaining home gardens while simultaneously publishing new observations of plants and animals.'
   mobot2:
     image: 'groups/botany-miscellany.jpg'
     alt: "Botany Miscellany: illustrated frontispiece"


Fix `go to line start` shortcut

Fix `go to line start` shortcut

--- client/ide/views/shortcutsview/shortcutsview.coffee
+++ client/ide/views/shortcutsview/shortcutsview.coffee
@@ -76,10 +76,10 @@
       'Go to left'             : 'Ctrl-B'
       'Go to right'            : 'Ctrl-F'
       'Go to line'             : 'Meta-G'
+      'Go to line start'       : { Others: 'Alt-Left',       Apple: 'Meta-Left' }
+      'Go to line start'       : { Others: 'Home',           Apple: 'Ctrl-A'    }
       'Go to line end'         : { Others: 'Alt-Right',      Apple: 'Meta-Right'}
       'Go to line end'         : { Others: 'End',            Apple: 'Ctrl-E'    }
-      'Go to line start'       : { Others: 'Alt-Left',       Apple: 'Meta-Left' }
-      'Go to line start'       : { Others: 'End',            Apple: 'Ctrl-A'    }
       'Go to page down'        : 'Ctrl-V'
       'Go to word left'        : { Others: 'Ctrl-Left',      Apple: 'Alt-Left'  }
       'Go to word right'       : { Others: 'Ctrl-Right',    Apple: 'Alt-Right' }


Sort client side because of sequelize bug

--- public/src/queuelist.cjsx
+++ public/src/queuelist.cjsx
@@ -69,7 +69,8 @@
   componentDidMount: ->
     @setState userId: helpers.getUserId()
     $.get '/api/queues', (data) =>
-      @setState queues: data
+      @setState queues: data.sort (queue1, queue2) ->
+        queue1.displayName < queue2.displayName

   render: ->
     queues = @state.queues.map (queue, idx, arr) =>


Added LogMapper, LogAxis, LogTicker and Formatter

--- bokehjs/src/coffee/common/base.coffee
+++ bokehjs/src/coffee/common/base.coffee
@@ -11,6 +11,7 @@

   "mapper/1d/categorical_mapper",
   "mapper/1d/linear_mapper",
+  "mapper/1d/log_mapper"
   "mapper/2d/grid_mapper",
   "mapper/color/linear_color_mapper",

@@ -25,6 +26,7 @@
   "renderer/guide/datetime_axis",
   "renderer/guide/grid",
   "renderer/guide/linear_axis",
+  "renderer/guide/log_axis",
   "renderer/overlay/box_selection",

   "source/column_data_source",
@@ -34,6 +36,8 @@
   "ticking/adaptive_ticker",
   "ticking/basic_tick_formatter",
   "ticking/basic_ticker",
+  "ticking/log_tick_formatter",
+  "ticking/log_ticker",
   "ticking/categorical_tick_formatter",
   "ticking/categorical_ticker",
   "ticking/composite_ticker",
@@ -108,6 +112,7 @@

     Glyph:                    'renderer/glyph/glyph_factory'
     LinearAxis:               'renderer/guide/linear_axis'
+    LogAxis:                  'renderer/guide/log_axis'
     CategoricalAxis:          'renderer/guide/categorical_axis'
     DatetimeAxis:             'renderer/guide/datetime_axis'
     Grid:                     'renderer/guide/grid'
@@ -121,7 +126,9 @@
     AdaptiveTicker:           'ticking/adaptive_ticker'
     BasicTicker:              'ticking/basic_ticker'
     BasicTickFormatter:       'ticking/basic_tick_formatter'
-    CategoricalTicker:        'ticking/categorical_ticker'
+    LogTicker:                'ticking/log_ticker'
+    LogTickFormatter:         'ticking/log_tick_formatter'
+    CategoricalTicker:        'ticking/categGorical_ticker'
     CategoricalTickFormatter: 'ticking/categorical_tick_formatter'
     CompositeTicker:          'ticking/composite_ticker'
     DatetimeTicker:           'ticking/datetime_ticker'
@@ -171,9 +178,9 @@
   mod_cache = {}
   collection_overrides = {}
   Collections = (typename) ->
-   if collection_overrides[typename]
-     return collection_overrides[typename]
-
+    if collection_overrides[typename]
+      return collection_overrides[typename]
+    console.log locations
     if not locations[typename]
       throw "./base: Unknown Collection #{typename}"


Add TODO on ActionsManager#_redirect about missing analytics session

See: 9dd1d57a
Due to several incompatibilities discovered with some of the shop
backends, we stopped appending the `analytics_session` query parameter.
Without this parameter, our GetParamEngine module is currently non-functional.
We have to figure out a way to unobtrusively pass this parameter,
perhaps using an obfuscated param name.

--- src/actions_manager.coffee
+++ src/actions_manager.coffee
@@ -69,7 +69,9 @@
         ## TODO: THROW ERROR ON REJECT
         @reporter.sendBeacon(url, payload).then => cb and cb(analytics_session)

-    _redirect: (url, analytics_session) ->
+    _redirect: (url, _analytics_session) ->
+      # TODO: Find a way to safely append analytics session as query param
+      # for GetParamEngine to work, see: 9dd1d57a
       Settings.redirectTo url

     # TODO: implement multiple actions per beacon maybe??


Hotfix backbone rel handler

Hotfix backbone rel handler

--- core/app/backbone/contrib/default_click_handler.coffee
+++ core/app/backbone/contrib/default_click_handler.coffee
@@ -8,6 +8,8 @@
     if !$link.attr("target") && $link.attr("rel") == 'backbone' && $link.attr("href") # and there's real link without target
       if navigateTo($link.attr("href")) # and it's routable via backbone
         e.preventDefault() #if we handle clicks, abort browser default.
+      else
+        $link.attr("target", "_blank") # all unhandled clicks: new window.


Reset modal state when closing. Closes #420.

--- core/htdocs_source/src/pods/openxpki/controller.coffee
+++ core/htdocs_source/src/pods/openxpki/controller.coffee
@@ -27,13 +27,11 @@
         return "alert-warning" if level is "warn"
         return "alert-info"

-    setupBootstrap: Em.on "didInsertElement", ->
-        $(".modal.oxi-main-modal").on "hidden.bs.modal", =>
-            @set "model.modal"
-
     autoshowModal: Em.observer "model.modal", ->
         $ ".modal.oxi-main-modal"
         .modal if @get "model.modal" then "show" else "hide"
+        .one "hidden.bs.modal", =>
+            @set "model.modal"

     autoshowErrorModal: Em.observer "model.error", ->
         $ ".modal.oxi-error-modal"


minor changes

minor changes

--- core/JOM.class.coffee
+++ core/JOM.class.coffee
@@ -1,7 +1,7 @@
 # Json Object Model

 # required: Schema Tree && Machine Data && ~Human Data (~ optional)
-# @tree describes how data should be structured (hierarchically)
+# @tree is schema, describes how data should be structured (hierarchically)
 # @human are data to display to user,
 #   e.g: a list of data from a drop down.
 #   animal = ['dog','cat','horse','mouse']
@@ -24,4 +24,6 @@

 class JOM
+  @data = {}
   constructor: (@tree, @machine, @human = {} )->
+    @data = {}


Y U STILL THERE (屮ﾟДﾟ)屮

Y U STILL THERE (屮ﾟДﾟ)屮

--- resources/assets/coffee/react/beatmap-discussions/new-reply.coffee
+++ resources/assets/coffee/react/beatmap-discussions/new-reply.coffee
@@ -76,8 +76,6 @@
         className: "#{bn}__footer"
         div className: "#{bn}__actions",
           div className: "#{bn}__actions-group",
-            @renderCancelButton()
-
             if @canResolve() && !@props.discussion.resolved
               @renderReplyButton
                 text: osu.trans('common.buttons.reply_resolve')


Fixed issue with empty post content

Fixed issue with empty post content

--- lib/ecrire/app/assets/javascripts/admin/editor/content.coffee
+++ lib/ecrire/app/assets/javascripts/admin/editor/content.coffee
@@ -43,6 +43,13 @@
       mutations = new Mutations(observedMutations, @element())
       if mutations.target?
         @updated(mutations)
+
+      if @element().children.length == 0
+        line = @lines('')[0]
+        @element().appendChild(line)
+        cursor = new Editor.Cursor(0)
+        cursor.update(@walker(line), 0)
+

     event = new CustomEvent('Editor:updated', {bubbles: true})
     @element().dispatchEvent(event)


Fix days of week calendar heading setup view

--- modules/date-picker/main/index.coffee
+++ modules/date-picker/main/index.coffee
@@ -171,11 +171,9 @@
   element = hx.select(elem)

   if data is 'days'
-    element.view('.hx-grid-row-heading')
-      .enter (d) ->
-        @classed('hx-grid-row-heading', true)
-        node = @append('div').class('hx-grid')
-        node.node()
+    element
+      .classed 'hx-grid-row-heading', true
+      .view '.hx-grid'
       .update (d) -> @text(d).node()
       .apply(datepicker.localiser.localiseWeek())
   else
@@ -879,4 +877,4 @@
   new DatePicker(selection.node(), options)
   selection

-hx.DatePicker = DatePicker+hx.DatePicker = DatePicker


can't edit team scores

can't edit team scores

--- controllers/teams.coffee
+++ controllers/teams.coffee
@@ -83,7 +83,7 @@
 # update
 app.put '/teams/:id', [m.loadTeam, m.ensureAccess], (req, res, next) ->
   unless req.user.admin
-    delete req.body[attr] for attr in ['slug', 'code', 'search']
+    delete req.body[attr] for attr in ['slug', 'code', 'search', 'scores']
     delete req.body.entry.url if req.body.entry
   _.extend req.team, req.body
   req.team.save (err) ->


old request find fix

old request find fix

--- noddy/service/v2/request.coffee
+++ noddy/service/v2/request.coffee
@@ -24,10 +24,6 @@
     action: () ->
       if r = oab_request.get this.urlParams.rid
         r.supports = API.service.oab.supports(this.urlParams.rid,this.userId) if this.userId
-        others = oab_request.search({url:r.url})
-        if others?
-          for o in others.hits.hits
-            r.other = o._source._id if o._source._id isnt r._id and o._source.type isnt r.type
         return {data: r}
       else
         return 404
@@ -203,6 +199,7 @@
   req.type ?= 'article'
   req.doi = req.url if not req.doi? and req.url? and req.url.indexOf('10.') isnt -1 and req.url.split('10.')[1].indexOf('/') isnt -1
   req.doi = '10.' + req.doi.split('10.')[1] if req.doi? and req.doi.indexOf('10.') isnt 0
+  req.url = req.url[0] if _.isArray req.url
   if req.url? and req.url.indexOf('eu.alma.exlibrisgroup.com') isnt -1
     req.url += (if req.url.indexOf('?') is -1 then '?' else '&') + 'oabLibris=' + Random.id()
     if req.title? and typeof req.title is 'string' and req.title.length > 0 and texist = oab_request.find {title:req.title,type:req.type}


auto preview in initial invocation is only when initial input was given

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -153,7 +153,10 @@
   start: ->
     activatePaneItemInAdjacentPane(@editor, split: settings.get('directionToOpen'))
     @grammar.activate()
-    @setPrompt(@input)
+    if @input
+      @setPrompt(@input)
+    else
+      @withIgnoreChange => @setPrompt(@input)
     @moveToPrompt(startInsert: true)
     @refresh()


improved Chinese Localization

improved Chinese Localization

Signed-off-by: iAladdin <679e1abc26a0ea5d010d9e2ef227195b64a25a15@me.com>

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -315,7 +315,7 @@
     equip: "装备"
     unequip: "取消装备"

-#  buy_gems:
+    buy_gems: "购买宝石"
     few_gems: "几个宝石"
     pile_gems: "一堆宝石"
     chest_gems: "一箱宝石"
@@ -324,7 +324,7 @@
     choose_hero: "请选择您的英雄"
     programming_language: "编程语言"
     programming_language_description: "您希望使用那门编程语言?"
-#    default: "默认"
+    default: "默认"
     experimental: "实验性的"
     python_blurb: "简单而强大, Python是一个伟大的通用编程语言。"
     javascript_blurb: "为web开发而生的语言。"
@@ -350,23 +350,23 @@
     granularity_change_history: "历史记录"

   options:
-#    general_options: "General Options" # Check out the Options tab in the Game Menu while playing a level
+    general_options: "一般设置" # Check out the Options tab in the Game Menu while playing a level
     volume_label: "音量"
     music_label: "音乐"
     music_description: "开/关背景音乐"
     autorun_label: "自动运行"
-#    autorun_description: "Control automatic code execution."
+    autorun_description: "控制是否自动运行代码"
     editor_config: "编辑器配置"
     editor_config_title: "编辑器配置"
-    editor_config_level_language_label: "这个等级的语言"
-#    editor_config_level_language_description: "Define the programming language for this particular level."
+    editor_config_level_language_label: "本关卡编程语言"
+    editor_config_level_language_description: "针对本关设定使用的编程语言。"
     editor_config_default_language_label: "默认编程语言"
     editor_config_default_language_description: "在开始新游戏前确认你要在这个等级的游戏中使用的编程语言。"
-    editor_config_keybindings_label: "按键设置s"
+    editor_config_keybindings_label: "快捷键绑定"
     editor_config_keybindings_default: "默认 (Ace)"
     editor_config_keybindings_description: "在命令编辑器中增加已知的快捷键。"
-#    editor_config_livecompletion_label: "Live Autocompletion"
-#    editor_config_livecompletion_description: "Displays autocomplete suggestions while typing."
+    editor_config_livecompletion_label: "实时代码补全"
+    editor_config_livecompletion_description: "当输入代码时显示代码自动补全建议。"
     editor_config_invisibles_label: "显示空白字符"
     editor_config_invisibles_description: "显示空格或TAB键等空白字符。"
     editor_config_indentguides_label: "显示缩进提示"
@@ -376,8 +376,8 @@

   about:
     why_codecombat: "为什么选择 CodeCombat?"
-    why_paragraph_1: "你想学编程？你不用上课。你需要的是写好多代码，并且享受这个过程。"
-    why_paragraph_2_prefix: "这才是编程的要义。编程必须要好玩。不是"
+    why_paragraph_1: "如果你想学习如何编程，你根本不需要上课。你需要的是写好多代码，并且享受这个过程。"
+    why_paragraph_2_prefix: "这才是编程的要义。编程应该是有趣的过程。不该是"
     why_paragraph_2_italic: "哇又一个奖章诶"
     why_paragraph_2_center: "那种“好玩”，而是"
     why_paragraph_2_italic_caps: "老妈，我得先把这关打完！"
@@ -388,16 +388,16 @@
     press_paragraph_1_link: "成套宣传包"
     press_paragraph_1_suffix: "里的所有材料. 所有商标和图像的使用都不必事先联系我们。"
     team: "团队"
-#    george_title: "CEO"
-#    george_blurb: "Businesser"
-#    scott_title: "Programmer"
-#    scott_blurb: "Reasonable One"
-#    nick_title: "Programmer"
-#    nick_blurb: "Motivation Guru"
-#    michael_title: "Programmer"
-#    michael_blurb: "Sys Admin"
-#    matt_title: "Programmer"
-#    matt_blurb: "Bicyclist"
+    george_title: "CEO"
+    george_blurb: "商人"
+    scott_title: "程序员"
+    scott_blurb: "理性至上"
+    nick_title: "程序员"
+    nick_blurb: "充满动力的大牛"
+    michael_title: "程序员"
+    michael_blurb: "系统管理员"
+    matt_title: "程序员"
+    matt_blurb: "自行车爱好者"

   versions:
     save_version_title: "保存新版本"
@@ -461,22 +461,22 @@
     wizard_color: "巫师 衣服 颜色"

   keyboard_shortcuts:
-    keyboard_shortcuts: "热键"
+    keyboard_shortcuts: "快捷键"
     space: "空格"
     enter: "回车"
     escape: "Esc"
     shift: "Shift"
     run_code: "运行当前代码"
     run_real_time: "实时运行"
-#    continue_script: "Continue past current script."
-#    skip_scripts: "Skip past all skippable scripts."
+    continue_script: "继续运行当前脚本。"
+    skip_scripts: "跳过所有能跳过的脚本。"
     toggle_playback: "继续/暂停按钮"
-#    scrub_playback: "Scrub back and forward through time."
-#    single_scrub_playback: "Scrub back and forward through time by a single frame."
-#    scrub_execution: "Scrub through current spell execution."
-#    toggle_debug: "Toggle debug display."
-#    toggle_grid: "Toggle grid overlay."
-#    toggle_pathfinding: "Toggle pathfinding overlay."
+    scrub_playback: "向前/向后移动一段时间"
+    single_scrub_playback: "向前/向后移动一帧"
+    scrub_execution: "向前/向后移动一句语句"
+    toggle_debug: "显示/关闭调试信息"
+    toggle_grid: "显示/关闭网格提示"
+    toggle_pathfinding: "显示/关闭路径寻找提示"
     beautify: "利用标准编码格式美化你的代码。"
     maximize_editor: "最大化/最小化代码编辑器"
     move_wizard: "在关卡中移动你的巫师角色。"
@@ -527,7 +527,7 @@
     grassy: "草地"
     fork_title: "派生新版本"
     fork_creating: "正在执行派生..."
-#    generate_terrain: "Generate Terrain"
+    generate_terrain: "Generate Terrain"
     more: "更多"
     wiki: "维基"
     live_chat: "在线聊天"
@@ -537,14 +537,14 @@
     level_tab_settings: "设定"
     level_tab_components: "组件"
     level_tab_systems: "系统"
-#    level_tab_docs: "Documentation"
+    level_tab_docs: "文档"
     level_tab_thangs_title: "目前所有物体"
     level_tab_thangs_all: "所有"
     level_tab_thangs_conditions: "启动条件"
     level_tab_thangs_add: "增加物体"
     delete: "删除"
     duplicate: "复制"
-#    rotate: "Rotate"
+    rotate: "旋转"
     level_settings_title: "设置"
     level_component_tab_title: "目前所有组件"
     level_component_btn_new: "创建新的组件"
@@ -577,7 +577,7 @@
 #    achievement_query_misc: "Key achievement off of miscellanea"
 #    achievement_query_goals: "Key achievement off of level goals"
     level_completion: "关卡完成"
-#    pop_i18n: "Populate I18N"
+    pop_i18n: "填写 I18N"

   article:
     edit_btn_preview: "预览"
@@ -611,12 +611,12 @@
     archmage_subscribe_desc: "通过电子邮件获得新的编码机会和公告。"
     artisan_summary_pref: "想要设计 CodeCombat 的关卡吗？人们玩的比我们做的快多了！现在我们的关卡编辑器还很基本，所以做起关卡来会有点麻烦，还会有bug。只要你有制作关卡的灵感，不管是简单的for循环还是"
     artisan_summary_suf: "这种东西，这个职业都很适合你。"
-#    artisan_introduction_pref: "We must construct additional levels! People be clamoring for more content, and we can only build so many ourselves. Right now your workstation is level one; our level editor is barely usable even by its creators, so be wary. If you have visions of campaigns spanning for-loops to"
-#    artisan_introduction_suf: ", then this class might be for you."
-#    artisan_attribute_1: "Any experience in building content like this would be nice, such as using Blizzard's level editors. But not required!"
-#    artisan_attribute_2: "A hankering to do a whole lot of testing and iteration. To make good levels, you need to take it to others and watch them play it, and be prepared to find a lot of things to fix."
-#    artisan_attribute_3: "For the time being, endurance en par with an Adventurer. Our Level Editor is super preliminary and frustrating to use. You have been warned!"
-#    artisan_join_desc: "Use the Level Editor in these steps, give or take:"
+    artisan_introduction_pref: "我们必须设计更多的关卡! 大家为了更多的游戏内容在高声呐喊，但是我们靠自己只能创建这些。现在你的电脑就是一关！我们的关卡编辑器刚刚完成了基本功能，所以创造关卡的时候请小心使用。只要你有制作关卡的灵感，不管是简单的for循环还是"
+    artisan_introduction_suf: "这种东西，这个职业都很适合你。"
+    artisan_attribute_1: "任何类似的创建内容经验都有加分，无论是暴雪的关卡编辑器，但这不是必须的条件。"
+    artisan_attribute_2: "渴望完成全部的测试和迭代。要做一个优秀的关卡，你需要把它给别的玩家观察他们怎么玩，然后时刻准备着找到很多细节去打磨。"
+    artisan_attribute_3: "探险家般的忍耐力。我们的关卡编辑器非常的初级，还会有很多不好用的地方。唔，我已经提前告诉你了~"
+    artisan_join_desc: "如下步骤使用关卡编辑器:"
     artisan_join_step1: "阅读文档."
     artisan_join_step2: "创建一个新关卡 以及探索已经存在的关卡."
     artisan_join_step3: "来我们的 HipChat 聊天室寻求帮助."
@@ -649,7 +649,7 @@
     diplomat_attribute_1: "既会说流利的英语，也熟悉自己的语言。编程是一件很复杂的事情，而要翻译复杂的概念，你必须对两种语言都在行！"
     diplomat_i18n_page_prefix: "你可以在我们的"
     diplomat_i18n_page: "翻译页面"
-    diplomat_i18n_page_suffix: "开始翻译游戏，或者在GibHub我们的页面上进行。"
+    diplomat_i18n_page_suffix: "开始翻译游戏，或者在 GitHub 我们的页面上进行。"
     diplomat_join_pref_github: "在"
     diplomat_github_url: " GitHub "
     diplomat_join_suf_github: "找到你的语言文件 (中文的是: codecombat/app/locale/zh-HNAS.coffee)，在线编辑它，然后提交一个合并请求。同时，选中下面这个复选框来关注最新的国际化开发！"
@@ -720,7 +720,7 @@
 #    tournament_ended: "Tournament ended"
 #    tournament_rules: "Tournament Rules"
 #    tournament_blurb: "Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details"
-#    tournament_blurb_criss_cross: "Win bids, construct paths, outwit opponents, grab gems, and upgrade your career in our Criss-Cross tournament! Check out the details"
+    tournament_blurb_criss_cross: "赢得竞拍，建造道路，智胜对手，夺取宝石，在纵横交错锦标赛中完成生涯晋级! 现在就查看详情！"
 #    tournament_blurb_blog: "on our blog"
     rules: "规则"
     winners: "胜利者"
@@ -745,7 +745,7 @@
     amount_achieved: "数量"
     achievement: "成就"
     category_contributor: "贡献"
-#    category_miscellaneous: "Miscellaneous"
+    category_miscellaneous: "其他"
     category_levels: "等级"
 #    category_undefined: "Uncategorized"
 #    current_xp_prefix: ""
@@ -944,8 +944,8 @@
     done_editing: "完成编辑"
     profile_for_prefix: "关于他的基本资料："
     profile_for_suffix: ""
-#    featured: "Featured"
-#    not_featured: "Not Featured"
+    featured: "被推荐"
+    not_featured: "未入选"
     looking_for: "寻找"
     last_updated: "最后一次更新："
     contact: "联系"


improved Chinese Localization

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -486,7 +486,7 @@
     introduction: "看看下面这些你可以参与的项目，如果有你喜欢的就加入进来吧。 我们期待着与您一起工作。"
     level_editor_prefix: "使用"
     level_editor_suffix: "来创建和编辑关卡。你可以通过这个工具来给你的同学，朋友，兄弟姐妹们设计谜题，或者用于教学或比赛。如果你觉得直接开始建立一个关卡可能非常困难，那么可以先从一个现成（但尚未完成）的关卡开始做起。"
-    thang_editor_prefix: "我们管游戏中的单位叫 '实体'。 利用"
+    thang_editor_prefix: "我们管游戏中的单位叫 '物品'。 利用"
     thang_editor_suffix: "来改良 CodeCombat 中的原材料。让游戏中的东西可以被捡起来扔出去，改变游戏动画的指向，调整一些东西的生命值，或上传您自制的素材。"
     article_editor_prefix: "你在游戏中发现了错误了吗？想要自己设计一些指令吗？来看看我们的"
     article_editor_suffix: "来帮助玩家从游戏中学到更多的知识。"
@@ -516,7 +516,7 @@
   editor:
     main_title: "CodeCombat 编辑器"
     article_title: "指令编辑器"
-    thang_title: "实体编辑器"
+    thang_title: "物品编辑器"
     level_title: "关卡编辑器"
     achievement_title: "目标编辑器"
     back: "后退"
@@ -564,7 +564,7 @@
     new_thang_title: "创建一个新物品类型"
     new_level_title: "创建一个新关卡"
     new_article_title_login: "登录以创建新指令"
-    new_thang_title_login: "登录以创建新实体"
+    new_thang_title_login: "登录以创建新物品"
     new_level_title_login: "登录以创建新关卡"
     new_achievement_title: "创建新目标"
     new_achievement_title_login: "登录以创建新目标"
@@ -787,24 +787,24 @@
     user_schema: "用户模式"
     user_profile: "用户信息"
     patches: "补丁"
-#    patched_model: "Source Document"
-#    model: "Model"
+    patched_model: "源文档"
+    model: "型号"
 #    system: "System"
 #    systems: "Systems"
-#    component: "Component"
-#    components: "Components"
-#    thang: "Thang"
-#    thangs: "Thangs"
+    component: "组件"
+   components: "组件"
+    thang: "物品"
+    thangs: "物品"
 #    level_session: "Your Session"
 #    opponent_session: "Opponent Session"
-#    article: "Article"
+    article: "文章"
     user_names: "用户名"
-#    thang_names: "Thang Names"
+    thang_names: "物品名称"
     files: "文件"
 #    top_simulators: "Top Simulators"
 #    source_document: "Source Document"
-#    document: "Document"
-#    sprite_sheet: "Sprite Sheet"
+    document: "wend"
+    sprite_sheet: "SpriteSheet"
 #    employers: "Employers"
 #    candidates: "Candidates"
 #    candidate_sessions: "Candidate Sessions"


Added test hub for Lineman Run (Express) to shortcut SignalR calls

--- Web/config/server.coffee
+++ Web/config/server.coffee
@@ -1,7 +1,30 @@
 module.exports =
   drawRoutes: (app) =>
-    ###
-    app.get '/api/greeting/:message', (req, res) ->
-      res.json
-        message: "OK, "+req.params.message
-    ###
+    app.get '/signalr/hubs', (req, res) ->
+      res.type('js')
+      res.send 200, testhub()
+
+testhub = () ->
+ '''
+(function ($, window, undefined) {
+  var allSolutions = [];
+  var newSolution = {"positions":[{"row":0,"column":5,"isValid":true},{"row":5,"column":6,"isValid":true},{"row":1,"column":3,"isValid":true},{"row":2,"column":1,"isValid":true},{"row":3,"column":7,"isValid":true},{"row":4,"column":4,"isValid":true},{"row":6,"column":0,"isValid":true},{"row":7,"column":2,isValid:true}],"$$hashKey":"005",hash:"513213744566072"};
+  $.connection.hub.start = function(){
+    return {
+      done: function(fn){return fn();}
+    };
+  }
+  $.connection.solutionsHub = {};
+  $.connection.solutionsHub.client = {};
+  $.connection.solutionsHub.server = {};
+  $.connection.solutionsHub.server.fetchSolutions = function(){
+    return {
+      done: function(fn){
+        return fn(JSON.stringify(allSolutions)); }
+    };
+  }
+  $.connection.solutionsHub.server.requestSolution = function(solution){
+    $.extend(solution, newSolution);
+    $.connection.solutionsHub.client.solutionAvailable(JSON.stringify(solution));
+  }
+}(window.jQuery, window));'''


Un-break maps (still wrong coords though)

--- app/models/subject.coffee
+++ app/models/subject.coffee
@@ -65,15 +65,19 @@
     @constructor.current = @
     @trigger 'select'

-  satelliteImage: -> """
-    //maps.googleapis.com/maps/api/staticmap
-    ?center=#{@coords.join ','}
-    &zoom=17
-    &size=570x400
-    &maptype=hybrid
-    &markers=size:tiny|#{@coords.join ','}
-    &sensor=false
-  """.replace '\n', ''
+  satelliteImage: ->
+    # TODO: Fix lat/lng!
+    coords = ['Serengeti', 'Tanzania']
+
+    """
+      //maps.googleapis.com/maps/api/staticmap
+      ?center=#{coords.join ','}
+      &zoom=8
+      &size=570x400
+      &maptype=hybrid
+      &markers=size:tiny|#{coords.join ','}
+      &sensor=false
+    """.replace '\n', '', 'g'

   talkHref: ->
     "http://talk.snapshotserengeti.org/objects/#{@zooniverseId}"


[Client] Start pagination test within a search module

--- NBlast.Client/tests/viewModels/searchSpec.coffee
+++ NBlast.Client/tests/viewModels/searchSpec.coffee
@@ -11,8 +11,17 @@
 	window.afterEach -> mocker.restore()

 	describe 'When search page is used', ->
+		describe 'When search view pagination is used', ->
+			it 'Should paginate nothing for an empty total', ->
+				# Given
+				sut = new SearchViewModel(1, '*')
+				# When
+				actual = sut.getPages()
+				# Then
+				actual.should.have.length(0)
+
 		describe 'When search view is instantiated', ->
-			it 'Should fails when init params are in wrong format', ->
+			it 'Should fails when init params are invalid', ->
 				# Given
 				# When
 				# Then
@@ -20,3 +29,13 @@
 					.to.throw('page param must be a number')
 				chai.expect( -> new SearchViewModel(1) )
 					.to.throw('expression param must be a string')
+
+			it 'Should initialize with correct data', ->
+				# Given
+				expectedExpression = 'level: up'
+				expectedPage = 10
+				# When
+				sut = new SearchViewModel(expectedPage, expectedExpression)
+				# Then
+				sut.page().should.be.equal(expectedPage)
+				sut.expression().should.be.equal(expectedExpression)


Fix WriteStream API

Fix WriteStream API

--- services/track-changes/test/unit/coffee/DocArchive/MongoAWS.coffee
+++ services/track-changes/test/unit/coffee/DocArchive/MongoAWS.coffee
@@ -39,7 +39,8 @@
 		beforeEach (done) ->
 			@awssdk.config = { update: sinon.stub() }
 			@awssdk.S3 = sinon.stub()
-			@S3S.WriteStream = MemoryStream.createWriteStream
+			@S3S.WriteStream = () ->
+				MemoryStream.createWriteStream()
 			@db.docHistory = {}
 			@db.docHistory.findOne = sinon.stub().callsArgWith(1, null, {"pack":"hello"})


Try to reconnect when the user types in the editor.

--- services/web/public/coffee/ide/connection/ConnectionManager.coffee
+++ services/web/public/coffee/ide/connection/ConnectionManager.coffee
@@ -21,6 +21,8 @@

 			@$scope.$on "editor:change", () =>
 				@lastUpdated = new Date()
+				if !@connected
+					@tryReconnect()

 			@ide.socket = io.connect null,
 				reconnect: false


better centering for tooltips

better centering for tooltips

--- src/plugins/rivets.coffee
+++ src/plugins/rivets.coffee
@@ -30,12 +30,13 @@
 					if $input.is('textarea')
 						top = input_position.top
 					else
-						top = '50%'
-						marginTop = ($(@).outerHeight() / -2) + 'px'
+						top = ($input.outerHeight() - $(@).outerHeight()) / 2
+						top += $input.position().top
+						top = Math.ceil(top)
+						top += 'px'

 					$(@).css({
 						'top': top
-						'margin-top': marginTop
 					})

 					# Fade in message


fix a bug

fix a bug

--- lib/gitCreatePackage/gitCreatePackage-info-view.coffee
+++ lib/gitCreatePackage/gitCreatePackage-info-view.coffee
@@ -94,6 +94,8 @@
             resolve true
           else
             resolve false
+        else
+          reject $.parseJSON(response.body).message

     .then (packageHave) =>
       # console.log packageHave


leave source map file for cache

leave source map file for cache

--- plugin/browserify.coffee
+++ plugin/browserify.coffee
@@ -52,7 +52,7 @@

     # read the generated source map from the file
     sourceMap = fs.readFileSync mapFileName, 'utf8'
-    fs.unlinkSync mapFileName
+    # leave source map file so it's cached

     # now that we have the compiled result as a string we can add it using CompileStep
     # inside try-catch because this shouldn't run when there's an error.


AppController: Set isReadOnly flag for ideViews when readOnly mode toggled.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -1768,6 +1768,7 @@
     return  if @isReadOnly

     @isReadOnly = yes
+    ideView.isReadOnly = yes  for ideView in @ideViews
     @forEachSubViewInIDEViews_ (pane) -> pane.makeReadOnly()
     @finderPane.makeReadOnly()
     @getView().setClass 'read-only'
@@ -1778,6 +1779,7 @@
     return  unless @isReadOnly

     @isReadOnly = no
+    ideView.isReadOnly = no  for ideView in @ideViews
     @forEachSubViewInIDEViews_ (pane) -> pane.makeEditable()
     @finderPane.makeEditable()
     @getView().unsetClass 'read-only'


Update mp3 plugin config to match new nginx setup

--- modacity.cson
+++ modacity.cson
@@ -54,6 +54,6 @@
 mp3:
   expirySeconds: 3600 #1 hour
   maxLengthSeconds: 480
-  baseDownloadUrl: "http://files.t3hz0r.com/greenbot-media"
-  mediaDir: "/srv/www/files.t3hz0r.com/greenbot-media"
+  baseDownloadUrl: "http://files.t3hz0r.com/greenbot/"
+  mediaDir: "/home/greenbot/www"
   tokenRefillRate: 0.05


re-activated component inspector

re-activated component inspector

--- src/javascript/game/metroid_signals_delegate.coffee
+++ src/javascript/game/metroid_signals_delegate.coffee
@@ -56,8 +56,6 @@

     @postOffice = new PostOffice()

-    # @playerControllerMailbox = @postOffice.newMailbox()
-    # KeyboardController3.bindKeys @playerControllerMailbox.address,
     @player1KbController = createKeyboardSignal @postOffice,
       "right": 'right'
       "left": 'left'
@@ -67,8 +65,6 @@
       "s": 'action1'
       "enter": 'start'

-    # @playerControllerGpMailbox = @postOffice.newMailbox()
-    # GamepadController2.bindButtons @playerControllerGpMailbox.address,
     @player1GpController = createGamepadSignal @postOffice,
       "DPAD_RIGHT": 'right'
       "DPAD_LEFT": 'left'
@@ -78,8 +74,6 @@
       "FACE_3": 'action1'
       "START_FORWARD": 'start'

-    # @adminControllerMailbox = @postOffice.newMailbox()
-    # KeyboardController3.bindKeys @adminControllerMailbox.address,
     @adminController = createKeyboardSignal @postOffice,
       "g": 'toggle_gamepad'
       "b": 'toggle_bgm'
@@ -99,10 +93,10 @@
     @dtMailbox = @postOffice.newMailbox()

     # TODO
-    # if componentInspector?
-    #   @componentInspectorMachine = new ComponentInspectorMachine(
-    #     componentInspector: componentInspector
-    #   )
+    if componentInspector?
+      @componentInspectorMachine = new ComponentInspectorMachine(
+        componentInspector: componentInspector
+      )

   dataToPreload: ->
     # TODO move this data to AdventureState?
@@ -225,11 +219,19 @@
     # State of history over time:
     history = adminState
       .foldp(updateHistory, initialHistory)
-      .dropRepeats(Immutable.is)
-
-    # When gameState changes, update view:
-    history.subscribe (h) =>
-      @viewMachine.update2(RollingHistory.current(h).get('gameState'))
+      .dropRepeats(Immutable.is)  # (if a new history is identical to the prior history, send no change)
+
+    innerGameState = history.map (h) ->
+      RollingHistory.current(h).get('gameState')
+
+    # Funnel updates into the view:
+    innerGameState.subscribe (s) =>
+      @viewMachine.update2(s)
+
+    # Funnel updates into the component inspector:
+    if @componentInspectorMachine?
+      innerGameState.subscribe (s) =>
+        @componentInspectorMachine.update s

   update: (dt) ->
     @dtMailbox.address.send dt


Fixed tests on Firefox.

Fixed tests on Firefox.

--- tests/tests.coffee
+++ tests/tests.coffee
@@ -1143,7 +1143,12 @@
     # TODO: This change is probably unnecessary? Could we prevent it?
     change = stateChanges.shift()
     @assertEqual change.componentId, forthComponentId
-    @assertEqual change.data, a: "10", b: "11"
+    if change.data
+      # TODO: In Chrome data is set. Why?
+      @assertEqual change.data, a: "10", b: "11"
+    else
+      # TODO: In Firefox data is undefined. Why?
+      @assertIsUndefined change.data

     # TODO: Not sure why this change happens?
     change = stateChanges.shift()


Prime queue state properties

Prime queue state properties

--- packages/emberella/lib/mixins/mq_mixin.coffee
+++ packages/emberella/lib/mixins/mq_mixin.coffee
@@ -150,7 +150,12 @@

     # Setup a new state manager with a reference to this object instance
     stateManager = Emberella.MQStateManager.create queueItem: @
-    set(this, 'mqStateManager', stateManager)
+    set(@, 'mqStateManager', stateManager)
+    get(@, 'mqStateManager.currentState')
+    get(@, 'isQueueItemWaiting')
+    get(@, 'isQueueItemInProgress')
+    get(@, 'isQueueItemComplete')
+    get(@, 'isQueueItemError')

   ###
     @property isQueueableItem