Minor refactoring and moving functionality to methods.

--- src/traits/HasValidationMsgView.coffee
+++ src/traits/HasValidationMsgView.coffee
@@ -128,6 +128,40 @@

   ###*
+  # Handles when a validation occurs.
+  #
+  # @method _onValidation
+  # @memberof stout-ui/traits/HasValidationMsgView
+  # @private
+  ###
+  _onValidation: (e) ->
+    msg = e.data.message
+    id = e.data.id
+    switch e.data.validation
+      when 'ok' then @removeValidationMessage(id or msg)
+      when 'hint' then @showHint msg, id
+      when 'warning' then @showValidationWarning msg, id
+      when 'error' then @showValidationError msg, id
+
+
+  ###*
+  # Handle a validation state change event. This occurs when the overall
+  # validation shifts from one state to another (e.g. "hint" to "error").
+  #
+  # @method _onValidationChange
+  # @memberof stout-ui/traits/HasValidationMsgView
+  # @private
+  ###
+  _onValidationChange: (e) ->
+    @classes.remove ERROR_CLS, WARN_CLS
+    @prefixedClasses.remove 'valid'
+    switch e.data.value
+      when 'ok' then @prefixedClasses.add 'valid'
+      when 'error' then @classes.add ERROR_CLS
+      when 'warning' then @classes.add WARN_CLS
+
+
+  ###*
   # Removes a validation message.
   #
   # @param {string} identifier - Either the messsage ID or exact message string
@@ -217,22 +251,8 @@

       # When the validation status of the view-model changes, update the
       # displayed validation classes (error, warning, hint, etc.).
-      @context.on 'change:validation', (e) =>
-        @classes.remove ERROR_CLS, WARN_CLS
-        @prefixedClasses.remove 'valid'
-        console.log 'validation change:', e.data.value, e
-        switch e.data.value
-          when 'ok' then @prefixedClasses.add 'valid'
-          when 'error' then @classes.add ERROR_CLS
-          when 'warning' then @classes.add WARN_CLS
+      @context.on 'change:validation', @_onValidationChange, @

       # When the view-model fires a validation event (meaning a validation just
       # occurred), update the validation messages.
-      @context.on 'validation', (e) =>
-        msg = e.data.message
-        id = e.data.id
-        switch e.data.validation
-          when 'ok' then @removeValidationMessage(id or msg)
-          when 'hint' then @showHint msg, id
-          when 'warning' then @showValidationWarning msg, id
-          when 'error' then @showValidationError msg, id
+      @context.on 'validation', @_onValidation, @


added warning text explaining what happens if rule/exclude is removed, closes #310

--- app/assets/javascripts/admin/events/form.coffee
+++ app/assets/javascripts/admin/events/form.coffee
@@ -48,14 +48,14 @@
     self = this

     $(document).on 'click', ".delete-rule", ->
-      if (confirm("Wirklich löschen?"))
+      if (confirm("Wirklich löschen? Alle zukünftigen Termine, die auf dieser Regel basieren, werden dabei gelöscht."))
         self.rules.splice $(this).attr("data-no"), 1
       repaint()
       reserialize()
       false

     $(document).on 'click', ".delete-exclude", ->
-      if (confirm("Wirklich löschen?"))
+      if (confirm("Wirklich löschen? Wenn dieser Termin in der Zukunft liegt wird er wieder angelegt sollte eine Regel diesen anlegen wollen."))
         self.exdates.splice $(this).attr("data-no"), 1
       repaint()
       reserialize()


Trying to call product properties on show page

--- app/assets/javascripts/sprangular/routes.coffee
+++ app/assets/javascripts/sprangular/routes.coffee
@@ -31,7 +31,7 @@
         product: (Status, Catalog, $route) ->
           slug = $route.current.params.id

-          Status.findCachedProduct(slug) || Catalog.find(slug)
+          Catalog.find(slug)

     .when '/t/:path*',
       controller: 'ProductListCtrl'


add tests for doctype

add tests for doctype

--- test/index-test.coffee
+++ test/index-test.coffee
@@ -27,3 +27,18 @@
         ]
         template = bHtml source
         assert.deepEqual template(context), expected
+
+  context 'doctype', ->
+    context 'simple', ->
+      it 'works', ->
+        source = '''
+          <!doctype html
+        '''
+        context = {}
+        expected = []
+        template = bHtml source
+        assert.deepEqual template(context), expected
+
+    context 'complex', ->
+      it 'works', ->
+        # no tests


removing click part temporarily

removing click part temporarily

--- test/js/totodoo.coffee
+++ test/js/totodoo.coffee
@@ -31,17 +31,14 @@
 			expect(text).to.eventually.equal 'Public list 1'
 			@timeout 4000, done()

+		it 'is the lenght of the default list items = 2', (done) ->
+			lis = @driver.findElements(js: 'return document.getElementById("todo-list").getElementsByTagName("li")')
+			expect(lis.length).to.eventually.equal 2
+			@timeout 4000, done()

 	describe 'List actions', ->

 		describe 'Select private list', ->
-
-			it 'is the name of the private list after click', (done) ->
-			listName = 'Private list 1'
-			@driver.findElement(linkText: listName).click()
-			text = @driver.findElement(js: 'return document.getElementById("listName")').getText()
-			expect(text).to.eventually.equal listName
-			@timeout 4000, done()

 		describe 'Create list', ->


Some testing

Some testing

--- src/videojs.chromecastComponent.coffee
+++ src/videojs.chromecastComponent.coffee
@@ -192,16 +192,17 @@
     clearInterval @timer
     @casting = false
     @removeClass "connected"
+
+    # vjs.insertFirst @player_.tech.el_, @player_.el()
+
     @player_.src @player_.options_["sources"]

-    vjs.insertFirst @player_.tech.el_, @player_.el()
-
-    if @apiMedia.playerState is chrome.cast.media.PlayerState.IDLE
-      @player_.currentTime 0
-      @player_.onPause()
-    else
-      @player_.currentTime @currentMediaTime
-      @player_.play() unless @paused
+    # if @apiMedia.playerState is chrome.cast.media.PlayerState.IDLE
+    #   @player_.currentTime 0
+    #   @player_.onPause()
+    # else
+    #   @player_.currentTime @currentMediaTime
+    #   @player_.play() unless @paused

     @apiMedia = null
     @apiSession = null


Stop the player

Stop the player

--- src/videojs.chromecastComponent.coffee
+++ src/videojs.chromecastComponent.coffee
@@ -82,6 +82,7 @@
     loadRequest.currentTime = @player_.currentTime()

     @apiSession.loadMedia loadRequest, @onMediaDiscovered.bind(this), @castError
+    @apiSession.addUpdateListener @onSessionUpdate.bind(this)

   onMediaDiscovered: (media) ->
     @apiMedia = media
@@ -93,6 +94,11 @@
     @player_.loadTech "ChromecastTech", {}
     @player_.userActive true
     @casting = true
+
+  onSessionUpdate: (event) ->
+    return unless @apiMedia
+
+    @onStopAppSuccess() if event == false

   onMediaStatusUpdate: (event) ->
     return unless @apiMedia


test代码使用include代替过时的contain

test代码使用include代替过时的contain

--- test/moduleSpec.coffee
+++ test/moduleSpec.coffee
@@ -89,7 +89,7 @@
             M.$include(o, m)

             o.should.have.property("__interfaces__")
-            o.__interfaces__.should.contain inter for inter in interfaces
+            o.__interfaces__.should.include inter for inter in interfaces


Test termination without space for terminator.

--- test/serializer.coffee
+++ test/serializer.coffee
@@ -176,6 +176,13 @@
       @deepEqual toArray(buffer), [ 0x41, 0x42, 0x43, 0x00 ]
       done 2

+  'test: write a fixed width zero terminated UTF-8 string with no terminator': (done) ->
+    serializer = new Serializer()
+    serializer.buffer "b8[3]z|utf8()", "ABC", (buffer) =>
+      @equal serializer.getBytesWritten(), 3
+      @deepEqual toArray(buffer), [ 0x41, 0x42, 0x43 ]
+      done 2
+
   'test: write a zero terminated UTF-8 string after skipping': (done) ->
     serializer = new Serializer()
     serializer.buffer "x16{0}, b8z|utf8()", "ABC", (buffer) =>


removes unused fs module from TemplateWatcher

--- lib/Watchers/Template.coffee
+++ lib/Watchers/Template.coffee
@@ -1,4 +1,3 @@
-fs = require 'graceful-fs'
 SourcemapWatcher = require './Sourcemap'
 SourceMapGenerator = require('source-map').SourceMapGenerator


:bug: Fix a typo

:bug: Fix a typo

--- lib/autocomplete-hack.coffee
+++ lib/autocomplete-hack.coffee
@@ -12,7 +12,7 @@
       selector: '.source.cpp, .source.hack, .source.cpp'
       disableForSelector: '.comment'
       getPrefix:(editor, bufferPosition)->
-        regex = /::([\$\w0-9_-]+)|(:[\$\w0-9_-]+)|([\$\w0-9_-]+)$/
+        regex = /::([\$\w0-9_-]+)$|(:[\$\w0-9_-]+)$|([\$\w0-9_-]+)$/
         line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition])
         match = line.match regex
         return '' unless match


Limit images file size to be less than 500kB

--- static/coffee/flows/controllers.coffee
+++ static/coffee/flows/controllers.coffee
@@ -203,8 +203,8 @@
         showDialog('Invalid Format', 'Audio attachments must be encoded as mp3 files.')
         return

-    if action.type in ['reply', 'send'] and (file.size > 20000000 or (file.name.endsWith('.jpg') and file.size > 5000000))
-      showDialog('File Size Exceeded', "The file size should be less than 5MB for images and less than 20MB for audio and video files. Please choose another file and try again.")
+    if action.type in ['reply', 'send'] and (file.size > 20000000 or (file.name.endsWith('.jpg') and file.size > 500000))
+      showDialog('File Size Exceeded', "The file size should be less than 500kB for images and less than 20MB for audio and video files. Please choose another file and try again.")
       return

     # if we have a recording already, confirm they want to replace it


Ensures map is displayed even if no reference layer is supplied

