Don't return an unresolved Promise on beforeEach()

The `editor.update()` method returns a Promise, which under some
circumstances (on specs mostly) never gets resolved.

Because of CoffeeScript's implicit returns, this Promise was returned on
the `beforeEach()` method of these tests.

Before https://github.com/atom/atom/pull/18917, any Promise that was
returned by a spec method was just discarded, so this behaviour didn't
cause any issue, but once we merge that PR this test is going to start
failing because jasmine will wait indefinitely until the Promise is
resolved.

--- spec/snippets-spec.coffee
+++ spec/snippets-spec.coffee
@@ -481,6 +481,10 @@
       describe "when the snippet spans multiple lines", ->
         beforeEach ->
           editor.update({autoIndent: true})
+          # editor.update() returns a Promise that never gets resolved, so we
+          # need to return undefined to avoid a timeout in the spec.
+          # TODO: Figure out why `editor.update({autoIndent: true})` never gets resolved.
+          return

         it "places tab stops correctly", ->
           expect(editor.getSoftTabs()).toBeTruthy()


fixed variant selection

fixed variant selection

--- app/assets/javascripts/shoppe/application.coffee
+++ app/assets/javascripts/shoppe/application.coffee
@@ -60,7 +60,7 @@
     $('#product_permalink').val(permalink $(this).val())
     $('#product_sku').val(permalink $(this).val())

-  $('.variantInfo input[name="setter_product_name"]').on 'change paste keyup keypress', ->
+  $('.variantInfo [name="setter_product_name"]').on 'change paste keyup keypress', ->
     $('#product_name').val($(this).val()).change()

   # When clicking the order search button, toggle the form


Add space to help text

Add space to help text

- Closes #1376

--- app/talk/comment-help.cjsx
+++ app/talk/comment-help.cjsx
@@ -8,6 +8,6 @@

Guide to commenting on talk

Talk comments are written in markdown

Mention users with @username

-
Mention subjects with @owner/project^subject_id or with ^subject_idif you are inside of the subject's talk

+
Mention subjects with @owner/project^subject_id or with ^subject_id if you are inside of the subject's talk

Create hashtags with #hashtag


Remove PromiseRenderer from popular tags

--- app/talk/popular-tags.cjsx
+++ app/talk/popular-tags.cjsx
@@ -1,6 +1,5 @@
 React = require 'react'
 talkClient = require 'panoptes-client/lib/talk-client'
-PromiseRenderer = require '../components/promise-renderer'
 {Link} = require 'react-router'

 module.exports = React.createClass
@@ -12,6 +11,12 @@

   contextTypes:
     geordi: React.PropTypes.object
+
+  getInitialState: ->
+    tags: []
+
+  componentWillMount: ->
+    @tagsRequest()

   tagsRequest: ->
     query =
@@ -23,7 +28,11 @@
       query.taggable_type = @props.type
       query.taggable_id = @props.id

-    talkClient.type('tags/popular').get query
+    talkClient
+      .type 'tags/popular'
+      .get query
+      .then (tags) =>
+        @setState {tags}

   tag: (talkTag, i) ->
     logClick = @context.geordi?.makeHandler? 'hashtag-sidebar'
@@ -35,11 +44,10 @@

   render: ->

-      <PromiseRenderer promise={@tagsRequest()}>{(tags) =>
-        if tags?.length
-

-            {@props.header ? null}
-
{tags.map(@tag)}

-

-      }</PromiseRenderer>
+      {if @state.tags?.length
+

+          {@props.header ? null}
+
{@state.tags.map(@tag)}

+

+      }


Update selection autoscroll specs with 3-valued logic

If autoscroll is true, we center the view on the selection. If false,
we do no autoscrolling at all. And if undefined we autoscroll to the
cursor.

--- spec/app/editor-spec.coffee
+++ spec/app/editor-spec.coffee
@@ -9,7 +9,7 @@
 _ = require 'underscore'
 fs = require 'fs'

-describe "Editor", ->
+fdescribe "Editor", ->
   [rootView, project, buffer, editor, cachedLineHeight] = []

   getLineHeight = ->
@@ -995,36 +995,47 @@
         expect(editor.scrollTop()).toBeGreaterThan(0)

     describe "selection autoscrolling and highlighting when setting selected buffer range", ->
-      it "only if autoscroll is true, centers the viewport on the selection if its vertical center is currently offscreen", ->
+      beforeEach ->
         setEditorHeightInLines(editor, 4)

-        editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true)
-        expect(editor.scrollTop()).toBe 0
-
-        editor.setSelectedBufferRange([[6, 0], [8, 0]], autoscroll: true)
-        expect(editor.scrollTop()).toBe 5 * editor.lineHeight
-
-        editor.setSelectedBufferRange([[0, 0], [1, 0]]) # autoscroll is false, the default
-        expect(editor.scrollTop()).toBe 5 * editor.lineHeight
-
-      it "highlights the selection if autoscroll is true", ->
-        editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true)
-        expect(editor.getSelectionView()).toHaveClass 'highlighted'
-        advanceClock(1000)
-        expect(editor.getSelectionView()).not.toHaveClass 'highlighted'
-
-        editor.setSelectedBufferRange([[3, 0], [5, 0]], autoscroll: true)
-        expect(editor.getSelectionView()).toHaveClass 'highlighted'
-
-        advanceClock(500)
-        spyOn(editor.getSelectionView(), 'removeClass').andCallThrough()
-        editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true)
-        expect(editor.getSelectionView().removeClass).toHaveBeenCalledWith('highlighted')
-        expect(editor.getSelectionView()).toHaveClass 'highlighted'
-
-
-        advanceClock(500)
-        expect(editor.getSelectionView()).toHaveClass 'highlighted'
+      describe "if autoscroll is true", ->
+        it "centers the viewport on the selection if its vertical center is currently offscreen", ->
+          editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true)
+          expect(editor.scrollTop()).toBe 0
+
+          editor.setSelectedBufferRange([[6, 0], [8, 0]], autoscroll: true)
+          expect(editor.scrollTop()).toBe 5 * editor.lineHeight
+
+        it "highlights the selection if autoscroll is true", ->
+          editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true)
+          expect(editor.getSelectionView()).toHaveClass 'highlighted'
+          advanceClock(1000)
+          expect(editor.getSelectionView()).not.toHaveClass 'highlighted'
+
+          editor.setSelectedBufferRange([[3, 0], [5, 0]], autoscroll: true)
+          expect(editor.getSelectionView()).toHaveClass 'highlighted'
+
+          advanceClock(500)
+          spyOn(editor.getSelectionView(), 'removeClass').andCallThrough()
+          editor.setSelectedBufferRange([[2, 0], [4, 0]], autoscroll: true)
+          expect(editor.getSelectionView().removeClass).toHaveBeenCalledWith('highlighted')
+          expect(editor.getSelectionView()).toHaveClass 'highlighted'
+
+          advanceClock(500)
+          expect(editor.getSelectionView()).toHaveClass 'highlighted'
+
+      describe "if autoscroll is false", ->
+        it "does not scroll to the selection or the cursor", ->
+          editor.scrollToBottom()
+          scrollTopBefore = editor.scrollTop()
+          editor.setSelectedBufferRange([[0, 0], [1, 0]], autoscroll: false)
+          expect(editor.scrollTop()).toBe scrollTopBefore
+
+      describe "if autoscroll is not specified", ->
+        it "autoscrolls to the cursor as normal", ->
+          editor.scrollToBottom()
+          editor.setSelectedBufferRange([[0, 0], [1, 0]])
+          expect(editor.scrollTop()).toBe 0

   describe "cursor rendering", ->
     describe "when the cursor moves", ->


Handle escape keypress with keydown spec helper

--- spec/vim-mode-spec.coffee
+++ spec/vim-mode-spec.coffee
@@ -33,7 +33,7 @@
       e.initTextEvent eventArgs...
       target.dispatchEvent e

-    key = "U+#{key.charCodeAt(0).toString(16)}"
+    key = "U+#{key.charCodeAt(0).toString(16)}" unless key == "escape"
     element ||= document.activeElement
     eventArgs = [true, true, null, key, 0, ctrl, alt, shift, meta] # bubbles, cancelable, view, key, location


Add indentation tests

Add indentation tests

Inspired by language-javascript's spec:
https://github.com/atom/language-javascript/blob/15ee0273b24547496bdfe3cbee8108c48d863241/spec/javascript-spec.coffee

--- spec/powershell-spec.coffee
+++ spec/powershell-spec.coffee
@@ -1,3 +1,5 @@
+{TextEditor} = require 'atom'
+
 describe "PowerShell grammar", ->

   grammar = null
@@ -366,3 +368,23 @@
       {tokens} = grammar.tokenizeLine("`  \n")
       expect(tokens[0].value).toEqual("`")
       expect(tokens[0]).toHaveScopes ["punctuation.separator.continuation.line.powershell"]
+
+  describe "indentation", ->
+    editor = null
+
+    beforeEach ->
+      editor = new TextEditor({})
+      editor.setGrammar(grammar)
+
+    expectPreservedIndentation = (text) ->
+      editor.setText(text)
+      editor.autoIndentBufferRows(0, text.split("\n").length - 1)
+      expect(editor.getText()).toBe text
+
+    it "indents foreach loops with braces", ->
+      expectPreservedIndentation """
+        foreach($foo in $myArray)
+        {
+          Write-Host "Value $foo"
+        }
+      """


Compact specs

Compact specs

--- spec/src/engine_spec.coffee
+++ spec/src/engine_spec.coffee
@@ -16,21 +16,15 @@
         # Hacky exception to make sure function executes
         Kona.Engine.running = true
         fn = -> throw new Error("Hello World!")
-        expect(->
-          Kona.Engine.queue(fn)
-        ).toThrow()
+        expect( -> Kona.Engine.queue(fn) ).toThrow()

     describe "::start", ->
       beforeEach -> spyOn(Kona.Engine, 'run')

-      # TODO: this test is gross
-      # not sure how to guarantee failure case runs before scene defined
       it "throws an exception and then starts successfully", ->
-        expect(->
-          Kona.Engine.start()
-        ).toThrow()
+        expect( -> Kona.Engine.start() ).toThrow()

         engineScene = new Kona.Scene { name: 'engineScene', active: true }
         Kona.Engine.start()
@@ -41,9 +35,7 @@
         Kona.Engine.running = false
         fn = -> throw new Error("Hello World!")
         Kona.Engine.queue(fn)
-        expect(->
-          Kona.Engine.start()
-        ).toThrow()
+        expect( -> Kona.Engine.start() ).toThrow()

       it "calls run()", ->


:eight_spoked_asterisk: correction of typo

--- app/workers/indexer.coffee
+++ app/workers/indexer.coffee
@@ -24,11 +24,11 @@

   return (finish) ->
     try
-      helpers.fs.ensureSync marianne_path
+      helpers.fs.ensureDirSync marianne_path
     catch e
       return finish e

-    user_dirs = fs.readdirSync marianne_path
+    user_dirs = helpers.fs.readdirSync marianne_path

     if user_dirs.length < 1
       log.info "[Indexer] Directory {marianne} contains no User Directories"


fix(gitlab.coffee): removed rogue line return

--- src/scripts/gitlab.coffee
+++ src/scripts/gitlab.coffee
@@ -88,8 +88,7 @@
         else
           switch hook.object_kind
             when "issue"
-              robot.send user, "Issue #{bold(hook.object_attributes.id)}: #{hook.object_attributes.title} (#{h
-ook.object_attributes.state})"
+              robot.send user, "Issue #{bold(hook.object_attributes.id)}: #{hook.object_attributes.title} (#{hook.object_attributes.state})"
             when "merge_request"
               robot.send user, "Merge Request #{bold(hook.object_attributes.id)}: #{hook.object_attributes.title} (#{hook.object_attributes.state}) between #{bold(hook.object_attributes.source_branch)} and #{bold(hook.object_attributes.target_branch)}"
           if hook.object_attributes.description


Personally reply to the person instead of a public message.

