writable-tracking-buffer: add option to double in size when out of room

--- src/tracking-buffer/writable-tracking-buffer.coffee
+++ src/tracking-buffer/writable-tracking-buffer.coffee
@@ -11,10 +11,11 @@
   When writing, automatically allocates new buffers if there's not enough space.
 ###
 class WritableTrackingBuffer
-  constructor: (@sizeIncrement, @encoding) ->
+  constructor: (@initialSize, @encoding, @doubleSizeGrowth) ->
+    @doubleSizeGrowth ||= false
     @encoding ||= 'ucs2'

-    @buffer = new Buffer(@sizeIncrement)
+    @buffer = new Buffer(@initialSize)
     @position = 0

     @.__defineGetter__("data", ->
@@ -31,10 +32,14 @@

   makeRoomFor: (requiredLength) ->
     if @buffer.length - @position < requiredLength
-      @newBuffer(requiredLength)
+      if @doubleSizeGrowth
+        @newBuffer(@buffer.length * 2)
+      else
+        @newBuffer(requiredLength)
+

   newBuffer: (size) ->
-    size ||= @sizeIncrement
+    size ||= @initialSize

     buffer = @buffer.slice(0, @position)


Do not decode entities in informatics submit (fix #169)

--- server/testSystems/informatics/InformaticsSubmitDownloader.coffee
+++ server/testSystems/informatics/InformaticsSubmitDownloader.coffee
@@ -65,9 +65,7 @@
             source = JSON.parse(page)?.data?.source || ""
             buf = Buffer.from(source, "utf8")
             source = iconv.decode(buf, "latin1")
-            #if source.length == 0
-            #    throw "Source with length 0"
-            return normalizeCode(entities.decode(source))
+            return normalizeCode(source)
         catch e
             logger.warn "Can't download source ", runid, href, e.stack
             throw e


Tear down Ace session when aceEditor is destroyed

This cleans up the Ace worker that parses syntax, preventing a memory leak

--- services/web/public/coffee/ide/editor/directives/aceEditor.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor.coffee
@@ -430,6 +430,8 @@
 				scope.$on '$destroy', () ->
 					if scope.sharejsDoc?
 						detachFromAce(scope.sharejsDoc)
+						session = editor.getSession()
+						session?.destroy()

 				scope.$emit "#{scope.name}:inited", editor


Use skinny arrow

Use skinny arrow

--- src/text-editor-component.coffee
+++ src/text-editor-component.coffee
@@ -442,7 +442,7 @@
         # clipboard.writeText is a sync ipc call on Linux and that
         # will slow down selections.
         ipc.send('write-text-to-selection-clipboard', selectedText)
-    @subscribe @props.editor.onDidChangeSelectionRange =>
+    @subscribe @props.editor.onDidChangeSelectionRange ->
       clearTimeout(timeoutId)
       timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)


Use ::rowForPosition and ::positionForRow instead of simple math

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -28,7 +28,8 @@
     @lineDecorationsByScreenRow = {}
     @lineNumberDecorationsByScreenRow = {}
     @customGutterDecorationsByGutterName = {}
-    @blockDecorationsDimensionsById = new Map
+    @blockDecorationsDimensionsById = {}
+    @blockDecorationsDimensionsByScreenRow = {}
     @screenRowsToMeasure = []
     @transferMeasurementsToModel()
     @transferMeasurementsFromModel()
@@ -426,7 +427,7 @@
       continue if rowsWithinTile.length is 0

       tile = @state.content.tiles[tileStartRow] ?= {}
-      tile.top = tileStartRow * @lineHeight - @scrollTop
+      tile.top = @positionForRow(tileStartRow) - @scrollTop
       tile.left = -@scrollLeft
       tile.height = @tileSize * @lineHeight
       tile.display = "block"
@@ -692,19 +693,35 @@

     return

+  rowForPosition: (position, floor = true) ->
+    top = 0
+    for tileRow in [0..@model.getScreenLineCount()] by @tileSize
+      for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1
+        nextTop = top + @lineHeight
+        if floor
+          return row if nextTop > position
+        else
+          return row if top >= position
+        top = nextTop
+    @model.getScreenLineCount()
+
+  positionForRow: (targetRow) ->
+    top = 0
+    for tileRow in [0..@model.getScreenLineCount()] by @tileSize
+      for row in [tileRow...Math.min(tileRow + @tileSize, @model.getScreenLineCount())] by 1
+        return top if row is targetRow
+        top += @lineHeight
+    top
+
   updateStartRow: ->
     return unless @scrollTop? and @lineHeight?

-    startRow = Math.floor(@scrollTop / @lineHeight)
-    @startRow = Math.max(0, startRow)
+    @startRow = Math.max(0, @rowForPosition(@scrollTop))

   updateEndRow: ->
     return unless @scrollTop? and @lineHeight? and @height?

-    startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight))
-    visibleLinesCount = Math.ceil(@height / @lineHeight) + 1
-    endRow = startRow + visibleLinesCount
-    @endRow = Math.min(@model.getScreenLineCount(), endRow)
+    @endRow = @rowForPosition(@scrollTop + @height + @lineHeight, false)

   updateRowsPerPage: ->
     rowsPerPage = Math.floor(@getClientHeight() / @lineHeight)
@@ -736,16 +753,17 @@
   getLinesHeight: ->
     @lineHeight * @model.getScreenLineCount()

-  getBlockDecorationsHeight: ->
-    sizes = Array.from(@blockDecorationsDimensionsById.values())
+  getBlockDecorationsHeight: (blockDecorations) ->
     sum = (a, b) -> a + b
-    height = sizes.map((size) -> size.height).reduce(sum, 0)
-    height
+    Array.from(blockDecorations).map((size) -> size.height).reduce(sum, 0)

   updateVerticalDimensions: ->
     if @lineHeight?
       oldContentHeight = @contentHeight
-      @contentHeight = Math.round(@getLinesHeight() + @getBlockDecorationsHeight())
+      allBlockDecorations = _.values(@blockDecorationsDimensionsById)
+      @contentHeight = Math.round(
+        @getLinesHeight() + @getBlockDecorationsHeight(allBlockDecorations)
+      )

     if @contentHeight isnt oldContentHeight
       @updateHeight()
@@ -1380,20 +1398,27 @@
       @emitDidUpdateState()

   setBlockDecorationDimensions: (decoration, width, height) ->
-    @blockDecorationsDimensionsById.set(decoration.id, {width, height})
+    screenRow = decoration.getMarker().getHeadScreenPosition().row
+    dimensions = {width, height}
+
+    @blockDecorationsDimensionsByScreenRow[screenRow] ?= {}
+    @blockDecorationsDimensionsByScreenRow[screenRow][decoration.id] = dimensions
+    @blockDecorationsDimensionsById[decoration.id] = dimensions

     @shouldUpdateBlockDecorations = true
     @shouldUpdateVerticalScrollState = true
     @emitDidUpdateState()

   updateBlockDecorations: ->
+    # TODO: Move this inside `DisplayBuffer`
     blockDecorations = {}
     for decoration in @model.getDecorations(type: "block")
       blockDecorations[decoration.id] = decoration

-    @blockDecorationsDimensionsById.forEach (value, key) =>
-      unless blockDecorations.hasOwnProperty(key)
-        @blockDecorationsDimensionsById.delete(key)
+    for screenRow, decorations of @blockDecorationsDimensionsByScreenRow
+      for decorationId of decorations when not blockDecorations.hasOwnProperty(decorationId)
+        delete @blockDecorationsDimensionsById[decorationId]
+        delete @blockDecorationsDimensionsByScreenRow[screenRow][decorationId]

     @shouldUpdateVerticalScrollState = true


Simplify

Simplify

--- src/trix/controllers/input_controller.coffee
+++ src/trix/controllers/input_controller.coffee
@@ -14,12 +14,6 @@
     "68": "d"
     "72": "h"
     "79": "o"
-
-  @keyModifiers:
-    control: "ctrlKey"
-    alt: "altKey"
-    shift: "shiftKey"
-    command: "metaKey"

   constructor: (@element) ->
     @deviceObserver = new Trix.DeviceObserver @element
@@ -40,17 +34,16 @@

   events:
     keydown: (event) ->
-      modifiers = (name for name, property of @constructor.keyModifiers when event[property])
-
       if keyName = @constructor.keyNames[event.keyCode]
-        context = if modifiers.length is 1 then @keys[modifiers[0]] else @keys
+        context = @keys
+        for modifier in ["ctrl", "alt", "shift"] when event["#{modifier}Key"]
+          modifier = "control" if modifier is "ctrl"
+          context = @keys[modifier]
         context[keyName]?.call(this, event)

-      keyboardCommandTriggers = ["control", "command"]
-
-      if modifiers.some((m) -> m in keyboardCommandTriggers)
+      if event.ctrlKey or event.metaKey
         if character = String.fromCharCode(event.keyCode).toLowerCase()
-          keys = modifiers.filter((m) -> m not in keyboardCommandTriggers)
+          keys = (modifier for modifier in ["alt", "shift"] when event["#{modifier}Key"])
           keys.push(character)
           if @delegate?.inputControllerDidReceiveKeyboardCommand(keys)
             event.preventDefault()


discussions: realtime updates- no more empty gray bar.

--- client/app/CommonViews/opinions/opinionviewheader.coffee
+++ client/app/CommonViews/opinions/opinionviewheader.coffee
@@ -134,13 +134,19 @@

   updateRemainingText:=>
+    {opinionCount,repliesCount} = @getData()
+
+    commentText =
+      if repliesCount > 0 then " and #{repliesCount} Comments"
+      else ''
+
     if not @parent? or  @parent.constructor is DiscussionActivityOpinionView
       if @getData().opinionCount > 1
-        @allItemsLink.updatePartial ""
+        @allItemsLink.updatePartial "View #{@getData().opinionCount} answers"+commentText
       else if @getData().opinionCount is 1
-        @allItemsLink.updatePartial ""
+        @allItemsLink.updatePartial "View one Answer"+commentText
       else
-        @allItemsLink.updatePartial "No Answers yet"
+        @allItemsLink.updatePartial "No Answers yet#{if repliesCount > 0 then '. View '+repliesCount+' Comments' else ''}"
     else
       remainingOpinions = @getData().opinionCount-@getDelegate().items.length
       if (remainingOpinions)<@maxCommentToShow


fix whitespace

fix whitespace

--- services/document-updater/app/coffee/HistoryManager.coffee
+++ services/document-updater/app/coffee/HistoryManager.coffee
@@ -18,15 +18,9 @@
 		logger.log { project_id, doc_id, url }, "flushing doc in track changes api"
 		request.post url, (error, res, body)->
 			if error?
-				logger.error(
-				   { error, doc_id, project_id},
-					"track changes doc to track changes api"
-				)
+				logger.error { error, doc_id, project_id}, "track changes doc to track changes api"
 			else if res.statusCode < 200 and res.statusCode >= 300
-				logger.error(
-					{ doc_id, project_id },
-					"track changes api returned a failure status code: #{res.statusCode}"
-				)
+				logger.error { doc_id, project_id }, "track changes api returned a failure status code: #{res.statusCode}"

 	_flushProjectChangesAsync: (project_id) ->
 		return if !Settings.apis?.project_history?


fix log message

fix log message

--- services/document-updater/app/coffee/ProjectManager.coffee
+++ services/document-updater/app/coffee/ProjectManager.coffee
@@ -21,7 +21,7 @@
 					jobs.push (callback) ->
 						DocumentManager.flushDocIfLoadedWithLock project_id, doc_id, (error) ->
 							if error? and error instanceof Errors.NotFoundError
-								logger.warn err: error, project_id: project_id, doc_id: doc_id, "found deleted doc when flushing, removing from redis"
+								logger.warn err: error, project_id: project_id, doc_id: doc_id, "found deleted doc when flushing"
 								callback()
 							else if error?
 								logger.error err: error, project_id: project_id, doc_id: doc_id, "error flushing doc"


Terminal: AppView: add removed restoreTabs notice

--- client/Terminal/AppView.coffee
+++ client/Terminal/AppView.coffee
@@ -1,3 +1,8 @@
+
+# Notes:                                  ~ GG
+# RestoreTab functionality removed with
+# 8c9f80cff24c93107de583cb864b0001f1e737af
+
 class WebTermAppView extends JView

   loadingPartial = 'Loading Terminal...'


View rename working, fixes #128

View rename working, fixes #128

--- client/code/view/subnav.coffee
+++ client/code/view/subnav.coffee
@@ -205,7 +205,7 @@
   initialize: ->
     @model.on 'change', @setDocumentTitle, @
     @model.on 'change', @render, this
-    # set this so we can override it in Cu.View.ViewSubnav
+    # set this so we can override it in Cu.View.ViewNav
     # (where the model to save is in fact the parent dataset's model)
     @modelToSave = @model