--- app/assets/javascripts/map_editor.js.coffee
+++ app/assets/javascripts/map_editor.js.coffee
@@ -3,8 +3,16 @@

 ((M, $) ->
   "use strict"
-
-
+  ###
+  Series managment:
+  Series allows to display multiple layers of data on a single map, which can be hidden with the selector.
+  I/ Serie handlers:
+    Specify series to work with in a hash:
+      key: the uniq identifier of the serie
+      value: the data of a serie, as a geojson
+
+  Example: show: {series: {land_parcels_serie: land_parcels, plants_serie: plants},layers: [{name: :land_parcels, label: :land_parcels.tl, serie: :land_parcels_serie, type: :simple }, {name: :plants, label: :plant.tl, serie: :plants_serie, reference: 'variety', stroke: 2, fillOpacity: 0.7, type: :categories}]}
+  ###
   M.layer = (layer, data, options) ->
     @layerTypes ?= {}
     if type = @layerTypes[layer.type]
@@ -19,22 +27,9 @@

   # allow to inject jquery objects and interpolate
   L.Map.Modal.prototype.reloadContent = (content) ->
-#    inject = L.Util.template(
-#      content,
-#      options
-#    )
     $(this._getInnerContentContainer()).find('.modal-body').empty()
-#    $(this._getInnerContentContainer()).find('.modal-body > :first-child').replaceWith($content)
     $(this._getInnerContentContainer()).find('.modal-body').append($(content))
     this.update()
-
-  # TODO: split serie and default behavior into separated constructors
-  # series managment:
-  # series is an array of serie:
-  # [{title (as string), data (as FeatureCollection (as json)), options (as hash)}]
-  # options is composed of:
-  # style: styles supported by Leaflet Draw
-  # popup: [{type: 'label' | 'input', property_label: string, property_value: feature property name}]

   $.widget "ui.mapeditor",
     options:
@@ -480,7 +475,7 @@
     _refreshReferenceLayerGroup: ->
       if this.reference?
         this.map.removeLayer this.reference
-      if this.options.show?
+      if this.options.show? and this.options.show.layers.length > 0
         if this.options.useFeatures

           if @options.show.series?


Prettified loading message.

Prettified loading message.

--- app/assets/javascripts/playground.js.coffee
+++ app/assets/javascripts/playground.js.coffee
@@ -12,12 +12,12 @@
         .addClass('alert')
         .addClass('alert-danger')
     onComplete: (swaggerApi, swaggerUi) ->
-      $('#message-bar.swagger-ui-wrap').remove()
+      swaggerUi.showMessage('loading API metadata ...')
       $('#swagger-ui-container .info').remove()
       $('#swagger-ui-container .footer').remove()
-      $('pre code').each((i, e) ->
-        hljs.highlightBlock(e)
-      )
+      setTimeout( =>
+          $('#message-bar.swagger-ui-wrap').fadeOut(300, => @remove())
+        3000)
     docExpansion: "list"

   window.swaggerUi.load()


dont need ; here

dont need ; here

--- app/assets/javascripts/tag-select.js.coffee
+++ app/assets/javascripts/tag-select.js.coffee
@@ -25,9 +25,9 @@
 $(document).on "page:load", jsTags

 jsContentPlans = ->
-  $("#source_url_content_plan_ids").select2();
-  $("#content_content_plan_ids").select2();
-  $("#search_content_plan_ids").select2();
+  $("#source_url_content_plan_ids").select2()
+  $("#content_content_plan_ids").select2()
+  $("#search_content_plan_ids").select2()

 $(document).ready jsContentPlans
 $(document).on "page:load", jsContentPlans


diable the copy button when zeroclipboard is broken

--- app/assets/javascripts/zeroclipboard.coffee
+++ app/assets/javascripts/zeroclipboard.coffee
@@ -37,5 +37,5 @@

 zeroClipboardClient.on('error', ->
   zeroClipboardClient.destroy()
-  zeroClipboardClient.style.display = 'none'
+  $button.addClass('disabled')
 )


Updated placeholder text

Updated placeholder text

--- snippets/findFirst.cson
+++ snippets/findFirst.cson
@@ -2,5 +2,5 @@
   "find().first()":
     "prefix": "findFirst"
     "body": """
-      ${1:wrapper}.find(${2:".class"}).first();
+      ${1:wrapper}.find(${2:"selector"}).first();
       """


Update mongoose.coffee

Update mongoose.coffee

--- app/lib/field-guide-content/mongoose.coffee
+++ app/lib/field-guide-content/mongoose.coffee
@@ -10,7 +10,7 @@

   information: [{
     label: 'Length'
-    value: '24 - 60 cm'
+    value: '24-60 cm'
   }, {
     label: 'Weight'
     value: '5 kg-340 g'


more lexing stuff. emacs added weird meta characters for no good reason. weird

--- assets/js/teacher.coffee
+++ assets/js/teacher.coffee
@@ -48,14 +48,14 @@
                 #{prog}
                </div>")
             .appendTo(current_row)
-            .each(() ->
+            .each () ->
                     console.log "calling click toggle listener on id #{id}"
                     ((closed_id) ->
                       $(closed_student).toggle () ->
-                      console.log "the closure id is #{closed_id}"
-                      $("#text-container-#{closed_id}").show("explode", 1000);
-                    , () ->
-                      $("#text-container-#{closed_id}").hide("explode", 1000);)(id);
+                        console.log "the closure id is #{closed_id}"
+                        $("#text-container-#{closed_id}").show("explode", 1000);
+                      , () ->
+                        $("#text-container-#{closed_id}").hide("explode", 1000);)(id);
     #also, delete any ids that are still client side but have disconnected from the server
     _.map $('.container').find('.student-box'),
           (el) ->


nearly there, toggle is a bit wonky

nearly there, toggle is a bit wonky

--- assets/js/teacher.coffee
+++ assets/js/teacher.coffee
@@ -50,8 +50,9 @@
             .appendTo(current_row)
             .each () ->
                     console.log "calling click toggle listener on id #{id}"
+                    console.log $(this)
                     ((closed_id) ->
-                      $(this).toggle () ->
+                      $("#nick-#{id}").toggle () ->
                         console.log "the closure id is #{closed_id}"
                         $("#text-container-#{closed_id}").show("explode", 1000);
                       , () ->


snake_case -> camelCase

snake_case -> camelCase

--- lib/fuzzy-finder-view.coffee
+++ lib/fuzzy-finder-view.coffee
@@ -4,7 +4,7 @@
 {repositoryForPath} = require './helpers'
 fs = require 'fs-plus'
 fuzzaldrin = require 'fuzzaldrin'
-fuzzaldrin_plus = require 'fuzzaldrin-plus'
+fuzzaldrinPlus = require 'fuzzaldrin-plus'

 module.exports =
 class FuzzyFinderView extends SelectListView
@@ -61,7 +61,7 @@
     filterQuery = @getFilterQuery()

     if @alternateScoring
-      matches = fuzzaldrin_plus.match(projectRelativePath, filterQuery)
+      matches = fuzzaldrinPlus.match(projectRelativePath, filterQuery)
     else
       matches = fuzzaldrin.match(projectRelativePath, filterQuery)

@@ -170,7 +170,7 @@

     filterQuery = @getFilterQuery()
     if filterQuery.length
-      filteredItems = fuzzaldrin_plus.filter(@items, filterQuery, key: @getFilterKey())
+      filteredItems = fuzzaldrinPlus.filter(@items, filterQuery, key: @getFilterKey())
     else
       filteredItems = @items


Added goto to manager

Added goto to manager

--- lib/goto/goto-manager.coffee
+++ lib/goto/goto-manager.coffee
@@ -1,5 +1,6 @@
 GotoClass = require './class-goto.coffee'
 GotoFunction = require './function-goto.coffee'
+GotoProperty = require './property-goto.coffee'
 {TextEditor} = require 'atom'
 parser = require '../services/php-file-parser.coffee'

@@ -14,6 +15,7 @@
     init: () ->
         @gotos.push new GotoClass()
         @gotos.push new GotoFunction()
+        @gotos.push new GotoProperty()
         for goto in @gotos
             goto.init(@)


Case-intensive keybindings search. Fixes #496

--- lib/keybindings-panel.coffee
+++ lib/keybindings-panel.coffee
@@ -72,10 +72,10 @@
     @keybindingRows.empty()
     for keyBinding in keyBindings
       {selector, keystrokes, command, source} = keyBinding
-      searchString = "#{selector}#{keystrokes}#{command}#{source}"
+      searchString = "#{selector}#{keystrokes}#{command}#{source}".toLowerCase()
       continue unless searchString

-      if /^\s*$/.test(filterString) or searchString.indexOf(filterString) != -1
+      if /^\s*$/.test(filterString) or searchString.indexOf(filterString?.toLowerCase()) != -1
         @appendKeyBinding(keyBinding)

   appendKeyBindings: (keyBindings) ->


Fix path issues on windows

Fix path issues on windows

--- lib/project-find-view.coffee
+++ lib/project-find-view.coffee
@@ -1,3 +1,4 @@
+path = require 'path'
 Q = require 'q'
 {_, $, $$$, EditorView, View} = require 'atom'

@@ -192,7 +193,9 @@
     relPath = if atom.project.getPath() == absPath
       ''
     else
-      absPath.replace(new RegExp("^#{atom.project.getPath()}/"), '')
+      projectPath = "#{atom.project.getPath()}#{path.sep}"
+      projectPath = projectPath.replace(/\\/g, '\\\\') if path.sep is '\\'
+      absPath.replace(new RegExp("^#{projectPath}"), '')

     @pathsEditor.setText(relPath)


Fix MPNS version checking

Fix MPNS version checking

--- lib/pushservices/mpns.coffee
+++ lib/pushservices/mpns.coffee
@@ -19,7 +19,7 @@
                         note = new mpns.toast()
                         note.text1 = payload.localizedTitle(info.lang)
                         note.text2 = payload.localizedMessage(info.lang)
-                        if @conf.paramTemplate and subscriber.version >= 7.5
+                        if @conf.paramTemplate and info.version >= 7.5
                             try
                                 note.param = payload.compileTemplate(@conf.paramTemplate)
                             catch e
@@ -29,7 +29,7 @@
                     when "tile" # live tile under WP 7.5 or flip tile under WP 8.0+
                         map = @conf.tileMapping
                         properties = ["id", "title", "backgroundImage", "backBackgroundImage", "backTitle", "backContent"]
-                        if subscriber.version >= 8.0
+                        if info.version >= 8.0
                             note = new mpns.flipTile()
                             properties.push(["smallBackgroundImage", "wideBackgroundImage", "wideBackContent", "wideBackBackgroundImage"]...)
                         else


:art:

:art:

--- lib/snippet-expansion.coffee
+++ lib/snippet-expansion.coffee
@@ -10,17 +10,12 @@
     @selections = [@cursor.selection]

     startPosition = @cursor.selection.getBufferRange().start
-    body = @snippet.body
-    indent = ""
+    # Get leading indentation level
+    indent = @editor.lineTextForBufferRow(startPosition.row).match(/^\s*/)[0]
+    # Add proper indentation to the snippet
+    body = @snippet.body.replace(/\n/g, '\n' + indent)
+
     tabStopRanges = []
-    if @snippet.lineCount > 1
-      lines = body.split('\n')
-      indent = @editor.lineTextForBufferRow(startPosition.row).match(/^\s*/)[0]
-      for line, index in lines when index isnt 0 # Do not include initial line
-        # Match initial line's indent
-        lines[index] = indent + line
-      body = lines.join('\n')
-
     for tabStop in @snippet.tabStops
       ranges = []
       for range in tabStop


Unfocus spec

Unfocus spec

--- spec/editor-spec.coffee
+++ spec/editor-spec.coffee
@@ -2868,7 +2868,7 @@
             expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "keyword.other.DML.sql"]

   describe ".normalizeTabsInBufferRange()", ->
-    fit "normalizes tabs depending on the editor's soft tab/tab length settings", ->
+    it "normalizes tabs depending on the editor's soft tab/tab length settings", ->
       editor.setTabLength(1)
       editor.setSoftTabs(true)
       editor.setText('\t\t\t')


Change grid at bottom to all white; adjust placement of text

--- manycurves/manycurves.coffee
+++ manycurves/manycurves.coffee
@@ -201,7 +201,7 @@
         .attr("x2", (d) -> xScaleCurve(d*60))
         .attr("y1", h[1])
         .attr("y2", 0)
-        .attr("stroke", darkGray)
+        .attr("stroke", "white")
   botAxes.selectAll("empty")
         .data(xTicks)
         .enter()
@@ -298,7 +298,7 @@
     thecurve.append("text")
           .datum(ind)
           .text("line #{ind*1+1}") # *1 to turn it into a number
-          .attr("x", xScaleCurve(7*60)+pad.inner)
+          .attr("x", xScaleCurve(7*60+10))
           .attr("y", (yScaleCurve(0)+yScaleCurve(-20))/2)
           .attr("text-anchor", "start")
           .attr("fill", darkBlue)


Use anchorNode instead of baseNode for Firefox compatibility (#44)

--- annotran/static/scripts/annotator/plugin/sentenceselection.coffee
+++ annotran/static/scripts/annotator/plugin/sentenceselection.coffee
@@ -160,7 +160,7 @@
     this.currentIndex = this.currentIndex + 1
     currentSelection = window.getSelection()

-    this.selectSentence(currentSelection.baseNode.parentElement)
+    this.selectSentence(currentSelection.anchorNode.parentElement)

     selection = Annotator.Util.getGlobal().getSelection()
     ranges = for i in [0...selection.rangeCount]


Remove redundant on_poll definition (already exists in AbstractJobsTable)

--- app/assets/javascripts/sidekiq/monitor/queue_jobs_table.js.coffee
+++ app/assets/javascripts/sidekiq/monitor/queue_jobs_table.js.coffee
@@ -87,8 +87,5 @@
     @api_params['queue'] = @queue
     @show_queue_stats()

-  on_poll: =>
-    @reload_table()
-
 $ ->
   new SidekiqMonitor.QueueJobsTable


check if form dirty in attemptBack

check if form dirty in attemptBack

--- app/components/profile-settings-page/profile-settings-page.coffee
+++ app/components/profile-settings-page/profile-settings-page.coffee
@@ -6,17 +6,16 @@
       global.cobudgetApp.membershipsLoaded
   url: '/profile_settings?previous_group_id'
   template: require('./profile-settings-page.html')
-  controller: (CurrentUser, Dialog, $location, Records, $scope, $stateParams, Toast, $window) ->
+  controller: (CurrentUser, Dialog, $location, $q, Records, $scope, $stateParams, Toast, $window) ->
     $scope.currentUser = CurrentUser()
     previousGroupId = $stateParams.previous_group_id || CurrentUser().primaryGroup().id
-    $scope.changesMade = false

     $scope.back = ->
         $location.search('previous_group_id', null)
         $location.path("/groups/#{previousGroupId}")

     $scope.attemptBack = ->
-      if $scope.changesMade
+      if $scope.accountDetailsForm.$dirty
         Dialog.custom
           scope: $scope
           template: require('./discard-changes-dialog.tmpl.html')


added query for previous message

added query for previous message

--- src/controllers/message_controller.coffee
+++ src/controllers/message_controller.coffee
@@ -58,6 +58,20 @@
     newMessageData.MessageId = message.id
     return newMessageData.save()
   .then ->
+    # We need to send a notification to all clients in this thread with this message, and the id of the message
+    # in this thread that comes right before this one in the serializable DB.
+    return Message.find({
+      limit: 1,
+      where:
+        ThreadId: thread.id,
+        id:
+          $lt: newMessage.id
+      order: 'id DESC'
+    })
+  .then (beforeMessage) ->
+    # beforeMessage is either null (no message before) or the message that should
+    # be displayed before this new one. This is used to enforce causal ordering for
+    # the chat clients.
     success newMessage
   .catch fail


Fix typo in confirm dialog title

Fix typo in confirm dialog title

--- src/core/components/confirm-dialog.coffee
+++ src/core/components/confirm-dialog.coffee
@@ -1,6 +1,6 @@
 Flow.ConfirmDialog = (_, _message, _opts={}, _go) ->
   defaults _opts,
-    title: 'Alert'
+    title: 'Confirm'
     acceptCaption: 'Yes'
     declineCaption: 'No'


indent

indent

--- lib/autocomplete-jedi.coffee
+++ lib/autocomplete-jedi.coffee
@@ -6,16 +6,16 @@
   jediServer: null

   activate: ->
-  	if !@jediServer
-  	  projectPath = atom.project.getPath()
-  	  command = "python " + __dirname + "/jedi-complete.py '" + projectPath + "'"
+    if !@jediServer
+      projectPath = atom.project.getPath()
+      command = "python " + __dirname + "/jedi-complete.py '" + projectPath + "'"

-  	  @jediServer = cp.exec command
+      @jediServer = cp.exec command

-  	@provider = new JediProvider()
+    @provider = new JediProvider()

   deactivate: ->
-  	@jediServer.kill()
+    @jediServer.kill()

   getProvider: ->
-  	return {providers: [@provider]}
+    return {providers: [@provider]}


fix #116

fix #116

merge setPosition code from https://github.com/atom/autocomplete

--- lib/autocomplete-view.coffee
+++ lib/autocomplete-view.coffee
@@ -216,27 +216,20 @@
   # above or below the cursor if needed.
   setPosition: ->
     { left, top } = @editor.pixelPositionForScreenPosition @editor.getCursorScreenPosition()
-    cursorLeft = left
-    cursorTop = top
-
-    # The top position if we would put it below the current line
-    belowPosition = cursorTop + @editorView.lineHeight
-
-    # The top position of the lower edge if we would put it below the current line
-    belowLowerPosition = belowPosition + @outerHeight()
-
-    # The position if we would put it above the line
-    abovePosition = cursorTop
-
-    if belowLowerPosition > @editorView.outerHeight() + @editorView.scrollTop()
-      # We can't put it below - put it above. Using CSS transforms to
-      # move it 100% up so that the lower edge is above the current line
-      @css left: cursorLeft, top: abovePosition
-      @css "-webkit-transform", "translateY(-100%)"
+    height = @outerHeight()
+    width = @outerWidth()
+
+    potentialTop = top + @editorView.lineHeight
+    potentialBottom = potentialTop - @editorView.scrollTop() + height
+    parentWidth = @parent().width()
+
+    left = parentWidth - width if left + width > parentWidth
+
+    if @aboveCursor or potentialBottom > @editorView.outerHeight()
+      @aboveCursor = true
+      @css(left: left, top: top - height, bottom: 'inherit')
     else
-      # We can put it below, remove possible previous CSS transforms
-      @css left: cursorLeft, top: belowPosition
-      @css "-webkit-transform", ""
+      @css(left: left, top: potentialTop, bottom: 'inherit')

   # Private: Replaces the current prefix with the given match
   #


bluebird

bluebird

--- chrome/content/zotero-better-bibtex/cache.coffee
+++ chrome/content/zotero-better-bibtex/cache.coffee
@@ -22,7 +22,7 @@
     throw new Error("#{v} is not an integer-string") if isNaN(_v)
     return _v

-  load: Zotero.Promise.coroutine(->
+  load: ->
     Zotero.BetterBibTeX.debug('cache.load')
     if Zotero.BetterBibTeX.pref.get('cacheReset') > 0
       @reset()
@@ -43,7 +43,6 @@
       })
     @cache.flushChanges()
     @access.flushChanges()
-  )

   verify: (entry) ->
     return entry unless Zotero.BetterBibTeX.pref.get('debug') || Zotero.BetterBibTeX.testing


Main: per feature per user role permission implementation

--- client/Main/maincontroller/maincontroller.coffee
+++ client/Main/maincontroller/maincontroller.coffee
@@ -75,6 +75,27 @@

     @forwardEvents KD.remote, ['disconnected', 'reconnected']

+
+  isFeatureDisabled: (name) ->
+
+    return no  if KD.checkFlag 'super-admin'
+    return no  unless name
+
+    {roles}            = KD.config
+    {disabledFeatures} = KD.getGroup()
+
+    return no  unless disabledFeatures
+
+    role = 'member'    if 'member'    in roles
+    role = 'moderator' if 'moderator' in roles
+    role = 'admin'     if 'admin'     in roles
+
+    return yes  if name in disabledFeatures[role]
+
+    return no
+
+
+
   accountChanged:(account, firstLoad = no)->
     account = KD.remote.revive account  unless account instanceof KD.remote.api.JAccount
     KD.userAccount = account


search: blur the input on enter

search: blur the input on enter

--- client/Social/Activity/views/activitypane.coffee
+++ client/Social/Activity/views/activitypane.coffee
@@ -157,6 +157,7 @@
     @tabView.tabHandleContainer.addSubView searchIcon

     @searchInput.on 'SearchRequested', (text) =>
+      @searchInput.setBlur()
       @tabView.showPane @tabView.panes.last
       @searchResults.clear()
       @search text


Fix notification for guest

Fix notification for guest

--- client/app/MainApp/notificationcontroller.coffee
+++ client/app/MainApp/notificationcontroller.coffee
@@ -16,7 +16,8 @@
     super

     @getSingleton('mainController').on "AccountChanged", (account)=>
-      @setListeners account
+      if account.bongo_.constructorName is 'JAccount'
+        @setListeners account

   setListeners:(account)->


clear interval on logout

clear interval on logout

--- client/coffeescript/templates/session_bar.coffee
+++ client/coffeescript/templates/session_bar.coffee
@@ -7,6 +7,7 @@

   events: {
     'click .logout_top_right': (event) ->
+      Meteor.clearInterval(window.carousel_handle)
       Meteor.logout()
   }
 )


Change the variable name.

Change the variable name.

--- client/ide/lib/workspace/idelayoutmanager.coffee
+++ client/ide/lib/workspace/idelayoutmanager.coffee
@@ -17,8 +17,8 @@

     super options, data

-    @isRestored     = no
-    @storedSnapshot = null
+    @isRestored = no
+    @snapshot   = null

   ###*


Resurrection: Defensive check to min width/height.

--- client/ide/lib/workspace/idelayoutmanager.coffee
+++ client/ide/lib/workspace/idelayoutmanager.coffee
@@ -416,6 +416,8 @@
         then size.width   * widthRatio
         else size.height  * heightRatio

+        value = Math.max 50, value
+
         view._windowDidResize()
         view.resizePanel value


Fix document upload

Fix document upload

--- client/shared/record_store/restful_client.coffee
+++ client/shared/record_store/restful_client.coffee
@@ -99,9 +99,9 @@
         @currentUpload.responseType = 'json'
         @currentUpload.addEventListener 'load', =>
           if (@currentUpload.status >= 200 && @currentUpload.status < 300)
-            data = JSON.parse(@currentUpload.response) if _.isString(@currentUpload.response)
-            @onUploadSuccess(data)
-            resolve(data)
+            @currentUpload.response = JSON.parse(@currentUpload.response) if _.isString(@currentUpload.response)
+            @onUploadSuccess(@currentUpload.response)
+            resolve(@currentUpload.response)
             @currentUpload = null
         @currentUpload.upload.addEventListener('progress', onProgress) if onProgress
         @currentUpload.addEventListener('error', reject)


Remove check for whitespace as input is already trimmed

--- lib/keybindings-panel.coffee
+++ lib/keybindings-panel.coffee
@@ -78,8 +78,7 @@
       continue unless searchString

       keywords = filterString.trim().toLowerCase().split(' ')
-      containKeyword = (keyword) -> return /^\s*$/.test(keyword) or searchString.indexOf(keyword) isnt -1
-      if keywords.every containKeyword
+      if keywords.every((keyword) -> searchString.indexOf(keyword) isnt -1)
         @appendKeyBinding(keyBinding)

   appendKeyBindings: (keyBindings) ->


Get newly mixed in computed properties for observe

--- addon/mixins/hash-bindings.coffee
+++ addon/mixins/hash-bindings.coffee
@@ -143,7 +143,7 @@
     remix = {}
     remix[name] = bindComputed
     mixin(@, remix)
-
+    get(@, name)
     @

   _attachHashChangeHandler: (name) ->


Ignore ProjectUtils if CollectionUtils is not defined

--- src/ProjectUtils.coffee
+++ src/ProjectUtils.coffee
@@ -1,3 +1,5 @@
+return unless Projects? and CollectionUtils?
+
 # Constructs a map of collection name to collection.

 reNumberAfterName = /(\d+)(\s*)$/


Fix another crash in APNs feedback

Fix another crash in APNs feedback

--- lib/pushservices/apns.coffee
+++ lib/pushservices/apns.coffee
@@ -14,11 +14,11 @@
         @payloadFilter = conf.payloadFilter

         # Handle Apple Feedbacks
-        conf.feedback = (time, tokenBuffer) ->
+        conf.feedback = (time, tokenBuffer) =>
             tokenResolver 'apns', tokenBuffer.toString(), (subscriber) =>
                 subscriber?.get (info) ->
                     if info.updated < time
-                        @logger.warn("APNS Automatic unregistration for subscriber #{subscriber.id}")
+                        @logger?.warn("APNS Automatic unregistration for subscriber #{subscriber.id}")
                         subscriber.delete()
         @feedback = new apns.Feedback(conf)


Allow false values to be defaults in .groc.json too

--- lib/utils/cli_helpers.coffee
+++ lib/utils/cli_helpers.coffee
@@ -11,7 +11,7 @@
       #   as part of `config`.
       #
       # Also, `default` is a reserved name, hence `defaultVal`.
-      defaultVal = extraDefaults?[optName] or optConfig.default
+      defaultVal = extraDefaults?[optName] ? optConfig.default

       # * We also want the ability to specify reactionary default values, so that the user can
       #   inspect the current state of things by tacking on a `--help`.


Tweak summary

Tweak summary

--- lib/window-panel-view.coffee
+++ lib/window-panel-view.coffee
@@ -6,4 +6,4 @@
     @h5 class: 'overview text-highlight'

   populate:  ->
-    @text("This window loaded in #{atom.getWindowLoadTime()}ms")
+    @text("This window took #{atom.getWindowLoadTime()}ms to load.")


Support other connections

Support other connections

--- client/functions.coffee
+++ client/functions.coffee
@@ -54,6 +54,7 @@
 			acl:"public-read"
 			uploader:"default"
 			unique_name:true
+			connection:Meteor

 		if ops.file
 			uploadFile(ops.file, ops, callback)
@@ -61,8 +62,9 @@
 			_.each ops.files, (file) ->
 				uploadFile(file, ops, callback)

-	delete: (path,callback) ->
-		Meteor.call "_s3_delete", path, callback
+	delete: (path, callback, connection) ->
+		conn = if connection then connection else Meteor
+		conn.call "_s3_delete", path, callback

 	b64toBlob: (b64Data, contentType, sliceSize) ->
 		data = b64Data.split("base64,")
@@ -120,7 +122,7 @@

 	id = S3.collection.insert initial_file_data

-	Meteor.call "_s3_sign",
+	ops.connection.call "_s3_sign",
 		path:ops.path
 		file_name: initial_file_data.file.name
 		file_type:file.type


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -15,6 +15,6 @@
     x: 50
     y: 30

-  equals engine.fi
+  equals engine.find('')

 module()


Oh noes, the unit test was incorrect

Oh noes, the unit test was incorrect

--- test/expected/coffee.coffee
+++ test/expected/coffee.coffee
@@ -1,3 +1,3 @@
 define 'a', -> 5
 define 'b', ['a'], (a) -> a * 4
-define('c', 'c', -> 10)
+define('c', -> 10)


Bug fix

Bug fix

--- test/generators/page.coffee
+++ test/generators/page.coffee
@@ -171,7 +171,7 @@
             assert.ok(!!data = res.body, 'got data')
             assert.equal(data.total_rows, 1, 'has the correct total_rows')
             assert.equal(data.rows.length, 1, 'has the correct row.length')
-            assert.deepEqual(expected = JSON.stringify(model.toJSON()), actual = JSON.stringify(data.rows[0]), "Expected: #{util.inspect(expected)}. Actual: #{util.inspect(actual)}")
+            assert.deepEqual(expected = model.toJSON(), actual = JSONUtils.parse(data.rows[0]), "Expected: #{util.inspect(expected)}. Actual: #{util.inspect(actual)}")
             done()

 # TODO: explain required set up


updated vendor ids, update filter if proprietary mac address is set, issue #4

--- amazing-dash-button.coffee
+++ amazing-dash-button.coffee
@@ -15,8 +15,18 @@
       @interfaceAddress = @config.interfaceAddress if @config.interfaceAddress?
       @debug = @config.debug || false
       @base = commons.base @, 'Plugin'
-      @capture = new cap.Cap()
       @buffer = new Buffer(65536)
+      # List of registered Mac addresses with IEEE as of 15 July 2017 for Amazon Technologies Inc.
+      # source: https://regauth.standards.ieee.org/standards-ra-web/pub/view.html#registries
+      # 00BB3A is marked as private and has been reported to be used for dash-buttons
+      @amazonVendorIds = [
+        "F0D2F1", "8871E5", "FCA183", "F0272D", "74C246",
+        "6837E9", "78E103", "38F73D", "50DCE7", "A002DC",
+        "0C47C9", "747548", "AC63BE", "FCA667", "18742E",
+        "00FC8B", "FC65DE", "6C5697", "44650D", "50F5DA",
+        "6854FD", "40B4CD", "4CEFC0", "007147", "84D6D0",
+        "34D270", "B47C9C", "00BB3A"
+      ]

       process.on "SIGINT", @_stop
       @_start()
@@ -72,6 +82,14 @@
         )
       )