--- src/scripts/polite.coffee
+++ src/scripts/polite.coffee
@@ -14,4 +14,4 @@

 module.exports = (robot) ->
   robot.respond /(thanks|thank you|cheers|nice one)/i, (msg) ->
-    msg.send msg.random responses
+    msg.reply msg.random responses


The creator says this base URL will work. Lets see what happens!

--- src/scripts/shodan.coffee
+++ src/scripts/shodan.coffee
@@ -15,7 +15,7 @@

 SHODAN_KEY = process.env.SHODAN_KEY

-shodan_url = "http://www.shodanhq.com/api/"
+shodan_url = "http://www.shodanhq.com"

 module.exports = (robot) ->
   robot.respond /shodan (.*)/i, (msg) ->


renamed vehicles controller to liveVehiclesController

--- lib/controllers/vehicles_controller.coffee
+++ lib/controllers/vehicles_controller.coffee
@@ -63,5 +63,5 @@
     juggernaut.subscribe Sensor.WEBSOCKET_CHANNEL, (data) =>
       console.log "Websocket data received", data
       message = Sensor.Message.create data
-      if vehicle = Sensor.vehiclesController.findById message.get('unit_id')
+      if vehicle = Sensor.liveVehiclesController.findById message.get('unit_id')
         vehicle.updateWithMessages message


Interpreter can now visualize arrays and hashes.

--- coffee/tutor.coffee
+++ coffee/tutor.coffee
@@ -84,19 +84,61 @@
   html += "

"
   html

+
+represent_value_simple = (value) ->
+  klass = value.$class().$to_s()
+  switch klass
+    when 'NilClass'   then null
+    when 'Numeric'    then value.valueOf()
+    when 'String'     then value.valueOf()
+    when 'TrueClass'  then true
+    when 'FalseClass' then false
+    when 'Array'      then ['REF', value.$object_id()]
+    when 'Hash'       then ['REF', value.$object_id()]
+    when 'Proc'       then ['REF', value.$object_id()]
+    else klass
+
+represent_value = (value, heap) ->
+  klass = value.$class().$to_s()
+  switch klass
+    when 'NilClass'   then null
+    when 'Numeric'    then value.valueOf()
+    when 'String'     then value.valueOf()
+    when 'TrueClass'  then true
+    when 'FalseClass' then false
+    when 'Array'
+      on_heap = ['LIST'].concat _.map value, (element) ->
+        represent_value element, heap
+      heap[value.$object_id()] = on_heap
+      ['REF', value.$object_id()]
+    when 'Hash'
+      on_heap = ['DICT'].concat _.map value.keys, (key) ->
+        [represent_value(key, heap), represent_value(value.map[key], heap)]
+      heap[value.$object_id()] = on_heap
+      ['REF', value.$object_id()]
+    when 'Proc'
+      on_heap = ['FUNCTION', "line # unknown", null]
+      heap[value.$object_id()] = on_heap
+      ['REF', value.$object_id()]
+    else klass
+
 new_trace_entry = (interpreter, line_num) ->
   locals = {}
   map = _.omit(interpreter.visibleState().varsStack[0].map, '__method_name')
+  heap = {}
   for pair in _.pairs(map)
     if pair[1].length == 2
-      locals[pair[0]] = pair[1][1].valueOf()
+      value = represent_value_simple(pair[1][1])
+      locals[pair[0]] = value
+      represent_value pair[1][1], heap
+
   trace_entry =
     ordered_globals:[]
     stdout: _.map(interpreter.getStdoutAndStderr(), (pair) -> pair[1]).join()
     func_name:"main"
     stack_to_render:[]
     globals:{}
-    heap:{}
+    heap:heap
     line:line_num
     event:"step_line"
   trace_entry.stack_to_render.push


stash

stash

--- apps/artworks/index.coffee
+++ apps/artworks/index.coffee
@@ -11,23 +11,25 @@
 app = module.exports = express()

 app.get '/api/orchestration/artworks/:id', (req, res, next) ->
-  fetchArtwork req.params.id, (err, data) ->
+  fetchAndCacheArtwork req.params.id, (err, data) ->
     return next err if err
     res.send data

 fetchAndCacheArtwork = (id, callback) ->
-  db.artworks.findOne { _id: ObjectId(id) }, (err, artwork) ->
+  db.artworks.findOne { id: id }, (err, artwork) ->
     return callback err if err
-    if doc
+    console.log 'moo', artwork
+    if artwork
       callback null, artwork
       callback = ->
     fetchArtwork id, (err, artwork) ->
       return callback err if err
-      db.artworks.save _.extend(artwork, _id: ObjectId artwork._id), (err, artwork) ->
-      callback null, artwork
-
+      _.extend artwork, _id: ObjectId artwork._id
+      db.artworks.save artwork, (err, artwork) ->
+        callback err, artwork

 fetchArtwork = (id, callback) ->
+  console.log 'MOOOO'
   artsyXapp (err, xappToken) ->
     return callback err if err
     request
@@ -51,7 +53,7 @@
         ], (err, [partner, artists]) ->
           return callback err if err
           data =
-            _id: artwork.id
+            id: artwork.id
             image_urls: imageUrls artwork
             artwork: artwork
             artists: artists._embedded.artists


toLatexTable: moving away from longtable for now. Got my format

--- queries/toLatexTable.coffee
+++ queries/toLatexTable.coffee
@@ -7,47 +7,58 @@
   data = opts.data
   return "can't create latex table, no data" unless data

+  useLongTable = opts.useLongTable or false
+  title = opts.title
+
+
   columns = opts.columns

   warning = "% auto compiled by tom at #{new Date}\n\n"

   aligns = _.pluck columns, "align"
-  columnAligns = "| " + aligns.join(" | ") + " |"
+  # columnAligns = "| " + aligns.join(" | ") + " |"
+  columnAligns = aligns.join " "

-  preamble = "\\begin{center} \n \\begin{longtable}{ #{columnAligns} } \n \\hline \n"
+  tableType = if useLongTable then "longtable" else "tabular"
+
+  preamble = "\\begin{table} \n"
+  preamble += "\t\\caption{#{title}} \n"
+  preamble += "\t\\begin{#{tableType}}{ #{columnAligns} } \\toprule "

   for column, i in columns
-    align = if i is 0 then "|c|" else "c|"
-    preamble += "\\multicolumn{1}{#{align}}{\\textbf{#{column.title}}}"
-    preamble += " & \n" unless i is (columns.length - 1)
+    # align = if i is 0 then "|c|" else "c|"
+    align = "c"
+    preamble += "\n\t\t \\multicolumn{1}{#{align}}{\\textbf{#{column.title}}}"
+    preamble += " &" unless i is (columns.length - 1)

-  preamble += "\\\\ \\hline \n \\endfirsthead \n\n"
+  preamble += "\\\\ \\midrule\n"

-  longtable = "\\multicolumn{#{columns.length}}{c} \n"
-  longtable += "{{\\bfseries \\tablename \\thetable{} #{longtableHeadText}}} \\\\ \n"
-  longtable += "\\hline \n"
-
-  for column, i in columns
-    align = if i is 0 then "|c|" else "c|"
-    longtable += "\\multicolumn{1}{#{align}}{\\textbf{#{column.title}}} "
-    longtable += " & \n" unless i is (columns.length - 1)
+  # longtable = "\\multicolumn{#{columns.length}}{c} \n"
+  # longtable += "{{\\bfseries \\tablename \\thetable{} #{longtableHeadText}}} \\\\ \n"
+  # longtable += "\\hline \n"
+  #
+  # for column, i in columns
+  #   align = if i is 0 then "|c|" else "c|"
+  #   longtable += "\\multicolumn{1}{#{align}}{\\textbf{#{column.title}}} "
+  #   longtable += " & \n" unless i is (columns.length - 1)
+  #
+  # longtable += "\\\\ \\hline \n \\endhead \n\n"
+  # longtable += "\\hline \\multicolumn{#{columns.length}}{|r|}{{#{longtableFootText}}} \\\\ \\hline \n"
+  # longtable += "\\endfoot\n \\hline\n \\endlastfoot\n\n"

-  longtable += "\\\\ \\hline \n \\endhead \n\n"
-  longtable += "\\hline \\multicolumn{#{columns.length}}{|r|}{{#{longtableFootText}}} \\\\ \\hline \n"
-  longtable += "\\endfoot\n \\hline\n \\endlastfoot\n\n"
-
-  body = "\\hline\n"
+  # body = "\\bottomrule\n"
+  body = ""
   for row in data
     for column, j in columns
       body += row[column.property]
-      body += if j is (columns.length - 1) then " \\\\ \n" else " & "
-    body += "\\hline\n"
+      body += if j is (columns.length - 1) then " \\\\\n " else " & "
+  body += "\\bottomrule\n"

+  footer = "\\end{#{tableType}}\n \\end{table}"

-  footer = "\n\n"
-  footer += "\\end{longtable}\n \\end{center}"
-
-  fullTable = warning + preamble + longtable + body + footer
+  fullTable = warning + preamble
+  fullTable += longtable if useLongTable
+  fullTable += body + footer
   return fullTable

 module.exports = objectToTable


Fix app.views.Content#isExternalUrl

Fix app.views.Content#isExternalUrl

--- assets/javascripts/views/content/content.coffee
+++ assets/javascripts/views/content/content.coffee
@@ -154,4 +154,4 @@
     try $.id decodeURIComponent(hash) catch

   isExternalUrl: (url) ->
-    url?[0..3] is 'http'
+    url?[0..4] in ['http:', 'https:']


be sure to check if review is allowed before going to review

--- src/task/index.cjsx
+++ src/task/index.cjsx
@@ -51,7 +51,7 @@
   isStepAllowed: (stepIndex) ->
     {task} = @state
     {taskId} = @props
-    (stepIndex <= task.steps.length) or
+    (stepIndex < task.steps.length) or
       (@isReviewStep(stepIndex) and @canReview()) or
       (@isContinueStep(stepIndex) and @shouldContinue())


use checker api for end

use checker api for end

--- src/template.coffee
+++ src/template.coffee
@@ -62,16 +62,12 @@
         # write it back so builder can use it to instantiate a new tag
         @xml.Tag = @xml.opts.Tag = ExtendedTag

-        ## FIXME replace this with new checker middleware api when possible
         # add self closing tag behavior
         # some of the html tags dont need a closing tag
-        end_tag = Tag::end
-        @xml.Tag::end = ->
-            if opts.self_closing is on or opts.self_closing.match @name
-                end_tag.call this, arguments...
-            else
-                @text("", force:yes) if @isempty
-                end_tag.call this, arguments...
+        @xml.register 'end', (tag, next) ->
+            unless opts.self_closing is on or opts.self_closing.match tag.name
+                tag.isempty = no
+            next(tag)

         # pipe events through
         EVENTS.forEach (event) =>


tooltips: various clean-up and adjustments

--- src/tooltips.coffee
+++ src/tooltips.coffee
@@ -5,28 +5,25 @@
     direction = options?.direction ? "east"
     out_duration = options?.out_duration ? 1000
     in_duration = options?.in_duration ? 0
-    pad = options?.pad ? 16
+    pad = options?.pad ? 8
     fill = options?.fill
     fontcolor = options?.fontcolor
     fontsize = options?.fontsize
-
-    pad = pad * 1.0
-
-    unless ["east", "west", "north", "south"].includes?(direction)
-            d3panels.displayError("tooltip_create: invalid direction (#{direction})")

     tipdiv = selection.append("div")
                .attr("class", "d3panels-tooltip #{tipclass}")
                .style("opacity", 0)

     if direction == "east"
-        triChar = '\u25C0'
+        triChar = '\u25C0'        # triangle pointing left
     else if direction == "west"
-        triChar = '\u25B6'
+        triChar = '\u25B6'        # triangle pointing right
     else if direction == "north"
-        triChar = '\u25BC'
+        triChar = '\u25BC'        # triangle pointing down
     else if direction == "south"
-        triChar = '\u25B2'
+        triChar = '\u25B2'        # triangle pointing up
+    else
+        d3panels.displayError("tooltip_create: invalid direction (#{direction})")

     tridiv = selection.append("div")
                       .attr("class", "d3panels-tooltip-tri #{tipclass}")