@@ -230,6 +230,7 @@
       $wrapper.hide()
       $label.text(@newName).show()
       @model.set 'displayName', @newName
+      console.log 'modelToSave', @modelToSave
       @modelToSave.save {},
         success: =>
           $label.addClass 'saved'
@@ -315,6 +316,12 @@
     @

 class Cu.View.ViewNav extends Cu.View.EditableSubnav
+  initialize: ->
+    super()
+    # override @modelToSave for Cu.View.EditableSubnav,
+    # as the view is saved by saving its dataset
+    @modelToSave = @model.get 'plugsInTo'
+
   className: 'subnav-wrapper'

   events:


Add autocomplete=off to login form

Add autocomplete=off to login form

--- client/components/Login.coffee
+++ client/components/Login.coffee
@@ -64,7 +64,7 @@
         <Grid fluid>

Вход в систему

-            <form onSubmit={@tryLogin}>
+            <form onSubmit={@tryLogin} autocomplete="off">
                 {
                 if not @state.loading


Be more specific

Be more specific

--- client/googletagmanager.coffee
+++ client/googletagmanager.coffee
@@ -6,6 +6,6 @@
     if enable
       logger.debug("Enabling Google Tag Manager")
     else
-      logger.debug("Not enabling Google Tag Manager:", Meteor.settings.public)
+      logger.debug("Not enabling Google Tag Manager, Meteor settings:", Meteor.settings.public)
     enable
 })


IMPROVED - logEvent works better with just one arg

--- source/Art/Engine/DevTools/GlobalEpochStats.coffee
+++ source/Art/Engine/DevTools/GlobalEpochStats.coffee
@@ -90,6 +90,7 @@

     logEvent: (name, id) ->
       now = currentSecond()
+      id ?= name
       @addSampleTime now
       colors =
         generateDrawCache: "green"


Added a new test case

Added a new test case

--- test/async-work-queue.iced
+++ test/async-work-queue.iced
@@ -95,6 +95,14 @@
         queue.push i, cb for i in [1 .. 1000]

+    it "should accept null callbacks", (done) ->
+        worker = (task, callback) -> callback null, task
+
+        queue = new AsyncWorkQueue worker
+        queue.push i for i in [1 .. 1000]
+        queue.push 1001, done
+
+
     it "should work with concurrency higher than 1", (done) ->
         ok = -> done()
         koCalled = false


Mark failing date tests as pending.

Mark failing date tests as pending.

--- test/date-time-spec.coffee
+++ test/date-time-spec.coffee
@@ -3,10 +3,10 @@

 describe 'DateTime', ->

-  it 'should parse', ->
+  xit 'should parse', ->
     DateTime.parse('1976-11-27').toISOString().should.equal('1976-11-27T05:00:00.000Z')

-  it 'should convert date time to absolute date time', ->
+  xit 'should convert date time to absolute date time', ->
     DateTime.format('this feb').substr(4).should.match(/-02-01/)
     DateTime.format('this feb +5h').substr(4).should.match(/-02-01 05:00/)
     DateTime.format('this feb +5h +10m').substr(4).should.match(/-02-01 05:10/)


Refactor map code.

Refactor map code.

--- app/assets/javascripts/places.js.coffee
+++ app/assets/javascripts/places.js.coffee
@@ -4,35 +4,32 @@
 $(() ->
   # make dianping position defalut
   # dpLatlng = new qq.maps.LatLng 31.215240, 121.420290
-  # myOptions
-  #   zoom: 13
-  #   center: dpLatlng
-  #   mapTypeId: qq.maps.MapTypeId.ROADMAP
-
+  currentPosition = new qq.maps.LatLng 31.215240, 121.420290
   map = new qq.maps.Map document.getElementById 'map-container'
+  map.panTo currentPosition
+  map.setZoom 15
   navigator.geolocation.getCurrentPosition (position) ->
     currentPosition = new qq.maps.LatLng position.coords.latitude, position.coords.longitude
     map.panTo currentPosition
-    map.setZoom 15
-    # new qq.maps.Marker
-    #   position: currentPosition
-    #   map: map
+  # new qq.maps.Marker
+  #   position: currentPosition
+  #   map: map

-    # TODO move following code to add place page
-    p_info = new qq.maps.InfoWindow
-      map: map
-      content: ""
-      zIndex: 10
-    map.info = p_info # map.window = p_info
-    marker = new qq.maps.Marker
-      position: currentPosition
-      map: map
-      draggable: true
-      # marker.setDraggable(true);
-    qq.maps.event.addListener map, "dragend", mapListener
-    qq.maps.event.addListener map, "zoom_changed", mapListener
-    qq.maps.event.addListener map, "click", (e) ->
-      marker.setPosition e.latLng
+  # TODO move following code to add place page
+  p_info = new qq.maps.InfoWindow
+    map: map
+    content: ""
+    zIndex: 10
+  map.info = p_info # map.window = p_info
+  marker = new qq.maps.Marker
+    position: currentPosition
+    map: map
+    draggable: true
+    # marker.setDraggable(true);
+  qq.maps.event.addListener map, "dragend", mapListener
+  qq.maps.event.addListener map, "zoom_changed", mapListener
+  qq.maps.event.addListener map, "click", (e) ->
+    marker.setPosition e.latLng
 )

 # function loadScript() {
@@ -47,7 +44,6 @@
   pixel = e.pixel;
   lat = e.latLng;
   # TODO send backend ,map scope
-  console.log map.getBounds()
   # $.ajax({
     # url: "",
       # data: map.getBounds(),


Utf8 decoding of debug info

Utf8 decoding of debug info

--- app/assets/javascripts/player.js.coffee
+++ app/assets/javascripts/player.js.coffee
@@ -27,7 +27,7 @@
       n = timing[1]

       if rest.length > 0
-        console.log 'rest: ' + rest
+        console.log 'rest: ' + Utf8.decode(rest)

       @currentData = rest + @data.slice(@dataIndex, @dataIndex + n)
       @dataIndex += n
@@ -36,7 +36,7 @@
       if rest.length > 20
         head = rest.slice(0, 10)
         hex = ("0x#{c.charCodeAt(0).toString(16)}" for c in head)
-        console.log "failed matching: '" + head + "' (" + hex.join() + ")"
+        console.log "failed matching: '" + Utf8.decode(head) + "' (" + hex.join() + ")"
         return

       unless window.stopped


remove debug statement.

remove debug statement.

--- app/assets/javascripts/signup.js.coffee
+++ app/assets/javascripts/signup.js.coffee
@@ -95,7 +95,6 @@

       errorCount = 0
       @prcessSchoolErrors(errors)
-      debugger
       dispError = ($el, msg) =>
         $el.addClass 'error-field'
         $el.prepend "
#{msg}
"


wikiへのリンクボタンが機能してなかったのを修正

wikiへのリンクボタンが機能してなかったのを修正

--- app/assets/javascripts/viewer.js.coffee
+++ app/assets/javascripts/viewer.js.coffee
@@ -1213,10 +1213,9 @@
     $("#view-modal").on 'click', 'button.wiki-link', (e) ->
       e.preventDefault()
       code = $(e.target).data('jobCode')
-      m = Searcher.forCode(code)
-      return false unless m
-
-      a = m.arcana
+      a = Searcher.forCode(code)
+      return false unless a
+
       lt = if a.wikiName is ''
         "Wikiで最新情報を確認する"
       else


Make tearDown teardown.

Make tearDown teardown.

Temp. disable event stream initializer in testing.

--- test/javascripts/support/test_initializer.js.coffee
+++ test/javascripts/support/test_initializer.js.coffee
@@ -1,4 +1,5 @@
 #= require support/mock_server