+    _addMacToFilter: (mac) ->
+      vendorId = mac.replace(/:/g, '').substring(0,6).toUpperCase()
+      if @amazonVendorIds.indexOf(vendorId) is -1
+        @_stop()
+        @amazonVendorIds.push vendorId
+        @base.debug "Adding vendor id #{vendorId} to packet filter"
+        @_start()
+
     _start: () ->
       if @interfaceAddress?
         device = cap.findDevice @interfaceAddress
@@ -85,23 +103,14 @@
         @base.error @noInterfacesFound
         return

-      # List of registered Mac addresses with IEEE as of 15 July 2017 for Amazon Technologies Inc.
-      # source: https://regauth.standards.ieee.org/standards-ra-web/pub/view.html#registries
-      # 00BB3A is marked as private and has been reported to be used for dash-buttons
-      amazonVendorIds = [
-        "F0D2F1", "8871E5", "F0272D", "74C246", "6837E9",
-        "78E103", "A002DC", "0C47C9", "747548", "AC63BE",
-        "FCA667", "18742E", "00FC8B", "FC65DE", "44650D",
-        "50F5DA", "6854FD", "40B4CD", "34D270", "84D6D0",
-        "B47C9C", "00BB3A"
-      ]
-      filter = amazonVendorIds.map( (vendorId) ->
+      filter = @amazonVendorIds.map( (vendorId) ->
         "(ether[6:2] == 0x#{vendorId.substring 0,4} and ether[8:1] == 0x#{vendorId.substring 4,6})"
       ).reduce( (left, right) ->
         left + " or " + right
       )

       pcapFilter = "(arp or (udp and src port 68 and dst port 67 and udp[247:4] == 0x63350103)) and (#{filter})"
+      @capture = new cap.Cap()
       linkType = @capture.open device, pcapFilter, 10 * 65536, @buffer
       try
         @capture.setMinBytes 0
@@ -111,8 +120,10 @@
       @capture.on "packet", @_rawPacketHandler

     _stop: () =>
-      @capture.removeListener "packet", @_rawPacketHandler
-      @capture.close();
+      if @capture?
+        @capture.removeListener "packet", @_rawPacketHandler
+        @capture.close();
+        @capture = null

     _rawPacketHandler: () =>
       ret = cap.decoders.Ethernet @buffer
@@ -181,6 +192,7 @@
           @callPending = setTimeout ( => @callPending = null), 3000

       super()
+      @plugin._addMacToFilter @macAddress
       @plugin.on 'candidateInfo', @candidateInfoHandler

     destroy: () ->


block/unblock msg queue using counter inst of flag

It is safer this way if block() is called multiple times before unblock().
As far as I know, this doesn't happen now.

--- client/ide.coffee
+++ client/ide.coffee
@@ -109,13 +109,13 @@
     # We need to be able to temporarily block the stream of messages coming from socket.io
     # Creating a floating window can only be done asynchronously and it's possible that a message
     # for it comes in before the window is ready.
-    mq = []; blocked = 0 # message queue
+    mq=[];blocked=0 # message queue
     runDownQueue = ->
-      while mq.length && !blocked then data = mq.shift(); (f = handlers[data[0]]) && f.apply ide, data[1..]
-      return
-    D.socket.onevent = ({data}) -> mq.push data; runDownQueue(); return
-    @block = -> blocked = 1; return
-    @unblock = -> blocked = 0; runDownQueue(); return
+      while mq.length&&!blocked then data=mq.shift();(f=handlers[data[0]])&&f.apply ide,data[1..]
+      return
+    D.socket.onevent=({data})->mq.push data;runDownQueue();return
+    @block=->blocked++;return
+    @unblock=->--blocked||runDownQueue();return

     # language bar
     $('.lbar-prefs').click -> prefsUI.showDialog 'layout'; return


Correcting issue in case no end node set.

--- src/main/webapp/lib/extensions/End.coffee
+++ src/main/webapp/lib/extensions/End.coffee
@@ -2,7 +2,7 @@
   window.beats.ReferenceHelper.resolver('node_id', 'node', 'node', 'end', 'End', false)

 window.beats.End::encode_references = ->
-  @set 'node_id', @get('node').id
+  @set 'node_id', @get('node')?.id

 window.beats.End::set_node = (node) ->
   @set 'node', node


Update doc tasks for new src layout

Update doc tasks for new src layout

--- tasks/docs-task.coffee
+++ tasks/docs-task.coffee
@@ -8,17 +8,17 @@

   grunt.registerTask 'build-docs', 'Builds the API docs in src/app', ->
     done = @async()
-    args = [commonArgs..., '-o', 'docs/api', 'src/app/']
+    args = [commonArgs..., '-o', 'docs/api', 'src/']
     grunt.util.spawn({cmd, args, opts}, done)

   grunt.registerTask 'lint-docs', 'Generate stats about the doc coverage', ->
     done = @async()
-    args = [commonArgs..., '--noOutput', 'src/app/']
+    args = [commonArgs..., '--noOutput', 'src/']
     grunt.util.spawn({cmd, args, opts}, done)

   grunt.registerTask 'missing-docs', 'Generate stats about the doc coverage', ->
     done = @async()
-    args = [commonArgs..., '--noOutput', '--missing', 'src/app/']
+    args = [commonArgs..., '--noOutput', '--missing', 'src/']
     grunt.util.spawn({cmd, args, opts}, done)

   grunt.registerTask 'deploy-docs', 'Publishes latest API docs to atom-docs.githubapp.com', ->


sagews process updates optimization -- instead of frequently scanning the whole document, we now only process the part that changed; this matters a lot for large document and may be important on mobile devices.

--- salvus/page/syncdoc.coffee
+++ salvus/page/syncdoc.coffee
@@ -926,19 +926,27 @@

 class SynchronizedWorksheet extends SynchronizedDocument
     constructor: (@editor, @opts) ->
-        if not @opts.history_browser
-            opts0 =
-                cursor_interval : @opts.cursor_interval
-                sync_interval   : @opts.sync_interval
-            super @editor, opts0, () =>
-                @process_sage_updates()
-
-            @init_worksheet_buttons()
-            @on 'sync', @process_sage_updates
-
-        @codemirror = @editor.codemirror
+        # these two lines are assumed, at least by the history browser
+        @codemirror  = @editor.codemirror
+        @codemirror1 = @editor.codemirror1
+
+        if @opts.history_browser
+            return
+
+        opts0 =
+            cursor_interval : @opts.cursor_interval
+            sync_interval   : @opts.sync_interval
+        super @editor, opts0, () =>
+            @process_sage_updates(caller:"constructor")
+
+        @init_worksheet_buttons()
+
+        @on 'sync', () =>
+            #console.log("sync")
+            @process_sage_update_queue()

         @editor.on 'show', (height) =>
+            @process_sage_updates(caller:"show")
             w = @cm_lines().width()
             for cm in @codemirrors()
                 for mark in cm.getAllMarks()
@@ -948,12 +956,12 @@
                             elt.css('width', (w-25) + 'px')
                         else if elt.hasClass('sagews-input')
                             elt.css('width', w + 'px')
-            @process_sage_updates()

         v = [@codemirror, @codemirror1]
+        v[0].name = '0'; v[1].name = '1'
         for cm in v
             cm.on 'beforeChange', (instance, changeObj) =>
-                #console.log("beforeChange: #{misc.to_json(changeObj)}")
+                #console.log("beforeChange (#{instance.name}): #{misc.to_json(changeObj)}")
                 # Set the evaluated flag to false for the cell that contains the text
                 # that just changed (if applicable)
                 if changeObj.origin? and changeObj.origin != 'setValue'
@@ -970,13 +978,34 @@
                     # since we just canceled the change.
                     @remove_cell_flags_from_changeObj(changeObj, ACTION_FLAGS)
                     @_apply_changeObj(changeObj)
-                    @process_sage_updates()
+                    @process_sage_updates(cm:instance, caller:"paste")
                     @sync()

-            cm.on 'change', () =>
-                if @editor._split_view
-                    @_process_sage_updates(cm, 0, true)
-
+            cm.sage_update_queue = []
+            cm.on 'change', (instance, changeObj) =>
+                if changeObj.origin != '+input' and (instance.name == '0' or @editor._split_view)
+                    start = changeObj.from.line
+                    stop  = changeObj.to.line + changeObj.text.length
+                    if not @_update_queue_start? or start < @_update_queue_start
+                        @_update_queue_start = start
+                    if not @_update_queue_stop? or stop > @_update_queue_stop
+                        @_update_queue_stop = stop
+
+                #if @editor._split_view
+                    # TODO: make faster by using change object to determine line range to consider!
+                #    @process_sage_updates
+                #        cm            : instance
+                #        ignore_output : true
+                #        caller: "change"
+
+    process_sage_update_queue: () =>
+        if @_update_queue_start?
+            @process_sage_updates
+                start  : @_update_queue_start
+                stop   : @_update_queue_stop
+                caller : 'queue'
+            @_update_queue_start = undefined
+            @_update_queue_stop  = undefined

     init_worksheet_buttons: () =>
         buttons = @element.find(".salvus-editor-codemirror-worksheet-buttons")
@@ -1067,7 +1096,7 @@
                 if marker.type == MARKERS.cell
                     for flag in ACTION_FLAGS
                         @remove_cell_flag(marker, flag)
-        @process_sage_updates()
+        @process_sage_updates(caller:"kill")
         async.series([
             (cb) =>
                 @send_signal
@@ -1211,24 +1240,41 @@
             cm.replaceRange(Array(k+1).join('\n'), {line:m+1, ch:0} )
             cm.setCursor(cursor)

-    process_sage_updates: (start) =>
-        #console.log("processing Sage updates")
+    # change the codemirror editor to reflect the proper sagews worksheet markup.
+    process_sage_updates: (opts={}) =>
+        opts = defaults opts,
+            start         : undefined    # process starting at this line (0-based); 0 if not given
+            stop          : undefined    # end at this line (0-based); last line if not given
+            cm            : undefined    # only markup changes, etc., using the given editor (uses all visible ones by default)
+            pad_bottom    : 10           # ensure there are this many blank lines at bottom of document
+            caller        : undefined
         # For each line in the editor (or starting at line start), check if the line
         # starts with a cell or output marker and is not already marked.
         # If not marked, mark it appropriately, and possibly process any
         # changes to that line.
-        @pad_bottom_with_newlines(10)
-        @_process_sage_updates(@codemirror, start)
-        if @editor._split_view
-            @_process_sage_updates(@editor.codemirror1, start)
-
-    _process_sage_updates: (cm, start, ignore_output) =>
+        ##tm = misc.mswalltime()
+        if opts.pad_bottom
+            @pad_bottom_with_newlines(opts.pad_bottom)
+        if not opts.cm?
+            @_process_sage_updates(@editor.codemirror, opts.start, opts.stop)
+            if @editor._split_view
+                @_process_sage_updates(@editor.codemirror1, opts.start, opts.stop)
+        else
+            @_process_sage_updates(opts.cm, opts.start, opts.stop)
+        ##console.log("process_sage_updates(opts=#{misc.to_json({caller:opts.caller, start:opts.start, stop:opts.stop})}): time=#{misc.mswalltime(tm)}ms")
+
+    _process_sage_updates: (cm, start, stop) =>

         if not start?
             start = 0
-
-        for line in [start...cm.lineCount()]
+        if not stop?
+            stop = cm.lineCount()-1
+
+        for line in [start..stop]
             x = cm.getLine(line)
+            #console.log("line=#{line}: '#{x}'")
+            if not x?
+                continue

             if x[0] == MARKERS.cell
                 marks = cm.findMarksAt({line:line, ch:1})
@@ -1286,45 +1332,51 @@

                     mark.flagstring = flagstring

-            else if x[0] == MARKERS.output and not ignore_output
-                marks = cm.findMarksAt({line:line, ch:1})
-                if marks.length == 0
-                    @mark_output_line(cm, line)
-                mark = cm.findMarksAt({line:line, ch:1})[0]
-                uuid = cm.getRange({line:line,ch:1}, {line:line,ch:37})
-                if mark? and mark.uuid != uuid # uuid changed -- completely new output
-                    #console.log("uuid change: new x = ", x)
-                    mark.processed = 38
-                    mark.uuid = uuid
-                    @elt_at_mark(mark).html('')
-                if mark? and mark.processed < x.length
-                    #console.log("length change; x = ", x)
-                    # new output to process
-                    t = x.slice(mark.processed, x.length-1)
-                    mark.processed = x.length
-                    for s in t.split(MARKERS.output)
-                        if s.length > 0
-                            output = @elt_at_mark(mark)
-                            # appearance of output shows output (bad design?)
-                            output.removeClass('sagews-output-hide')
-                            try
-                               @process_output_mesg(mesg:JSON.parse(s), element:output)
-                            catch e
-                                log("BUG: error rendering output: '#{s}' -- #{e}")
-
-            else if x.indexOf(MARKERS.output) != -1 and not ignore_output
-                #console.log("correcting merge/paste issue with output marker line (line=#{line})")
-                ch = x.indexOf(MARKERS.output)
-                cm.replaceRange('\n', {line:line, ch:ch})
-                @process_sage_updates(line)
-                return
-
-            else if x.indexOf(MARKERS.cell) != -1 and not ignore_output
-                #console.log("correcting merge/paste issue with cell marker (line=#{line})")
-                ch = x.indexOf(MARKERS.cell)
-                cm.replaceRange('\n', {line:line, ch:ch})
-                @process_sage_updates(line)
-                return
+            else
+                if x[0] == MARKERS.output
+                    marks = cm.findMarksAt({line:line, ch:1})
+                    if marks.length == 0
+                        @mark_output_line(cm, line)
+                    mark = cm.findMarksAt({line:line, ch:1})[0]
+                    if mark?
+                        uuid = cm.getRange({line:line,ch:1}, {line:line,ch:37})
+                        if misc.is_valid_uuid_string(uuid)
+                            if mark.uuid != uuid # uuid changed -- completely new output
+                                mark.processed = 38
+                                mark.uuid = uuid
+                                output = @elt_at_mark(mark)
+                                output.html('')
+                            if mark.processed < x.length-1
+                                # new output to process
+                                t = x.slice(mark.processed, x.length-1)
+                                for s in t.split(MARKERS.output)
+                                    if s.length > 0
+                                        output = @elt_at_mark(mark)
+                                        # appearance of output shows output (bad design?)
+                                        output.removeClass('sagews-output-hide')
+                                        try
+                                            @process_output_mesg(mesg:JSON.parse(s), element:output)
+                                            mark.processed += 1 + s.length
+                                        catch e
+                                            log("BUG: error rendering output: '#{s}' -- #{e}")
+                                            break
+                                    else
+                                        mark.processed += 1
+
+
+                else if x.indexOf(MARKERS.output) != -1
+                    #console.log("correcting merge/paste issue with output marker line (line=#{line})")
+                    ch = x.indexOf(MARKERS.output)
+                    cm.replaceRange('\n', {line:line, ch:ch})
+                    @process_sage_updates(start:line, stop:line+2, caller:"fix output")
+                    return
+
+                else if x.indexOf(MARKERS.cell) != -1
+                    #console.log("correcting merge/paste issue with cell marker (line=#{line})")
+                    ch = x.indexOf(MARKERS.cell)
+                    cm.replaceRange('\n', {line:line, ch:ch})
+                    @process_sage_updates(start:line, stop:line+2, caller:"fix input")
+                    return

     ##################################################################################
     # Toggle visibility of input/output portions of cells -
@@ -1929,7 +1981,7 @@
             @remove_cell_flag(opts.marker, FLAGS.hide_input)

         # update so that client sees rendering
-        @process_sage_updates()
+        @process_sage_updates(start:block.start, stop:block.end+1, caller:"execute_cell_client_side")
         @sync()

     split_cell_at: (pos) =>
@@ -2001,7 +2053,7 @@
         @focused_codemirror().focus()
         @focused_codemirror().setCursor(pos)
         @cell_start_marker(line)
-        @process_sage_updates()
+        @process_sage_updates(start:line, stop:line+1, caller:"insert_new_cell")
         @sync()

     cell_start_marker: (line) =>
@@ -2014,6 +2066,7 @@
             cm.replaceRange('\n',{line:line+1,ch:0})
         uuid = misc.uuid()
         cm.replaceRange(MARKERS.cell + uuid + MARKERS.cell + '\n', {line:line, ch:0})
+        @process_sage_updates(start:line, stop:line+1, caller:"cell_start_marker")
         return {marker:@mark_cell_start(cm, line), created:true}

     remove_cell_flags_from_changeObj: (changeObj, flags) =>


sagews: split mode appears non-rendered again due to other changes; also increase document load timeout.

--- salvus/page/syncdoc.coffee
+++ salvus/page/syncdoc.coffee
@@ -26,7 +26,7 @@
 # seconds to wait for synchronized doc editing session, before reporting an error.
 # Don't make this too short, since when we open a link to a file in a project that
 # hasn't been opened in a while, it can take a while.
-CONNECT_TIMEOUT_S = 45
+CONNECT_TIMEOUT_S = 70

 DEFAULT_TIMEOUT   = 35

@@ -948,27 +948,35 @@
                             elt.css('width', (w-25) + 'px')
                         else if elt.hasClass('sagews-input')
                             elt.css('width', w + 'px')
-
-        @codemirror.on 'beforeChange', (instance, changeObj) =>
-            #console.log("beforeChange: #{misc.to_json(changeObj)}")
-            # Set the evaluated flag to false for the cell that contains the text
-            # that just changed (if applicable)
-            if changeObj.origin? and changeObj.origin != 'setValue'
-                line = changeObj.from.line
-                mark = @find_input_mark(line)
-                if mark?
-                    @remove_cell_flag(mark, FLAGS.this_session)
-
-            if changeObj.origin == 'paste'
-                changeObj.cancel()
-                # WARNING: The Codemirror manual says "Note: you may not do anything
-                # from a "beforeChange" handler that would cause changes to the
-                # document or its visualization."  I think this is OK below though
-                # since we just canceled the change.
-                @remove_cell_flags_from_changeObj(changeObj, ACTION_FLAGS)
-                @_apply_changeObj(changeObj)
-                @process_sage_updates()
-                @sync()
+            @process_sage_updates()
+
+        v = [@codemirror, @codemirror1]
+        for cm in v
+            cm.on 'beforeChange', (instance, changeObj) =>
+                #console.log("beforeChange: #{misc.to_json(changeObj)}")
+                # Set the evaluated flag to false for the cell that contains the text
+                # that just changed (if applicable)
+                if changeObj.origin? and changeObj.origin != 'setValue'
+                    line = changeObj.from.line
+                    mark = @find_input_mark(line)
+                    if mark?
+                        @remove_cell_flag(mark, FLAGS.this_session)
+
+                if changeObj.origin == 'paste'
+                    changeObj.cancel()
+                    # WARNING: The Codemirror manual says "Note: you may not do anything
+                    # from a "beforeChange" handler that would cause changes to the
+                    # document or its visualization."  I think this is OK below though
+                    # since we just canceled the change.
+                    @remove_cell_flags_from_changeObj(changeObj, ACTION_FLAGS)
+                    @_apply_changeObj(changeObj)
+                    @process_sage_updates()
+                    @sync()
+
+            cm.on 'change', () =>
+                if @editor._split_view
+                    @_process_sage_updates(cm, 0, true)
+

     init_worksheet_buttons: () =>
         buttons = @element.find(".salvus-editor-codemirror-worksheet-buttons")
@@ -1214,7 +1222,7 @@
         if @editor._split_view
             @_process_sage_updates(@editor.codemirror1, start)

-    _process_sage_updates: (cm, start) =>
+    _process_sage_updates: (cm, start, ignore_output) =>

         if not start?
             start = 0
@@ -1278,7 +1286,7 @@

                     mark.flagstring = flagstring

-            else if x[0] == MARKERS.output
+            else if x[0] == MARKERS.output and not ignore_output
                 marks = cm.findMarksAt({line:line, ch:1})
                 if marks.length == 0
                     @mark_output_line(cm, line)
@@ -1304,14 +1312,14 @@
                             catch e
                                 log("BUG: error rendering output: '#{s}' -- #{e}")

-            else if x.indexOf(MARKERS.output) != -1
+            else if x.indexOf(MARKERS.output) != -1 and not ignore_output
                 #console.log("correcting merge/paste issue with output marker line (line=#{line})")
                 ch = x.indexOf(MARKERS.output)
                 cm.replaceRange('\n', {line:line, ch:ch})
                 @process_sage_updates(line)
                 return

-            else if x.indexOf(MARKERS.cell) != -1
+            else if x.indexOf(MARKERS.cell) != -1 and not ignore_output
                 #console.log("correcting merge/paste issue with cell marker (line=#{line})")
                 ch = x.indexOf(MARKERS.cell)
                 cm.replaceRange('\n', {line:line, ch:ch})


Added more tests for inject.add calls

Added more tests for inject.add calls

--- test/all/module.coffee
+++ test/all/module.coffee
@@ -86,4 +86,26 @@

     expect(injectObj).to.be.a('object')
     expect(injectObj.Universe).to.be.a('function')
-    expect(injectObj.Universe()).to.equal(42)+    expect(injectObj.Universe()).to.equal(42)
+
+  it 'udefine module definition with injection shorthand', ->
+    injectObj = {}
+
+    udefine.inject.add 'pqr',
+      root: injectObj
+
+    udefine 'pqr', -> -> 43
+
+    expect(injectObj).to.be.a('object')
+    expect(injectObj.pqr).to.be.a('function')
+    expect(injectObj.pqr()).to.equal(43)
+
+  unless hasModule
+    it 'udefine module definition with injection shorthand (globals)', ->
+
+    udefine.inject.add 'stu'
+
+    udefine 'stu', -> -> 44
+
+    expect(window.stu).to.be.a('function')
+    expect(window.stu()).to.equal(44)


add the new tests

add the new tests

--- test/browser/main.iced
+++ test/browser/main.iced
@@ -1,5 +1,6 @@

 mods =
+  multiples : require "../files/multiples.iced"
   unbox_cant_verify : require '../files/unbox_cant_verify.iced'
   zip : require '../files/zip.iced'
   ecc1 : require '../files/ecc1.iced'


viewer reload fix

viewer reload fix

--- client/app/Applications/Viewer.kdapplication/AppController.coffee
+++ client/app/Applications/Viewer.kdapplication/AppController.coffee
@@ -99,7 +99,8 @@
     @viewerHeader.setPath(path)

   refreshIFrame:->
-    @iframe.$().attr 'src', @path
+    @iframe.$().contents().location.reload(yes)
+    # @iframe.$().attr 'src', @path

   isDocumentClean:->
     @clean


Bugfix for an exception that was happening during features extraction

--- extension/packages/marker.coffee
+++ extension/packages/marker.coffee
@@ -91,8 +91,9 @@
     while el.classList?.length == 0 and el not instanceof HTMLDocument
       suffix = "#{ suffix } #{ el.tagName }"
       el = el.parentNode
-    for className in el.classList
-      features["#{ el.tagName }.#{ className }#{ suffix }"] = 10
+    if el and el.classList
+      for className in el.classList
+        features["#{ el.tagName }.#{ className }#{ suffix }"] = 10

     # Element id
     if @element.id


test correction

test correction

--- test/data_tests.coffee
+++ test/data_tests.coffee
@@ -54,6 +54,7 @@
         it "When I send a request to get Note with id 123", (done) ->
             client.get "data/123/", (error, response, body) =>
                 @response = response
+                done()

         it "Then error 404 should be returned", ->
             @response.statusCode.should.equal(404)
@@ -64,5 +65,5 @@
                 @body = JSON.parse body
                 done()

-        it "Then {} should be returned", ->
-            @body.should.equal {"value":"val"}
+        it "Then { _id: '321', value: 'val'} should be returned", ->
+            @body.should.deep.equal {"_id": '321', "value":"val"}


Enable 404 unit test

Enable 404 unit test

--- test/unit/index.coffee
+++ test/unit/index.coffee
@@ -81,11 +81,11 @@
       .end(done)

 describe 'Error Handling', ->
-  xit 'Should display a 404 error', (done) ->
+  it 'Should display a 404 error', (done) ->
     request(app)
       .get('/xxsdsdsdsd')
       .expect(404)
-      .expect(/Not Found/)
+      .expect(/Not found/)
       .end(done)

 describe "Status End Point", ->


Disable one unit test on travis

Disable one unit test on travis

--- test/unit/pouch.coffee
+++ test/unit/pouch.coffee
@@ -323,32 +323,34 @@
                     done()

-        describe 'byRecursivePath (bis)', ->
-            @timeout 60000
-
-            # jsverify only works with Promise for async stuff
-            return unless typeof Promise is 'function'
-
-            it 'gets the nested files and folders', (done) ->
-                base = 'byRecursivePath'
-                property = jsv.forall 'nearray nestring', (paths) =>
-                    paths = uniq paths.concat([base])
+    # Disable this test on travis because it can be really slow...
+    return if process.env.TRAVIS
+    describe 'byRecursivePath (bis)', ->
+        @timeout 60000
+
+        # jsverify only works with Promise for async stuff
+        return unless typeof Promise is 'function'
+
+        it 'gets the nested files and folders', (done) ->
+            base = 'byRecursivePath'
+            property = jsv.forall 'nearray nestring', (paths) =>
+                paths = uniq paths.concat([base])
+                new Promise (resolve, reject) =>
+                    @pouch.resetDatabase (err) ->
+                        if err
+                            reject err
+                        else
+                            resolve()
+                .then =>
+                    Promise.all paths.map (p) =>
+                        doc = _id: path.join(base, p), docType: 'folder'
+                        @pouch.db.put doc
+                .then =>
                     new Promise (resolve, reject) =>
-                        @pouch.resetDatabase (err) ->
+                        @pouch.byRecursivePath base, (err, docs) ->
                             if err
                                 reject err
                             else
-                                resolve()
-                    .then =>
-                        Promise.all paths.map (p) =>
-                            doc = _id: path.join(base, p), docType: 'folder'
-                            @pouch.db.put doc
-                    .then =>
-                        new Promise (resolve, reject) =>
-                            @pouch.byRecursivePath base, (err, docs) ->
-                                if err
-                                    reject err
-                                else
-                                    resolve docs.length is paths.length
-                jsv.assert(property, tests: 10).then (res) ->
-                    if res is true then done() else done res
+                                resolve docs.length is paths.length
+            jsv.assert(property, tests: 10).then (res) ->
+                if res is true then done() else done res


createInfoWindow (Balloon) on YMap

createInfoWindow (Balloon) on YMap

--- vendor/assets/javascripts/gmaps4rails/yandex/objects/marker.coffee
+++ vendor/assets/javascripts/gmaps4rails/yandex/objects/marker.coffee
@@ -7,18 +7,14 @@
   @extend  Gmaps4Rails.Marker.Class
   @extend  Gmaps4Rails.Configuration

-  @CONF:
-    infobox:  "description" #description or htmlContent
-
   constructor: (args, controller)->
     @controller = controller

     markerLatLng = @createLatLng(args.lat, args.lng)

-    #// Marker sizes are expressed as a Size of X,Y
     @serviceObject = new ymaps.Placemark(markerLatLng, {
-        balloonContent: @description,
-        iconContent: "TEST"
+        balloonContent: '"' + @description + '"',
+        iconContent: "ICON!!!"
         }
     );

@@ -39,4 +35,4 @@
     @serviceObject.setOptions({ visible: false })

   createInfoWindow : () ->
-    @serviceObject.infoWindow = @description if @description?+    @serviceObject.properties.set("balloonContent", @description) if @description?


sepereated createGroup callback function

--- servers/lib/server/handlers/createteam.coffee
+++ servers/lib/server/handlers/createteam.coffee
@@ -34,8 +34,6 @@
   clientIPAddress     = req.headers['x-forwarded-for'] || req.connection.remoteAddress
   # parsing booling from string
   alreadyMember       = alreadyMember is 'true'
-  # this function will be bound with necessary data in the queue later
-  handleGroupCreation = ->

   queue = [

@@ -80,91 +78,92 @@
         queue.next()

     ->
-      # binding createGroup function to be used as callback in both login and convert
-      handleGroupCreation = createGroup.bind null, client, req, res, body
-      queue.next()
-
-    ->
+      # generating callback function to be used in both login and convert
+      createGroupKallback = generateCreateGroupKallback client, req, res, body
+
       if alreadyMember
-      then JUser.login client.sessionToken, body, handleGroupCreation
-      else JUser.convert client, body, handleGroupCreation
+      then JUser.login client.sessionToken, body, createGroupKallback
+      else JUser.convert client, body, createGroupKallback

   ]

   daisy queue

-createGroup = (client, req, res, body, err, result) ->
-
-  return res.status(400).send getErrorMessage err  if err?
-
-  { # companyName, team name, basically a title, can be changed
-    companyName
-    # slug is team slug, unique name. Can not be changed
-    slug
-    redirect
-    # invitees are comma separated emails which will be invited to that team
-    invitees
-    # domains holds allowed domains for signinup without invitation, comma separated
-    domains
-    # username of the user, can be already registered or a new one for creation
-    username
-  } = body
-
-  { JUser, JGroup, JInvitation } = koding.models
-
-  # don't set the cookie we don't want that
-  # bc we're going to redirect the page to the
-  # group subdomain, if you can set the cookie for
-  # the subdomain - SY cc/ @cihangir
-
-  # res.cookie 'clientId', result.newToken, path : '/'
-
-  # set session token for later usage down the line
-  owner                      = result.account
-  redirect                  ?= '/'
-  client.sessionToken        = result.newToken or result.replacementToken
-  client.connection.delegate = result.account
-
-  return  if validateGroupData(body, res) isnt yes
-
-  JGroup.create client,
-    slug            : slug
-    title           : companyName
-    visibility      : 'hidden'
-    initialData     : body
-    allowedDomains  : convertToArray domains # clear & convert domains into array
-    defaultChannels : []
-  , owner, (err, group) ->
-
-    if err or not group
-      console.error 'Error while creating the group', err
-      return res.status(500).send "Couldn't create the group."
-
-    queue = [
-
-      # add other parallel operations here
-      -> createInvitations client, invitees, (err) ->
-          console.error "Err while creating invitations", err  if err
-          queue.fin()
-
-    ]
-
-    dash queue, (err)->
-      # do not block group creation
-      console.error "Error while creating group artifacts", body, err if err
-
-      # temp code for creating the team-access cookie for development
-      # sandbox and latest share the same config file
-      # so sandbox.koding.com covers both cases ಠ_ಠ - SY
-      if /ngrok|sandbox\.koding/.test KONFIG.hostname
-        console.log "setting cookie for #{slug}.dev.koding.com"
-        res.cookie 'team-access', 'yes', domain : ".dev.koding.com"
-
-      # handle the request as an XHR response:
-      return res.status(200).end() if req.xhr
-      # handle the request with an HTTP redirect:
-      res.redirect 301, redirect
+generateCreateGroupKallback = (client, req, res, body) ->
+
+  # returning a callback function
+  return (err, result) ->
+
+    return res.status(400).send getErrorMessage err  if err?
+
+    { # companyName, team name, basically a title, can be changed
+      companyName
+      # slug is team slug, unique name. Can not be changed
+      slug
+      redirect
+      # invitees are comma separated emails which will be invited to that team
+      invitees
+      # domains holds allowed domains for signinup without invitation, comma separated
+      domains
+      # username of the user, can be already registered or a new one for creation
+      username
+    } = body
+
+    { JUser, JGroup, JInvitation } = koding.models
+
+    # don't set the cookie we don't want that
+    # bc we're going to redirect the page to the
+    # group subdomain, if you can set the cookie for
+    # the subdomain - SY cc/ @cihangir
+
+    # res.cookie 'clientId', result.newToken, path : '/'
+
+    # set session token for later usage down the line
+    owner                      = result.account
+    redirect                  ?= '/'
+    client.sessionToken        = result.newToken
+    client.connection.delegate = result.account
+
+    return  if validateGroupData(body, res) isnt yes
+
+    JGroup.create client,
+      slug            : slug
+      title           : companyName
+      visibility      : 'hidden'
+      initialData     : body
+      allowedDomains  : convertToArray domains # clear & convert domains into array
+      defaultChannels : []
+    , owner, (err, group) ->
+
+      if err or not group
+        console.error 'Error while creating the group', err
+        return res.status(500).send "Couldn't create the group."
+
+      queue = [
+
+        # add other parallel operations here
+        -> createInvitations client, invitees, (err) ->
+            console.error "Err while creating invitations", err  if err
+            queue.fin()
+
+      ]
+
+      dash queue, (err)->
+        # do not block group creation
+        console.error "Error while creating group artifacts", body, err if err
+
+        # temp code for creating the team-access cookie for development
+        # sandbox and latest share the same config file
+        # so sandbox.koding.com covers both cases ಠ_ಠ - SY
+        if /ngrok|sandbox\.koding/.test KONFIG.hostname
+          console.log "setting cookie for #{slug}.dev.koding.com"
+          res.cookie 'team-access', 'yes', domain : ".dev.koding.com"
+
+        # handle the request as an XHR response:
+        return res.status(200).end() if req.xhr
+        # handle the request with an HTTP redirect:
+        res.redirect 301, redirect

 validateGroupData = (body, res) ->


Improve slugify function

Improve slugify function

now replace foreign chars


Read Note

Read Note

--- coffeescript/db.coffee
+++ coffeescript/db.coffee
@@ -75,6 +75,7 @@
 				name: name
 				notebook: notebook
 				content: content
+				date: Math.round(new Date() / 1000)
 			}
 		return id

@@ -111,6 +112,9 @@
 	# @param {String} id The note id
 	# @return {Object} note Note metadata with content
 	###
+	readNote: (id) ->
+		note = fs.readFileSync(path.join(@notebookdir, @filenameNote(id)))
+		JSON.parse note.toString()

 	###
 	# Deletes a notebook
@@ -129,3 +133,4 @@
 # notebook = noteddb.createNotebook("Getting Started")
 # noteddb.createNote("Awesome", "b67fe9194949bd46", "YOLO SWAGGGG")
 console.log noteddb.readNotebook(noteddb.readNotebooks()[0])
+console.log noteddb.readNote("875feea4d0f966aa")


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/flickerable.coffee
+++ test/flickerable.coffee
@@ -12,7 +12,7 @@
   equals player.I.flickerDuration, 30

 test 'it should be able to set one param at a time', ->
-
+  player = GameObje

 test 'it should change the alpha value when the flicker duration has elapsed', ->
   player = GameObject


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/flickerable.coffee
+++ test/flickerable.coffee
@@ -1,4 +1,17 @@
 module "Flickerable"

+# sets up player object before each test
+QUnit.testStart = ->
+  window.player = GameObject
+    x: 10
+    y: 20
+    width: 10
+    height: 20
+    speed: 3
+
+  player.include Controllable
+
+  # Mock the keydown object
+  window.keydown = {}

 module()


[test fix] update queues syntax to match api

--- test/redis-model.coffee
+++ test/redis-model.coffee
@@ -550,8 +550,15 @@
       await fakeJob "test2",   data, "delayed", ideally defer queueAndJob2

       await instance.queues ideally defer queues
-      expected =
-        test:
+      expected = [{
+          active:    0
+          completed: 0
+          delayed:   1
+          failed:    0
+          name:      "test2"
+          stuck:     0
+          wait:      0
+        },{
           active:    0
           completed: 0
           delayed:   0
@@ -559,14 +566,7 @@
           name:      "test"
           stuck:     1
           wait:      0
-        test2:
-          active:    0
-          completed: 0
-          delayed:   1
-          failed:    0
-          name:      "test2"
-          stuck:     0
-          wait:      0
+        }]

       expect(queues).to.deep.equal expected


Test: Added test for multiple mocks to the same url with different verbs

--- test/requestType.coffee
+++ test/requestType.coffee
@@ -94,3 +94,33 @@
     complete: (xhr, textStatus) ->
       equal(xhr.responseText, "Uppercase", "Response text was not a match")
       start()
+
+asyncTest "Multiple handlers can exist for the same url with different verbs", ->
+  $.fauxjax.new
+    type: "GET"
+    url: "/faux-request"
+
+  $.fauxjax.new
+    type: "POST"
+    url: "/faux-request"
+    dataType: "json"
+    data: {empty: "data"}
+
+  $.ajax
+    type: "GET"
+    url: "/faux-request"
+    success: (data, textStatus, xhr) ->
+      ok(true, "Handler with a GET verb does exist for this request it should be successfully mocked")
+    complete: (xhr, textStatus) ->
+      start()
+
+  stop()
+
+  $.ajax
+    type: "POST"
+    url: "/faux-request"
+    data: {empty: "data"}
+    success: (data, textStatus, xhr) ->
+      ok(true, "Handler with a POST verb does exist for this request it should be successfully mocked")
+    complete: (xhr, textStatus) ->
+      start()


replace test initialization

replace test initialization

--- test/user-object.coffee
+++ test/user-object.coffee
@@ -1,12 +1,12 @@
 assert=require 'assert'

-user_object=require '../lib/user-object'
+my_user=require '../'

 describe 'User object',->
     describe 'authenticate & versioning',->
         config=null
         beforeEach ->
-            config=new user_object.UserConfig()
+            config=my_user.init()

         it 'user id',->
             user=config.create "id"
@@ -124,7 +124,7 @@
     describe 'user data',->
         config=null
         beforeEach ->
-            config=new user_object.UserConfig()
+            config=my_user.init()

         it 'save & store data',->
             user=config.create "id"


Restrict visa valid digit lengths to 13/16

As per http://en.wikipedia.org/wiki/Bank_card_number#Issuer_identification_number_.28IIN.29 visa numbers are only 13/16 digits long. This prevents numbers like `4111 1111 1111 22` from incorrectly passing `validateCardNumber`

--- src/jquery.payment.coffee
+++ src/jquery.payment.coffee
@@ -77,7 +77,7 @@
       type: 'visa'
       pattern: /^4/
       format: defaultFormat
-      length: [13..16]
+      length: [13, 16]
       cvcLength: [3]
       luhn: true
   }


Sensible class for textarea container, as this is indeed the content of this "evidenceish"

--- core/app/backbone/views/sub_comments/sub_comments_add_view.coffee
+++ core/app/backbone/views/sub_comments/sub_comments_add_view.coffee
@@ -74,7 +74,7 @@
         x

-

+


Replace for real

Replace for real

--- scripts/poo-tracker.coffee
+++ scripts/poo-tracker.coffee
@@ -10,7 +10,7 @@

 module.exports = (robot) ->
   checkRedisForShit = ->
-    redis_clent.lindex POO_TRACKER_KEY, -1, (err, reply) ->
+    redis_client.lindex POO_TRACKER_KEY, -1, (err, reply) ->
       return console.error("Failed lindex with key '#{POO_TRACKER_KEY}' and index -1: #{err}") if err
       return if reply == null


scores review link selector update

scores review link selector update

--- test-integration/helpers/ui/scores.coffee
+++ test-integration/helpers/ui/scores.coffee
@@ -20,7 +20,7 @@
   hsNameLink:
     css: '.name-cell a.student-name'
   hsReviewLink:
-    css: 'a.review-plan'
+    css: '.review-link a'
   periodTab:
     css: '.nav-tabs li:nth-child(2)'
   displayAs:


Add 'Getting started' to list of priority types

--- assets/javascripts/collections/types.coffee
+++ assets/javascripts/collections/types.coffee
@@ -7,7 +7,7 @@
       (result[@_groupFor(type)] ||= []).push(type)
     result.filter (e) -> e.length > 0

-  GUIDES_RGX = /guide|tutorial/i
+  GUIDES_RGX = /guide|tutorial|getting\ started/i

   _groupFor: (type) ->
     if GUIDES_RGX.test(type.name)


config change

config change

--- both/config/_config.coffee
+++ both/config/_config.coffee
@@ -1,7 +1,7 @@
 @Config =
 	name: 'My App'
 	title: 'Make Meteor Apps. Fast.'
-	subtitle: 'Create beautiful and functioning Meteor apps really quick'
+	subtitle: 'Create beautiful  Meteor apps really fast'
 	logo: ->
 		'<b>' + @name + '</b>'
 	footer: ->


close menu after clicking download

close menu after clicking download

--- resources/assets/lib/play-detail-menu.coffee
+++ resources/assets/lib/play-detail-menu.coffee
@@ -81,4 +81,5 @@
                   mode: @props.score.beatmap.mode
                   user: @props.score.user_id
           'data-turbolinks': false
+          onClick: @onClick
           osu.trans 'users.show.extra.top_ranks.download_replay'


Change yellow cf boundary

Change yellow cf boundary

--- server/calculations/calculateCfRating.coffee
+++ server/calculations/calculateCfRating.coffee
@@ -2,8 +2,8 @@
 import CfResult from '../models/cfResult'
 import logger from '../log'

-colors = [[0, "gray"], [1200, "green"], [1400, "#03A89E"], [1600, "blue"],
-         [1900, "#a0a"], [2200, "#bb0"], [2300, "#FF8C00"], [2400, "red"]];
+colors = [[0, "gray"], [1200, "green"], [1400, "#03A89E"], [1600, "blue"],
+         [1900, "#a0a"], [2100, "#bb0"], [2300, "#FF8C00"], [2400, "red"]];

 getRating = (user) ->
     href = "http://codeforces.com/api/user.info?handles=" + user.cf.login
@@ -27,15 +27,15 @@
     change = 0
     contests = 0
     first = true
-
+
     for elem in data
         thisDate = new Date(elem["ratingUpdateTimeSeconds"] * 1000)
         await new CfResult(
-            userId: user._id,
-            contestId: elem["contestId"],
-            time: thisDate,
-            place: elem["rank"],
-            oldRating: elem["oldRating"],
+            userId: user._id,
+            contestId: elem["contestId"],
+            time: thisDate,
+            place: elem["rank"],
+            oldRating: elem["oldRating"],
             newRating: elem["newRating"]
         ).upsert()
         if (not first)  # very first contest has no meaning as start rating is 1500
@@ -61,10 +61,8 @@
         return {}
     color = colorByRating(rating)
     activityAndProgress = await getActivityAndProgress(user)
-    return
-        rating: rating,
-        color: color,
-        activity: activityAndProgress.activity,
+    return
+        rating: rating,
+        color: color,
+        activity: activityAndProgress.activity,
         progress: activityAndProgress.progress
-
-


re-use existing token in sendResetPassword method

--- server/methods/mail/sendResetPassword.coffee
+++ server/methods/mail/sendResetPassword.coffee
@@ -23,21 +23,22 @@
 		else
 			user = users[0]

-		Meteor.users.update find, $set: 'services.password.reset': token: token
+		if user.services? && user.services.password? && user.services.password.reset? && user.services.password.reset.token
+			token = user.services.password.reset.token
+		else
+			Meteor.users.update find, $set: 'services.password.reset': token: token

-		Meteor.call 'sendMail',
-			recipient: obj.email
-			sender: 'JW Management'
-			from: 'support@jwmanagement.org'
-			subject: TAPi18n.__('mail.resetPassword.subject', '', user.profile.language)
-			template: 'resetPassword'
-			language: user.profile.language
-			data:
-				token: token
+		try
+			Meteor.call 'sendMail',
+				recipient: obj.email
+				sender: 'JW Management'
+				from: 'support@jwmanagement.org'
+				subject: TAPi18n.__('mail.resetPassword.subject', '', user.profile.language)
+				template: 'resetPassword'
 				language: user.profile.language
-				content: getMailTexts 'resetPassword', user.profile.language
-		, (err, res) ->
-			if err
-				console.log 'sendMail failed: ' + err
-			else
-				true
+				data:
+					token: token
+					language: user.profile.language
+					content: getMailTexts 'resetPassword', user.profile.language
+		catch e
+			console.error e


kill stray console.log

kill stray console.log

--- shared/src/components/notifications/bar.cjsx
+++ shared/src/components/notifications/bar.cjsx
@@ -57,8 +57,6 @@
     @setState({displayTimer})

   render: ->
-    #console.log @state.notices, Notifications.getActive()
-
     <div className={
       classnames("openstax-notifications-bar", @props.className, {viewable: not isEmpty(@state.notices)})
     }>


Alert degrades gracefully under node.js

Alert degrades gracefully under node.js

--- components/Alert.coffee
+++ components/Alert.coffee
@@ -10,7 +10,10 @@

     @inPorts.in.on 'data', (data) =>
       if data isnt undefined
-        alert data
+        if noflo.isBrowser()
+          alert data
+        else
+          console.log("Alert: " + data)
         @outPorts.out.send true
         @outPorts.out.disconnect()


Rename data('context') to data('ev-state') to avoid init-time collision with context option.

--- app/assets/javascripts/browse_everything/behavior.js.coffee
+++ app/assets/javascripts/browse_everything/behavior.js.coffee
@@ -20,7 +20,7 @@
       done:   (func) -> ctx.callbacks.done.add(func)   ; return this
       cancel: (func) -> ctx.callbacks.cancel.add(func) ; return this
       fail:   (func) -> ctx.callbacks.fail.add(func)   ; return this
-    $(obj).data('context',ctx)
+    $(obj).data('ev-state',ctx)
     ctx

   toHiddenFields = (data) ->
@@ -154,8 +154,8 @@
       url: $('a.ev-link',node.row).attr('href')
       data:
         parent: node.row.data('tt-id')
-        accept: dialog.data('context').opts.accept
-        context: dialog.data('context').opts.context
+        accept: dialog.data('ev-state').opts.accept
+        context: dialog.data('ev-state').opts.context
     .done (html) ->
       setProgress('100')
       clearInterval progressIntervalID
@@ -182,12 +182,12 @@
   $(window).on('resize', -> sizeColumns($('table#file-list')))

   $.fn.browseEverything = (options) ->
-    ctx = $(this).data('context')
+    ctx = $(this).data('ev-state')
     options = $(this).data() unless (ctx? or options?)
     if options?
       ctx = initialize(this[0], options)
       $(this).click () ->
-        dialog.data('context',ctx)
+        dialog.data('ev-state',ctx)
         dialog.load ctx.opts.route, () ->
           setTimeout refreshFiles, 500
           ctx.callbacks.show.fire()
@@ -213,7 +213,7 @@

   $(document).on 'click', 'button.ev-cancel', (event) ->
     event.preventDefault()
-    dialog.data('context').callbacks.cancel.fire()
+    dialog.data('ev-state').callbacks.cancel.fire()
     $('.ev-browser').modal('hide')

   $(document).on 'click', 'button.ev-submit', (event) ->
@@ -222,7 +222,7 @@
     $('body').css('cursor','wait')
     main_form = $(this).closest('form')
     resolver_url = main_form.data('resolver')
-    ctx = dialog.data('context')
+    ctx = dialog.data('ev-state')
     $(main_form).find('input[name=context]').val(ctx.opts.context)
     $.ajax resolver_url,
       type: 'POST'
@@ -254,8 +254,8 @@
     $.ajax
       url: $(this).val(),
       data:
-        accept: dialog.data('context').opts.accept
-        context: dialog.data('context').opts.context
+        accept: dialog.data('ev-state').opts.accept
+        context: dialog.data('ev-state').opts.context
     .done (data) ->
       $('.ev-files').html(data)
       indicateSelected();
@@ -304,7 +304,7 @@
 auto_toggle = ->
   triggers = $('*[data-toggle=browse-everything]')
   triggers.each () ->
-    ctx = $(this).data('context')
+    ctx = $(this).data('ev-state')
     $(this).browseEverything($(this).data()) unless ctx?

 if Turbolinks? && Turbolinks.supported


call replace with multiple commas better

--- app/assets/javascripts/effective_price/initialize.js.coffee
+++ app/assets/javascripts/effective_price/initialize.js.coffee
@@ -20,12 +20,12 @@

 # Assign the hidden input a value of 100x value
 $(document).on 'change keyup', "input[type='text'].effective_price", (event) ->
-  value = (parseFloat($(event.target).val().replace(',', '')) || 0.00) * 100.00
+  value = parseFloat($(event.target).val().replace(/,/g, '') || 0.00) * 100.00
   $(event.target).siblings("input[type='hidden']").first().val(value.toFixed(0))

 # Format the value for display as currency (USD)
 $(document).on 'change', "input[type='text'].effective_price", (event) ->
-  value = parseFloat($(event.target).val().replace(',', ''))
+  value = parseFloat($(event.target).val().replace(/,/g, ''))

   if isNaN(value) == false
     value = value.toFixed(2).toString()


[*] new scroll

[*] new scroll

--- src/methods/scroll.coffee
+++ src/methods/scroll.coffee
@@ -2,48 +2,71 @@
   opts.gutter ?= 10
   opts.pad ?= 10
   elmnt = @element
-  elmnt.style.position = 'relative'
-  elmnt.style.overflow = 'hidden'
   elmnt.innerHTML = '
' + elmnt.innerHTML + '
'
   elmnt.innerHTML += '

'
   elmnt.innerHTML += '

'
   content = elmnt.querySelector '.mscr-content'
   content.style.width = elmnt.offsetWidth + 'px'
   content.style.height = elmnt.offsetHeight + 'px'
+  elmnt.style.height = opts.height if opts.height?
   elmnt.style.width = opts.width if opts.width?
+  cw = parseInt content.offsetWidth
+  ch = parseInt content.offsetHeight
+  ew = parseInt elmnt.offsetWidth
+  eh = parseInt elmnt.offsetHeight
+  rw = ew / cw
+  rh = eh / ch
+  elmnt.style.position = 'relative'
+  elmnt.style.overflow = 'hidden'
   content.style.position = 'absolute'
   content.style.top = content.style.left = 0
-  elmnt.style.height = opts.height if opts.height?
-  rw = parseInt(elmnt.offsetWidth) / parseInt content.offsetWidth
-  rh = parseInt(elmnt.offsetHeight) / parseInt content.offsetHeight
   horizontal = rw < 1
   vertical = rh < 1
+  previous = null
+  moving = no
+  xy =
+    X : 0
+    Y : 0
+  movingEvent = (e, xy, t) ->
+    e.preventDefault()
+    moving = xy
+    previous = e
+    elmnt.classList.add 'show-scrollbar'
+    t.classList.add 'moving'
   if horizontal
     trackH = elmnt.querySelector '.mscr-track-h'
-    dragH = elmnt.querySelector '.mscr-drag-h'
+    dragH = trackH.querySelector '.mscr-drag-h'
     trackH.style.position = 'absolute'
     trackH.style.bottom = trackH.style.left = 0
     trackH.style.height = opts.gutter + 'px'
     dragH.style.position = 'absolute'
     dragH.style.top = dragH.style.left = 0
     dragH.style.height = opts.gutter + 'px'
+    rw = (ew - parseInt opts.gutter) / cw
+    w = ew
+    if vertical
+      w -= parseInt opts.gutter
+    trackH.style.width = w + 'px'
+    dragH.style.width = ~~(w * rw) + 'px'
+    dragH.addEventListener 'mousedown', (e) ->
+      movingEvent e, 'X', @
   if vertical
     trackV = elmnt.querySelector '.mscr-track-v'
-    dragV = elmnt.querySelector '.mscr-drag-v'
+    dragV = trackV.querySelector '.mscr-drag-v'
     trackV.style.position = 'absolute'
     trackV.style.top = trackV.style.right = 0
     trackV.style.width = opts.gutter + 'px'
     dragV.style.position = 'absolute'
     dragV.style.top = dragV.style.left = 0
     dragV.style.width = opts.gutter + 'px'
-  if horizontal and vertical
-    rw = (parseInt(elmnt.offsetWidth) - opts.gutter) / parseInt content.offsetWidth
-    rh = (parseInt(elmnt.offsetHeight) - opts.gutter) / parseInt content.offsetHeight
-  previous = null
-  moving = no
-  xy =
-    X : 0
-    Y : 0
+    dragV.addEventListener 'mousedown', (e) ->
+      movingEvent e, 'Y', @
+    rh = (eh - parseInt opts.gutter) / ch
+    h = eh
+    if horizontal
+      h -= opts.gutter
+    trackV.style.height = h + 'px'
+    dragV.style.height = ~~(h * rh) + 'px'
   document.addEventListener 'mouseup', (e) ->
     e.preventDefault()
     moving = no
@@ -62,7 +85,7 @@
     e = e.originalEvent if e.originalEvent?
     elmnt.classList.add 'show-scrollbar'
     moving = if e.shiftKey || e.wheelDeltaX then 'X' else 'Y'
-    move opts.pad * (if (e.wheelDelta || -e.detail) < 0 then 1 else -1)
+    move(opts.pad * (if (e.wheelDelta || -e.detail) < 0 then 1 else -1))
     moving = no
     elmnt.classList.remove 'show-scrollbar'
     xy.X = parseInt dragH.style.left if horizontal
@@ -70,31 +93,6 @@
     false
   elmnt.addEventListener 'mousewheel', wheeling, false
   elmnt.addEventListener 'DOMMouseScroll', wheeling, false
-  if dragH?
-    dragH.addEventListener 'mousedown', (e) ->
-      e.preventDefault()
-      moving = 'X'
-      previous = e
-      elmnt.classList.add 'show-scrollbar'
-      @classList.add 'moving'
-  if dragV?
-    dragV.addEventListener 'mousedown', (e) ->
-      e.preventDefault()
-      moving = 'Y'
-      previous = e
-      elmnt.classList.add 'show-scrollbar'
-      @classList.add 'moving'
-  w = parseInt elmnt.offsetWidth
-  h = parseInt elmnt.offsetHeight
-  if vertical and horizontal
-    w -= opts.gutter
-    h -= opts.gutter
-  if horizontal
-    trackH.style.width = w + 'px'
-    dragH.style.width = ~~(w * rw) + 'px'
-  if vertical
-    trackV.style.height = h + 'px'
-    dragV.style.height = ~~(h * rh) + 'px'
   move = (pad) ->
     pad += xy[moving]
     if moving is 'X'
@@ -103,12 +101,12 @@
       left = max if left > max
       left = 0 if left < 0
       dragH.style.left = left + 'px'
-      content.style.left = ~~(-left/rw) + 'px'
+      content.style.left = parseInt(-left/rw) + 'px'
     else
       top = pad
       max = parseInt(trackV.offsetHeight) - parseInt(dragV.offsetHeight)
       top = max if top > max
       top = 0 if top < 0
       dragV.style.top = top + 'px'
-      content.style.top = ~~(-top/rh) + 'px'
+      content.style.top = parseInt(-top/rh) + 'px'
   @


OHIE-127: Update channel.route model

OHIE-127: Update channel.route model

--- src/model/channels.coffee
+++ src/model/channels.coffee
@@ -18,6 +18,8 @@
   "cert":               Schema.Types.ObjectId
   "status":             type: String, default: 'enabled', enum: ['enabled', 'disabled']
   "forwardAuthHeader":  type: Boolean, default: false
+  "mediator":           String # if present indicates that the route is linked to a specific mediator (URN)
+  "mediatorEndpoint":   String

 AlertsDef =
   "status":       type: String, required: true


Fix in FileUploadsStore

Fix in FileUploadsStore

--- src/flux/stores/file-upload-store.coffee
+++ src/flux/stores/file-upload-store.coffee
@@ -86,7 +86,7 @@
       throw new Error "You need to pass the ID of the message (draft) this Action refers to"

   _getFileStats: ({messageClientId, filePath}) ->
-    fs.stat originPath, (err, stats) =>
+    fs.stat filePath, (err, stats) =>
       if err
         reject("#{filePath} could not be found, or has invalid file permissions.")
       else


Remove useless variable

Remove useless variable

--- src/frontend/modules/notification.coffee
+++ src/frontend/modules/notification.coffee
@@ -38,8 +38,6 @@
              audio = document.getElementById notifName
              if !@active
                   do audio.play
-
-NOTIF = Notification

 if window?
-     window.Notification = NOTIF+     window.Notification = Notification


Remove the debugging message on account creation.

--- server/lib/accounts.coffee
+++ server/lib/accounts.coffee
@@ -77,7 +77,6 @@
 	# when inserting first user give them admin privileges otherwise make a regular user
 	firstUser = RocketChat.models.Users.findOne({ _id: { $ne: 'rocket.cat' }}, { sort: { createdAt: 1 }})
 	roleName = if firstUser?._id is _id then 'admin' else 'user'
-	console.log "The role for #{_id} is #{roleName}."

 	RocketChat.authz.addUsersToRoles(_id, roleName)
 	RocketChat.callbacks.run 'afterCreateUser', options, user


trivial change to trigger a build

trivial change to trigger a build

--- src/apl.coffee
+++ src/apl.coffee
@@ -16,5 +16,5 @@
 # [coffee-subscript](https://github.com/ngn/coffee-subscript#readme) and
 # others.
 compile = require("./compiler").compile
-exports.compile = (code) -> "(function(){#{compile code, embedded: true}})()"
+exports.compile = (code) -> "(function(){#{compile code, embedded: true}})()"
 exports.fn = compile: (code) -> "function(){#{compile code, embedded: true}}"


Fix integration tests

Fix integration tests

--- src/app.coffee
+++ src/app.coffee
@@ -45,7 +45,8 @@
     constructor: (basePath) ->
         @lang = 'fr'
         basePath ?= path.homedir()
-        @basePath = path.resolve '.cozy-desktop', basePath
+        basePath  = path.resolve basePath
+        @basePath = path.join basePath, '.cozy-desktop'
         @config = new Config @basePath
         @pouch  = new Pouch @config
         @events = new EventEmitter


disable colors in non-TTY

disable colors in non-TTY

--- src/app.coffee
+++ src/app.coffee
@@ -4,7 +4,7 @@
 logger.exitOnError = false
 logger.remove logger.transports.Console
 logger.add logger.transports.Console,
-    colorize:   true
+    colorize:   process.stdout.isTTY
     timestamp:  true
 logger.add logger.transports.File,
     filename:   'ingress-exporter.log'


Remove property always set in constructor

--- src/pane-container.coffee
+++ src/pane-container.coffee
@@ -241,5 +241,3 @@
     @$activePane
       .switch((activePane) -> activePane?.$activeItem)
       .distinctUntilChanged()
-else
-  PaneContainer::activePane = null


load notebook list on client startup

load notebook list on client startup


log op versions pushed to track changes

log op versions pushed to track changes

--- app/coffee/WebRedisManager.coffee
+++ app/coffee/WebRedisManager.coffee
@@ -1,6 +1,7 @@
 Settings = require('settings-sharelatex')
 rclient = require("redis-sharelatex").createClient(Settings.redis.web)
 async = require "async"
+logger = require('logger-sharelatex')

 module.exports = WebRedisManager =
 	getPendingUpdatesForDoc : (doc_id, callback)->
@@ -25,6 +26,8 @@
 	pushUncompressedHistoryOps: (project_id, doc_id, ops = [], callback = (error, length) ->) ->
 		if ops.length == 0
 			return callback(new Error("cannot push no ops")) # This should never be called with no ops, but protect against a redis error if we sent an empty array to rpush
+		opVersions = ops.map (op) -> op?.v
+		logger.log project_id: project_id, doc_id: doc_id, op_versions: opVersions, "pushing uncompressed history ops"
 		jsonOps = ops.map (op) -> JSON.stringify op
 		async.parallel [
 			(cb) -> rclient.rpush "UncompressedHistoryOps:#{doc_id}", jsonOps..., cb


fix guest obs content

fix guest obs content

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -566,6 +566,7 @@
   showSummaryScreen: ->
     @hideMarkingButtons()
     @nextSubjectButton.show()
+    @setGuestObsContent()
     @classifySummary.fadeIn(150)
     # @finishSubject()


modify js to toggle .missing on box

modify js to toggle .missing on box

--- app/assets/javascripts/workflows.js.coffee
+++ app/assets/javascripts/workflows.js.coffee
@@ -282,15 +282,8 @@
 @missing_data_path = ->
   active_row().hasClass('missing')

-@update_missing_data_path_view = ->
-  if (missing_data_path())
-    $('#script-details-view').css('color', 'red');
-    $('#job-details-staged-dir').css('border-color', 'red');
-    $('#missing-path-note').show();
-  else
-    $('#script-details-view').css('color', 'black');
-    $('#job-details-staged-dir').css('border-color', 'black');
-    $('#missing-path-note').hide();
+@update_missing_data_path_view = ->
+  $('#script-details-view').toggleClass("missing", missing_data_path());

 $ ->
   $('#reset-template-data').on 'click', ->