@@ -39,9 +36,12 @@
     tridiv.style("color", fill) if fill?
     tridiv.style("font-size", "#{fontsize}px") if fontsize?

-    tripad = tridiv.style("font-size").replace("px", "")*1.0
+    # make sure we have the font size
+    fontsize = tridiv.style("font-size").replace("px", "")*1.0
+    pad = pad * 1.0 + fontsize

     objects.on("mouseover.#{tipclass}", (d,i) ->
+          # mouse position; make sure these are numbers
           mouseX = d3.event.pageX*1.0
           mouseY = d3.event.pageY*1.0

@@ -50,33 +50,34 @@
           tipbox_height = tipdiv.node().getBoundingClientRect().height*1.0
           tipbox_width = tipdiv.node().getBoundingClientRect().width*1.0

-          shiftX = shiftY = 0
+          shiftX = shiftY = 0 # <- for triangle, relative to tool tip box
           if direction == "east"
               posX = mouseX + pad
               posY = mouseY - tipbox_height/2.0
+
+              divpad = tipdiv.style("padding-left").replace("px", "")*1.0
+
               triChar = '\u25C0'
-              shiftX = -tripad
-              shiftY = tripad/4
+              shiftX = -fontsize - divpad
+              shiftY = tipbox_height/2.0 - fontsize/2
           else if direction == "west"
               posX = mouseX - tipbox_width*1.0 - pad
               posY = mouseY - tipbox_height/2.0
               triChar = '\u25B6'
-              shiftX = -tripad + tipbox_width
-              shiftY = tripad/4
+              shiftX = -fontsize + tipbox_width
+              shiftY = fontsize/4
           else if direction == "north"
               posX = mouseX - tipbox_width/2.0
               posY = mouseY - tipbox_height - pad
               triChar = '\u25BC'
-              shiftX = tipbox_width/2.0-tripad
-              shiftY = pad+tripad
+              shiftX = tipbox_width/2.0-fontsize
+              shiftY = pad+fontsize
           else if direction == "south"
               posX = mouseX - tipbox_width/2.0
               posY = mouseY + pad
               triChar = '\u25B2'
-              shiftX = tipbox_width/2.0-tripad
-              shiftY = -pad+tripad
-
-
+              shiftX = tipbox_width/2.0-fontsize
+              shiftY = -pad+fontsize

           tipdiv.style("left", "#{posX}px")
                 .style("top", "#{posY}px")
@@ -92,7 +93,7 @@
                 .style("width", tipbox_width)
                 .style("height", tipbox_height)
                 .transition()
-                .duration(in_duration/1.2)
+                .duration(in_duration)
                 .style("opacity", 1.0))

     objects.on("mouseout.#{tipclass}", (d) ->
@@ -100,7 +101,7 @@
                   .duration(out_duration)
                   .style("opacity", 0)
               tridiv.transition()
-                  .duration(out_duration/1.2)
+                  .duration(out_duration)
                   .style("opacity", 0))

     tipdiv


Fix online_no_throttling_10_per_second calculation

--- src/time_estimates.coffee
+++ src/time_estimates.coffee
@@ -2,7 +2,7 @@
   estimate_attack_times: (guesses) ->
     crack_times_seconds =
       online_throttling_100_per_hour: guesses / (100 / 3600)
-      online_no_throttling_10_per_second: guesses / 1e2
+      online_no_throttling_10_per_second: guesses / 10
       offline_slow_hashing_1e4_per_second: guesses / 1e4
       offline_fast_hashing_1e10_per_second: guesses / 1e10


Cleaned up json-api a little

Cleaned up json-api a little

--- src/types/json-api.coffee
+++ src/types/json-api.coffee
@@ -36,7 +36,7 @@
   for p in path
     elem = elem[key]
     key = p
-    throw 'bad path' if typeof elem == 'undefined'
+    throw new Error 'bad path' if typeof elem == 'undefined'
   {elem, key}

 pathEquals = (p1, p2) ->
@@ -45,17 +45,17 @@
     return false if e != p2[i]
   true

-json['api'] =
-  'provides': {'json':true}
+json.api =
+  provides: {json:true}

-  'get': -> @snapshot
-  'at': (path...) -> new SubDoc this, depath path
+  get: -> @snapshot
+  at: (path...) -> new SubDoc this, depath path

-  'getAt': (path) ->
+  getAt: (path) ->
     {elem, key} = traverse @snapshot, path
     return elem[key]

-  'setAt': (path, value, cb) ->
+  setAt: (path, value, cb) ->
     {elem, key} = traverse @snapshot, path
     op = {p:path}
     if elem.constructor == Array
@@ -64,21 +64,21 @@
     else if typeof elem == 'object'
       op.oi = value
       op.od = elem[key] if typeof elem[key] != 'undefined'
-    else throw 'bad path'
+    else throw new Error 'bad path'
     @submitOp [op], cb

-  'removeAt': (path, cb) ->
+  removeAt: (path, cb) ->
     {elem, key} = traverse @snapshot, path
-    throw 'no element at that path' unless typeof elem[key] != 'undefined'
+    throw new Error 'no element at that path' unless typeof elem[key] != 'undefined'
     op = {p:path}
     if elem.constructor == Array
       op.ld = elem[key]
     else if typeof elem == 'object'
       op.od = elem[key]
-    else throw 'bad path'
+    else throw new Error 'bad path'
     @submitOp [op], cb

-  'insertAt': (path, pos, value, cb) ->
+  insertAt: (path, pos, value, cb) ->
     {elem, key} = traverse @snapshot, path
     op = {p:path.concat pos}
     if elem[key].constructor == Array
@@ -87,29 +87,29 @@
       op.si = value
     @submitOp [op], cb

-  'moveAt': (path, from, to, cb) ->
+  moveAt: (path, from, to, cb) ->
     op = [{p:path.concat(from), lm:to}]
     @submitOp op, cb

-  'addAt': (path, amount, cb) ->
+  addAt: (path, amount, cb) ->
     op = [{p:path, na:amount}]
     @submitOp op, cb

-  'deleteTextAt': (path, length, pos, cb) ->
+  deleteTextAt: (path, length, pos, cb) ->
     {elem, key} = traverse @snapshot, path
-    op = [{'p':path.concat(pos), 'sd':elem[key][pos...(pos + length)]}]
+    op = [{p:path.concat(pos), sd:elem[key][pos...(pos + length)]}]
     @submitOp op, cb

-  'addListener': (path, event, cb) ->
+  addListener: (path, event, cb) ->
     l = {path, event, cb}
     @_listeners.push l
     l
-  'removeListener': (l) ->
+  removeListener: (l) ->
     i = @_listeners.indexOf l
     return false if i < 0
     @_listeners.splice i, 1
     return true
-  '_register': ->
+  _register: ->
     @_listeners = []
     @on 'change', (op) ->
       for c in op
@@ -129,7 +129,7 @@
             # The op remained, so grab its new path into the listener.
             l.path = xformed[0].p
           else
-            throw "Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components."
+            throw new Error "Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components."
         to_remove.sort (a, b) -> b - a
         for i in to_remove
           @_listeners.splice i, 1
@@ -167,6 +167,6 @@
           else if (common = @type.commonPath match_path, path)?
             if event == 'child op'
               if match_path.length == path.length
-                throw "paths match length and have commonality, but aren't equal?"
+                throw new Error "paths match length and have commonality, but aren't equal?"
               child_path = c.p[common+1..]
               cb(child_path, c)


Skip document validation when the content is empty

--- src/zombie/history.coffee
+++ src/zombie/history.coffee
@@ -166,7 +166,8 @@
                   document.open()
                   document.write body
                   document.close()
-                  if document.documentElement
+
+                  if document.documentElement or response.headers["content-length"] == '0'
                     browser.emit "loaded", browser
                   else
                     error = "Could not parse document at #{URL.format(url)}"


re-enable fast client reconnect caching, i.e., don't destroy client state for a few minutes.

--- src/smc-hub/client.coffee
+++ src/smc-hub/client.coffee
@@ -173,17 +173,15 @@

         @conn.on "end", () =>
             dbg("connection: hub <--> client(id=#{@id}, address=#{@ip_address})  -- CLOSED")
-            @destroy()
             # CRITICAL -- of course we need to cancel all changefeeds when user disconnects,
             # even temporarily, since messages could be dropped otherwise. (The alternative is to
             # cache all messages in the hub, which has serious memory implications.)
-            #@query_cancel_all_changefeeds()
+            @query_cancel_all_changefeeds()
             # Actually destroy Client in a few minutes, unless user reconnects
             # to this session.  Often the user may have a temporary network drop,
             # and we keep everything waiting for them for short time
             # in case this happens.
-            #@_destroy_timer = setTimeout(@destroy, 1000*CLIENT_DESTROY_TIMER_S)
-            #
+            @_destroy_timer = setTimeout(@destroy, 1000*CLIENT_DESTROY_TIMER_S)

         dbg("connection: hub <--> client(id=#{@id}, address=#{@ip_address})  ESTABLISHED")


Avoid more allocations in transformContent

--- src/tokenized-line.coffee
+++ src/tokenized-line.coffee
@@ -8,7 +8,25 @@
 TrailingWhitespaceRegex = /\s*$/
 RepeatedSpaceRegex = /[ ]/g
 CommentScopeRegex = /(\b|\.)comment/
+TabCharCode = 9
+SpaceCharCode = 32
+SpaceString = ' '
+TabStringsByLength = {
+  1: ' '
+  2: '  '
+  3: '   '
+  4: '    '
+}
+
 idCounter = 1
+
+getTabString = (length) ->
+  TabStringsByLength[length] ?= buildTabString(length)
+
+buildTabString = (length) ->
+  string = SpaceString
+  string += SpaceString for i in [1...length] by 1
+  string

 module.exports =
 class TokenizedLine
@@ -53,18 +71,18 @@
       # advance to next token tag
       tokenIndex++ while @tags[tokenIndex] < 0

-      character = @text[bufferColumn]
+      charCode = @text.charCodeAt(bufferColumn)

       # split out unicode surrogate pairs
       if isPairedCharacter(@text, bufferColumn)
         prefix = tokenOffset
         suffix = @tags[tokenIndex] - tokenOffset - 2
-        splitTokens = []
-        splitTokens.push(prefix) if prefix > 0
-        splitTokens.push(2)
-        splitTokens.push(suffix) if suffix > 0
-
-        @tags.splice(tokenIndex, 1, splitTokens...)
+
+        i = tokenIndex
+        @tags.splice(i, 1)
+        @tags.splice(i++, 0, prefix) if prefix > 0
+        @tags.splice(i++, 0, 2)
+        @tags.splice(i, 0, suffix) if suffix > 0

         firstNonWhitespaceColumn ?= screenColumn
         lastNonWhitespaceColumn = screenColumn + 1
@@ -79,7 +97,7 @@
         tokenOffset = 0

       # split out leading soft tabs
-      else if character is ' '
+      else if charCode is SpaceCharCode
         if firstNonWhitespaceColumn?
           substringEnd += 1
         else
@@ -90,7 +108,8 @@
             @tags.splice(tokenIndex + 1, 0, suffix) if suffix > 0

           if @invisibles?.space
-            text += @text.substring(substringStart, substringEnd) if substringEnd > substringStart
+            if substringEnd > substringStart
+              text += @text.substring(substringStart, substringEnd)
             substringStart = substringEnd
             text += @invisibles.space
             substringStart += 1
@@ -102,28 +121,29 @@
         tokenOffset++

       # expand hard tabs to the next tab stop
-      else if character is '\t'
-        text += @text.substring(substringStart, substringEnd) if substringEnd > substringStart
+      else if charCode is TabCharCode
+        if substringEnd > substringStart
+          text += @text.substring(substringStart, substringEnd)
         substringStart = substringEnd

         tabLength = @tabLength - (screenColumn % @tabLength)
         if @invisibles?.tab
           text += @invisibles.tab