+#= require support/factories

 document.write('

')
 document.write('');


exit if mock server fails to start

exit if mock server fails to start

--- test/acceptance/coffee/helpers/Client.coffee
+++ test/acceptance/coffee/helpers/Client.coffee
@@ -30,7 +30,10 @@
 		express = require("express")
 		app = express()
 		app.use express.static(directory)
-		app.listen(port, host)
+		app.listen(port, host).on "error", (error) ->
+			console.error "error starting server:", error.message
+			process.exit(1)
+

 	syncFromCode: (project_id, file, line, column, callback = (error, pdfPositions) ->) ->
 		request.get {


add note about deleting the migration key entries

--- app/coffee/RedisMigrationManager.coffee
+++ app/coffee/RedisMigrationManager.coffee
@@ -44,6 +44,12 @@
 #   the flag
 # - when all docupdaters are setting the flag (and writing to the new redis),
 #   finish the migration by writing all data to the new redis
+#
+# Final stage
+#
+# When all the queues are migrated, remove the migration code and return to a
+# single client pointing at the new redis.  Delete the
+# ProjectHistory:MigrationKey:* entries in the new redis.
 #
 # Rollback
 #


add comment about locking in redis migration

--- app/coffee/RedisMigrationManager.coffee
+++ app/coffee/RedisMigrationManager.coffee
@@ -97,6 +97,14 @@
 	exec: (callback) ->
 		# decide which client to use
 		project_id = getProjectId(@queueKey)
+		# Put a lock around finding and updating the queue to avoid time-of-check to
+		# time-of-use problems. When running in the "switch" phase we need a lock to
+		# guarantee the order of operations. (Example: docupdater A sees an old
+		# queue at t=t0 and pushes onto it at t=t1, project history clears the queue
+		# between t0 and t1, and docupdater B sees the empty queue, sets the
+		# migration flag and pushes onto the new queue at t2. Without a lock it's
+		# possible to have t2 < t1 if docupdater A is slower than B - then there
+		# would be entries in the old and new queues, which we want to avoid.)
 		LockManager.getLock project_id, (error, lockValue) =>
 			return callback(error) if error?
 			releaseLock = (args...) =>


fallback for browsers that don't implement requestAnimationFrame

--- static/coffee/common/plot.coffee
+++ static/coffee/common/plot.coffee
@@ -23,8 +23,10 @@
 delayAnimation = (f) ->
   return f()

-if window.requestAnimationFrame
-  delayAnimation = window.requestAnimationFrame
+
+delayAnimation = window.requestAnimationFrame ||
+        window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
+        window.msRequestAnimationFrame || delayAnimation;

 #Returns a function, that, when invoked, will only be triggered at
 #most once during a given window of time.  If the browser supports
@@ -49,8 +51,7 @@
     if (remaining <= 0 and !pending)
       clearTimeout(timeout);
       pending = true;
-      window.requestAnimationFrame(later)
-      #result = func.apply(context, args);
+      delayAnimation(later)
     else if (!timeout)
       timeout = setTimeout(
        (->


NEW: leading support

NEW: leading support

--- source/Art/Engine/Forms/TextInputElement.coffee
+++ source/Art/Engine/Forms/TextInputElement.coffee
@@ -1,6 +1,12 @@
 # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input
-{defineModule, log, object, merge, select, inspect, wordsArray, timeout, max} = require 'art-standard-lib'
+{
+  defineModule, log, object, merge, select, inspect, wordsArray, timeout, max
+  isNumber
+} = require 'art-standard-lib'
 {rgbColor, point} = require 'art-atomic'
+
+{Layout:ArtTextLayout} = require 'art-text'
+defaultLeading = ArtTextLayout.defaultLayoutOptions.leading

 Foundation = require 'art-foundation'
 {iOSDetect} = Foundation.Browser
@@ -24,6 +30,16 @@
   # TODO: these need to become ElementProperties that update the DOMElement when changed.
   defaultFontSize = 16
   @concreteProperty
+    leading:
+      preprocess: (v) ->
+        if isNumber v
+          v
+        else
+          defaultLeading
+
+      postSetter: (v) ->
+        @domElement?.style.lineHeight = "#{v * 100 | 0}%"
+
     placeholder:  postSetter: (v) -> @domElement?.placeholder = v ? ""
     maxLength:    postSetter: (v) -> @domElement?.maxLength   = v ? null
     fontFamily:   "sans-serif", postSetter: (v) -> @domElement?.fontFamily  = v ? "sans-serif"
@@ -99,6 +115,7 @@
         padding:          "0"
         textAlign:        options.align || "left"
         verticalAlign:    "bottom"
+        lineHeight:       "#{(options.leading || defaultLeading)*100 | 0}%"

       on: merge
         cut:      (keyboardEvent) => @delayedCheckIfValueChanged()


Classroom: Showing a loader view until class manifest fetched and parsed.

--- client/app/Applications/Classroom.kdapplication/Views/classroomclassesview.coffee
+++ client/app/Applications/Classroom.kdapplication/Views/classroomclassesview.coffee
@@ -51,6 +51,10 @@
     @noRelatedClass.show()   unless @getData().related
     @noEnrolledClass.show()  unless @getData().enrolled

+  viewAppended: ->
+    super
+    thumbView.loader.show() for thumbView in @thumbViews
+
   pistachio: ->
     """
       {{> @enrolledContainer}}


fix smoothstate path

fix smoothstate path

--- source/assets/javascripts/application.js.coffee
+++ source/assets/javascripts/application.js.coffee
@@ -1,3 +1,3 @@
-#= require "jquery/dist/jquery.min.js"
-#= require "smoothstate/jquery.smoothState.js"
+#= require "jquery/dist/jquery.min"
+#= require "smoothstate/src/jquery.smoothState"
 #= require "initialize"


removed an unused function

removed an unused function

--- source/javascripts/index/holidaySchedule.coffee
+++ source/javascripts/index/holidaySchedule.coffee
@@ -19,7 +19,6 @@
     else
       moment(@mincha()).subtract(1, 'hour')
   afternoon_shiur: -> $(".#{@chag()}.afternoon-shiur").removeClass("hidden").find(".time").html(time_format(@afternoon_shiur_time()))
-  plag_is_before_615: -> @zmanim.plag().isBefore(@today().hour(18).minute(15))
   set_hakochabim: -> time_format(@zmanim.setHaKochabim3Stars())
   rabbenu_tam: -> time_format(moment(@sunset).add(73, 'minute'))
   set_hakochabim_geonim: -> @zmanim.setHaKochabimGeonim()


Skipping broken CalculateAge in months test

--- Src/coffeescript/cql-execution/test/elm/clinical/test.coffee
+++ Src/coffeescript/cql-execution/test/elm/clinical/test.coffee
@@ -171,7 +171,10 @@
   it 'should execute age in years', ->
     @years.exec(@ctx).should.equal @full_months // 12

-  it 'should execute age in months', ->
+  it.skip 'should execute age in months', ->
+    # This test is failing if you run it in the first half of any
+    # given month and passing for the second half.
+
     # what is returned will depend on whether the day in the current month has
     # made it to the 17th day of the month as declared in the birthday
     dayOfMonth = @today


redundant filter code has been abstracted out

--- app/scripts/alchemy/core/filters.coffee
+++ app/scripts/alchemy/core/filters.coffee
@@ -145,6 +145,8 @@
     #update filters
     update: () ->
         vis = alchemy.vis
+        checked = null
+        highlight = null
         # tagList = $('#tags-list')
         checkboxes = $("[type='checkbox']")
         relationshipTypeList = $('#filter-relationships :checked')
@@ -152,7 +154,6 @@
             "node" : vis.selectAll('g'),
             "edge" : vis.selectAll('line'),
         }
-        # alchemy.filters.updateBoxStatus()

         for box in checkboxes
             state = if box.checked then "active" else "inactive"
@@ -160,58 +161,41 @@

             d3.select("#li-#{box.name}")
                 .classed({'active-label': box.checked,'inactive-label': !box.checked})
-
-        checked = null
-        hoverState = null
+
+        reFilter = (boxName, state, checked, highlight) ->
+	        ["node", "edge"].forEach (t) ->
+	            graphElements[t].filter(".#{boxName}")
+	                .attr("class", "#{t} #{boxName} #{state}")
+	        for node in graphElements["node"].filter(".#{state}")[0]
+	            graphElements["edge"].filter("[id*='#{node.id[7..13]}']")
+	                .classed({"inactive": !checked, "active": checked, "inactive-hover": highlight})
+
+	        console.log  "refiltered #{boxName} with state #{state} and checked #{checked} and highlight #{highlight}"
+
         d3.selectAll(".inactive-label")
             .on "mouseenter", () ->
                 if !checked
                     boxName = this.outerText
-                    console.log boxName
-                    hoverState = true
-                    console.log "mousenter #{boxName} " + state + " checked " + checked
+                    highlight = true
                     state = "inactive-hover"
-
-                    # filter(checked, hoverState)
-                    # filter out the right nodes
-                    ["node", "edge"].forEach (t) ->
-                        graphElements[t].filter(".#{boxName}")
-                            .attr("class", "#{t} #{boxName} #{state}")
-                    for node in graphElements["node"].filter(".#{state}")[0]
-                        graphElements["edge"].filter("[id*='#{node.id[7..13]}']")
-                            .classed({"inactive": !checked, "active": checked, "inactive-hover": hoverState})
+                    reFilter(boxName, state, checked, highlight)
+

             .on "mouseleave", () ->
-                # console.log d3.select(this)
-                console.log "mouseleave #{boxName} " + state +  " " + checked +  " " + hoverState
                 boxName = this.outerText
-                if hoverState and !checked
-                    hoverState = false
+                if highlight and !checked
+                    highlight = false
                     state = "inactive"
-                    console.log "mouseleave #{boxName} " + state
-                    ["node", "edge"].forEach (t) ->
-                        graphElements[t].filter(".#{boxName}")
-                            .attr("class", "#{t} #{boxName} #{state}")
-                    for node in graphElements["node"].filter(".#{state}")[0]
-                        graphElements["edge"].filter("[id*='#{node.id[7..13]}']")
-                            .classed({"inactive": !checked, "active": checked, "inactive-hover":hoverState})
+                    reFilter(boxName, state, checked, highlight)
+

             .on "click", () ->
                 boxName = this.outerText
-                hoverState = false
-                console.log "click + hoverState " + hoverState
+                highlight = false
                 #get checked property and toggle it
                 checked = d3.select("#li-#{boxName} input").property("checked")
-                console.log checked + " " + state
                 state = if checked then "active" else "inactive"
-
-                ["node", "edge"].forEach (t) ->
-                    graphElements[t].filter(".#{boxName}")
-                        .attr("class", "#{t} #{boxName} #{state}")
-                console.log state
-                for node in graphElements["node"].filter(".#{state}")[0]
-                    graphElements["edge"].filter("[id*='#{node.id[7..13]}']")
-                        .classed({"inactive": !checked, "active": checked, "inactive-hover":hoverState})
+                reFilter(boxName, state, checked, highlight)

                 # d3.select("#li-#{boxName} input").property("checked", !checked)


Added updated paramertise method from devise branch

--- src/core/javascripts/controllers/base.js.coffee
+++ src/core/javascripts/controllers/base.js.coffee
@@ -1093,13 +1093,11 @@
   $scope.supportsTouch = () ->
     Modernizr.touch

-  String.prototype.parameterise = () ->
-    return this.trim().replace(/\s/g,'-').toLowerCase()

   $rootScope.$on 'show:loader', () ->
     $scope.loading = true
   $rootScope.$on 'hide:loader', () ->
     $scope.loading = false

-  String.prototype.parameterise = () ->
-    return this.trim().replace(/\s/g,'-').toLowerCase()
+  String.prototype.parameterise = (seperator = '-') ->
+    return this.trim().replace(/\s/g,seperator).toLowerCase()


fix same id in item factura_compra fixed #18

--- app/assets/javascripts/autocomplete_factura_compra.js.coffee
+++ app/assets/javascripts/autocomplete_factura_compra.js.coffee
@@ -31,8 +31,8 @@
       select: (event, ui) ->
         $this = $(this)
         $this.prop("disabled", true)
-        # $this.closest(".fields").find(".producto_id").val ui.item.id
-        $(".producto_id").val ui.item.id
+        $this.closest(".fields").find(".producto_id").val ui.item.id
+        # $(".producto_id").val ui.item.id
         # $this.closest(".fields").find(".nombre_item").text($this.val() + " $")
         $this.closest(".fields").find(".nombre_generico").val ui.item.nombre_generico
         $this.closest(".fields").find(".nombre_generico").prop("disabled", true)


fix an error with this

fix an error with this

--- packages/tap-i18n-master/lib/tap_i18n/tap_i18n-common.coffee
+++ packages/tap-i18n-master/lib/tap_i18n/tap_i18n-common.coffee
@@ -1,6 +1,8 @@
 fallback_language = globals.fallback_language

 TAPi18n = ->
+  _this = this
+
   EventEmitter.call @

   @_fallback_language = fallback_language
@@ -35,8 +37,8 @@

     Meteor.startup ->
       # If tap-i18n is enabled for that project
-      if @_enabled()
-        @_registerHTTPMethod()
+      if _this._enabled()
+        _this._registerHTTPMethod()

   @__ = @_getPackageI18nextProxy(globals.project_translations_domain)


Update parent on change Call the onChange callback after a state change, to force a re-render of the parent if required.

--- app/classifier/tasks/drawing/hide-previous-marks-toggle.cjsx
+++ app/classifier/tasks/drawing/hide-previous-marks-toggle.cjsx
@@ -15,7 +15,9 @@
     @props.classification._hideMarksBefore = count
     checked = count > 0
     @setState { checked }
-    # this is a hack to force the parent to render again with marks hidden
+    @updateParent()
+
+  updateParent: () ->
     annotations = @props.classification.annotations.slice()
     annotation = annotations[annotations.length - 1]
     @props.onChange annotation


Indentation fixes

Indentation fixes

--- assets/components/strava-speed-label/StravaSpeedLabel.coffee
+++ assets/components/strava-speed-label/StravaSpeedLabel.coffee
@@ -1,14 +1,14 @@
 StravaTimeLabel = Polymer({
   is: 'strava-speed-label'

-	properties:
-		speed:
+  properties:
+    speed:
       type: Number
       notify: true
       observer: 'speedChanged'
     formattedSpeed: String

-	speedChanged: (newValue) ->
+  speedChanged: (newValue) ->
     if newValue? && newValue != NaN
       @formattedSpeed = (newValue * 3.6).toFixed(1)
-})+})


Use less workers

Use less workers

--- config/main.rc.coffee
+++ config/main.rc.coffee
@@ -28,7 +28,7 @@
   webserver     :
     login       : 'prod-webserver'
     port        : 3020
-    clusterSize : 10
+    clusterSize : 2
     queueName   : socialQueueName+'web'
     watch       : no
   mongo         : mongo
@@ -62,11 +62,11 @@
     login       : 'prod-authworker'
     queueName   : socialQueueName+'auth'
     authResourceName: 'auth'
-    numberOfWorkers: 1
+    numberOfWorkers: 2
     watch       : no
   social        :
     login       : 'prod-social'
-    numberOfWorkers: 10
+    numberOfWorkers: 2
     watch       : no
     queueName   : socialQueueName
   cacheWorker   :


typo in spec template

typo in spec template

--- lib/ti/templates/specs/app_spec.coffee
+++ lib/ti/templates/specs/app_spec.coffee
@@ -1,3 +1,3 @@
 describe 'Jasmine', ->
-  it 'should require your to write your tests', ->
-    expect(yourCode).toBeLotsBetter()+  it 'should require you to write your tests', ->
+    expect(yourCode).toBeLotsBetter()


Update sr label to match visual label

Update sr label to match visual label

--- lms/static/coffee/src/customwmd.coffee
+++ lms/static/coffee/src/customwmd.coffee
@@ -133,7 +133,7 @@
           .append($("<div>").attr("id", "wmd-preview#{_append}").addClass("wmd-panel wmd-preview"))
       $wmdPanel = $("<div>").addClass("wmd-panel")
                  .append($("<div>").attr("id", "wmd-button-bar#{_append}"))
-                 .append($("<label>").addClass("sr").attr("for", wmdInputId).text(gettext("Post body")))
+                 .append($("<label>").addClass("sr").attr("for", wmdInputId).text(gettext("Your question or idea (required)")))
                  .append($("<textarea>").addClass("wmd-input").attr("id", wmdInputId).html(initialText))
                  .append($wmdPreviewContainer)
       $elem.append($wmdPanel)


Patch the browser process too

Patch the browser process too

--- src/browser/main.coffee
+++ src/browser/main.coffee
@@ -56,11 +56,22 @@
 setupCrashReporter = ->
   crashReporter.start(productName: 'Atom', companyName: 'GitHub')

+isPortableInstall = ->
+  return false unless process.platform is 'win32'
+  return false unless (process and process.type)
+
+  ourPath = process.execPath.toLowerCase()
+  return (ourPath.indexOf(process.env.LOCALAPPDATA.toLowerCase()) is 0)
+
 setupAtomHome = ->
   return if process.env.ATOM_HOME
   atomHome = path.join(app.getHomeDir(), '.atom')
   try
     atomHome = fs.realpathSync(atomHome)
+    fs.statSync(atomHome)
+  catch
+    atomHome = path.join(path.dirname(process.execPath), '.atom') if isPortableInstall()
+
   process.env.ATOM_HOME = atomHome

 setupCompileCache = ->


enforced code nit 2

enforced code nit 2

--- src/browser/main.coffee
+++ src/browser/main.coffee
@@ -47,7 +47,7 @@
     cwd = args.executedFrom?.toString() or process.cwd()
     args.pathsToOpen = args.pathsToOpen.map (pathToOpen) ->
       normalizedPath = fs.normalize(pathToOpen)
-      if url.parse(pathToOpen || '').protocol?
+      if url.parse(pathToOpen).protocol?
         pathToOpen
       else if cwd
         path.resolve(cwd, normalizedPath)


add some languages for code button

add some languages for code button

--- src/buttons/code.coffee
+++ src/buttons/code.coffee
@@ -98,8 +98,14 @@
       <div class="settings-field">
         <select class="select-lang">
           <option value="-1">选择程序语言</option>
+          <option value="bash">Bash</option>
           <option value="c++">C++</option>
+          <option value="cs">C#</option>
           <option value="css">CSS</option>
+          <option value="erlang">Erlang</option>
+          <option value="less">Less</option>
+          <option value="scss">Sass</option>
+          <option value="diff">Diff</option>
           <option value="coffeeScript">CoffeeScript</option>
           <option value="html">Html,XML</option>
           <option value="json">JSON</option>


ActivityTicker: Tags are fetched for Like Activities where the target is StatusUpdate

--- client/Social/Activity/ContentDisplays/ActivityTicker.coffee
+++ client/Social/Activity/ContentDisplays/ActivityTicker.coffee
@@ -170,7 +170,13 @@
           return console.log "subject is not found", err, data.subject if err or not subject

           eventObj = {source, target, subject, as:"like"}
-          @addNewItem eventObj, 0
+          if subject.bongo_.constructorName is "JNewStatusUpdate"
+            @fetchTags subject, (err, tags)=>
+              return log "discarding event, invalid data"  if err
+              subject.tags = tags
+              @addNewItem eventObj, 0
+          else
+            @addNewItem eventObj, 0

   addComment: (data) ->
     {origin, reply, subject, replier} = data


Show nickname in memberlist to prevent confusion

--- client/app/Applications/Members.kdapplication/AppView.coffee
+++ client/app/Applications/Members.kdapplication/AppView.coffee
@@ -77,7 +77,8 @@

-
{{> @profileLink}}
 {{> @location}}
+
{{> @profileLink}}
 {{#(profile.nickname)}}
+          {{> @location}}

{{ @utils.applyTextExpansions #(profile.about), yes}}


webterm: temporarily disable "User Opened Terminal" event tracking

--- client/app/Applications/WebTerm.kdapplication/AppView.coffee
+++ client/app/Applications/WebTerm.kdapplication/AppView.coffee
@@ -14,7 +14,7 @@
     @addSubView @container

     @terminal = new WebTerm.Terminal @container.$()
-    KD.track "userOpenedTerminal", KD.getSingleton("groupsController").getCurrentGroup()
+    # KD.track "userOpenedTerminal", KD.getSingleton("groupsController").getCurrentGroup()
     @options.advancedSettings ?= no
     if @options.advancedSettings
       @advancedSettings = new KDButtonViewWithMenu


sidebar: use link content as link title for accesibility

--- client/app/lib/sidebar/components/ownedresourceheader.coffee
+++ client/app/lib/sidebar/components/ownedresourceheader.coffee
@@ -56,7 +56,7 @@
     <header ref={(header) => @_header = header} className={className}>
       <h4 className='SidebarSection-headerTitle'>

-        <Link onClick={onTitleClick}>{title}</Link>
+        <Link onClick={onTitleClick} title={title}>{title}</Link>

         {onMenuIconClick and
           <MenuIcon onClick={onMenuIconClick} />}


fix regex

fix regex

--- src/chickendance.coffee
+++ src/chickendance.coffee
@@ -18,7 +18,7 @@

 module.exports = (robot) ->

-  robot.respond /chicken dance( me)? (.+)/i, (msg) ->
+  robot.respond /chicken dance/i, (msg) ->
     msg.send msg.random images

 images = [


Do not allow to delete root domains

Do not allow to delete root domains

--- client/app/Applications/Environments.kdapplication/views/DomainListItemView.coffee
+++ client/app/Applications/Environments.kdapplication/views/DomainListItemView.coffee
@@ -5,21 +5,25 @@
     options.cssClass = 'domain-item'
     super options, data

-    {createdAt} = @getData()
+    {createdAt, domain} = @getData()

     @createdAgo = new KDTimeAgoView {}, createdAt
-    @deleteIcon = new KDCustomHTMLView
-      tagName    : "a"
-      cssClass   : "delete-link"
-      attributes :
-        title    : "Remove domain."
-      click      : (event)=>
-        domain = @getData()
-        event.preventDefault()
-        event.stopPropagation()
-        @deletionModal = new DomainDeletionModal {}, @getData()
-        @deletionModal.on "domainRemoved", =>
-          @getDelegate().emit "domainRemoved", this
+
+    if /^([\w\-]+)\.kd\.io$/.test domain
+      @deleteIcon = new KDView
+    else
+      @deleteIcon = new KDCustomHTMLView
+        tagName    : "a"
+        cssClass   : "delete-link"
+        attributes :
+          title    : "Remove domain."
+        click      : (event)=>
+          domain = @getData()
+          event.preventDefault()
+          event.stopPropagation()
+          @deletionModal = new DomainDeletionModal {}, @getData()
+          @deletionModal.on "domainRemoved", =>
+            @getDelegate().emit "domainRemoved", this

   click: (event)->


Fix typo with config constants

Fix typo with config constants

--- config.coffee
+++ config.coffee
@@ -25,7 +25,7 @@

 # auth magic
 config.auth = {}
-config.auth.clientId = process.env.CLIENT_ID
-config.auth.secret = process.env.CLIENT_SECRET
+config.auth.clientId = process.env.SPOTIFY_CLIENT_ID
+config.auth.secret = process.env.SPOTIFY_CLIENT_SECRET

 module.exports = config


WIP - extract (TESTS ADDED)

WIP - extract (TESTS ADDED)

--- test/tests/CaffeineScript/Parser/Extract.coffee
+++ test/tests/CaffeineScript/Parser/Extract.coffee
@@ -16,8 +16,9 @@
       "c = a extract b": "let c, b; c = b = a.b;"
       "d = a extract b, c": "let d, b, c; d = (b = a.b, c = a.c);"

-  #   nested:
-  #     "a extract b extract c": "let b, c; c = (b = a.b, c = b.c);"
+    nested:
+      "a extract b extract c": "let b, c; b = a.b, c = b.c;"
+      "d = a extract b extract c": "let d, b, c; d = (b = a.b, c = b.c);"

   #   default:
   #     "a extract b = c": "let b; b = (a.b !== undefined ? a.b : c);"


MachineSettingsSpecsView: Make view creation simple.

--- client/app/lib/providers/machinesettingsspecsview.coffee
+++ client/app/lib/providers/machinesettingsspecsview.coffee
@@ -26,18 +26,16 @@

   createIcon: (title, spec) ->

-    @iconWrapper.addSubView wrapper = new KDCustomHTMLView
+    @iconWrapper.addSubView new KDCustomHTMLView
       cssClass : "spec #{kd.utils.slugify(title).toLowerCase()}"
-
-    wrapper.addSubView new KDCustomHTMLView
-      tagName  : 'figure'
-      partial  : ''
-
-    wrapper.addSubView new KDCustomHTMLView
-      cssClass : 'label'
       partial  : """
-
#{title}

-        #{spec}
+

+
+

+

+
#{title}

+          #{spec}
+

       """


If loadsFromOrigin emit a signal when its finished

--- client/landing/app/CommonViews/linkviews/linkview.coffee
+++ client/landing/app/CommonViews/linkviews/linkview.coffee
@@ -1,16 +1,16 @@
 class LinkView extends KDCustomHTMLView

   constructor:(options = {}, data)->
-
+
     options.tagName or= 'a'
     data            or= fake : yes
     data.profile    or= {}
-
+
     data.profile.firstName or= "a koding"
     data.profile.lastName  or= "user"

     super options, data
-
+
     if data.fake and options.origin
       @loadFromOrigin options.origin

@@ -19,6 +19,7 @@
     callback = (data)=>
       @setData data
       @render()
+      @emit "OriginLoadComplete", data

     if origin.constructorName
       bongo.cacheable origin.constructorName, origin.id, (err, origin)=>
@@ -27,6 +28,6 @@
       callback origin

   viewAppended:->
-
+
     @setTemplate @pistachio()
     @template.update()


Make the code self-documented

Make the code self-documented

--- backbone.tableview.coffee
+++ backbone.tableview.coffee
@@ -1,3 +1,42 @@
+###
+TableView
+---------
+###
+
+###
+A View that can be used with any backbone collection, and draws a table with it.
+Optionally it supports pagination, search, and any number of filters
+("inputs", "button", "option"). Eg (Users is a Backbone.Collection):
+
+    class UserTableView extends Backbone.TableView
+        title: "My Users Table"
+        collection: new Users()
+        columns:
+            name:
+                header: "My Name"
+            type:
+                header: "Type"
+            last_login:
+                header: "Last Login Time"
+                draw: (model) ->
+                    new Date(model.get 'time')
+            description:
+                header: "Description"
+                nosort: true
+                draw: (model) ->
+                    some_weird_formatting_function(model.get('some_text'))
+        pagination: true
+        search:
+            query: "name"
+            detail: "Search by Name"
+        filters:
+            from:
+                type: "input"
+                className: "date"
+                init: new Date()
+                get: (val) ->
+                    ... process the date val ...
+###
 class Backbone.TableView extends Backbone.View
     tagName: "div"
     titleTemplate: _.template "
<%= model %>
"
@@ -70,6 +109,7 @@
         "click    .pager-next":   "nextPage"
         "click    th":            "toggleSort"

+    # Binds the collection update event for rendering
     initialize: ->
         @collection.on "reset", @renderData
         @data = @options.initialData or @initialData or {}
@@ -77,10 +117,12 @@
         @data.size = @options.size or @size or 10
         return @

+    # Set data and update collection
     setData: (id, val) =>
         @data[id] = val
         @update()

+    # Creates a filter from a filter config definition
     createFilter: (name, filter) =>
         switch filter.type
             when "input"
@@ -95,6 +137,7 @@
         filter.setData = @setData
         return filter

+    # Update collection only if event was trigger by an enter
     updateSearchOnEnter: (e) =>
         if e.keyCode == 13
             val = e.currentTarget.value
@@ -105,10 +148,12 @@
             @update()
         return @

+    # Update the collection given all the options/filters
     update: =>
         @collection.fetch data: @data
         return @

+    # Render the collection in the tbody section of the table
     renderData: =>
         $("tbody", @$el).html @dataTemplate
             collection: @collection
@@ -116,12 +161,14 @@
             empty:      @empty or "No records to show"
         return @

+    # Go to the previous page in the collection
     prevPage: =>
         if @data.page > 1
             @data.page = @data.page - 1
             $(".page", @$el).html @data.page
             @update()

+    # Go to the next page in the collection
     nextPage: =>
         # Since we don't have a collection count, for now we use the size of
         # the last GET as an heuristic to limit the use of nextPage
@@ -130,6 +177,7 @@
             $(".page", @$el).html @data.page
             @update()

+    # Toggle/Select sort column and direction, and update table accodingly
     toggleSort: (e) =>
         el = e.currentTarget
         cl = el.className
@@ -146,9 +194,13 @@
         @data.sort_col = el.abbr
         @update()

+    # Apply a template to a model and return the result (string), or empty
+    # string if model is false/undefined
     applyTemplate: (template, model) ->
         (model and template model: model) or ""

+    # Render skeleton of the table, creating filters and other additions,
+    # and trigger an update of the collection
     render: =>
         @$el.html @template
             columns:    @columns
@@ -164,6 +216,11 @@
             filtersDiv.append " "
         @update()

+###
+Filters
+-------
+###
+
 class Filter extends Backbone.View
     tagName: "div"
     className: "inline"
@@ -172,6 +229,7 @@
         @id = @options.id
         @setData = @options.setData

+    # Helper function to prettify names (eg. hi_world -> Hi World)
     prettyName: (str) ->
         str.charAt(0).toUpperCase() + str.substring(1).replace(/_(\w)/g, (match, p1) -> " " + p1.toUpperCase())


update css cache.

update css cache.

--- docpad.coffee
+++ docpad.coffee
@@ -40,10 +40,10 @@

 			# Styles
 			styles: [
-				"/styles/bootstrap.css?v=1.1",
-				"/styles/style.css?v=1.2",
-				"/styles/site.css?v=1.2",
-				"/styles/monokai-sublime.css?v=1.1"
+				"/styles/bootstrap.css?v=2.0",
+				"/styles/style.css?v=2.0",
+				"/styles/site.css?v=2.0",
+				"/styles/monokai-sublime.css?v=2.0"
 			]

 			# Scripts


One-lined a function, removed closing of all contentregions when it is not needed

--- core/app/backbone/controllers/channels_controller.coffee
+++ core/app/backbone/controllers/channels_controller.coffee
@@ -62,8 +62,7 @@
       @commonChannelViews(channel)
       @makePermalinkEvent(channel.url())

-      @restoreChannelView channel_id, =>
-        new ChannelView(model: channel)
+      @restoreChannelView channel_id, => new ChannelView(model: channel)

       @channel_views.clear()

@@ -84,7 +83,6 @@
     @getChannelFact(username, channel_id, fact_id, true)

   getChannelFact: (username, channel_id, fact_id, for_stream=false) ->
-    app.closeAllContentRegions()
     @main = new TabbedMainRegionLayout();
     app.mainRegion.show(@main)


OX-139 jump to active node keyboard shortcut

--- app/assets/javascripts/controller.coffee
+++ app/assets/javascripts/controller.coffee
@@ -192,6 +192,7 @@
           @setActiveNode(id, true)
           @sceneController.skeleton.setBranchPoint(false)
         )
+      "h" : @centerActiveNode

       #Zoom in/out
       "i" : =>
@@ -328,10 +329,13 @@
   setActiveNode : (nodeId, centered) =>
     @model.Route.setActiveNode(nodeId)
     if centered
-      @flycam.setGlobalPos(@model.Route.getActiveNodePos())
+      @centerActiveNode()
     @flycam.hasChanged = true
     @gui.update()
     @sceneController.skeleton.setActiveNode()
+
+  centerActiveNode : =>
+    @flycam.setGlobalPos(@model.Route.getActiveNodePos())

   deleteActiveNode : =>
     @model.Route.deleteActiveNode()


Questionnaire details event to start if DOM element is present

--- app/assets/javascripts/ors_api.js.coffee
+++ app/assets/javascripts/ors_api.js.coffee
@@ -1,4 +1,6 @@
 $(document).on 'ready page:load', ->
   new Questionnaires($('.questionnaires-container'), $('.questionnaires'))
-  new QuestionnaireDetails($('.questionnaire-info'),
-    $('.questionnaire-tab-container'), $('.questionnaire-details'), $('.questions'))
+
+  if $('.questionnaire-info').length > 0
+    new QuestionnaireDetails($('.questionnaire-info'),
+      $('.questionnaire-tab-container'), $('.questionnaire-details'), $('.questions'))


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -18,7 +18,8 @@
     x: 5
     y: 10

-  obj4 = engine.
+  obj4 = engine.add
+    class:

   indexSum = 0


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -4,7 +4,7 @@
   engine = Engine()

   engine.add
-    type: ''
+    f

 module()


Task #3538: Remove tribe_git from the history

--- app/coffee/modules/common/history.coffee
+++ app/coffee/modules/common/history.coffee
@@ -28,7 +28,7 @@

 IGNORED_FIELDS = {
     "userstories.userstory": [
-        "watchers", "kanban_order", "backlog_order", "sprint_order", "finish_date"
+        "watchers", "kanban_order", "backlog_order", "sprint_order", "finish_date", "tribe_gig"
     ],
     "tasks.task": [
         "watchers", "us_order", "taskboard_order"


Fixed: an exception was missing in the before hook

--- app/controllers/alarms_controller.coffee
+++ app/controllers/alarms_controller.coffee
@@ -8,7 +8,7 @@
             @alarm = alarm
             next()
 # Make this pre-treatment only before update and delete action.
-, except: ['create']
+, except: ['create', 'all']

 before ->


read json url from location hash

read json url from location hash

--- app/controllers/lightcurve-viewer.coffee
+++ app/controllers/lightcurve-viewer.coffee
@@ -5,6 +5,16 @@
 translate                  = require 't7e'
 {CanvasGraph, Marks, Mark} = require "../lib/canvas-graph"
 $ = window.jQuery
+
+
+getParameterByName = (name) ->
+  name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]")
+  regex = new RegExp("[\\?&]" + name + "=([^&#]*)")
+  results = regex.exec(location.search)
+  (if not results? then "" else decodeURIComponent(results[1].replace(/\+/g, " ")))
+
+JSON_FILE = getParameterByName "JsonFile"
+console.log 'JSON_FILE: ', JSON_FILE

 class LightcurveViewer extends BaseController
   className: 'lightcurve-viewer'
@@ -44,7 +54,7 @@

   loadSubjectData: () ->
     $('#graph-container').addClass 'loading-lightcurve'
-    jsonFile = @subject.selected_light_curve.location
+    jsonFile = JSON_FILE #@subject.selected_light_curve.location
     # console.log 'jsonFile: ', jsonFile # DEBUG CODE

     # handle ui elements


Fix #3419: don't require `input` to have `placeholder` attribute

--- xforms/jvm/src/main/assets/ops/javascript/orbeon/xforms/control/Placeholder.coffee
+++ xforms/jvm/src/main/assets/ops/javascript/orbeon/xforms/control/Placeholder.coffee
@@ -16,15 +16,6 @@
 Events = ORBEON.xforms.Events
 Init = ORBEON.xforms.Init
 YD = YAHOO.util.Dom
-
-# Returns true if this control has a placeholder
-isPlaceholderControl = (control) ->
-    if $(control).is('.xforms-input, .xforms-textarea')
-        inputOrTextarea = findInputOrTextarea control
-        placeholder = YD.getAttribute inputOrTextarea, "placeholder"
-        placeholder != null
-    else
-        false

 findInputOrTextarea = (control) ->
     input = (control.getElementsByTagName "input")[0]
@@ -54,7 +45,7 @@
 # When the label/hint changes, set the value of the placeholder
 do ->
     Controls.lhhaChangeEvent.subscribe (event) ->
-        if isPlaceholderControl event.control
+        if $(event.control).is('.xforms-input, .xforms-textarea')
             labelHint = Controls.getControlLHHA event.control, event.type
             if not labelHint?
                 # Update placeholder attribute and show it


Stubs for new journals

Stubs for new journals

--- content/groups.coffee
+++ content/groups.coffee
@@ -66,4 +66,24 @@
   journalofquekett:
     image: 'groups/quekett-journal.jpg'
     alt: "Quekett Journal: illustrated frontispiece"
-    description: '1868-present, this journal, which acts as the official journal for one of the longest running amateur microscopical societies. The society, which was established in 1865, came directly out of the community developed through the natural history periodical Science Gossip – with founding members including the publisher Robert Hardwicke and editor and mycologist Mordecai Cubitt Cooke.'+    description: '1868-present, this journal, which acts as the official journal for one of the longest running amateur microscopical societies. The society, which was established in 1865, came directly out of the community developed through the natural history periodical Science Gossip – with founding members including the publisher Robert Hardwicke and editor and mycologist Mordecai Cubitt Cooke.'
+  'journal-of-the-royal-microscopical-society':
+    image: ''
+    alt: ''
+    description: ''
+  "the-geologist-a-popular-monthly-magazine-of-geology":
+    image: ''
+    alt: ''
+    description: ''
+  "journal-of-botany-british-and-foreign":
+    image: ''
+    alt: ''
+    description: ''
+  "the-geological-magazine-or-monthly-journal-of-geology":
+    image: ''
+    alt: ''
+    description: ''
+  "the-journal-of-horticulture-cottage-gardener-and-country-gentlemen":
+    image: ''
+    alt: ''
+    description: ''


Add support for jQuery 2.0.

Add support for jQuery 2.0.

--- src/main/webapp/coffeescripts-hidden/load.coffee
+++ src/main/webapp/coffeescripts-hidden/load.coffee
@@ -87,7 +87,7 @@
 isAcceptableJQuery = ->
   jQueryVersionPieces = jQuery.fn.jquery.split(".")

-  if (jQueryVersionPieces[1] >= 5) || (Number(jQueryVersionPieces[1]) == 4 && jQueryVersionPieces[2] >= 3)
+  if (jQueryVersionPieces[0] > 1) || (jQueryVersionPieces[1] >= 5) || (Number(jQueryVersionPieces[1]) == 4 && jQueryVersionPieces[2] >= 3)
     true
   else
     false


Add record to record arrays in adapter

Add record to record arrays in adapter

Ember Model does not do it automatically. I had a patch, which was
changing that, but after giving it more thought, I think it's not a good
idea - this should be up to adapter if the records are going into
record arrays.

--- assets/scripts/lib/travis/adapter.coffee
+++ assets/scripts/lib/travis/adapter.coffee
@@ -15,6 +15,7 @@

     @sideload(klass, data)
     records.load(klass, dataToLoad)
+    @addToRecordArrays(records.get('content'))

   buildURL: ->
     @_super.apply(this, arguments).replace(/\.json$/, '')
@@ -22,26 +23,38 @@
   didFind: (record, id, data) ->
     @sideload(record.constructor, data)
     @_super(record, id, data)
+    @addToRecordArrays(record)

   didFindAll: (klass, records, data) ->
     @sideload(klass, data)
     @_super(klass, records, data)
+    @addToRecordArrays(records.get('content'))

   didFindQuery: (klass, records, params, data) ->
     @sideload(klass, data)
     @_super(klass, records, params, data)
+    @addToRecordArrays(records.get('content'))

   didCreateRecord: (record, data) ->
     @sideload(record.constructor, data)
     @_super(record, data)
+    @addToRecordArrays(record)

   didSaveRecord: (record, data) ->
     @sideload(record.constructor, data)
     @_super(record, data)
+    @addToRecordArrays(record)

   didDeleteRecord: (record, data) ->
     @sideload(record.constructor, data)
     @_super(record, data)
+    @addToRecordArrays(record)
+
+  addToRecordArrays: (records) ->
+    records = [records] unless Ember.isArray(records)
+    for record in records
+      record.constructor.addToRecordArrays(record)
+

   sideload: (klass, data) ->
     for name, records of data


fixed casperjs tests

fixed casperjs tests

--- tests/javascript/functional/moj.SubTotal.test.coffee
+++ tests/javascript/functional/moj.SubTotal.test.coffee
@@ -20,8 +20,9 @@
   casper.thenEvaluate ->
     $("input[type=number]").val(0)
     $("input[value=0][type=radio]").click()
+    $("[type=submit][value=submit]").click()
   casper.then ->
-    @fill 'form', {}, true
+    # @fill 'form', {}, true
     @waitForUrl /.*your_income/
   casper.thenEvaluate ->
     $("input[type=number]").val(0)


Updated groupedplane function

Updated groupedplane function

--- attitude/display/assets/functions.coffee
+++ attitude/display/assets/functions.coffee
@@ -28,8 +28,6 @@
   createFeature 'LineString', d.nominal

 createGroupedPlane = (opts)->
-  color = opts.color or 'red'
-  opacity = opts.opacity or 0.5
   (p)->
     e = combinedErrors p.covariance, p.axes, opts
     el = d3.select @
@@ -37,15 +35,12 @@
       .datum createErrorSurface(e)
       .attrs
         class: 'error'
-        fill: color
-        'fill-opacity': opacity

     el.append "path"
       .datum createNominalPlane(e)
       .attrs
         class: 'nominal'
-        fill: 'none'
-        stroke: color
+

 createErrorEllipse = (opts)->
   ##


Issue #102: support a shebang in the Atom plugin

--- tools/atom-plugin/language-micro/grammars/micro.cson
+++ tools/atom-plugin/language-micro/grammars/micro.cson
@@ -2,6 +2,12 @@
 'scopeName': 'source.micro'
 'fileTypes': ['micro']
 'patterns': [
+  # shebang
+  {
+    'match': '^#!.*'
+    'name': 'comment.line.micro'
+  }
+
   # singleline comments
   {
     'match': '\\bnb(?!:)\\b.*'


fix axis rule placement errors due to insufficient precision

--- static/coffee/renderers/guide/linear_axis.coffee
+++ static/coffee/renderers/guide/linear_axis.coffee
@@ -96,8 +96,6 @@

     @formatter = new ticking.BasicTickFormatter()

-    @formatter = new ticking.BasicTickFormatter()
-
   render: () ->

     ctx = @plot_view.ctx
@@ -368,8 +366,7 @@
     i = @get('dimension')
     j = (i + 1) % 2

-    ranges = [@get_obj('plot').get_obj('x_range'),
-      @get_obj('plot').get_obj('y_range')]
+    ranges = [@get_obj('plot').get_obj('x_range'), @get_obj('plot').get_obj('y_range')]

     user_bounds = @get('bounds') ? 'auto'
     range_bounds = [ranges[i].get('min'), ranges[i].get('max')]
@@ -392,8 +389,8 @@

     [start, end] = @get('computed_bounds')

-    xs = new Float32Array(2)
-    ys = new Float32Array(2)
+    xs = new Float64Array(2)
+    ys = new Float64Array(2)
     coords = [xs, ys]

     loc = @get('location') ? 'min'
@@ -425,9 +422,6 @@
     cross_range = ranges[j]

     [start, end] = @get('computed_bounds')
-
-    tmp = Math.min(start, end)
-    end = Math.max(start, end)

     interval = ticking.auto_interval(start, end)
     ticks = ticking.auto_ticks(null, null, start, end, interval)


Changed TaskHistory comparator

Changed TaskHistory comparator

--- SingularityUI/app/collections/HistoricalTasks.coffee
+++ SingularityUI/app/collections/HistoricalTasks.coffee
@@ -5,7 +5,7 @@
     model: Backbone.Model

     comparator: (task0, task1) =>
-        -(task0.get("startedAt") - task1.get("startedAt"))
+        -(task0.get("updated") - task1.get("updated"))

     url: ->
         params =


Disable 'disabled' solve problem links.

Disable 'disabled' solve problem links.

--- picoCTF-web/web/coffee/problems.coffee
+++ picoCTF-web/web/coffee/problems.coffee
@@ -52,7 +52,8 @@
           renderProblemSubmit: renderProblemSubmit,
           renderProblemReview: renderProblemReview
         })
-
+
+        $("li.disabled>a").removeAttr "href"
         $(".problem-hint").hide()
         $(".problem-submit").on "submit", submitProblem
         $(".info-span").on "click", toggleHint


Fix symbol

Fix symbol

--- app/assets/javascripts/backend/golumn/item.js.coffee
+++ app/assets/javascripts/backend/golumn/item.js.coffee
@@ -2,7 +2,7 @@
   constructor: (@id, @name, @status, @sex, @show_path = '', @parent) ->

     @sexClass = ko.pureComputed () =>
-      @sex == 'male' ? "icon-mars" : (@sex == 'female' ? 'icon-venus' : '')
+      if @sex == 'male' then "icon-mars" else if @sex == 'female' then 'icon-venus' else ''

     @statusClass = ko.pureComputed () =>
       return "status-#{@status}"


Fix table select keyhandler to not break forms.

--- app/assets/javascripts/bookyt/table_select.js.coffee
+++ app/assets/javascripts/bookyt/table_select.js.coffee
@@ -8,6 +8,9 @@
     $(document).on 'click click.rails', '[data-table-key=13]', () ->
       $(this).trigger('submit.rails') if @state == 'edit'

+    $(document).on 'click', 'form[data-table-select]', @enterNavigate
+    $(document).on 'focus', ':not(form[data-table-select] *)', @exitNavigate
+
     @enterNavigate()
     @selectFirstRow()

@@ -17,6 +20,9 @@

   enterNavigate: () =>
     @state = 'navigate'
+
+  exitNavigate: () =>
+    @state = ''

   navigationKeyHandler: (e) =>
     switch e.which


bugfix

bugfix

--- app/assets/javascripts/general/init_charts.js.coffee
+++ app/assets/javascripts/general/init_charts.js.coffee
@@ -27,8 +27,8 @@
 chartTextHpvReplacements =
   0: 'NO-HPV'
   1: 'HPV-PAR-SENT-SUB'
-  2: 'HPV-PAR-SENT-SUBNV'
-  3: 'HPV-PAR-SENT'
+  3: 'HPV-PAR-SENT-SUBNV'
+  2: 'HPV-PAR-SENT'
   4: 'HPV-TOP'
   5: 'HPV-TOP-PAR-SENT'
   6: 'HPV-PAR'


BF-121: debugging and testing on server

BF-121: debugging and testing on server

--- app/assets/javascripts/model/binary/pullqueue.coffee
+++ app/assets/javascripts/model/binary/pullqueue.coffee
@@ -99,10 +99,12 @@
     @loadBucketByAddress3(bucket_x, bucket_y, bucket_z, zoomStep).then(

       (colors) =>
+        console.log "success: ", [bucket_x, bucket_y, bucket_z]
         Cube.setBucketByAddress3(bucket_x, bucket_y, bucket_z, zoomStep, colors)
         $(window).trigger("bucketloaded", [[bucket_x, bucket_y, bucket_z]])

       =>
+        console.log "fail: ", [bucket_x, bucket_y, bucket_z]
         Cube.setBucketByAddress3(bucket_x, bucket_y, bucket_z, zoomStep, null)

     ).always =>


fix race conditions in node deletion, tree splitting and active node setting, which caused an infinite event loop

--- app/assets/javascripts/oxalis/model/settings/backbone_to_oxalis_adapter_model.coffee
+++ app/assets/javascripts/oxalis/model/settings/backbone_to_oxalis_adapter_model.coffee
@@ -46,27 +46,32 @@
       # ####################################
       # Listen to changes in the OxalisModel

-      @listenTo(@skeletonTracingModel, "newTree", (id) -> @skeletonTracingAdapter.set("activeTreeId", id))
-      @listenTo(@skeletonTracingModel, "newActiveTree", (id) -> @skeletonTracingAdapter.set("activeTreeId", id))
-      @listenTo(@skeletonTracingModel, "newActiveNode", (id) -> @skeletonTracingAdapter.set("activeNodeId", id))
-      @listenTo(@skeletonTracingModel, "newActiveNodeRadius", (id) -> @skeletonTracingAdapter.set("radius", id))
+      @listenTo(@skeletonTracingModel, "newTree", (id) -> @skeletonTracingAdapter.set("activeTreeId", id, {triggeredByModel: true}))
+      @listenTo(@skeletonTracingModel, "newActiveTree", (id) -> @skeletonTracingAdapter.set("activeTreeId", id, {triggeredByModel: true}))
+      @listenTo(@skeletonTracingModel, "newActiveNode", (id) -> @skeletonTracingAdapter.set("activeNodeId", id, {triggeredByModel: true}))
+      @listenTo(@skeletonTracingModel, "newActiveNodeRadius", (id) -> @skeletonTracingAdapter.set("radius", id, {triggeredByModel: true}))

       # ######################################
       # Listen to changes in the BackboneModel

-      # some calls are deferred, so the backbone change is propagated first, as the property in question
-      # may be reset again if it is invalid
-      @listenTo(@skeletonTracingAdapter, "change:activeTreeId", (model, id) ->
-        _.defer( => @skeletonTracingModel.setActiveTree(id) )
+      # Some calls are deferred, so the backbone change is propagated first, as the property in question
+      # may be reset again if it is invalid.
+      # If the original event was triggered by the oxalis model (options.triggeredByModel), the change in the backbone model
+      # doesn't need to be propagated again. This lead to race conditions, if the property was changed in the oxalis
+      # model in the mean time.
+      @listenTo(@skeletonTracingAdapter, "change:activeTreeId", (model, id, options) ->
+        if not options.triggeredByModel
+          _.defer( => @skeletonTracingModel.setActiveTree(id) )
       )

       @listenTo(@skeletonTracingAdapter, "change:somaClicking", (model, bool) ->
         @oxalisModel.user.set("newNodeNewTree", bool)
       )

-      @listenTo(@skeletonTracingAdapter, "change:activeNodeId", (model, id) ->
-        _.defer( => @skeletonTracingModel.setActiveNode(id) )
+      @listenTo(@skeletonTracingAdapter, "change:activeNodeId", (model, id, options) ->
+        if not options.triggeredByModel
+          _.defer( => @skeletonTracingModel.setActiveNode(id) )
       )

       @listenTo(@skeletonTracingAdapter, "change:particleSize", (model, size) ->
@@ -77,8 +82,9 @@
         @oxalisModel.user.set("overrideNodeRadius", bool)
       )

-      @listenTo(@skeletonTracingAdapter, "change:radius", (model, radius) ->
-        _.defer( => @skeletonTracingModel.setActiveNodeRadius(radius) )
+      @listenTo(@skeletonTracingAdapter, "change:radius", (model, radius, options) ->
+        if not options.triggeredByModel
+          _.defer( => @skeletonTracingModel.setActiveNodeRadius(radius) )
       )

       @listenTo(@skeletonTracingAdapter, "change:boundingBox", (model, string) ->


Add analytics to home path

Add analytics to home path

--- apps/settings/components/home_path_field/view.coffee
+++ apps/settings/components/home_path_field/view.coffee
@@ -1,4 +1,5 @@
 Backbone = require 'backbone'
+analytics = require '../../../../lib/analytics.coffee'
 template = -> require('./index.jade') arguments...

 module.exports = class HomePathFieldView extends Backbone.View
@@ -14,6 +15,10 @@
     $target = $(e.currentTarget)
     @model.set 'home_path', $target.val()

+    analytics.track.submit 'User switched home path',
+      label: 'Home path'
+      value: $target.text()
+
   render: ->
     @$el.html template
       user: @model


Disable JS minify temporarily on RC

Disable JS minify temporarily on RC

--- config/main.rc.coffee
+++ config/main.rc.coffee
@@ -83,7 +83,7 @@
   client        :
     pistachios  : yes
     version     : version
-    minify      : yes
+    minify      : no
     watch       : no
     js          : "./website/js/kd.#{version}.js"
     css         : "./website/css/kd.#{version}.css"


mostly changes to the minimized view look to make it more intuitive when a default view is set

--- @client/opinion_views.coffee
+++ @client/opinion_views.coffee
@@ -93,7 +93,7 @@
       for tag in user_tags
         name = tag.key
         if (tag.visibility == 'open' || is_admin) && \
-           (tag.self_report?.input in ['dropdown', 'boolean', 'checklist']) && \
+           ( (tag.self_report?.input or tag.input) in ['dropdown', 'boolean', 'checklist']) && \
            !tag.no_opinion_view # set in the user_tags customization to prevent an opinion view from automatically getting created

           attributes.push
@@ -104,7 +104,7 @@
                 fetch(u).tags[name] == value
               else
                 fetch(u).tags[name]
-            options: tag.self_report.options or (if tag.self_report.input == 'boolean' then [true, false])
+            options: tag.self_report?.options or tag.options or (if (tag.self_report?.input or tag.input) == 'boolean' then [true, false])
             input_type: tag.self_report?.input

   attributes
@@ -286,7 +286,7 @@
     }, {
       key: 'weighed_by_influence'
       name: translator 'opinion_views.weights_influence', 'Influence'
-      label: translator 'opinion_views.weights_influence_label', 'Add weight to the opinions of people who have contributed proposals and arguments that other people have found valuable.'
+      label: translator 'opinion_views.weights_influence_label', 'Add weight to the opinions of people who have contributed proposals and pro/con reasons that other people have found valuable.'
       weight: (u, opinion, proposal) ->
         if !influencer_scores_initialized
           build_influencer_network()
@@ -370,6 +370,7 @@

     active_views[view_key] =
       key: view.key
+      name: view.name
       view_type: view_type
       get_salience: (u, opinion, proposal) ->
         if view.salience?
@@ -390,7 +391,7 @@
         group = view.group(u, opinion, proposal)
         group ?= i18n().unreported
         group
-      options: if view.group? then view.options
+      options: view.options # if view.group? then view.options

   # invalidate_proposal_sorts()
@@ -581,7 +582,7 @@
         callback: ->
           clear_all()
           toggle_opinion_filter just_you_filter
-      },
+      }
       {
         key: 'custom'
         label: translator 'opinion_views.view_buttons_custom', 'Custom view'
@@ -592,7 +593,7 @@
               @local.minimized = !@local.minimized
               save @local
             else
-              reset_to_default_view()
+              reset_to_default_view(true)
           else
             @local.minimized = false
             clear_all()
@@ -605,15 +606,15 @@
       opinion_views_ui.active = 'all'
       save opinion_views_ui

-    reset_to_default_view = ->
+    reset_to_default_view = (force_all) ->
       clear_all()

       dfault = customization('opinion_views_default')

-      if !show_others || dfault?.active == 'you'
+      if !show_others || (dfault?.active == 'you' && !force_all)
         toggle_opinion_filter just_you_filter
         opinion_views_ui.active = 'you'
-      else if dfault
+      else if dfault && !force_all
         # LIMITATION: default date views not implemented

         opinion_views_ui.active = 'custom'
@@ -627,7 +628,7 @@
             attribute = attributes.find (attr) -> attr?.key == key
             opinion_views_ui.visible_attribute_values[attribute.key] = vals
             opinion_views_ui.activated_attributes[attribute.key] = true
-            update_view_for_attribute attribute
+            construct_view_for_attribute attribute

         if dfault.group_by
           attribute = attributes.find (attr) -> attr?.key == dfault.group_by
@@ -674,9 +675,33 @@
         SPAN
           style:
             display: 'flex'
+            position: 'relative'

           ToggleButtons view_buttons, opinion_views_ui,
             minWidth: 290
+
+          if opinion_views_ui.active == 'custom'
+            triangle_left = (document.querySelector('[data-view-state="custom"]')?.offsetLeft or 60) + 35 - (if needs_expansion then (@props.style.width + @props.additional_width - width ) else 0)
+
+            if @user_has_set_a_view() && @local.minimized
+              DIV
+                className: 'custom_view_triangle'
+                style:
+                  left: triangle_left
+                  bottom: if browser.is_mobile then -5 else -5
+                  width: 0
+                  height: 0
+                  borderLeft: '12px solid transparent'
+                  borderRight: '12px solid transparent'
+                  borderTop: '7px solid #2478CC'
+            else
+              DIV
+                className: 'custom_view_triangle'
+                style:
+                  left: triangle_left
+                  bottom: if browser.is_mobile then -25 else -24
+                dangerouslySetInnerHTML: __html: """<svg width="25px" height="13px" viewBox="0 0 25 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Artboard" transform="translate(-1086.000000, -586.000000)" fill="#FFFFFF" stroke="#979797"><polyline id="Path" points="1087 599 1098.5 586 1110 599"></polyline></g></g></svg>"""
+

           SPAN
             style:
@@ -686,6 +711,7 @@

       if opinion_views_ui.active == 'custom'
         needs_expansion = @props.additional_width && @props.style?.width
+
         width = 0
         if needs_expansion
           if has_other_filters
@@ -698,24 +724,21 @@
             width: if width then width
             position: 'relative'
             right: if needs_expansion then width - @props.style.width
-            marginTop: 18

           if @local.minimized
-            DIV null,
+
+            DIV
+              style:
+                marginTop: 12
+
               NonInteractiveOpinionViews
                 more_views_positioning: @props.more_views_positioning

           else
-            triangle_left = (document.querySelector('[data-view-state="custom"]')?.offsetLeft or 60) + 35 - (if needs_expansion then (@props.style.width + @props.additional_width - width ) else 0)
-            DIV null,
-              DIV
-                style:
-                  position: 'absolute'
-                  left: triangle_left
-                  top: -16
-
-                dangerouslySetInnerHTML: __html: """<svg width="25px" height="13px" viewBox="0 0 25 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Artboard" transform="translate(-1086.000000, -586.000000)" fill="#FFFFFF" stroke="#979797"><polyline id="Path" points="1087 599 1098.5 586 1110 599"></polyline></g></g></svg>"""
+            DIV
+              style:
+                marginTop: 18

               DIV
                 style:
@@ -740,7 +763,18 @@
     opinion_views = fetch 'opinion_views'
     user_has_set_a_view = false
     for k,v of opinion_views.active_views
-      user_has_set_a_view ||= v.view_type in ['filter', 'weight', 'group', 'date-filter']
+      if v.view_type == 'filter' && v.options
+        # user hasn't set a view if they've selected all the attribute values, as that essentially means "all"
+        opinion_views_ui = fetch 'opinion_views_ui'
+        checked = 0
+        for val in v.options
+          if !!opinion_views_ui.visible_attribute_values[v.key][val]
+            checked += 1
+
+        user_has_set_a_view ||= checked > 0 && checked < v.options.length
+
+      else
+        user_has_set_a_view ||= v.view_type in ['filter', 'weight', 'group', 'date-filter']
     user_has_set_a_view

   MinimizeExpandButton: ->
@@ -801,7 +835,7 @@
     save opinion_views

 # Creates a view function on the fly given the values selected for an attribute by a user
-update_view_for_attribute = (attribute) ->
+construct_view_for_attribute = (attribute) ->
   opinion_views_ui = fetch 'opinion_views_ui'
   attr_key = attribute.key
   # having no selections for an attribute paradoxically means that all values are valid.
@@ -820,15 +854,20 @@

     passes = false
     for passing_val in passing_vals
-      if passing_val == 'true'
-        passing_val = true
-      else if passing_val == 'false'
-        passing_val = false

       if attribute.pass
-        passes ||= passing_val == attribute.pass(u)
+        val = attribute.pass(u)
+        passes ||= passing_val == val
+
+        if passing_val == 'true'
+          passes ||= true == val
+        else if passing_val == 'false'
+          passes ||= false == val
+          passes ||= undefined == val
+
       else
         passes ||= val_for_user == passing_val || (is_array && passing_val in val_for_user)
+

     passes

@@ -837,6 +876,7 @@
     # pass: pass
     salience: (u) -> if pass(u) then 1 else .1
     weight:   (u) -> if pass(u) then 1 else .1
+    options: attribute.options

   toggle_opinion_filter view, has_one_enabled

@@ -955,7 +995,7 @@
                     is_grouped = opinion_views_ui.group_by == attribute.key
                     checked = !!opinion_views_ui.visible_attribute_values[attribute.key][val]

-                    val_name = val
+                    val_name = "#{val}"
                     shortened = false
                     if val_name.length > 25
                       val_name = "#{val_name.substring(0,22)}..."
@@ -980,7 +1020,7 @@
                                 # create a view on the fly for this attribute
                                 opinion_views_ui.visible_attribute_values[attribute.key][val] = e.target.checked
                                 save opinion_views_ui
-                                update_view_for_attribute(attribute)
+                                construct_view_for_attribute(attribute)

                             if is_grouped
                               SPAN
@@ -1088,7 +1128,7 @@
                   display: 'inline-block'

                 BUTTON
-                  'data-tooltip': weight.label
+                  'data-tooltip': if !browser.is_mobile then weight.label
                   className: "weight opinion_view_button #{if activated_weights[weight.key] then 'active' else ''}"
                   onClick: ->
                     toggle_weight weight
@@ -1133,12 +1173,14 @@
       continue if !opinion_views_ui.activated_attributes[attribute.key]

       is_grouped = opinion_views_ui.group_by == attribute.key
-
+      label = null
       checked = []
       unchecked = []
+
       if attribute.key == 'date'
         date_toggle_state = fetch 'opinion-date-filter'
-        continue if !opinion_views.active_views['date'] || date_toggle_state.active == 'all' || (!date_toggle_state.start && !date_toggle_state.end)
+
+        continue if !opinion_views.active_views['date'] || date_toggle_state.active == 'all' || (date_toggle_state.active == 'custom' && !date_toggle_state.start && !date_toggle_state.end)

         if date_toggle_state.active == 'custom'
           if !date_toggle_state.start
@@ -1158,6 +1200,22 @@
         else
           filter_str = default_date_options().find((o) -> o.key == date_toggle_state.active).label

+      else if attribute.input == 'boolean' || \
+             (attribute.options.length == 2 && ['yes', true, 'true', 'no', false, 'false'].filter( (val) -> attribute.options.includes(val)).length == 2 )
+        for val in attribute.options
+          if !!opinion_views_ui.visible_attribute_values[attribute.key][val]
+            if val in ['yes', true, 'true']
+              label = translator
+                id: "opinion_views.narrow_to"
+                filter_string: ""
+                "Narrowed to {filter_string}"
+            else
+              label = translator
+                id: "opinion_views.filter_out"
+                filter_string: ""
+                "Excluding {filter_string}"
+            break
+        filter_str = null
       else
         for val in attribute.options
           if !!opinion_views_ui.visible_attribute_values[attribute.key][val]
@@ -1193,7 +1251,7 @@
             filter_str = translator
               id: "opinion_views.filter_out"
               filter_string: filter_str
-              "Filter out {filter_string}"
+              "Excluding {filter_string}"

         else
           filter_str = checked_string
@@ -1201,12 +1259,12 @@
             filter_str = translator
               id: "opinion_views.narrow_to"
               filter_string: filter_str
-              "Narrow to {filter_string}"
+              "Narrowed to {filter_string}"

       minimized_views.push
         name: attribute.name
-        label: ''
+        label: label
         icon: if is_grouped then group_by_icon # else filter_icon
         filters: filter_str
         toggle: do (attribute, is_grouped) -> ->
@@ -1223,15 +1281,17 @@
       continue if !activated_weights[weight.key]
       minimized_views.push
         name: weight.name
-        label: translator 'opinion_views.mini_weigh', 'Weigh by'
+        label: translator 'opinion_views.mini_weigh', 'Weighing by'
         # icon: weigh_icon
         toggle: do (weight) -> ->
           toggle_weight weight

     UL
+      className: 'minimized_view_list'
       style:
-        listStyle: 'none'
-        textAlign: if @props.more_views_positioning == 'centered' then 'center' else 'right'
+        width: if @props.more_views_positioning == 'centered' then 'fit-content'
+        textAlign: 'right' # if @props.more_views_positioning == 'centered' then 'center' else 'right'
+
       for mini in minimized_views
         do (mini) ->
           LI
@@ -1570,6 +1630,9 @@
     margin-right: 18px;
     flex-shrink: 0;
   }
+  .minimized_view .svg.opinion_view_class{
+    margin-right: 4px;
+  }

   .opinion_view_name {
     margin-right: 18px;
@@ -1583,7 +1646,7 @@
     margin-bottom: 8px;
   }

-  .attribute_group, .minimized_view {
+  .attribute_group {
     border-radius: 8px;
     padding: 8px 16px;
   }
@@ -1604,30 +1667,44 @@
     padding-right: 16px;
   }
   .attribute_close, .minimized_view_close {
-    font-size: 12px;
+    font-size: 13px;
     color: #000000;
     background-color: transparent;
     border: none;
   }
+  .custom_view_triangle {
+    position: absolute;
+    z-index: 1;
+  }
+
   .minimized_view_close {
     position: absolute;
-    right: -17px;
-  }
-
+    right: -22px;
+    top: 1px;
+  }
+  .minimized_view_list {
+    list-style: none;
+    margin: auto;
+  }
   .minimized_view_wrapper {
     margin-bottom: 4px;
-    margin-left: 24px;
-    display: inline-block;
     font-size: 12px;
     position: relative;
   }
+  li.minimized_view_wrapper:not(:first-child) {
+    margin-left: 24px;
+  }

   .minimized_view {
-    color: #1a5fa5;
+    color: #1059a2;
+    # border: 1px solid #2478cc;
     background-color: #e4edf7;
     width: fit-content;
     position: relative;
     display: inline-block;
+    padding: 6px 12px;
+    border-radius: 8px;
+    min-width: 200px;
   }
   .minimized_view_name {
     font-weight: 700;
@@ -1695,16 +1772,20 @@
     font-size: 12px;
     border: 1px solid;
     border-color: #{focus_blue};
-    border-right: none;
     padding: 4px 16px;
   }
+  .toggle_buttons li:not(:last-child) button {
+    border-right: none;
+  }
   .toggle_buttons li:first-child button {
     border-radius: 8px 0 0 8px;
-    border-right: none;
   }
   .toggle_buttons li:last-child button {
     border-radius: 0px 8px 8px 0px;
-    border-right: 1px solid;
+  }
+
+  .toggle_buttons li, .toggle_buttons li button {
+    margin: 0;
   }

   button.sort_proposals, .toggle_buttons .active button {


Modify EOL code from CRLF to LF

Modify EOL code from CRLF to LF

--- test/lib/pesudo_random.coffee
+++ test/lib/pesudo_random.coffee
@@ -1,28 +1,28 @@
-
-Math._random ?= Math.random
-
-pesudoRandomValues = []
-
-pesudoRandom = ->
-  pesudoRandomValues.shift() ? Math._random()
-
-pesudoRandom.MIN_VALUE = 0
-pesudoRandom.MAX_VALUE = 0.99999999
-
-pesudoRandom.push = ->
-  pesudoRandomValues.push arguments...
-  @
-
-pesudoRandom.set = ->
-  @clear()
-    .push arguments...
-
-pesudoRandom.clear = ->
-  pesudoRandomValues = []
-  @
-
-pesudoRandom.attach = ->
-  beforeEach -> pesudoRandom.clear()
-  @
-
-module.exports = Math.random = pesudoRandom
+
+Math._random ?= Math.random
+
+pesudoRandomValues = []
+
+pesudoRandom = ->
+  pesudoRandomValues.shift() ? Math._random()
+
+pesudoRandom.MIN_VALUE = 0
+pesudoRandom.MAX_VALUE = 0.99999999
+
+pesudoRandom.push = ->
+  pesudoRandomValues.push arguments...
+  @
+
+pesudoRandom.set = ->
+  @clear()
+    .push arguments...
+
+pesudoRandom.clear = ->
+  pesudoRandomValues = []
+  @
+
+pesudoRandom.attach = ->
+  beforeEach -> pesudoRandom.clear()
+  @
+
+module.exports = Math.random = pesudoRandom


Reorder tests

Reorder tests

--- tests/batman/view/filter_parsing_test.coffee
+++ tests/batman/view/filter_parsing_test.coffee
@@ -20,6 +20,41 @@
     deepEqual @spy.lastCallArguments, ["bar"]
     QUnit.start()

+asyncTest "should parse one segment keypaths as arguments", ->
+  helpers.render '

', Batman(foo: "bar"), (node) =>
+    equal node.html(), "testValue"
+    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
+    deepEqual @spy.lastCallArguments, [1, "bar"]
+    QUnit.start()
+
+asyncTest "should parse one segment keypaths as arguments anywhere in the list of arguments", ->
+  helpers.render '

', Batman(foo: "a", bar: "b", baz: "c"), (node) =>
+    equal node.html(), "testValue"
+    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
+    deepEqual @spy.lastCallArguments, [1, "a", 2, "b", 3, "c"]
+    QUnit.start()
+
+asyncTest "should not pass arguments implicitly to the named filter", ->
+  helpers.render '

', Batman(foo: "a", bar: "b", baz: "c"), (node) =>
+    equal node.html(), "testValue"
+    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
+    deepEqual @spy.lastCallArguments, ['testValue']
+    QUnit.start()
+
+asyncTest "should parse many segment keypaths as arguments anywhere in the list of arguments", ->
+  helpers.render '

', Batman(qux: Batman(foo: "a", bar: "b", baz: "c")), (node) =>
+    equal node.html(), "testValue"
+    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
+    deepEqual @spy.lastCallArguments, [1, "a", 2, "b", 3, "c"]
+    QUnit.start()
+
+asyncTest "should parse many segment keypaths as arguments", ->
+  helpers.render '

', Batman(foo: Batman(bar: "baz")), (node) =>
+    equal node.html(), "testValue"
+    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
+    deepEqual @spy.lastCallArguments, [1, "baz"]
+    QUnit.start()
+
 asyncTest "should parse many segment keypaths that begin with numbers", ->
   helpers.render '

', Batman("404_title": Batman(bar: "baz")), (node) =>
     equal node.html(), "testValue"
@@ -32,41 +67,6 @@
     equal node.html(), "testValue"
     ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
     deepEqual @spy.lastCallArguments, ["baz"]
-    QUnit.start()
-
-asyncTest "should parse one segment keypaths as arguments", ->
-  helpers.render '

', Batman(foo: "bar"), (node) =>
-    equal node.html(), "testValue"
-    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
-    deepEqual @spy.lastCallArguments, [1, "bar"]
-    QUnit.start()
-
-asyncTest "should parse one segment keypaths as arguments anywhere in the list of arguments", ->
-  helpers.render '

', Batman(foo: "a", bar: "b", baz: "c"), (node) =>
-    equal node.html(), "testValue"
-    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
-    deepEqual @spy.lastCallArguments, [1, "a", 2, "b", 3, "c"]
-    QUnit.start()
-
-asyncTest "should not pass arguments implicitly to the named filter", ->
-  helpers.render '

', Batman(foo: "a", bar: "b", baz: "c"), (node) =>
-    equal node.html(), "testValue"
-    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
-    deepEqual @spy.lastCallArguments, ['testValue']
-    QUnit.start()
-
-asyncTest "should parse many segment keypaths as arguments anywhere in the list of arguments", ->
-  helpers.render '

', Batman(qux: Batman(foo: "a", bar: "b", baz: "c")), (node) =>
-    equal node.html(), "testValue"
-    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
-    deepEqual @spy.lastCallArguments, [1, "a", 2, "b", 3, "c"]
-    QUnit.start()
-
-asyncTest "should parse many segment keypaths as arguments", ->
-  helpers.render '

', Batman(foo: Batman(bar: "baz")), (node) =>
-    equal node.html(), "testValue"
-    ok @spy.lastCallArguments.pop() instanceof Batman.DOM.AbstractBinding
-    deepEqual @spy.lastCallArguments, [1, "baz"]
     QUnit.start()

 asyncTest "should parse keypaths containing true as arguments", ->


tests for tool privacy - failing, see #130

--- test/integration/api.coffee
+++ test/integration/api.coffee
@@ -42,7 +42,7 @@

     describe 'Tools', ->
       context 'POST /api/tools', ->
-        context 'when I create a private tool', ->
+        context 'when I create a tool (without specifying privacy)', ->
           before (done) ->
             request.post
               uri: "#{serverURL}/api/tools"
@@ -50,7 +50,6 @@
                 name: @toolName
                 type: 'view'
                 gitUrl: 'git://github.com/scraperwiki/spreadsheet-tool.git'
-                #public: false <--- defaults to false
             , (err, res, body) =>
               @response = res
               @tool = JSON.parse res.body
@@ -71,6 +70,9 @@

           it 'is owned by me', ->
             @user.should.equal @tool.user
+
+          it 'is private', ->
+            @tool.public.should.be.false

           context 'when I update that tool', ->
             before (done) ->
@@ -93,6 +95,9 @@
             it 'updates the tool', ->
               @response.should.have.status 200

+            it 'is still private', ->
+              @tool.public.should.be.false
+
             it 'shows a recent "updated" timestamp', ->
               should.exist @tool.created
               should.exist @tool.updated
@@ -102,6 +107,26 @@
             # but it's hard.
             xit 'returns the updated tool', ->
               @tool.manifest.displayName.should.equal 'View Data 2'
+
+        context 'When I create a private tool', ->
+          before (done) ->
+            request.post
+              uri: "#{serverURL}/api/tools"
+              form:
+                name: "#{@toolName}-private"
+                type: 'view'
+                gitUrl: 'git://github.com/scraperwiki/test-app-tool.git'
+                public: false
+            , (err, res, body) =>
+              @response = res
+              @tool = JSON.parse res.body
+              done()
+
+          it 'creates a new tool', ->
+            @response.should.have.status 201
+
+          it 'is private', ->
+            @tool.public.should.be.false

         context 'When I create a public tool', ->
           before (done) ->
@@ -120,6 +145,9 @@
           it 'creates a new tool', ->
             @response.should.have.status 201

+          it 'is public', ->
+            @tool.public.should.be.true
+
       context 'GET /api/tools', ->
         before (done) ->
           request.get "#{serverURL}/api/tools", (err, res) =>
@@ -130,8 +158,14 @@
         it 'returns a list of tools', ->
           @tools.length.should.be.above 0

-        it "includes my tool", ->
+        it "includes my first tool", ->
           should.exist(_.find @tools, (x) => x.name == @toolName)
+
+        it "includes my private tool", ->
+          should.exist(_.find @tools, (x) => x.name == "#{@toolName}-private")
+
+        it "includes my public tool", ->
+          should.exist(_.find @tools, (x) => x.name == "#{@toolName}-public")

         it "includes public tools", ->
           should.exist(_.find @tools, (x) => x.name == "test-app")
@@ -270,6 +304,9 @@

       it "does not see ickletest's private tool in tool list", ->
         should.not.exist(_.find @tools, (x) => x.name == @toolName)
+
+      it "does not see ickletest's private tool in tool list", ->
+        should.not.exist(_.find @tools, (x) => x.name == "#{@toolName}-private")

       it "does see ickletest's public tool in tool list", ->
         should.exist(_.find @tools, (x) => x.name == "#{@toolName}-public")


Fix clearStorageData options

Fix clearStorageData options

--- views/components/settings/parts/poi-config.cjsx
+++ views/components/settings/parts/poi-config.cjsx
@@ -123,7 +123,7 @@
         useFixedResolution: true
       @handleSetWebviewWidth()
   handleClearCookie: (e) ->
-    remote.getCurrentWebContents().session.clearStorageData ['cookies'], ->
+    remote.getCurrentWebContents().session.clearStorageData {storages: ['cookies']}, ->
       toggleModal '删除 Cookies', '删除成功。'
   handleClearCache: (e) ->
     remote.getCurrentWebContents().session.clearCache ->


Add test for JSON document checking

Add test for JSON document checking

--- test/testJsonPointer.coffee
+++ test/testJsonPointer.coffee
@@ -56,6 +56,14 @@
       expect(evaluate(p)).to.be.undefined for p in pointers

+    it "should throw an error if target is not valid JSON document", () ->
+      invalidTargets = [null, "", [], {}, "invalid", 1, "{o}"]
+      pointer = ""
+
+      evaluate = (target) -> () -> jsonPointer.get target, pointer
+      expect(evaluate(t)).to.throw Error for t in invalidTargets
+
+
     describe "pointer validation", () ->
       target =
         foo: "bar"


Fix broken link tag in JS rendering on request details

--- evesrp/static/js/request.coffee
+++ evesrp/static/js/request.coffee
@@ -76,7 +76,7 @@
             else if attr == 'pilot'
                 text = request.pilot.name
             # TODO: HTML Escape `text`
-            $element.html "a href=\"#{ link }\" target=\"_blank\">#{ text }"
+            $element.html "#{ text }"

 submitAction = (ev) ->


camelcase vars in tests

camelcase vars in tests

--- test/tests/ColorTest.coffee
+++ test/tests/ColorTest.coffee
@@ -57,66 +57,66 @@

 	it "should mix colors with HUSL by default", ->

-		Orange = new Color "orange"
-		Yellow = new Color "yellow"
-		Mix = Orange.mix(Yellow, .5)
+		orange = new Color "orange"
+		yellow = new Color "yellow"
+		mix = orange.mix(yellow, .5)

-		g = Math.floor(Mix.g)
-		b = Math.floor(Mix.b)
+		g = Math.floor(mix.g)
+		b = Math.floor(mix.b)

-		Mix.r.should.equal 255
+		mix.r.should.equal 255
 		g.should.equal 210
 		b.should.equal 67

 	it "should mix colors with HSL", ->

-		Blue = new Color "blue"
-		Yellow = new Color "yellow"
-		Mix = Blue.mix(Yellow, .5, true, "hsl")
+		blue = new Color "blue"
+		yellow = new Color "yellow"
+		mix = blue.mix(yellow, .5, true, "hsl")

-		b = Math.floor(Mix.b)
+		b = Math.floor(mix.b)

-		Mix.r.should.equal 0
-		Mix.g.should.equal 255
+		mix.r.should.equal 0
+		mix.g.should.equal 255
 		b.should.equal 127

 	it "should mix colors with RGB", ->

-		Red = new Color "red"
-		Yellow = new Color "yellow"
-		Orange = Red.mix(Yellow, .5, true, "rgb")
+		red = new Color "red"
+		yellow = new Color "yellow"
+		orange = red.mix(yellow, .5, true, "rgb")

-		Orange.r.should.equal 255
-		Orange.g.should.equal 127.5
-		Orange.b.should.equal 0
+		orange.r.should.equal 255
+		orange.g.should.equal 127.5
+		orange.b.should.equal 0

 	it "should match hue values if one of colors is transparent", ->

-		Red = new Color "red"
-		WhiteTransparent = new Color "rgba(255, 255, 255, 0)"
-		Mix = Red.mix(WhiteTransparent, .5, true, "hsl")
+		red = new Color "red"
+		whiteTransparent = new Color "rgba(255, 255, 255, 0)"
+		mix = red.mix(whiteTransparent, .5, true, "hsl")

-		Mix.r.should.equal 255
-		Mix.g.should.equal 0
-		Mix.b.should.equal 0
-		Mix.a.should.equal 0.5
+		mix.r.should.equal 255
+		mix.g.should.equal 0
+		mix.b.should.equal 0
+		mix.a.should.equal 0.5

 	it "should match hue values if one of colors is gray", ->

-		Blue = new Color "blue"
-		White = new Color "hsl(200, 100, 100)"
-		Mix = Blue.mix(White, .5, true, "hsl")
+		blue = new Color "blue"
+		white = new Color "hsl(200, 100, 100)"
+		mix = blue.mix(white, .5, true, "hsl")

-		Blue.toHsl().h.should.eql Mix.toHsl().h
+		blue.toHsl().h.should.eql mix.toHsl().h

 	it "should return css name if possible", ->

-		Aqua = "aqua"
+		aqua = "aqua"

 		layer = new Layer
-			backgroundColor: Aqua
+			backgroundColor: aqua

-		layer.backgroundColor.toName().should.eql Aqua
+		layer.backgroundColor.toName().should.eql aqua

 	it "should clamp constructor options", ->


added firefox to the party

added firefox to the party

--- test/unit/karma.conf.coffee
+++ test/unit/karma.conf.coffee
@@ -1,17 +1,22 @@
 # This is the config file for the unit tests for the app. For more info visit the
 # [karma config page](http://karma-runner.github.io/0.10/config/configuration-file.html)
 module.exports = (config) ->
+  browsers = [
+    'PhantomJS'
+  ]
+
+  # You may not need this but since [Travis](https://travis-ci.org/ahmednuaman/radian) is being used to keep an
+  # eye on the code then [PhantomJS](http://phantomjs.org) is used as the runner browser.
+  if !process.env.TRAVIS
+    browsers.push 'Chrome', 'Firefox'
+
   exclude = (path) ->
     pattern: path
     included: false

   config.set
     basePath: '../../'
-    browsers: [
-      # You may not need this but since [Travis](https://travis-ci.org/ahmednuaman/radian) is being used to keep an
-      # eye on the code then [PhantomJS](http://phantomjs.org) is used as the runner browser.
-      if process.env.TRAVIS then 'PhantomJS' else 'Chrome'
-    ]
+    browsers: browsers
     files: [
       exclude 'assets/vendor/**/*.js'
       exclude 'assets/js/**/*.js'