-        else
-          text += ' '
-        text += ' ' for i in [1...tabLength] by 1
+          text += getTabString(tabLength - 1) if tabLength > 1
+        else
+          text += getTabString(tabLength)

         substringStart += 1
         substringEnd += 1

         prefix = tokenOffset
         suffix = @tags[tokenIndex] - tokenOffset - 1
-        splitTokens = []
-        splitTokens.push(prefix) if prefix > 0
-        splitTokens.push(tabLength)
-        splitTokens.push(suffix) if suffix > 0
-
-        @tags.splice(tokenIndex, 1, splitTokens...)
+
+        i = tokenIndex
+        @tags.splice(i, 1)
+        @tags.splice(i++, 0, prefix) if prefix > 0
+        @tags.splice(i++, 0, tabLength)
+        @tags.splice(i, 0, suffix) if suffix > 0

         screenColumn += tabLength
         bufferColumn++


Fix assignment of last non-whitespace column for paired characters

--- src/tokenized-line.coffee
+++ src/tokenized-line.coffee
@@ -67,10 +67,10 @@
         @tags.splice(tokenIndex, 1, splitTokens...)

         firstNonWhitespaceColumn ?= screenColumn
-        lastNonWhitespaceColumn = screenColumn
+        lastNonWhitespaceColumn = screenColumn + 1

         text += @text.substr(bufferColumn, 2)
-        screenColumn++
+        screenColumn += 2
         bufferColumn += 2

         tokenIndex++ if prefix > 0


Improved command argument seperators

Improved command argument seperators

--- lib/atox-terminal.coffee
+++ lib/atox-terminal.coffee
@@ -25,7 +25,7 @@
     ]

   help: (cid) ->
-    @event.emit 'Terminal', {cid: cid, msg: 'Commands: (seperator: ", ")'}
+    @event.emit 'Terminal', {cid: cid, msg: 'Commands: (Strings are encased with "s)'}

     for i in @cmds
       @event.emit 'Terminal', {cid: cid, msg: "     \"/#{i.cmd}\":  #{i.desc}"}
@@ -46,7 +46,7 @@
     return unless cmd.indexOf('/') == 0
     cmd = cmd.replace('/', '')

-    args = cmd.split /,\s+/ #TODO: Change command seperator
+    args = @handleArgs cmd

     @err "Empty cmd" if args.length == 0
     return           if args.length == 0
@@ -67,6 +67,36 @@

     @err "Command /#{@cmd} not found"

+  handleArgs: (args) ->
+    args = args.split /\"/
+    if args.length > 1
+      for i in [0...args.length] by 1
+        a = -1
+        if i != 0 and i % 2 != 0
+          a = args[i-1].split /\s+/
+          a = a.concat(args[i].replace(/\"/, /''/).split(/\s+/).join(" "))
+        else if i == args.length-1
+          a = args[i].split /\s+/
+        if a != -1
+          if argsTemp?
+            argsTemp = argsTemp.concat(a)
+          else
+            argsTemp = a
+      i = 0
+      x = argsTemp.length
+      while i < x
+        if(argsTemp[i].length == 0)
+          argsTemp.splice(i, 1)
+          x-=1
+        else
+          i += 1
+      args = argsTemp
+    else
+      args = args.join()
+      args = args.split /\s+/
+
+    return args;
+
   err: (what) ->
     @event.emit 'notify', {
       type: 'err'


removes close element (that was hidden)

removes close element (that was hidden)

--- src/ui/views/about.coffee
+++ src/ui/views/about.coffee
@@ -35,11 +35,6 @@
                       versionToInt(releasedVersion) > versionToInt(localVersion)
     #
     div class: 'about', ->
-        div ->
-            span onclick: (e) ->
-                window.close()
-            , ->
-                span class: 'close-me material-icons', ''
         div ->
             img src: path.join __dirname, '..', '..', 'icons', 'icon@8.png'
         div class: 'name', ->


Corrected Incorrect Usage

Corrected Incorrect Usage

We were missing the outdated return from the package functions.

--- lib/deps/outdated.coffee
+++ lib/deps/outdated.coffee
@@ -30,7 +30,7 @@
   do iterOutdaters = ->
     outdater = outdaters.pop()
     if not outdater? then return callback null, packages.length > 0, packages
-    outdater dir, opts, (err, _packages) ->
+    outdater dir, opts, (err, outdated, _packages) ->
       packages = packages.concat _packages
       if err? then return callback err, packages.length > 0, packages
       iterOutdaters()


Bugfix: fix error when param is first

Bugfix: fix error when param is first

--- lib/documentation.coffee
+++ lib/documentation.coffee
@@ -48,6 +48,7 @@
           description: throws[1]

       else if param = /^@param\s+[\[\{](.+?)[\]}]\s+\[([^\]]+)](?:\s+(.+))?/i.exec line
+        @params ?= []
         if paramNameVal = /^([^ ]+)\s*=\s*([^ ]+)/.exec param[2]
           paramName = paramNameVal[1]
           defValue = paramNameVal[2]


Some code style fixes

Some code style fixes

--- src/vcs/Collection.coffee
+++ src/vcs/Collection.coffee
@@ -14,8 +14,7 @@
         for handler in @handlers
           do (handler) ->
             handler.setBufferedOutput(true)
-            outputPromise = Promise.settle(promises.concat()).then ->
-              handler.setBufferedOutput(false)
+            outputPromise = Promise.settle(promises.concat()).then -> handler.setBufferedOutput(false)
             promises.push(Promise.all([handler[methodName](args...), outputPromise]))
         Promise.settle(promises).then (results) ->
           # Keep result future rejected if any


Fix bug where HasProperties._immediate_references included 'this'

This was breaking test_document.coffee (_all_models tracking)

--- bokehjs/src/coffee/common/has_properties.coffee
+++ bokehjs/src/coffee/common/has_properties.coffee
@@ -397,6 +397,7 @@
   _immediate_references: () ->
     result = {}
     HasProperties._value_record_references(@, result, false) # false = no recurse
+    delete result[@id]
     _.values(result)

   attach_document: (doc) ->


remove spurious log

remove spurious log

--- bokehjs/src/coffee/renderer/glyph/circle.coffee
+++ bokehjs/src/coffee/renderer/glyph/circle.coffee
@@ -124,6 +124,8 @@

       return (x[4].i for x in @index.search([x0, y0, x1, y1]))

+    # circle does not inherit from marker (since it also accepts radius) so we
+    # must supply a draw_legend for it  here
     draw_legend: (ctx, x0, x1, y0, y1) ->
       reference_point = @get_reference_point() ? 0

@@ -136,8 +138,6 @@
       sy[reference_point] = (y0+y1)/2
       radius = { }
       radius[reference_point] = Math.min(Math.abs(x1-x0), Math.abs(y1-y0))*0.4
-
-      console.log indices, sx, sy, radius

       @_render(ctx, indices, @glyph_props, sx, sy, radius)


now the embedding system works properly

now the embedding system works properly

--- bokeh/server/static/coffee/embed_core.coffee
+++ bokeh/server/static/coffee/embed_core.coffee
@@ -38,7 +38,7 @@
       plot_collection = base.Collections(modeltype);
       model = plot_collection.get(modelid);
       view = new model.default_view({model : model});
-      view.render()
+      #view.render()
       _.delay(-> $(element).append(view.$el)))


fix scope/context issue breaking multiselect

--- bokehjs/src/coffee/widget/multiselect.coffee
+++ bokehjs/src/coffee/widget/multiselect.coffee
@@ -25,11 +25,11 @@
     @render_selection()
     return @

-  render_selection: () ->
+  render_selection: () =>
     values = {}
     _.map(@mget('value'), (x) -> values[x] = true)
-    @$('option').each((el) ->
-      el = $(el)
+    @$('option').each((el) =>
+      el = @$(el)
       if values[el.attr('value')]
         el.attr('selected', 'selected')
     )


#385: also migrate caches

#385: also migrate caches

--- chrome/content/zotero-better-bibtex/db.coffee
+++ chrome/content/zotero-better-bibtex/db.coffee
@@ -96,7 +96,10 @@
     @serialized.removeWhere({itemID})
     @keys.removeWhere((o) -> o.itemID == itemID && o.citekeyFormat)

+  cacheExpiry: 1000 * 60 * 60 * 24 * 30
+
   save: (all) ->
+    Zotero.BetterBibTeX.debug('DB.save:', {all})
     if all || @db.main.autosaveDirty()
       @db.main.removeCollection('metadata')
       metadata = @db.main.addCollection('metadata')
@@ -106,25 +109,26 @@
       @db.main.autosaveClearFlags()

     if all
-      cutoff = Date.now()
-      cutoff.setDate(now.getDate() - 30)
-      @cache.removeWhere((o) -> o.accessed < cutoff)
+      @cache.removeWhere((o) => o.accessed < @cacheExpiry)
       @db.volatile.save((err) -> throw(err) if (err))

   adapter:
     saveDatabase: (name, serialized, callback) ->
       file = Zotero.BetterBibTeX.createFile(name)
-      stream = FileUtils.openAtomicFileOutputStream(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE)
-      stream.write(serialized, serialized.length)
-      stream.close()
+
+      Zotero.File.putContents(file, serialized)
+
       callback(true)
+      Zotero.BetterBibTeX.debug('DB.saveDatabase:', {name, file: file.path})

     loadDatabase: (name, callback) ->
       file = Zotero.BetterBibTeX.createFile(name)
+      Zotero.BetterBibTeX.debug('DB.loadDatabase:', {name, file: file.path})
       if file.exists()
         callback(Zotero.File.getContents(file))
       else
         callback(null)
+      Zotero.BetterBibTeX.debug('DB.loadDatabase: done', {name, file: file.path})

   SQLite:
     parseTable: (name) ->
@@ -170,7 +174,27 @@
       db.remove(true) if db.exists()

       db = Zotero.BetterBibTeX.createFile('serialized-items.json')
-      db.remove(true) if db.exists()
+      if db.exists()
+        @serialized.removeDataOnly()
+        try
+          serialized = JSON.parse(Zotero.File.getContents(db))
+        catch
+          serialized = {}
+
+        if Array.isArray(serialized.data)
+          Zotero.BetterBibTeX.debug('DB.migrate: serialized')
+          migrated = 0
+          for item in serialized.data
+            migrated += 1
+            delete item.meta
+            delete item['$loki']
+            for attachment in item.attachments || []
+              delete attachment.meta
+              delete attachment['$loki']
+            @serialized.insert(item)
+          Zotero.BetterBibTeX.debug('DB.migrate: serialized=', migrated)
+
+        db.remove(true)

       db = Zotero.getZoteroDatabase('betterbibtex')
       return unless db.exists()
@@ -181,13 +205,16 @@

       # the context stuff was a mess
       if @tableExists('betterbibtex.autoexport') && !@table_info('betterbibtex.autoexport').context
+        Zotero.BetterBibTeX.debug('DB.migrate: autoexport')
         Zotero.BetterBibTeX.DB.autoexport.removeDataOnly()

         if @table_info('betterbibtex.autoexport').collection
           Zotero.DB.query("update betterbibtex.autoexport set collection = (select 'library:' || libraryID from groups where 'group:' || groupID = collection) where collection like 'group:%'")
           Zotero.DB.query("update betterbibtex.autoexport set collection = 'collection:' || collection where collection <> 'library' and collection not like '%:%'")

+        migrated = 0
         for row in Zotero.DB.query('select * from betterbibtex.autoexport')
+          migrated += 1
           Zotero.BetterBibTeX.DB.autoexport.insert({
             collection: row.collection
             path: row.path
@@ -198,27 +225,64 @@
             exportedRecursively: (row.exportedRecursively == 'true')
             status: 'pending'
           })
+        Zotero.BetterBibTeX.debug('DB.migrate: autoexport=', migrated)
+
+      if @tableExists('betterbibtex.cache')
+        Zotero.BetterBibTeX.debug('DB.migrate: cache')
+        Zotero.BetterBibTeX.DB.cache.removeDataOnly()
+
+        migrated = 0
+        for row in Zotero.DB.query('select * from betterbibtex.cache')
+          row.itemID = parseInt(row.itemID)
+          continue unless typeof row.itemID == 'number'
+
+          continue unless row.exportCharset
+          continue unless row.exportNotes in ['true', 'false']
+          continue unless row.getCollections in ['true', 'false']
+          continue unless row.translatroID
+          continue unless row.useJournalAbbreviation in ['true', 'false']
+          continue unless row.citekey
+          continue unless row.bibtex
+
+          migrated += 1
+          Zotero.BetterBibTeX.DB.cacheautoexport.insert({
+            itemID: row.itemID
+            exportCharset: row.exportCharset
+            exportNotes: (row.exportNotes == 'true')
+            getCollections: (row.getCollections == 'true')
+            translatroID: row.translatroID
+            useJournalAbbreviation: (row.useJournalAbbreviation == 'true')
+            citekey: row.citekey
+            bibtex: row.bibtex
+            accessed: Date.now()
+          })
+        Zotero.BetterBibTeX.debug('DB.migrate: cache=', migrated)

       if @tableExists('betterbibtex.keys')
+        Zotero.BetterBibTeX.debug('DB.migrate: keys')
         Zotero.BetterBibTeX.DB.keys.removeDataOnly()
         pinned = @table_info('betterbibtex.autoexport').pinned

+        migrated = 0
         for row in Zotero.DB.query('select k.*, i.libraryID from betterbibtex.keys k join items i on k.itemID = i.itemID')
-          if pinned
-            continue unless row.pinned == 1
-          else
-            continue if row.citekeyFormat
+          continue if pinned && row.pinned != 1
+          migrated += 1
+
+          row.citekeyFormat = null unless row.citekeyFormat

           Zotero.BetterBibTeX.DB.keys.insert({
             itemID: parseInt(row.itemID)
             citekey: row.citekey
-            citekeyFormat: null
+            citekeyFormat: row.citekeyFormat
             libraryID: row.libraryID
           })
+        Zotero.BetterBibTeX.debug('DB.migrate: keys=', migrated)

       Zotero.DB.query('DETACH betterbibtex')

       db.moveTo(null, 'betterbibtex.sqlite.bak')
+
+      Zotero.BetterBibTeX.DB.save(true)

       Zotero.BetterBibTeX.flash('Better BibTeX: database updated', 'Database update finished')
       Zotero.BetterBibTeX.flash('Better BibTeX: cache has been reset', 'Cache has been reset due to a version upgrade. First exports after upgrade will be slower than usual')


:green_heart: Round left position to the nearest integer

--- spec/autocomplete-manager-integration-spec.coffee
+++ spec/autocomplete-manager-integration-spec.coffee
@@ -1945,4 +1945,4 @@
     left = editorView.pixelPositionForBufferPosition(bufferPosition).left
     left += editorView.offsetLeft
     left = gutterWidth + left if requiresGutter()
-    "#{left}px"
+    "#{Math.round(left)}px"


Troubleshoot: forceComplete is added for defensive checks

--- client/Main/troubleshoot/healthchecker.coffee
+++ client/Main/troubleshoot/healthchecker.coffee
@@ -14,6 +14,7 @@
     @startTime = Date.now()
     @setPingTimeout()
     @cb @finish.bind(this)
+    return @forceComplete "undefined callback"  unless troubleshoot

   setPingTimeout: ->
     @pingTimeout = setTimeout =>
@@ -38,4 +39,9 @@
     @startTime = null

   getResponseTime: ->
-    @finishTime - @startTime+    @finishTime - @startTime
+
+  forceComplete: (err) ->
+    warn err  if err
+    @status = "fail"
+    @emit "healthCheckCompleted"


styleguide: Pass args to `super`

styleguide: Pass args to `super`

--- client/app/lib/kite/klienteventmanager.coffee
+++ client/app/lib/kite/klienteventmanager.coffee
@@ -15,7 +15,7 @@
   ###
   constructor: (options = {}, machine) ->

-    super
+    super options, machine

     unless machine
       throw new Error kd.err "KlientEventManager requires a Machine"


adding code sample for #wrap

adding code sample for #wrap

--- src/array_extensions.coffee
+++ src/array_extensions.coffee
@@ -478,7 +478,7 @@

 @param {Function} iterator
 @param {Object} [context] Optional context parameter to be
-used as `this` when calling the iterator function.
+used as this when calling the iterator function.

 @type Array
 @returns An array in the form of [trueCollection, falseCollection]


Refactored income calculator coffee script

* Remove un-needed function

--- app/assets/javascripts/income_calculator.coffee
+++ app/assets/javascripts/income_calculator.coffee
@@ -15,7 +15,7 @@
   $income = $('#income').val()
   $low_threshold = $min_val + $children_val + $add_single_supp
   $high_threshold = $low_threshold + Math.min(4000, $curr_fee * 2)
-  if fullRemittance()
+  if $income < $low_threshold
     $('#fee-payable').text '£0'
     $('#fee-remit').text formatCurrency($curr_fee)
   else if $income > $high_threshold
@@ -34,9 +34,6 @@
   $('#clear_btn').show()
   $('#r2_calculator :input').attr 'disabled', true
   return true
-
-fullRemittance = ->
-  return ($income < $low_threshold)

 formatCurrency = (val) ->
   '£' + val.toFixed(2)


Introduces new window function jquery_ui_tabs_initialization.

--- app/assets/javascripts/voluntary/base.js.coffee
+++ app/assets/javascripts/voluntary/base.js.coffee
@@ -5,33 +5,37 @@

   $( '.accordions' ).each (k, v) ->
     $(v).accordion({ autoHeight: false });
-
-  $('.tabs').each (k, v) ->
-    $(v).tabs
-      autoHeight: false
-      beforeActivate: (event, ui) ->
-        unless ui.newTab.find('a').attr('href').indexOf('#') == 0
-          ui.newTab.find('.ajax_spinner').show()
-          ui.newPanel.empty()
-
-      beforeLoad: (event, ui) ->
-        ui.jqXHR.error ->
+
+  window.jquery_ui_tabs_initialization = ->
+    $('.tabs').each (k, v) ->
+      $(v).tabs
+        autoHeight: false
+        beforeActivate: (event, ui) ->
+          unless ui.newTab.find('a').attr('href').indexOf('#') == 0
+            ui.newTab.find('.ajax_spinner').show()
+            ui.newPanel.empty()
+
+        beforeLoad: (event, ui) ->
+          ui.jqXHR.error ->
+            unless ui.tab.find('a').attr('href').indexOf('#') == 0
+              ui.tab.find('.ajax_spinner').hide()
+
+            json = null
+
+            try
+              json = jQuery.parseJSON(ui.jqXHR.responseText)
+            catch e
+
+            error = if json && json['error'] then json['error'] else 'Something went wrong'
+
+            ui.panel.html error
+
+        load: (event, ui) ->
           unless ui.tab.find('a').attr('href').indexOf('#') == 0
             ui.tab.find('.ajax_spinner').hide()
-
-          json = null
-
-          try
-            json = jQuery.parseJSON(ui.jqXHR.responseText)
-          catch e
-
-          error = if json && json['error'] then json['error'] else 'Something went wrong'
-
-          ui.panel.html error
-
-      load: (event, ui) ->
-        unless ui.tab.find('a').attr('href').indexOf('#') == 0
-          ui.tab.find('.ajax_spinner').hide()
+            window.jquery_ui_tabs_initialization()
+
+  window.jquery_ui_tabs_initialization()

   $(document).on "click", ".ui-tabs-panel .pagination a", (event) ->
     event.preventDefault()


activityticker: getCurrentGroup on ready

--- client/Social/Activity/ContentDisplays/ActivityTicker.coffee
+++ client/Social/Activity/ContentDisplays/ActivityTicker.coffee
@@ -137,8 +137,9 @@
     {constructorName, id} = member
     KD.remote.cacheable constructorName, id, (err, account)=>
       return console.error "account is not found", err if err or not account
-      source = KD.getSingleton("groupsController").getCurrentGroup()
-      @addNewItem {as: "member", target: account, source  }
+      KD.getSingleton("groupsController").ready =>
+        source = KD.getSingleton("groupsController").getCurrentGroup()
+        @addNewItem {as: "member", target: account, source  }

   checkGuestUser: (account) ->
     if account.profile and accountNickname = account.profile.nickname


ace: added Delphi highlighting to code snips and stuff

--- client/app/Applications/Ace.kdapplication/acesettings.coffee
+++ client/app/Applications/Ace.kdapplication/acesettings.coffee
@@ -109,6 +109,7 @@
     html        : ["HTML"         , "htm|html|xhtml"]
     c_cpp       : ["C/C++"        , "c|cc|cpp|cxx|h|hh|hpp"]
     clojure     : ["Clojure"      , "clj"]
+    delphi      : ["Delphi"       , "delphi"]
     jade        : ["Jade"         , "jade"]
     java        : ["Java"         , "java"]
     javascript  : ["JavaScript"   , "js"]


Use capitalized nicknames for guests as firstName and lastName

--- client/app/Applications/Members.kdapplication/AppView.coffee
+++ client/app/Applications/Members.kdapplication/AppView.coffee
@@ -114,8 +114,13 @@
     super options, data

   createCommons:(account)->
+
+    if account.type is 'unregistered'
+    then name = account.profile.nickname.capitalize()
+    else name = "#{account.profile.firstName} #{account.profile.lastName}"
+
     contentDisplayController = KD.getSingleton "contentDisplayController"
-    headerTitle              = "Activities which #{account.profile.firstName} #{account.profile.lastName} liked"
+    headerTitle              = "Activities which #{name} liked"

     @addSubView header = new HeaderViewSection
       type  : "big"
@@ -144,10 +149,16 @@
     super options, data

   createCommons:(account, filter)->
-    headerTitle = if filter is "following" then "Members who #{account.profile.firstName} #{account.profile.lastName} follows" else "Members who follow #{account.profile.firstName} #{account.profile.lastName}"
-    @addSubView header = new HeaderViewSection
-      type  : "big"
-      title : headerTitle
+
+    if account.type is 'unregistered'
+    then name = account.profile.nickname.capitalize()
+    else name = "#{account.profile.firstName} #{account.profile.lastName}"
+
+    if filter is "following"
+    then title = "Members who #{name} follows"
+    else title = "Members who follow #{name}"
+
+    @addSubView header = new HeaderViewSection {type : "big", title}

     @addSubView subHeader = new KDCustomHTMLView
       tagName  : "h2"


follow/unfollow state is not decorated when state is passed via argument

--- client/app/Applications/Members.kdapplication/AppView.coffee
+++ client/app/Applications/Members.kdapplication/AppView.coffee
@@ -203,17 +203,22 @@

     super options, data

-    unless @getData().followee?
+    decorator = (following)=>
+      if @getData().followee
+        @setClass 'following-btn'
+        @setState "Unfollow"
+      else
+        @setState "Follow"
+        @unsetClass 'following-btn'
+
+    if @getData().followee?
+      decorator @getData().followee
+    else
       KD.whoami().isFollowing? @getData().getId(), "JAccount", \
         (err, following) =>
           @getData().followee = following
           warn err  if err and KD.isLoggedIn()
-          if @getData().followee
-            @setClass 'following-btn'
-            @setState "Unfollow"
-          else
-            @setState "Follow"
-            @unsetClass 'following-btn'
+          decorator following

   decorateState:(name)->


only load ace editor once

only load ace editor once

somehow, ace's onload will be triggered twice...

--- app/assets/javascripts/app-ng.coffee
+++ app/assets/javascripts/app-ng.coffee
@@ -23,7 +23,7 @@
     # ace editor
     # ------------------------------------------------------------------------------------------------------------------
     vm.ace_editor = ''
-    vm.scroll_synced = false
+    vm.ace_editor_loaded = false

     # ------------------------------------------------------------------------------------------------------------------
     # markdown help functions
@@ -42,20 +42,22 @@

     $scope.aceLoaded = (editor) =>
         $html_div = $('#md_html_div')
-        if not vm.scroll_synced
+        vm.ace_editor = editor
+        if not vm.ace_editor_loaded
+            # sync scroll
             scrollSyncor.syncScroll(editor, $html_div)
-            vm.scroll_synced = true
-        vm.ace_editor = editor
-        vm.refresh_all_notes()
-        # set ace base path
-        ace.config.set('basePath', '/javascripts/ace')
-        vm.ace_editor.setTheme('ace/theme/tomorrow_night_eighties')
-        # make effect settings
-        vm.set_keyboard_handler(vm.current_keyboard_handler)
-        vm.set_show_gutter(vm.current_show_gutter)
-        vm.set_ace_theme(vm.current_ace_theme)
-        # reset app status
-        vm.reset_status()
+            vm.ace_editor_loaded = true
+            # get note list
+            vm.refresh_all_notes()
+            # set ace base path
+            ace.config.set('basePath', '/javascripts/ace')
+            vm.ace_editor.setTheme('ace/theme/tomorrow_night_eighties')
+            # make effect settings
+            vm.set_keyboard_handler(vm.current_keyboard_handler)
+            vm.set_show_gutter(vm.current_show_gutter)
+            vm.set_ace_theme(vm.current_ace_theme)
+            # reset app status
+            vm.reset_status()

     $scope.aceChanged = (e) ->
         $html_div = $('#md_html_div')


Do not require function.js twice.

Do not require function.js twice.

--- app/assets/javascripts/app.js.coffee
+++ app/assets/javascripts/app.js.coffee
@@ -4,7 +4,6 @@
 #= require i18n.js
 #= require bootstrap
 #= require bootstrap-select
-#= require function
 #= require custom-bootstrap.js.coffee
 #= require function
 #= require handlebars


Search integer fields now possible!

Search integer fields now possible!

--- reactive-table.coffee
+++ reactive-table.coffee
@@ -60,6 +60,9 @@
           # it should be parsed successfully by JSON.parse
           match.forEach (el) ->
             kv_pair = "{\"#{el}\"}".split(':').join '":"'
+            # Remove double quotes if number is detected
+            # http://stackoverflow.com/questions/5834901/jquery-automatic-type-conversion-during-parse-json
+            kv_pair.replace(/"(-?\d)/g, "$1").replace(/(\d)"/g, "$1")
             _.extend query, JSON.parse kv_pair
         else
           # By default search is case insensitive


Use of instead of free in softWrap by default

--- client/landing/app/Applications/Ace.kdapplication/ace.coffee
+++ client/landing/app/Applications/Ace.kdapplication/ace.coffee
@@ -40,7 +40,7 @@
     @setShowPrintMargin     @appStorage.getValue 'showPrintMargin',     no
     @setHighlightActiveLine @appStorage.getValue 'highlightActiveLine', yes
     @setShowInvisibles      @appStorage.getValue 'showInvisibles',      no
-    @setSoftWrap            @appStorage.getValue 'softWrap',            'free'
+    @setSoftWrap            @appStorage.getValue 'softWrap',            'off'
     @setFontSize            @appStorage.getValue 'fontSize',            10
     @setTabSize             @appStorage.getValue 'tabSize',             4
     @setEditorListeners()


Hackathon: videos re-enabled

Hackathon: videos re-enabled

--- client/landing/site.hackathon/coffee/home/AppViewFull.coffee
+++ client/landing/site.hackathon/coffee/home/AppViewFull.coffee
@@ -4,9 +4,9 @@
 TWEET_TEXT       = 'I\'ve applied for the world\'s first global virtual #hackathon by @koding. Join my team!'
 SHARE_URL        = 'http://koding.com/Hackathon'

-VIDEO_URL        = ''#https://koding-cdn.s3.amazonaws.com/campaign/hackathon/intro-bg.webm'
-VIDEO_URL_MP4    = ''#https://koding-cdn.s3.amazonaws.com/campaign/hackathon/intro-bg.mp4'
-VIDEO_URL_OGG    = ''#https://koding-cdn.s3.amazonaws.com/campaign/hackathon/intro-bg.ogv'
+VIDEO_URL        = 'https://koding-cdn.s3.amazonaws.com/campaign/hackathon/intro-bg.webm'
+VIDEO_URL_MP4    = 'https://koding-cdn.s3.amazonaws.com/campaign/hackathon/intro-bg.mp4'
+VIDEO_URL_OGG    = 'https://koding-cdn.s3.amazonaws.com/campaign/hackathon/intro-bg.ogv'

 DAYS             = ['SUNDAY','MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY']
 MONTHS           = ['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER']


bug fix on widgets

bug fix on widgets

--- app/assets/javascripts/dom.js.coffee
+++ app/assets/javascripts/dom.js.coffee
@@ -3,7 +3,7 @@

   events:
     'mouseover .product-widget'     : 'widgetMouseover'
-    'mouseout .product-widget'      : 'widgetMousetout'
+    'mouseout .product-widget'      : 'widgetMouseout'
     'ajax:complete'                 : 'ajaxComplete'
     'click .popup-close-button'     : 'closePopup'


added turkcell

added turkcell

--- client/landing/site.hackathon/coffee/home/AppViewFull.coffee
+++ client/landing/site.hackathon/coffee/home/AppViewFull.coffee
@@ -443,6 +443,12 @@

The top-performing team that has an oDesk.com or Elance.com account will receive an extra $2,500.
               Be sure to add your oDesk or Elance userID to the questionnaire that we send you.

+

+

+

+

+
+
Turkcell is contributing $5,000 to the total prize money!


Make sure order contains numbers

Make sure order contains numbers

Fixes #3094

--- app/assets/volume/spreadsheet.coffee
+++ app/assets/volume/spreadsheet.coffee
@@ -123,7 +123,7 @@
         slots = (container for containerId, container of volume.containers when top != !container.top) # [Row] = Slot
         ### jshint ignore:end ###

-        order = Object.keys(slots)  # Permutation Array of Row in display order
+        order = [0..slots.length-1] # Permutation Array of Row in display order

         records = {}                # [Category_id][Metric_id][Count] :: Data
         counts = new Array(slots.length) # [Row][Category_id] :: Count


minor hittype change bug

minor hittype change bug

--- admin/mturkAdmin.coffee
+++ admin/mturkAdmin.coffee
@@ -91,9 +91,15 @@

   "submit .-ts-change-hittype": (e, tmpl) ->
     e.preventDefault()
+    htId = tmpl.find("select[name=hittype]").value
+    HITTypeId = HITTypes.findOne(htId).HITTypeId
+    unless HITTypeId
+      bootbox.alert("Register that HIT Type first")
+      return
+
     params =
       HITId: @HITId
-      HITTypeId: tmpl.find("select[name=hittype]").value
+      HITTypeId: HITTypeId
     Meteor.call "ts-admin-change-hittype", params, (err, res) ->
       bootbox.alert(err.reason) if err


update fileutil

update fileutil

--- components/backend/utilities/fileutils.coffee
+++ components/backend/utilities/fileutils.coffee
@@ -17,20 +17,30 @@
       title = title.replace /\.(?=[^.]*$)/, "_"+Date.now()+"_copy."
     return title

+  prependFilename: (link, text)->
+    # replace last dot in filename
+    link.replace /\.(?=[^.]*$)/, '_'+text+'.'
+
+  isImage: (file)-> file.getFieldValue("type").split("/")[0] is "image"
+
   deleteFile: (file, done)->
-    fs.unlinkSync dir+file.getFieldValue("title")
-    if file.getFieldValue("type").split("/")[0] is "image"
-      fs.unlinkSync dir+file.getFieldValue(type) for type in types
+    return if file?
+    if fs.exists dir.file.getFieldValue("title")
+      fs.unlinkSync dir+file.getFieldValue("title")
+    if @isImage file
+      for type in types
+        if fs.exists dir+file.getFieldValue(type)
+          fs.unlinkSync dir+file.getFieldValue(type)

   createImages: (file, done)->
+    that = @
     filename = file.getFieldValue "title"
     quality = parseInt setting.getFieldValue('quality')
     image = gm(dir+filename).size (err, size) ->
       portrait = if size.width < size.height then true else false
       async.each types, (type, cb)->
           maxSize = setting.getFieldValue type
-          # replace last dot in filename
-          targetName = filename.replace /\.(?=[^.]*$)/, '_'+type+'.'
+          targetName = that.prependFilename filename, type
           file.setFieldValue type, targetName
           image.quality quality
           if portrait then image.resize null, maxSize
@@ -39,20 +49,37 @@
       , ->
         file.save done

-  copyImage: (file, done)->

-  updateFile: (file, done)->
+  updateFile: (file, done)=>
+    title = file.getFieldValue "link"
     link = file.getFieldValue "link"
     if title != link
       safe = @safeFilename link
       file.setFieldValue title:safe
       @moveFile file, done

+  copyFile: (file, done)->
+    oldLink = file.getFieldValue "link"
+    newLink = @prependFilename oldLink, 'child'
+    fs.writeFileSync dir+newLink, fs.readFileSync(dir+oldLink)
+    if @isImage file
+      for type in types
+        oldLink = file.getFieldValue type
+        newLink = @prependFilename oldLink, 'child'
+        fs.writeFileSync dir+newLink, fs.readFileSync(dir+oldLink)
+    done()
+
+
+  # improveable
   moveFile: (file, done)->
-    oldLink = file.getFieldValue type
-    newLink = file.getFieldValue('title')
+    oldLink = file.getFieldValue "link"
+    newLink = @prependFilename oldLink, 'child'
     fs.renameSync dir+oldLink, dir+newLink
-    file.setFieldValue type, newLink
+    if @isImage file
+      for type in types
+        oldLink = file.getFieldValue type
+        newLink = @prependFilename oldLink, 'child'
+        fs.renameSync dir+oldLink, dir+newLink
     done()

   cropImage: (file, done)->


Try to approximate user location at the start of the flow

--- components/inquiry_questionnaire/index.coffee
+++ components/inquiry_questionnaire/index.coffee
@@ -17,6 +17,9 @@
   modal.view.$el.off 'click', '.js-modalize-backdrop'

   modal.load (done) ->
+    # Try to get a location incase one doesn't exist,
+    # don't wait for it though
+    user.approximateLocation()
     user.fetch
       success: ->
         if user.id?
@@ -26,9 +29,6 @@
         else
           # Create an anonymous session before continuing
           user.save().then done
-      error: ->
-        console.log 'Nothing really works, does it?'
-        done()

   questionnaire.state.on 'abort', ->
     modal.close()


Remove --style flag from yapf

Remove --style flag from yapf

The --style flag accepts different types of input. From their documentation - "accepts one of the predefined styles (e.g., pep8 or google), a path to a configuration file that specifies the desired style, or a dictionary of key/value pairs."

By not explicitly specifying the --style flag, yapf will look for the following, in that order

1. In the [style] section of a .style.yapf file in either the current directory or one of its parent directories.
2. In the [yapf] secionf of a setup.cfg file in either the current directory or one of its parent directories.
3. In the ~/.config/yapf/style file in your home directory.

--- src/beautifiers/yapf.coffee
+++ src/beautifiers/yapf.coffee
@@ -16,7 +16,6 @@
   beautify: (text, language, options) ->
     @run("yapf", [
       "-i"
-      ["--style=pep8"]
       tempFile = @tempFile("input", text)
       ], help: {
         link: "https://github.com/google/yapf"


support for geth versions that return multiple contracts in compiled object; maintain compatibility with older geth versions

--- tasks/deploy.coffee
+++ tasks/deploy.coffee
@@ -25,16 +25,31 @@
       source = grunt.file.read(contractFile)

       grunt.log.writeln("deploying #{contractFile}")
-      contract = web3.eth.compile.solidity(source)
-      contractAddress = web3.eth.sendTransaction({from: primaryAddress, data: contract.code})
-      grunt.log.writeln("deployed at #{contractAddress}")
+      compiled_contracts = web3.eth.compile.solidity(source)

-      abi = JSON.stringify(contract.info.abiDefinition)
-      className = source.match(/contract (\w+)(?=\s[is|{])/g)[0].replace("contract ","")
+      #TODO: refactor this into a common method
+      if compiled_contracts.info is undefined
+        for className, contract of compiled_contracts
+          contractAddress = web3.eth.sendTransaction({from: primaryAddress, data: contract.code})
+          grunt.log.writeln("deployed #{className} at #{contractAddress}")

-      result += "var #{className}Abi = #{abi};"
-      result += "var #{className}Contract = web3.eth.contract(#{className}Abi);"
-      result += "var #{className} = new #{className}Contract('#{contractAddress}');";
+          abi = JSON.stringify(contract.info.abiDefinition)
+
+          result += "var #{className}Abi = #{abi};"
+          result += "var #{className}Contract = web3.eth.contract(#{className}Abi);"
+          result += "var #{className} = new #{className}Contract('#{contractAddress}');";
+      else
+        #for geth < 0.9.23
+        contract = compiled_contracts
+        contractAddress = web3.eth.sendTransaction({from: primaryAddress, data: contract.code})
+        grunt.log.writeln("deployed at #{contractAddress}")
+
+        abi = JSON.stringify(contract.info.abiDefinition)
+        className = source.match(/contract (\w+)(?=\s[is|{])/g)[0].replace("contract ","")
+
+        result += "var #{className}Abi = #{abi};"
+        result += "var #{className}Contract = web3.eth.contract(#{className}Abi);"
+        result += "var #{className} = new #{className}Contract('#{contractAddress}');";

     destFile = grunt.config.get("deploy.dest")
     grunt.file.write(destFile, result)


port of scannable cite translator

port of scannable cite translator

--- chrome/content/zotero-better-bibtex/cayw.coffee
+++ chrome/content/zotero-better-bibtex/cayw.coffee
@@ -85,7 +85,7 @@
     else
       formatted = []
       for citation in citations
-        fromatted.push(@config.keyprefix + citation.citekey + @config.keypostfix)
+        formatted.push(@config.keyprefix + citation.citekey + @config.keypostfix)
       if formatted.length == 0
         formatted = ''
       else
@@ -146,6 +146,22 @@
       volume: "vol."
     }

+    class Mem
+      constructor: (@isLegal) ->
+        @lst = []
+
+      set: (str, punc, slug) ->
+        punc = '' unless punc
+        switch
+          when str        then @lst.push(str + punc)
+          when !@isLegal  then @lst.push(slug)
+
+      setlaw: (str, punc) ->
+        punc = '' unless punc
+        @lst.push(str + punc) if str && @isLegal
+
+      get: -> @lst.join(' ')
+
     formatted = []
     for citation in citations
       item = Zotero.Items.get(citation.id)
@@ -159,17 +175,32 @@
       citation.prefix ?= ''
       citation.suffix ?= ''

-      label = item.firstCreator
-      label ||= item.getField('shortTitle')
-      label ||= item.getField('title')
-
-      date = Zotero.Date.strToDate(item.getField('date')).year
-      date ||= item.getField('date')
-      date ||= 'no date'
-
-      label = "#{label} #{date}".trim()
-
+      title = new Mem(isLegal)
+      title.set(item.firstCreator, ',', 'anon.')
+
+      includeTitle = false
+      try # Prefs.get throws an error if the pref is not found
+        includeTitle = Zotero.Prefs.get('translators.ODFScan.includeTitle')
+      if includeTitle || !item.firstCreator
+        title.set(item.getField('shortTitle') || item.getField('title'), ',', '(no title)')
+
+      try
+        title.setlaw(item.getField('authority'), ',')
+      try
+        title.setlaw(item.getField('volume'))
+      try
+        title.setlaw(item.getField('reporter'))
+      title.setlaw(item.getField('pages'))
+
+      year = new Mem(isLegal)
+      try
+        year.setlaw(item.getField('court'), ',')
+      date = Zotero.Date.strToDate(item.getField('date'))
+      year.set((if date.year then date.year else item.getField('date')), '', 'no date')
+
+      label = (title.get() + ' ' + year.get()).trim()
       label = "-#{label}" if citation['suppress-author']
+
       formatted.push("{#{citation.prefix}|#{label}|#{locator}|#{citation.suffix}|#{id}}")
     return formatted.join('')
 }


Removed explicit trigger('change') in Range1d.reset

--- bokehjs/src/coffee/models/ranges/range1d.coffee
+++ bokehjs/src/coffee/models/ranges/range1d.coffee
@@ -43,4 +43,3 @@
     # start/end change events not silenced to trigger callbacks
     @setv({start: @_initial_start, end: @_initial_end})
     @_set_auto_bounds()
-    @trigger('change')


ask data properly when radius units are screen for circles

--- bokehjs/src/coffee/renderer/glyph/circle.coffee
+++ bokehjs/src/coffee/renderer/glyph/circle.coffee
@@ -16,6 +16,7 @@
         @sradius = @sdist(@renderer["#{rd}mapper"], @[rd], @radius)
       else
         @sradius = @radius
+        @max_size = 2 * @max_radius
     else
       @sradius = (s/2 for s in @size)

@@ -24,7 +25,7 @@
     vr = @renderer.plot_view.frame.get('v_range')

     # check for radius first
-    if @radius?
+    if @radius? and @distances.radius.units == "data"
       sx0 = hr.get('start')
       sx1 = hr.get('end')
       [x0, x1] = @renderer.xmapper.v_map_from_target([sx0, sx1], true)
@@ -45,6 +46,10 @@
       sy0 = vr.get('start') - @max_size
       sy1 = vr.get('end') + @max_size
       [y0, y1] = @renderer.ymapper.v_map_from_target([sy0, sy1], true)
+
+    # rbush expects x0, y0 to be min, x1, y1 max
+    if x0 > x1 then [x0, x1] = [x1, x0]
+    if y0 > y1 then [y0, y1] = [y1, y0]

     return (x[4].i for x in @index.search([x0, y0, x1, y1]))


Support external CSS

Support external CSS

--- src/scripts/pdf_a4_portrait.coffee
+++ src/scripts/pdf_a4_portrait.coffee
@@ -46,7 +46,8 @@
 # Set up content
 # --------------
 content = page.evaluate ->
-  styles = document.querySelector('head style')?.outerHTML || ''
+  styles = document.querySelectorAll('link,style')
+  styles = Array::reduce.call(styles, ((string, node) -> string+node.outerHTML),'')
   if $header = document.getElementById('pageHeader')
     header = $header.outerHTML
     $header.parentNode.removeChild($header)


add setter for innerHTML using html/parser update the childIndex on insertBefore and removeChild toString on DOM stuff defaults to pretty=false, deep=true

--- dom.coffee
+++ dom.coffee
@@ -1,3 +1,5 @@
+htmlparse = require("./html/parser").parse
+
 NotSupported = () ->
 	throw Error "NOT_SUPPORTED"

@@ -208,6 +210,9 @@
 				newNode._private.childIndex = i
 				newNode._private.parentNode = @
 				refNode._private.childIndex = i + 1
+				i = 0
+				for c in @childNodes
+					c._private.childIndex = i++
 	appendChild: (node) ->
 		if node.nodeType is Node.DOCUMENT_FRAGMENT_NODE
 			# could be optimized to do a single splice
@@ -223,6 +228,9 @@
 			node._private.parentNode = null
 			node._private.childIndex = -1
 			@childNodes.splice(i, 1)
+			i = 0
+			for c in @childNodes
+				c._private.childIndex = i++
 		else
 			throw Error "Cannot removeChild a non-child."
 	replaceChild: (newNode, oldNode) ->
@@ -240,7 +248,7 @@
 				newNode._private.childIndex = i
 				oldNode.parentNode = null
 				@childNodes.splice(i, 1, newNode)
-	toString: (pretty=false,deep=false,indentLevel=0) ->
+	toString: (pretty=false,deep=true,indentLevel=0) ->
 		if pretty
 			indent = repeat("  ", indentLevel)
 			newline = "\n"
@@ -544,6 +552,13 @@
 			for c in @childNodes
 				h.push c.toString()
 			return h.join('')
+		@__defineSetter__ 'innerHTML', (v) =>
+			fragment = htmlparse(v, @ownerDocument)
+			for c in @childNodes
+				c._private.parentNode = null
+				c._private.childIndex = -1
+			@childNodes.length = 0
+			@appendChild fragment
 		@__defineGetter__ 'innerText', () =>
 			t = []
 			for c in @childNodes
@@ -605,7 +620,7 @@
 	# focus: NotSupported
 	# blur: NotSupported
 	# render
-	toString: (pretty=false, deep=false, indentLevel = 0) ->
+	toString: (pretty=false, deep=true, indentLevel = 0) ->
 		try
 			name = @nodeName.toLowerCase()
 		catch err
@@ -659,7 +674,7 @@
 		super a...
 		@__defineSetter__ 'parentNode', (v) =>
 			throw Error "DocumentFragment cannot have a parentNode"
-	toString: (pretty=false, deep=false) ->
+	toString: (pretty=false, deep=true) ->
 		ret = []
 		r = 0
 		for c in @childNodes


Hides all inactive students

Hides all inactive students

--- src/components/course-settings/period-roster.cjsx
+++ src/components/course-settings/period-roster.cjsx
@@ -25,6 +25,7 @@

   render: ->
     students = _.sortBy(RosterStore.getStudentsForPeriod(@props.courseId, @props.period.id), 'last_name')
+    students = _.filter(students, (student) -> student.active)

Period: {@props.period.name}

       <BS.Table striped bordered condensed hover className="roster">


renamed @createNestedMap to @createMapStructure, misc cleanup

--- lib/ObjectTemplate.coffee
+++ lib/ObjectTemplate.coffee
@@ -28,13 +28,13 @@
       # @updateContext handles the context type automatically
       key = if @config.arrayToMap then @chooseKey(element) else index
       # don't call @processMap because it can lead to double nesting if @config.nestTemplate is true
-      value = @createNestedMap(element)
+      value = @createMapStructure(element)
       @updateContext context, element, value, key
     context

   processMap: (node) =>

-    context = @createNestedMap node
+    context = @createMapStructure node

     if @config.nestTemplate and (nested_key = @chooseKey(node))
       nested_context = {}
@@ -43,7 +43,7 @@

     context

-  createNestedMap: (node) =>
+  createMapStructure: (node) =>

     context = {}

@@ -53,19 +53,21 @@
     for key, value of node when @config.processable node, value, key
       # call @getNode() to register the use of the property on that node
       nested = @getNode(node, key)
-      value = @chooseValue nested, {}
+      value = @chooseValue nested
       @updateContext context, nested, value, key
     context

   chooseKey: (node) =>
     result = @config.getKey node
+
     switch result.name
       when 'value'    then result.value
       when 'path'     then @getNode node, result.value
       else null

-  chooseValue: (node, context) =>
+  chooseValue: (node, context = {}) =>
     result = @config.getValue node
+
     switch result.name
       when 'value'    then result.value
       when 'path'     then @getNode node, result.value
@@ -161,16 +163,5 @@
     paths.push(path) if path and paths.indexOf(path) == -1
     paths

-  templates: (name, config) =>
-    if !@templateCache
-      @templateCache = if @parent then @parent.templateCache else {}
-    if name and !config
-      @templateCache[name] or null
-    else if name and config
-      @templateCache[name] = @processConfig config
-      @templateCache
-    else
-      @templateCache
-
 # register module
 module.exports = ObjectTemplate


Fix coffeescript error formatting.

Fix coffeescript error formatting.

--- src/compilers/coffee.coffee
+++ src/compilers/coffee.coffee
@@ -2,6 +2,10 @@

 formatErrorMessage = (source, filename, err) ->
   {first_line, last_line, first_column, last_column} = err.location
+
+  # last_line no longer exists randomly
+  unless last_line?
+    last_line = first_line

   # get lines from source
   lines = source.split '\n'
@@ -11,6 +15,7 @@
   lines[first_line] = (l.substring 0, first_column) + '\x1B[91m' + l.substring first_column

   # insert end
+
   l = lines[last_line]

   if first_line == last_line


Don't be making LadderSubmissionViews in the CastButtonView unless it's a mirror match.

--- app/views/play/level/tome/CastButtonView.coffee
+++ app/views/play/level/tome/CastButtonView.coffee
@@ -164,6 +164,7 @@
   updateLadderSubmissionViews: ->
     @removeSubView subview for key, subview of @subviews when subview instanceof LadderSubmissionView
     placeholder = @$el.find('.ladder-submission-view')
+    return unless placeholder.length
     @ladderSubmissionView = new LadderSubmissionView session: @options.session, level: @options.level, mirrorSession: @mirrorSession
     @insertSubView @ladderSubmissionView, placeholder


SocialWorker: JGroup: replace dash calls with async.parallel

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -8,7 +8,7 @@

   { Relationship } = require 'jraphical'

-  { Inflector, ObjectId, ObjectRef, secure, dash, signature } = require 'bongo'
+  { Inflector, ObjectId, ObjectRef, secure, signature } = require 'bongo'

   JPermissionSet = require './permissionset'
   { permit }     = JPermissionSet
@@ -677,9 +677,9 @@
     JGroupRole.all { isDefault: yes }, (err, roles) ->
       if err then callback err
       else
-        queue = roles.map (role) -> ->
-          group.addRole role, queue.fin.bind queue
-        dash queue, callback
+        queue = roles.map (role) -> (fin) ->
+          group.addRole role, fin
+        async.parallel queue, -> callback()

   # isInAllowedDomain checks if given email's domain is in allowed domains
   isInAllowedDomain: (email) ->
@@ -1042,7 +1042,7 @@
     async.series queue, (err) ->
       return callback err  if err
       return callback null, membershipPolicy
-
+

   destroyMemebershipPolicy:(callback) ->
     @fetchMembershipPolicy (err, policy) ->
@@ -1204,12 +1204,12 @@
         @updateCounts()
         @emit 'MemberAdded', member  if 'member' in roles

-      queue = roles.map (role) => =>
-        @addMember member, role, queue.fin.bind queue
+      queue = roles.map (role) => (fin) =>
+        @addMember member, role, fin

       # We were creating group member VMs here before
       # I've deleted them, ask me if you need more information ~ GG
-      dash queue, -> kallback()
+      async.parallel queue, -> kallback()

   each:(selector, rest...) ->
     selector.visibility = 'visible'
@@ -1271,12 +1271,10 @@
         JSession = require '../session'
         JSession.remove { username: nickname, groupName: @slug }, callback

-      queue = roles.map (role) => =>
-        Joinable::leave.call this, client, { as:role }, (err) ->
-          return kallback err if err
-          queue.fin()
-
-      dash queue, kallback
+      queue = roles.map (role) => (fin) =>
+        Joinable::leave.call this, client, { as:role }, fin
+
+      async.parallel queue, kallback

   kickMember: permit
     advanced: [
@@ -1320,22 +1318,19 @@
               groupName : client.context.group
             }, (err) -> callback err

-          queue = roles.map (role) => =>
+          queue = roles.map (role) => (fin) =>
             @removeMember account, role, (err) =>
-              return callback err  if err
+              return fin err  if err
               @updateCounts()
               @cycleChannel()
-              queue.fin()
+              fin()

           # add current user into blocked accounts
-          queue.push =>
-
+          queue.push (fin) =>
             # addBlockedAccount is generated by bongo
-            @addBlockedAccount account, (err) ->
-              return callback err  if err
-              queue.fin()
-
-          dash queue, kallback
+            @addBlockedAccount account, fin
+
+          async.parallel queue, kallback
   ###*
    * UnblockMember removes the blockage on the member for joining to a group.
    *
@@ -1388,12 +1383,11 @@
               callback err

             # give rights to new owner
-            queue = difference(['member', 'admin'], newOwnersRoles).map (role) => =>
-              @addMember account, role, (err) ->
-                return kallback err if err
-                queue.fin()
-
-            dash queue, ->
+            queue = difference(['member', 'admin'], newOwnersRoles).map (role) => (fin) =>
+              @addMember account, role, fin
+
+            async.parallel queue, (err) ->
+              return kallback err  if err
               # transfer ownership
               owner.update { $set: { targetId: account.getId() } }, kallback


fix a faulty merge

fix a faulty merge

--- workers/social/lib/social/models/group/index.coffee
+++ workers/social/lib/social/models/group/index.coffee
@@ -51,7 +51,7 @@
         'fetchUserRoles','changeMemberRoles','canOpenGroup', 'canEditGroup'
         'fetchMembershipPolicy','modifyMembershipPolicy','requestInvitation'
         'fetchInvitationRequests','countPendingInvitationRequests'
-        'sendSomeInvitations'
+        'sendSomeInvitations','fetchReadme'
       ]
     schema          :
       title         :
@@ -103,7 +103,7 @@
       invitationRequest:
         targetType  : 'JInvitationRequest'
         as          : 'owner'
-      readMe        :
+      readme        :
         targetType  : 'JReadme'
         as          : 'owner'


Fix spec

Fix spec

--- components/cta_bar/test/view.coffee
+++ components/cta_bar/test/view.coffee
@@ -69,7 +69,7 @@
         @view.$('.cta-bar-defer').click()

       it 'logs the dismissal', ->
-        @setStub.args[0].should.eql ['foobar', true, expires: 31536000]
+        @setStub.args[0].should.eql ['foobar', 1, expires: 31536000]

     describe '#previouslyDismissed', ->
       it 'returns false unless it has been dismissed previously', ->


prefer single quotes to double

prefer single quotes to double

--- app/assets/javascripts/components/loading-indicator.cjsx
+++ app/assets/javascripts/components/loading-indicator.cjsx
@@ -6,7 +6,7 @@
   displayName: 'LoadingIndicator'

   render: ->
-
+
       Loading
       •
       •


Speichern prüfen

Speichern prüfen

--- app/assets/javascripts/controllers/score/lists.js.coffee
+++ app/assets/javascripts/controllers/score/lists.js.coffee
@@ -3,6 +3,9 @@
 listEntries = null

 buildForm = () ->
+  $(window).bind 'beforeunload', ->
+    'Wollen Sie die veränderte Reihenfolge verwerfen?'
+
   form = $('.score_list_form').hide().removeClass('hide').slideDown().submit () ->
     params =
       authenticity_token: form.find('input[name=authenticity_token]').val()
@@ -54,6 +57,8 @@
     table.animate opacity: 1, () ->

 bindSortedTable = () ->
+  $(window).unbind 'beforeunload'
+
   $('.sorted_table tbody').sortable(
     forcePlaceholderSize: true
     items: 'tr'


Disable providers caching

Disable providers caching

--- src/data/base-entity.coffee
+++ src/data/base-entity.coffee
@@ -79,8 +79,8 @@

 			defer.promise
 		typedKeys: () ->
-			if @constructor._cachedTypedKeys
-				return Q(@constructor._cachedTypedKeys)
+#			if @constructor._cachedTypedKeys
+#				return Q(@constructor._cachedTypedKeys)
 			defer = Q.defer()
 			keys = {}
 			@keys()


will change wording of alert

will change wording of alert

fixes a ticket

Test Plan:
- tests pass
- Erin is happy

Change-Id: Id5c05d11c215d2535c2f0a700b02449568ff2f5f
Reviewed-on: https://gerrit.instructure.com/155467
Reviewed-by: Venk Natarajan <f9eab59b292314637a3c917fea4742741e36a19b@instructure.com>
Tested-by: Jenkins
Product-Review: Steven Burnett <a0b450117c5d20a22d5c4d52bb8ae13adee003d8@instructure.com>
QA-Review: Steven Burnett <a0b450117c5d20a22d5c4d52bb8ae13adee003d8@instructure.com>

--- app/coffeescripts/views/wiki/WikiPageDeleteDialog.coffee
+++ app/coffeescripts/views/wiki/WikiPageDeleteDialog.coffee
@@ -32,7 +32,7 @@
   class WikiPageDeleteDialog extends DialogFormView
     setViewProperties: false
     wrapperTemplate: -> '

'
-    template: -> I18n.t 'delete_confirmation', 'Are you sure you wish to delete this page?'
+    template: -> I18n.t 'delete_confirmation', 'Are you sure you want to delete this page?'

     @optionProperty 'wiki_pages_path'
     @optionProperty 'focusOnCancel'


Removed read operations from the API

Removed read operations from the API

--- api.coffee
+++ api.coffee
@@ -7,8 +7,6 @@

 # regular expression objects
-DEVICES_REGEX = /^\/devices\/?$/
-DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_]+)\/?$/
 TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-\_]+)\/tasks\/?$/

 connectionRequest = (deviceId, callback) ->
@@ -83,58 +81,29 @@
   )
 else
   server = http.createServer (request, response) ->
-    request.content = ''
+    chunks = []
+    bytes = 0

     request.addListener 'data', (chunk) ->
-      request.content += chunk
+      chunks.push(chunk)
+      bytes += chunk.length
+
+    request.getBody = (encoding) ->
+      # Write all chunks into a Buffer
+      body = new Buffer(bytes)
+      offset = 0
+      chunks.forEach((chunk) ->
+        chunk.copy(body, offset, 0, chunk.length)
+        offset += chunk.length
+      )
+
+      # Return encoded (default to UTF8) string
+      return body.toString(encoding || 'utf8', 0, body.byteLength)

     request.addListener 'end', () ->
       urlParts = url.parse(request.url, true)
-      if DEVICES_REGEX.test(urlParts.pathname)
+      if TASKS_REGEX.test(urlParts.pathname)
         if request.method == 'POST'
-          req = JSON.parse(request.content)
-          query = devicesCollection.find(req.query)
-          query.sort(req.sort) if req.sort?
-          query.skip(req.skip) if req.skip?
-          query.limit(if req.limit? req.limit else 100)
-
-          query.toArray((err, devices) ->
-            # TODO handle error
-            response.writeHead 200
-            response.end(JSON.stringify(devices))
-          )
-        else
-          response.writeHead 405, {'Allow': 'POST'}
-          response.end('405 Method Not Allowed')
-      else if DEVICE_REGEX.test(urlParts.pathname)
-        if request.method != 'GET'
-          response.writeHead 405, {'Allow': 'GET'}
-          response.end('405 Method Not Allowed')
-          return
-
-        deviceId = DEVICE_REGEX.exec(urlParts.pathname)[1]
-        devicesCollection.findOne({_id : deviceId}, (err, device) ->
-          if err
-            console.log("Error: " + err)
-            response.writeHead 500
-            response.end(err)
-          else if device is null
-            response.writeHead 404
-            response.end()
-          else
-            response.writeHead(200)
-            response.end(JSON.stringify(device))
-        )
-      else if TASKS_REGEX.test(urlParts.pathname)
-        if request.method == 'GET'
-          deviceId = TASKS_REGEX.exec(urlParts.pathname)[1]
-          query = tasksCollection.find({'device' : deviceId}).sort(['timestamp'])
-
-          query.toArray((err, tasks) ->
-            response.writeHead 200
-            response.end(JSON.stringify(tasks))
-          )
-        else if request.method == 'POST'
           deviceId = TASKS_REGEX.exec(urlParts.pathname)[1]
           if request.content
             # queue given task


Referring to models by requiring

Referring to models by requiring

--- api.coffee
+++ api.coffee
@@ -3,20 +3,21 @@
 mongo = mongoose.mongo
 ObjectID = mongo.ObjectID
 bus = require './bus'
+
 models = require('./db').models
 User = models.User
 Competition = models.Competition
 Vote = models.Vote
+CompetitionMembership = models.CompetitionMembership
+CompetitionCandidate = models.CompetitionCandidate

 # Provides a set of methods for voting data CRUD operations
 class API
   constructor: (@db) ->
     throw "must provide db connection" unless @db
-    @User = @db.models.User
-    @Competition = @db.models.Competition

   createUser: (name) ->
-    create = Q.nbind(@User.create, @User)
+    create = Q.nbind(User.create, User)
     create(name: name)
     .then (val) ->
       bus.emit('user:created', val)