Update spotify commands

Update spotify commands

--- src/spot.coffee
+++ src/spot.coffee
@@ -14,6 +14,7 @@
 #   hubot play back - Plays the previous song.
 #   hubot playing? - Returns the currently-played song.
 #   hubot play <song> - Play a particular song. This plays the first most popular result.
+#   hubot query <song> - Searches Spotify for a particular song, shows what "Play <song>" would play.
 #   hubot volume? - Returns the current volume level.
 #   hubot volume [0-100] - Sets the volume.
 #   hubot volume+ - Bumps the volume.
@@ -21,12 +22,17 @@
 #   hubot mute - Sets the volume to 0.
 #   hubot [name here] says turn it down - Sets the volume to 15 and blames [name here].
 #   hubot say <message> - Tells hubot to read a message aloud.
-#   hubot find <song> - See if Spotify knows about a song without attempting to play it.
+#   hubot how much longer? - Hubot tells you how much is left on the current track
+#   hubot find x music <search> - Searches and pulls up x (or 3) most popular matches
+#   hubot play #n - Play the nth track from the last search results
+#   hubot album #n - Pull up album info for the nth track in the last search results
+#   hubot last find - Pulls up the most recent find query
 #   hubot airplay <Apple TV> - Tell Spot to broadcast to the specified Apple TV.
-#   hubot spot - Start or restart the Spotify client.
-#
-# Author:
-#   mcminton
+#   hubot spot - Start the Spotify client.
+#   hubot respot - Restart the Spotify client.
+# Authors:
+#   mcminton, <Shad Downey> github:andromedado
+VERSION = '1.3.1'

 URL = "#{process.env.HUBOT_SPOT_URL}"

@@ -34,6 +40,97 @@
   message.http("#{URL}#{path}")
     .query(options)[action]() (err, res, body) ->
       callback(err,res,body)
+
+recordUserQueryResults = (message, results) ->
+  uQ = message.robot.brain.get('userQueries') || {}
+  uD = uQ[message.message.user.id] = uQ[message.message.user.id] || {}
+  uD.queries = uD.queries || []
+  uD.queries.push(
+    text: message.message.text
+    time: now()
+    results: results
+  )
+  message.robot.brain.set('userQueries', uQ)
+
+getLastResultsRelevantToUser = (robot, user) ->
+  uQ = robot.brain.get('userQueries') || {}
+  uD = uQ[user.id] = uQ[user.id] || {}
+  uD.queries = uD.queries || []
+  lastUQTime = 0
+  if (uD.queries.length)
+    lastUQTime = uD.queries[uD.queries.length - 1].time
+  lqT = robot.brain.get('lastQueryTime')
+  if (lqT && lqT > lastUQTime && lqT - lastUQTime > 60)
+    return robot.brain.get('lastQueryResults')
+  if (uD.queries.length)
+    return uD.queries[uD.queries.length - 1].results
+  return null
+
+explain = (data) ->
+  if not data.artists
+    return 'nothin\''
+  artists = []
+  artists.push(a.name) for a in data.artists
+  A = []
+  if data.album
+    album = data.album.name
+    if data.album.released
+      album += ' [' + data.album.released + ']'
+    A = ['Album: ' + album]
+  return ['Track: ' + data.name].concat(A).concat([
+    'Artist: ' + artists.join(', '),
+    'Length: ' + calcLength(data.length)
+    ]).join("\n")
+
+now = () ->
+  return ~~(Date.now() / 1000)
+
+render = (explanations) ->
+  str = ""
+  for exp, i in explanations
+    str += '#' + (i + 1) + "\n" + exp + "\n"
+  return str
+
+renderAlbum = (album) ->
+  artists = []
+  if not album.artists
+    artists.push('No one...?')
+  else
+    artists.push(a.name) for a in album.artists
+  pt1 = [
+    '#ALBUM#',
+    'Name: ' + album.name,
+    'Artist: ' + artists.join(', '),
+    'Released: ' + album.released,
+    'Tracks:'
+    ].join("\n") + "\n"
+  explanations = (explain track for track in album.tracks)
+  return pt1 + render(explanations)
+
+showResults = (robot, message, results) ->
+  if not results or not results.length
+    return message.send(':small_blue_diamond: I found nothin\'')
+  explanations = (explain track for track in results)
+  message.send(':small_blue_diamond: I found:')
+  message.send(render(explanations))
+
+calcLength = (seconds) ->
+  iSeconds = parseInt(seconds, 10)
+  if (iSeconds < 60)
+    return (Math.round(iSeconds * 10) / 10) + ' seconds'
+  rSeconds = iSeconds % 60
+  if (rSeconds < 10)
+    rSeconds = '0' + rSeconds
+  return Math.floor(iSeconds / 60) + ':' + rSeconds
+
+playTrack = (track, message) ->
+  if not track or not track.uri
+    message.send(":flushed:")
+    return
+  message.send(":small_blue_diamond: Switching to: " + track.name)
+  spotRequest message, '/play-uri', 'post', {'uri' : track.uri}, (err, res, body) ->
+    if (err)
+      message.send(":flushed: " + err)

 module.exports = (robot) ->

@@ -60,6 +157,10 @@
       message.send("#{URL}/playing.png")
       message.send(":notes:  #{body}")

+  robot.respond /album art\??/i, (message) ->
+    spotRequest message, '/playing', 'get', {}, (err, res, body) ->
+      message.send("#{URL}/playing.png")
+
   robot.respond /volume\?/i, (message) ->
     spotRequest message, '/volume', 'get', {}, (err, res, body) ->
       message.send("Spot volume is #{body}. :mega:")
@@ -82,15 +183,84 @@
       message.send("Spot volume set to #{body}. :mega:")

   robot.respond /play (.*)/i, (message) ->
+    playNum = message.match[1].match(/#(\d+)\s*$/)
+    if (playNum)
+      r = getLastResultsRelevantToUser(robot, message.message.user)
+      i = parseInt(playNum[1], 10) - 1
+      if (r && r[i])
+        playTrack(r[i], message)
+        return
+    if (message.match[1].match(/^that$/i))
+      lastSingle = robot.brain.get('lastSingleQuery')
+      if (lastSingle)
+        playTrack(lastSingle, message)
+        return
+      lR = robot.brain.get('lastQueryResults')
+      if (lR && lR.length)
+        playTrack(lR[0], message)
+        return
     params = {q: message.match[1]}
     spotRequest message, '/find', 'post', params, (err, res, body) ->
       message.send(":small_blue_diamond: #{body}")
-
+
+  robot.respond /album .(\d+)/i, (message) ->
+    r = getLastResultsRelevantToUser(robot, message.message.user)
+    n = parseInt(message.match[1], 10) - 1
+    if (!r || !r[n])
+      message.send(":small_blue_diamon: out of bounds...")
+      return
+    spotRequest message, '/album-info', 'get', {'uri' : r[n].album.uri}, (err, res, body) ->
+      album = JSON.parse(body)
+      album.tracks.forEach((track) ->
+        track.album = track.album || {}
+        track.album.uri = r[n].album.uri
+      )
+      recordUserQueryResults(message, album.tracks)
+      message.send(renderAlbum album)
+
+  robot.respond /(how much )?(time )?(remaining|left)\??$/i, (message) ->
+    spotRequest message, '/how-much-longer', 'get', {}, (err, res, body) ->
+      message.send(":small_blue_diamond: #{body}")
+
+  robot.respond /query (.*)/i, (message) ->
+    params = {q: message.match[1]}
+    spotRequest message, '/single-query', 'get', params, (err, res, body) ->
+      track = JSON.parse(body)
+      robot.brain.set('lastSingleQuery', track)
+      message.send(":small_blue_diamond: I found:")
+      message.send(explain track)
+
+  robot.respond /find ?(\d+)? music (.*)/i, (message) ->
+    limit = message.match[1] || 3
+    params = {q: message.match[2]}
+    spotRequest message, '/query', 'get', params, (err, res, body) ->
+      try
+        data = JSON.parse(body)
+        if (data.length > limit)
+          data = data.slice(0, limit)
+        robot.brain.set('lastQueryResults', data)
+        robot.brain.set('lastQueryTime', now())
+        recordUserQueryResults(message, data)
+        showResults(robot, message, data)
+      catch error
+        message.send(":small_blue_diamond: :flushed: " + error.message)
+
+  robot.respond /last find\??/i, (message) ->
+    data = robot.brain.get 'lastQueryResults'
+    if (!data || data.length == 0)
+      message.send(":small_blue_diamond: I got nothin'")
+      return
+    recordUserQueryResults(message, data)
+    showResults(robot, message, data)
+
   robot.respond /say (.*)/i, (message) ->
     what = message.match[1]
     params = {what: what}
     spotRequest message, '/say', 'put', params, (err, res, body) ->
       message.send(what)
+
+  robot.respond /say me/i, (message) ->
+    message.send('no way ' + message.message.user.name);

   robot.respond /(.*) says.*turn.*down.*/i, (message) ->
     name = message.match[1]
@@ -99,11 +269,8 @@
     spotRequest message, '/volume', 'put', params, (err, res, body) ->
       message.send("Spot volume set to #{body}. :mega:")

-  robot.respond /find (.*)/i, (message) ->
-    search = message.match[1]
-    params = {q: search}
-    spotRequest message, '/just-find', 'post', params, (err, res, body) ->
-      message.send(body)
+  robot.respond /spot version\??/i, (message) ->
+    message.send(':small_blue_diamond: Well, ' + message.message.user.name + ', my Spot version is presently ' + VERSION)

   robot.respond /airplay (.*)/i, (message) ->
     params = {atv: message.match[1]}
@@ -116,4 +283,4 @@

   robot.respond /respot/i, (message) ->
     spotRequest message, '/respot', 'put', {}, (err, res, body) ->
-      message.send(body)
+      message.send(body)


adjusts tick chart padding

adjusts tick chart padding

--- components/tick_chart/index.coffee
+++ components/tick_chart/index.coffee
@@ -5,7 +5,7 @@
     width = $container.width()
     height = $container.height()
     margin = {top: 40, right: 0, bottom: 30, left: 40}
-    ticks = tickNum || parseInt(height/25)
+    tickCount = tickNum || parseInt(height/25)

     x = d3.scale.linear().range [0, width - 52]
     y = d3.scale.linear().range [height, 0]
@@ -16,7 +16,8 @@
       .orient("left")
       .innerTickSize(-width)
       .tickFormat((d) -> "#{d}Å")
-      .ticks(10)
+      .tickPadding(10)
+      .ticks(tickCount)

     line = d3.svg.line()
       .x((point, index) -> x(index))
@@ -42,6 +43,7 @@
       .call(yAxis)
       .append("text")
       .attr("transform", "rotate(-90)")
+      .attr("transform", "translate(-10,0)")
       .attr("y", 6)
       .attr("dy", ".71em")


Scenario 16. added

Scenario 16. added

Scenario 16. Double tag,

--- test/scenario-based-testing.coffee
+++ test/scenario-based-testing.coffee
@@ -174,4 +174,14 @@
     # console.log output
     expectedOutput = fs.readFileSync './test/scenarios/scenario-15-expected.html', 'utf8'
     # console.log expectedOutput
+    expect(output).to.equal(expectedOutput)
+
+  it 'Scenario 16. Double tag', ->
+
+    input = fs.readFileSync './test/scenarios/scenario-16-input.cohtml', 'utf8'
+    # console.log input
+    output = toHtml5 input
+    # console.log output
+    expectedOutput = fs.readFileSync './test/scenarios/scenario-16-expected.html', 'utf8'
+    # console.log expectedOutput
     expect(output).to.equal(expectedOutput)


Separating LmsInfo into parent and child component so we don't have to deal with any of the ref weirdness that comes with rendering with LoadableItem.  This fixes the problem where the close button on the popover didn't work.

--- tutor/src/components/task-plan/lms-info.cjsx
+++ tutor/src/components/task-plan/lms-info.cjsx
@@ -15,18 +15,7 @@

 DUE_FORMAT = 'M/D/YYYY [at] h:mma'

-LmsInfo = React.createClass
-
-  propTypes:
-    courseId: React.PropTypes.string.isRequired
-    plan: React.PropTypes.shape(
-      id: React.PropTypes.string.isRequired
-      title: React.PropTypes.string.isRequired
-      shareable_url: React.PropTypes.string
-    ).isRequired
-
-  mixins: [BindStoreMixin]
-  bindStore: TeacherTaskPlanStore
+LmsInfoLink = React.createClass

   focusInput: (ev) ->
     ev.currentTarget.querySelector('input').select()
@@ -50,7 +39,6 @@

{period.name}: {moment(dates.due_at).format(DUE_FORMAT)}
}

-

   renderPopOver: ->
     {title, description} = @props.plan
@@ -81,7 +69,7 @@
   getStats: ->
     TaskPlanStatsStore.get(this.props.plan.id) or {}

-  renderLink: ->
+  render: ->
     return null unless @getStats().shareable_url

       <BS.OverlayTrigger trigger="click"
@@ -92,9 +80,24 @@
       >
         Get assignment link
       </BS.OverlayTrigger>
-

+
+LmsInfo = React.createClass
+
+  propTypes:
+    courseId: React.PropTypes.string.isRequired
+    plan: React.PropTypes.shape(
+      id: React.PropTypes.string.isRequired
+      title: React.PropTypes.string.isRequired
+      shareable_url: React.PropTypes.string
+    ).isRequired
+
+  mixins: [BindStoreMixin]
+  bindStore: TeacherTaskPlanStore
+
+  renderLink: ->
+    <LmsInfoLink {...@props} />

   render: ->
   #  return null unless @props.plan.shareable_url
@@ -106,6 +109,4 @@
     />

-
-
 module.exports = LmsInfo


Tests: Fixed postMessageWithImage test.

Tests: Fixed postMessageWithImage test.

--- tests/src/activity/activity.coffee
+++ tests/src/activity/activity.coffee
@@ -279,6 +279,7 @@
     helpers.beginTest(browser)

     image = 'http://placehold.it/200x100'
+    selector = activitySelector + ' .activity-content-wrapper .embed-image-view img'

     browser
       .click                  '[testpath="public-feed-link/Activity/Topic/public"]'
@@ -286,13 +287,10 @@
       .click                  '[testpath="ActivityTabHandle-/Activity/Public/Recent"]'
       .click                  '[testpath=ActivityInputView]'
       .setValue               '[testpath=ActivityInputView]', image
+      .click                  '.channel-title'
+      .waitForElementVisible  '.activity-input-widget .embed-image-view', 20000
       .click                  '[testpath=post-activity-button]'
-      .pause                  6000 # required
-
-    selector = activitySelector + ' .activity-content-wrapper .embed-image-view img'
-
-    browser
-      .waitForElementVisible selector, 20000# Assertion
+      .waitForElementVisible   selector, 20000# Assertion
       .end()


chore(logs): removed logs

chore(logs): removed logs

--- vue/src/shared/services/group_service.coffee
+++ vue/src/shared/services/group_service.coffee
@@ -37,8 +37,6 @@
       name: 'group_page.options.manage_subscription'
       icon: 'mdi-rocket'
       canPerform: ->
-        console.log 'Session.user().isAdminOf(group.parent())', Session.user().isAdminOf(group.parent())
-        console.log 'group.subscriptionPlan', group.subscriptionPlan
         group.isParent() && Session.user().isAdminOf(group) && group.subscriptionPlan?
       perform: ->
         window.location = AppConfig.baseUrl + "upgrade"


Ace: Require Range.

Ace: Require Range.

--- client/Ace/ace.coffee
+++ client/Ace/ace.coffee
@@ -45,6 +45,10 @@
         @vimKeyboardHandler = vimMode.handler

       @emacsKeyboardHandler = 'ace/keyboard/emacs'
+
+      requirejs ['ace/range'], (range) =>
+        @once 'ace.ready', =>
+          {@Range} = range

   setContent: (content, emitFileContentChangedEvent = yes) ->


show error more clearly

show error more clearly

--- client/general.coffee
+++ client/general.coffee
@@ -59,7 +59,8 @@
   else if name is "NeuronCatalogConfig"
     coll = NeuronCatalogConfig
   else
-    console.error "unknown collection name "+name
+    bootbox.alert('Error: unknown collection name '+name)
+    throw "unknown collection name "+name
   coll

 # --------------------------------------------


RootHeader, MessageTags, MessageLabels port to React; fix raw

--- client/message.coffee
+++ client/message.coffee
@@ -109,8 +109,10 @@
   sortTags @tags

 Template.registerHelper 'linkToTag', ->
+  linkToTag Template.parentData().group
+linkToTag = (group) ->
   #pathFor 'tag',
-  #  group: Template.parentData().group
+  #  group: group
   #  tag: @key
   search = @key
   if 0 <= search.indexOf ' '
@@ -123,7 +125,7 @@
     else
       search = "\"#{search}\""
   pathFor 'search',
-    group: Template.parentData().group
+    group: group
     search: "tag:#{search}"

 Template.registerHelper 'folded', ->
@@ -135,6 +137,31 @@
   root: ->
     if @root
       Messages.findOne @root
+
+RootHeader = React.memo ({message}) ->
+  return null unless message.root
+  root = useTracker ->
+    Messages.findOne message.root
+  , [message.root]
+  formattedTitle = useMemo ->
+    formatTitleOrFilename root.title, false, false, true
+  , [root]
+
+

+

+

+
+
+
+

+
+
+      <MessageTags message={message}/>
+      <MessageLabels message={message}/>
+

+

+RootHeader.displayName = 'RootHeader'

 Template.registerHelper 'formatTitle', ->
   formatTitleOrFilename @, false
@@ -272,11 +299,9 @@

-      {###
-      +rootHeader
-      ###}
+      <RootHeader message={message}/>
       <Submessage message={message}/>
-

+

         {if authors
           <>

@@ -322,7 +347,7 @@
       ###}

-      <TableOfContents message={message}/>
+      <TableOfContents message={message} root={true}/>

 Message.displayName = 'Message'
@@ -1103,11 +1128,43 @@
 Template.messageTags.helpers
   tags: historify 'tags', sortTags

+MessageTags = React.memo ({message}) ->
+
+    {for tag in sortTags message.tags
+      <>
+
+
+            {tag.key}
+
+
+        ' '
+      </>
+    }
+
+MessageTags.displayName = 'MessageTags'
+
 Template.messageLabels.helpers
   deleted: historify 'deleted'
   published: historify 'published'
   minimized: historify 'minimized'
   private: historify 'private'
+
+MessageLabels = React.memo ({message}) ->
+
+    {if message.deleted
+      Deleted
+    }
+    {unless message.published
+      Unpublished
+    }
+    {if message.private
+      Private
+    }
+    {if message.minimized
+      Minimized
+    }
+
+MessageLabels.displayName = 'MessageLabels'

 Template.messageNeighbors.helpers
   parent: ->
@@ -1911,7 +1968,7 @@
   , []
   editing = editingMessage message, user
   raw = useTracker ->
-    messageRaw.get message._id
+    not editing and messageRaw.get message._id
   , [message._id]
   folded = useTracker ->
     (messageFolded.get message._id) and
@@ -1928,18 +1985,21 @@
     history? or
     messagePreview.get(message._id) ? messagePreviewDefault()
   , [history?, message._id]
-  formattedTitle = useTracker ->
+  formattedTitle = useMemo ->
     for bold in [true, false]
       ## Only render unbold title if we have children (for back pointer)
-      continue unless bold or message.children.length
-      if messageRaw.get message._id
+      continue unless bold or message.children.length > 0
+      if raw
         "#{_.escape historified.title}"
       else
         formatTitleOrFilename historified, false, false, bold  ## don't say (untitled)
-  , [message._id, historified.title, historified.format]
-  formattedBody = useTracker ->
-    formatBody historified.format, historified.body
-  , [historified.format, historified.body]
+  , [historified.title, historified.format, raw, message.children.length > 0]
+  formattedBody = useMemo ->
+    if raw
+      "
#{_.escape historified.body}
"
+    else
+      formatBody historified.format, historified.body
+  , [historified.body, historified.format, raw]
   children = useChildren message
   ref = useRefTooltip()

@@ -1995,8 +2055,8 @@
           }

-          {###+messageTags###}
-          {###+messageLabels###}
+          <MessageTags message={historified}/>
+          <MessageLabels message={historified}/>

       }
       {###http://stackoverflow.com/questions/22390272/how-to-create-a-label-with-close-icon-in-bootstrap###}
@@ -2040,9 +2100,7 @@
               ###}

-          {###
-          +messageLabels
-          ###}
+          <MessageLabels message={message}/>

       }
@@ -2260,10 +2318,8 @@
                   }

-                  {###
-                  +messageTags
-                  +messageLabels
-                  ###}
+                  <MessageTags message={message}/>
+                  <MessageLabels message={message}/>

             }


Use reactive-dict to store temporary message flags. Fix #50

--- client/message.coffee
+++ client/message.coffee
@@ -90,16 +90,15 @@

 submessageCount = 0

+messageRaw = new ReactiveDict
+messageFolded = new ReactiveDict
+messageHistory = new ReactiveDict
+messageKeyboard = new ReactiveDict
+defaultKeyboard = 'ace'
+
 Template.submessage.onCreated ->
   @count = submessageCount++
-  @keyboard = new ReactiveVar 'ace'
   @editing = new ReactiveVar null
-  @history = new ReactiveVar null
-  @raw = new ReactiveVar false
-  ## @folded is normally true or false, but we use a special (falsey) null
-  ## value to indicate that it could still be automatically folded if it's
-  ## detected that that would be a better default.
-  @folded = new ReactiveVar null
   @autorun =>
     return unless Template.currentData()?
     #@myid = Template.currentData()._id
@@ -138,11 +137,13 @@
     #  e.dataTransfer.setData 'text/plain', ""

   ## Fold deleted messages by default on initial load.
-  @folded.set true if @data.deleted
+  messageFolded.set @data._id, true if @data.deleted
+
+  ## Fold referenced attached files by default on initial load.
   #@$.children('.panel').children('.panel-body').find('a[href|="/gridfs/fs/"]')
   #console.log @$ 'a[href|="/gridfs/fs/"]'
-  template = @
-  id2template[@data._id] = template
+  tid = @data._id
+  id2template[@data._id] = @
   scrollToMessage @data._id if scrollToLater == @data._id
   attachment = @data.format == 'file'
   #images = Session.get 'images'
@@ -156,11 +157,11 @@
         attachment: null
         count: 0
     if attachment
-      images[id].attachment = template
+      images[id].attachment = tid
     else
       images[id].count += 1
-    if images[id].count > 0 and images[id].attachment? and images[id].attachment.folded.get() == null
-      images[id].attachment.folded.set true
+    if images[id].count > 0 and images[id].attachment? and not messageFolded.get(images[id].attachment)?
+      messageFolded.set images[id].attachment, true
     #console.log images
   #Session.set 'images', images

@@ -182,7 +183,7 @@
       images[id].count -= 1

 historify = (x) -> () ->
-  history = Template.instance().history.get()
+  history = messageHistory.get @_id
   if history?
     history[x]
   else
@@ -242,9 +243,9 @@
       #editor.setOption 'spellcheck', true

   keyboard: ->
-    capitalize Template.instance().keyboard.get()
+    capitalize messageKeyboard.get(@_id) ? defaultKeyboard
   activeKeyboard: (match) ->
-    if Template.instance().keyboard.get() == match
+    if (messageKeyboard.get(@_id) ? defaultKeyboard) == match
       'active'
     else
       ''
@@ -253,17 +254,17 @@
   published: historify 'published'

   tex2jax: ->
-    history = Template.instance().history.get() ? @
+    history = messageHistory.get(@_id) ? @
     if history.format in mathjaxFormats
       'tex2jax'
     else
       ''
   title: historify 'title'
   formatTitle: ->
-    history = Template.instance().history.get() ? @
+    history = messageHistory.get(@_id) ? @
     title = history.title
     return title unless title
-    if Template.instance().raw.get()
+    if messageRaw.get @_id
       "#{_.escape title}"
     else
       title = formatTitle history.format, title
@@ -271,10 +272,10 @@
       console.warn "Sanitized '#{title}' -> '#{sanitized}'" if sanitized != title
       sanitized
   formatBody: ->
-    history = Template.instance().history.get() ? @
+    history = messageHistory.get(@_id) ? @
     body = history.body
     return body unless body
-    if Template.instance().raw.get()
+    if messageRaw.get @_id
       "
#{_.escape body}
"
     else
       body = formatBody history.format, body
@@ -292,13 +293,13 @@
   canReply: -> canPost @group, @_id
   canAttach: -> canPost @group, @_id

-  history: -> Template.instance().history.get()?
+  history: -> messageHistory.get(@_id)?
   forHistory: ->
     _id: @_id
-    history: Template.instance().history
-
-  folded: -> Template.instance().folded.get()
-  raw: -> Template.instance().raw.get()
+    history: messageHistory.get @_id
+
+  folded: -> messageFolded.get @_id
+  raw: -> messageRaw.get @_id

 Template.registerHelper 'messagePanelClass', ->
   if @deleted
@@ -318,12 +319,12 @@
   'click .foldButton': (e, t) ->
     e.preventDefault()
     e.stopPropagation()
-    t.folded.set not t.folded.get()
+    messageFolded.set @_id, not messageFolded.get @_id

   'click .rawButton': (e, t) ->
     e.preventDefault()
     e.stopPropagation()
-    t.raw.set not t.raw.get()
+    messageRaw.set @_id, not messageRaw.get @_id

   'click .editButton': (e, t) ->
     e.preventDefault()
@@ -357,7 +358,7 @@
   'click .editorKeyboard': (e, t) ->
     e.preventDefault()
     e.stopPropagation()
-    t.keyboard.set kb = e.target.getAttribute 'data-keyboard'
+    messageKeyboard.set @_id, kb = e.target.getAttribute 'data-keyboard'
     t.editor.setKeyboardHandler if kb == 'ace' then '' else 'ace/keyboard/' + kb
     $(e.target).parent().dropdown 'toggle'

@@ -397,10 +398,10 @@
   'click .historyButton': (e, t) ->
     e.preventDefault()
     e.stopPropagation()
-    if t.history.get()?
-      t.history.set null
-    else
-      t.history.set _.clone @
+    if messageHistory.get(@_id)?
+      messageHistory.set @_id, null
+    else
+      messageHistory.set @_id, _.clone @

   'click .superdeleteButton': (e) ->
     e.preventDefault()
@@ -466,8 +467,8 @@
     @slider.setValue diffs.length-1
     #@slider.off 'change'
     @slider.on 'change', (e) =>
-      @data.history.set diffs[e.newValue]
-    @data.history.set diffs[diffs.length-1]
+      messageHistory.set @data._id, diffs[e.newValue]
+    messageHistory.set @data._id, diffs[diffs.length-1]

 uploader = (template, button, input, callback) ->
   Template[template].events {


Fixed infinite scroll. It seems there was a bad merge. Fixes #326

--- client/results.coffee
+++ client/results.coffee
@@ -37,6 +37,7 @@

     return # Make sure CoffeeScript does not return anything

+Template.results.rendered = ->
   if Session.get 'currentSearchQueryReady'
     searchLimitIncreasing = false


Set version to 4.0.0 in client

Set version to 4.0.0 in client

--- client/version.coffee
+++ client/version.coffee
@@ -1 +1 @@
-@version = '[master]'
+@version = '4.0.0'


made port behaviour better in page test + clean exit

--- test/page.coffee
+++ test/page.coffee
@@ -34,8 +34,7 @@
     </html>
   """

-app.listen 23765
-
+app.listen()

 describe "Pages"
   "A Phantom page":
@@ -47,7 +46,7 @@

     "can open a URL on localhost":
       topic: t (page) ->
-        page.open "http://127.0.0.1:23765/", (status) =>
+        page.open "http://127.0.0.1:#{app.address().port}/", (status) =>
           @callback null, page, status

       "and succeed": (err, page, status) ->
@@ -113,4 +112,5 @@
             fs.unlink fileName

     teardown: (page, ph) ->
+      app.close()
       ph.exit()


another test case to freeze behaviour

another test case to freeze behaviour

--- test/spec.coffee
+++ test/spec.coffee
@@ -41,6 +41,34 @@
         expect("a/b.c").not.to.match(tester)
         expect("a/b.c/").not.to.match(tester)
         expect("/a/b.c/").to.match(tester)
+
+    it '/**/*', ->
+        tester = glob_rules.tester("/**/b*")
+        expect("").not.to.match(tester)
+        expect("/").not.to.match(tester)
+        expect("a").not.to.match(tester)
+        expect("/a").not.to.match(tester)
+        expect("a/").not.to.match(tester)
+        expect("a/b").not.to.match(tester)
+        expect("/a/b").to.match(tester)
+        expect("a/b.c").not.to.match(tester)
+        expect("/a/b.c").to.match(tester)
+        expect("a/b.c/").not.to.match(tester)
+        expect("/a/b.c/").not.to.match(tester)
+
+    it '**/*', ->
+        tester = glob_rules.tester("/**/b*")
+        expect("").not.to.match(tester)
+        expect("/").not.to.match(tester)
+        expect("a").not.to.match(tester)
+        expect("/a").not.to.match(tester)
+        expect("a/").not.to.match(tester)
+        expect("a/b").not.to.match(tester)
+        expect("/a/b").to.match(tester)
+        expect("a/b.c").not.to.match(tester)
+        expect("/a/b.c").to.match(tester)
+        expect("a/b.c/").not.to.match(tester)
+        expect("/a/b.c/").not.to.match(tester)

     it '**/b*', ->
         tester = glob_rules.tester("**/b*")


fix tests

fix tests

--- test/test.coffee
+++ test/test.coffee
@@ -29,7 +29,7 @@

 describe 'basics', ->

-  before (done) -> compile_fixture.call(@, 'basic', done)
+  before (done) -> compile_fixture.call(@, 'basic', -> done())

   it 'should precompile a basic template', ->
     p = path.join(@public, 'tpl/all.js')
@@ -52,7 +52,7 @@

 describe 'extract', ->

-  before (done) -> compile_fixture.call(@, 'extract', done)
+  before (done) -> compile_fixture.call(@, 'extract', -> done())

   it 'should compile a template to both client and static if extract is false', ->
     p1 = path.join(@public, 'tpl/all.js')
@@ -65,7 +65,7 @@

 describe 'concat', ->

-  before (done) -> compile_fixture.call(@, 'concat', done)
+  before (done) -> compile_fixture.call(@, 'concat', -> done())

   it 'should compile templates separately if concat is false', ->
     p1 = path.join(@public, 'tpl/helper.js')
@@ -82,7 +82,7 @@

 describe 'no output', ->

-  before (done) -> compile_fixture.call(@, 'no-out', done)
+  before (done) -> compile_fixture.call(@, 'no-out', -> done())

   it 'should compile templates with no output specified', ->
     p = path.join(@public, 'js/templates.js')


bring back stylus error test

bring back stylus error test

--- test/test.coffee
+++ test/test.coffee
@@ -313,9 +313,8 @@
     @stylus.render('.test\n  foo: main-width\n  bar: main-height', opts)
       .done((res) => should.match_expected(@stylus, res.result, path.join(@path, 'plugins2.styl'), done))

-  # Open stylus issue for this: https://github.com/stylus/stylus/issues/2082
-  it.skip 'should correctly handle errors', (done) ->
-    @stylus.render("214m2/3l")
+  it 'should correctly handle errors', (done) ->
+    @stylus.render("error('oh noes!')")
       .done(should.not.exist, (-> done()))

   it 'should expose sourcemaps', (done) ->


test: Change wording

test: Change wording

--- test/test.coffee
+++ test/test.coffee
@@ -34,7 +34,7 @@

 describe 'method', ->
   describe 'escapeTags', ->
-    it 'should return an escaped string', ->
+    it 'should return an expected result', ->
       assert.strictEqual bitterServer.escapeTags('
Night & "Day"
'),
         '<h1>Night & "Day"</h1>'


get GOPATH in before hook

get GOPATH in before hook

--- test/test.coffee
+++ test/test.coffee
@@ -13,78 +13,64 @@
 opts =
   config: path.join(_path, 'locals.json')
 go_path = ''
+temp_test_path =''

 before ->
   sprout.add(tpl, test_template_path)
     .then -> rimraf.sync(test_path)
     .then -> sprout.init(tpl, test_path, opts)
-
+    .then ->
+      exec('echo $GOPATH')
+        .then (streams) ->
+          go_path = streams[0].trim()
+          if go_path.length > 0
+            temp_test_path = path.join(go_path, 'src', locals.project_base_dir, locals.user_id, locals.project_name)
+            Promise.resolve()
+          else
+            Promise.reject();

 after ->
   sprout.remove(tpl)
   if go_path.length > 0
-    new_test_path = path.join(go_path, 'src', locals.project_base_dir, locals.user_id, locals.project_name)
-    if fs.existsSync(new_test_path) is on
-      rmdir.sync new_test_path
-
+    if fs.existsSync(temp_test_path) is on
+      rmdir.sync temp_test_path

 describe 'init', ->
-  it 'checks if go runtime is installed', (done) ->
-    exec('go version')
-      .then( done() )
-      .catch (e) ->
-        done()
-        throw new Error('go is either not installed or not added to PATH environment variable.')
+  it 'creates new project from template', (done) ->
+    tgt = path.join(temp_test_path, 'README.md')
+    exists = fs.existsSync(tgt)
+    fs.existsSync(tgt).should.be.ok
+    contents = fs.readFileSync(tgt, 'utf8')
+    contents.should.match /#gcore-api/
+    done()

-  exec('echo $GOPATH')
-    .then( (streams) =>
-      stdout_stream = streams[0]
-      go_path = S.trim(stdout_stream)
-      #check if there a GOPATH value set
+  it 'matches model in user_controller.go', (done) ->
+    tgt = path.join(temp_test_path, 'controllers', 'user_controller.go')
+    fs.existsSync(tgt).should.be.ok
+    contents = fs.readFileSync(tgt, 'utf8')
+    contents.should.match /.*"github.com\/roideuniverse\/gcore-api\/models".*/
+    done()

-      if go_path.length <= 0
-        throw "Go path is either not set or empty"
+  #mathces package path in main.go
+  it 'matched package path in main.go', (done) ->
+    tgt = path.join(temp_test_path, 'main.go')
+    fs.existsSync(tgt).should.be.ok
+    contents = fs.readFileSync(tgt, 'utf8')
+    counts = contents.should.match /.*"github.com\/roideuniverse\/gcore-api\/controllers".*/
+    done()

-      new_test_path = path.join(go_path, 'src', locals.project_base_dir, locals.user_id, locals.project_name)
+  #matched package_path in readme
+  it 'matched package path in the readme', (done) ->
+    tgt = path.join(temp_test_path, 'README.md')
+    fs.existsSync(tgt).should.be.ok
+    contents = fs.readFileSync(tgt, 'utf8')
+    counts = contents.should.match /go\sget\sgithub.com\/roideuniverse\/gcore-api/
+    done()

-      it 'creates new project from template', (done) ->
-        tgt = path.join(new_test_path, 'README.md')
-        exists = fs.existsSync(tgt)
-        fs.existsSync(tgt).should.be.ok
-        contents = fs.readFileSync(tgt, 'utf8')
-
-        contents.should.match /#gcore-api/
-        done()
-
-      it 'matches model in user_controller.go', (done) ->
-        tgt = path.join(new_test_path, 'controllers', 'user_controller.go')
-        fs.existsSync(tgt).should.be.ok
-        contents = fs.readFileSync(tgt, 'utf8')
-        contents.should.match /.*"github.com\/roideuniverse\/gcore-api\/models".*/
-        done()
-
-      #mathces package path in main.go
-      it 'matched package path in main.go', (done) ->
-        tgt = path.join(new_test_path, 'main.go')
-        fs.existsSync(tgt).should.be.ok
-        contents = fs.readFileSync(tgt, 'utf8')
-        counts = contents.should.match /.*"github.com\/roideuniverse\/gcore-api\/controllers".*/
-        done()
-
-      #matched package_path in readme
-      it 'matched package path in the readme', (done) ->
-        tgt = path.join(new_test_path, 'README.md')
-        fs.existsSync(tgt).should.be.ok
-        contents = fs.readFileSync(tgt, 'utf8')
-        counts = contents.should.match /go\sget\sgithub.com\/roideuniverse\/gcore-api/
-        done()
-
-
-      #matched project_name in readme
-      it 'matched project_name in the readme', (done) ->
-        tgt = path.join(new_test_path, 'README.md')
-        fs.existsSync(tgt).should.be.ok
-        contents = fs.readFileSync(tgt, 'utf8')
-        counts = contents.should.match /gcore-api/
-        done()
-    )+  #matched project_name in readme
+  it 'matched project_name in the readme', (done) ->
+    tgt = path.join(temp_test_path, 'README.md')
+    fs.existsSync(tgt).should.be.ok
+    contents = fs.readFileSync(tgt, 'utf8')
+    counts = contents.should.match /gcore-api/
+    done()


They're not the best tests, but they are tests

--- test/test.coffee
+++ test/test.coffee
@@ -2,18 +2,27 @@

 # Manual import since the lib isn't modular
 fs = require 'fs'
-coffee = require 'coffee-script'
+CoffeeScript = require 'coffee-script'
+
+# Stub
+self = {
+  addEventListener: ->
+}

 # Libraries
 eval(fs.readFileSync("#{__dirname}/../src/adapter.js", "utf8"))
-eval(CoffeeScript.compile(fs.readFileSync("#{__dirname}/../src/stdlib.coffee"), {bare: true}))
-eval(CoffeeScript.compile(fs.readFileSync("#{__dirname}/../src/instruments.coffee"), {bare: true}))
+eval(CoffeeScript.compile(fs.readFileSync("#{__dirname}/../src/stdlib.coffee", 'utf8'), {bare: true}))
+eval(CoffeeScript.compile(fs.readFileSync("#{__dirname}/../src/instruments.coffee", 'utf8'), {bare: true}))

 describe('Instruments', ->
-  describe('buildFunction', ->
+  describe('buildSample', ->
     it('should exist', ->
-      assert(!!buildFunction)
+      assert(!!buildSample)
+    )
+  )
+  describe('GlobalMixer', ->
+    it('should exist', ->
+      assert(!!GlobalMixer)
     )
   )
 )
-


Age in seconds

Age in seconds

--- test/text.coffee
+++ test/text.coffee
@@ -18,11 +18,11 @@
 test "text fades out", ->
   text = TextEffect()

-  text.update()
+  text.update(1)

   equals text.I.alpha, 1

-  text.update()
+  text.update(1)

   equals text.I.alpha, 1 - 1/40


Better element selection.

Better element selection.

--- foreground.coffee
+++ foreground.coffee
@@ -103,9 +103,9 @@

   getPosition: (element) ->
     { top, bottom } = element.getBoundingClientRect()
-    if bottom <= 0
+    if top <= 0
       "above"
-    else if window.innerHeight <= top
+    else if window.innerHeight <= bottom
       "below"
     else
       "visible"


coco 8a8cc53: meta-heregex

coco 8a8cc53: meta-heregex


Add Figure's constructor and preliminary glyph functions

--- bokehjs/src/coffee/api/plotting.coffee
+++ bokehjs/src/coffee/api/plotting.coffee
@@ -1,48 +1,170 @@
-api = require("../api")
+_ = require("underscore")
+$ = require("jquery")
+{Document} = require("../document")
+embed = require("../embed")
+models = require("./models")

-class Figure
+class Figure extends models.Plot

-  _glyph_function: (cls, args) ->
-    return new api.GlyphRenderer()
+  constructor: (attrs={}) ->
+    attrs = _.clone(attrs)

-  annular_wedge:     () -> return @_glyph_function(api.AnnularWedge,     arguments.slice(0))
-  annulus:           () -> return @_glyph_function(api.Annulus,          arguments.slice(0))
-  arc:               () -> return @_glyph_function(api.Arc,              arguments.slice(0))
-  bezier:            () -> return @_glyph_function(api.Bezier,           arguments.slice(0))
-  gear:              () -> return @_glyph_function(api.Gear,             arguments.slice(0))
-  image:             () -> return @_glyph_function(api.Image,            arguments.slice(0))
-  image_rgba:        () -> return @_glyph_function(api.ImageRGBA,        arguments.slice(0))
-  image_url:         () -> return @_glyph_function(api.ImageURL,         arguments.slice(0))
-  line:              () -> return @_glyph_function(api.Line,             arguments.slice(0))
-  multi_line:        () -> return @_glyph_function(api.MultiLine,        arguments.slice(0))
-  oval:              () -> return @_glyph_function(api.Oval,             arguments.slice(0))
-  patch:             () -> return @_glyph_function(api.Patch,            arguments.slice(0))
-  patches:           () -> return @_glyph_function(api.Patches,          arguments.slice(0))
-  quad:              () -> return @_glyph_function(api.Quad,             arguments.slice(0))
-  quadratic:         () -> return @_glyph_function(api.Quadratic,        arguments.slice(0))
-  ray:               () -> return @_glyph_function(api.Ray,              arguments.slice(0))
-  rect:              () -> return @_glyph_function(api.Rect,             arguments.slice(0))
-  segment:           () -> return @_glyph_function(api.Segment,          arguments.slice(0))
-  text:              () -> return @_glyph_function(api.Text,             arguments.slice(0))
-  wedge:             () -> return @_glyph_function(api.Wedge,            arguments.slice(0))
+    attrs.x_range = @_get_range(attrs.x_range)
+    attrs.y_range = @_get_range(attrs.y_range)

-  asterisk:          () -> return @_glyph_function(api.Asterisk,         arguments.slice(0))
-  circle:            () -> return @_glyph_function(api.Circle,           arguments.slice(0))
-  circle_cross:      () -> return @_glyph_function(api.CircleCross,      arguments.slice(0))
-  circle_x:          () -> return @_glyph_function(api.CircleX,          arguments.slice(0))
-  cross:             () -> return @_glyph_function(api.Cross,            arguments.slice(0))
-  diamond:           () -> return @_glyph_function(api.Diamond,          arguments.slice(0))
-  diamond_cross:     () -> return @_glyph_function(api.DiamondCross,     arguments.slice(0))
-  inverted_triangle: () -> return @_glyph_function(api.InvertedTriangle, arguments.slice(0))
-  square:            () -> return @_glyph_function(api.Square,           arguments.slice(0))
-  square_cross:      () -> return @_glyph_function(api.SquareCross,      arguments.slice(0))
-  square_x:          () -> return @_glyph_function(api.SquareX,          arguments.slice(0))
-  triangle:          () -> return @_glyph_function(api.Triangle,         arguments.slice(0))
-  x:                 () -> return @_glyph_function(api.X,                arguments.slice(0))
+    x_axis_type = attrs.x_axis_type ? "auto"
+    y_axis_type = attrs.y_axis_type ? "auto"
+    delete attrs.x_axis_type
+    delete attrs.y_axis_type

-figure = (attrs) -> return new Figure(attrs)
+    x_minor_ticks = attrs.x_minor_ticks ? "auto"
+    y_minor_ticks = attrs.y_minor_ticks ? "auto"
+    delete attrs.x_minor_ticks
+    delete attrs.y_minor_ticks
+
+    x_axis_location = attrs.x_axis_location ? "below"
+    y_axis_location = attrs.y_axis_location ? "left"
+    delete attrs.x_axis_location
+    delete attrs.y_axis_location
+
+    x_axis_label = attrs.x_axis_label ? ""
+    y_axis_label = attrs.y_axis_label ? ""
+    delete attrs.x_axis_label
+    delete attrs.y_axis_label
+
+    super(attrs)
+
+    x_axiscls = @_get_axis_class(x_axis_type, this.x_range)
+    if x_axiscls?
+      if x_axiscls == models.LogAxis
+        this.x_mapper_type = 'log'
+      xaxis = new x_axiscls({plot: this})
+      if xaxis.ticker instanceof models.ContinuousTicker
+        xaxis.ticker.num_minor_ticks = @_get_num_minor_ticks(x_axiscls, x_minor_ticks)
+      axis_label = x_axis_label
+      if axis_label.length != 0
+        xaxis.axis_label = axis_label
+      xgrid = new models.Grid({plot: this, dimension: 0, ticker: xaxis.ticker})
+      if x_axis_location == "above"
+        this.above = (this.above ? []).concat([xaxis])
+      else if x_axis_location == "below"
+        this.below = (this.below ? []).concat([xaxis])
+
+    y_axiscls = @_get_axis_class(y_axis_type, this.y_range)
+    if y_axiscls?
+      if y_axiscls == models.LogAxis
+        this.y_mapper_type = 'log'
+      yaxis = new y_axiscls({plot: this})
+      if yaxis.ticker instanceof models.ContinuousTicker
+        yaxis.ticker.num_minor_ticks = @_get_num_minor_ticks(y_axiscls, y_minor_ticks)
+      axis_label = y_axis_label
+      if axis_label.length != 0
+        yaxis.axis_label = axis_label
+      xgrid = new models.Grid({plot: this, dimension: 1, ticker: yaxis.ticker})
+      if y_axis_location == "left"
+        this.left = (this.left ? []).concat([yaxis])
+      else if y_axis_location == "right"
+        this.right = (this.right ? []).concat([yaxis])
+
+  _glyph: (cls, params, args) ->
+    params = params.split(",")
+
+    if args.length == 1
+      [attrs] = args
+    else
+      [args..., opts] = args
+
+    return new models.GlyphRenderer()
+
+  _marker: (cls, args) -> @_glyph(cls, "x,y", args)
+
+  annular_wedge:     (args...) -> @_glyph(models.AnnularWedge, "x,y,inner_radius,outer_radius,start_angle,end_angle", args)
+  annulus:           (args...) -> @_glyph(models.Annulus,      "x,y,inner_radius,outer_radius",                       args)
+  arc:               (args...) -> @_glyph(models.Arc,          "x,y,radius,start_angle,end_angle",                    args)
+  bezier:            (args...) -> @_glyph(models.Bezier,       "x0,y0,x1,y1,cx0,cy0,cx1,cy1",                         args)
+  gear:              (args...) -> @_glyph(models.Gear,         "x,y,module,teeth",                                    args)
+  image:             (args...) -> @_glyph(models.Image,        "color_mapper,image,rows,cols,x,y,dw,dh",              args)
+  image_rgba:        (args...) -> @_glyph(models.ImageRGBA,    "image,rows,cols,x,y,dw,dh",                           args)
+  image_url:         (args...) -> @_glyph(models.ImageURL,     "url,x,y,w,h",                                         args)
+  line:              (args...) -> @_glyph(models.Line,         "x,y",                                                 args)
+  multi_line:        (args...) -> @_glyph(models.MultiLine,    "xs,ys",                                               args)
+  oval:              (args...) -> @_glyph(models.Oval,         "x,y,width,height",                                    args)
+  patch:             (args...) -> @_glyph(models.Patch,        "x,y",                                                 args)
+  patches:           (args...) -> @_glyph(models.Patches,      "xs,ys",                                               args)
+  quad:              (args...) -> @_glyph(models.Quad,         "left,right,bottom,top",                               args)
+  quadratic:         (args...) -> @_glyph(models.Quadratic,    "x0,y0,x1,y1,cx,cy",                                   args)
+  ray:               (args...) -> @_glyph(models.Ray,          "x,y,length",                                          args)
+  rect:              (args...) -> @_glyph(models.Rect,         "x,y,width,height",                                    args)
+  segment:           (args...) -> @_glyph(models.Segment,      "x0,y0,x1,y1",                                         args)
+  text:              (args...) -> @_glyph(models.Text,         "x,y,text",                                            args)
+  wedge:             (args...) -> @_glyph(models.Wedge,        "x,y,radius,start_angle,end_angle",                    args)
+
+  asterisk:          (args...) -> @_marker(models.Asterisk,         args)
+  circle:            (args...) -> @_marker(models.Circle,           args)
+  circle_cross:      (args...) -> @_marker(models.CircleCross,      args)
+  circle_x:          (args...) -> @_marker(models.CircleX,          args)
+  cross:             (args...) -> @_marker(models.Cross,            args)
+  diamond:           (args...) -> @_marker(models.Diamond,          args)
+  diamond_cross:     (args...) -> @_marker(models.DiamondCross,     args)
+  inverted_triangle: (args...) -> @_marker(models.InvertedTriangle, args)
+  square:            (args...) -> @_marker(models.Square,           args)
+  square_cross:      (args...) -> @_marker(models.SquareCross,      args)
+  square_x:          (args...) -> @_marker(models.SquareX,          args)
+  triangle:          (args...) -> @_marker(models.Triangle,         args)
+  x:                 (args...) -> @_marker(models.X,                args)
+
+  _get_range: (range) ->
+    if not range?
+      return new models.DataRange1d()
+    if range instanceof models.Range
+      return range
+    if _.isArray(range)
+      if _.all((x) -> _.isString(x) for x in range)
+        return new models.FactorRange({factors: range})
+      if range.length == 2
+        return new models.Range1d({start: range[0], end: range[1]})
+
+  _get_axis_class: (axis_type, range) ->
+    if not axis_type?
+      return null
+    if axis_type == "linear"
+      return models.LinearAxis
+    if axis_type == "log"
+      return models.LogAxis
+    if axis_type == "datetime"
+      return models.DatetimeAxis
+    if axis_type == "auto"
+      if range instanceof models.FactorRange
+        return models.CategoricalAxis
+      if range instanceof models.Range1d
+        # TODO: return models.DatetimeAxis (Date type)
+        return models.LinearAxis
+
+  _get_num_minor_ticks: (axis_class, num_minor_ticks) ->
+    if _.isNumber(num_minor_ticks)
+      if num_minor_ticks <= 1
+        throw new ValueError("num_minor_ticks must be > 1")
+      return num_minor_ticks
+    if not num_minor_ticks?
+      return 0
+    if num_minor_ticks == 'auto'
+      if axis_class == models.LogAxis
+        return 10
+      return 5
+
+figure = (attrs={}) -> new Figure(attrs)
+
+show = (figure) ->
+  doc = new Document()
+  doc.add_root(figure)
+
+  div = $("<div class='bk-root'>")
+  $("body").append(div)
+
+  embed.add_document_static(div, doc)

 module.exports = {
   Figure: Figure
   figure: figure
+  show  : show
 }


Enable autocomplete setting for ace editor

--- client/Ace/ace.coffee
+++ client/Ace/ace.coffee
@@ -84,11 +84,7 @@
       @setKeyboardHandler     @appStorage.getValue('keyboardHandler')     ? 'default'
       @setScrollPastEnd       @appStorage.getValue('scrollPastEnd')       ? yes
       @setOpenRecentFiles     @appStorage.getValue('openRecentFiles')     ? yes
-
-    requirejs ['ace/ext-language_tools'], =>
-      @editor.setOptions
-        enableBasicAutocompletion: yes
-        enableSnippets: yes
+      @setEnableAutocomplete  @appStorage.getValue('enableAutocomplete')  ? yes    ,no

   saveStarted:->
     @lastContentsSentForSave = @getContents()
@@ -256,6 +252,9 @@
   getOpenRecentFiles:->
     @appStorage.getValue('openRecentFiles') ? yes

+  getEnableAutocomplete:->
+    @appStorage.getValue('enableAutocomplete') ? yes
+
   getSettings:->
     theme               : @getTheme()
     syntax              : @getSyntax()
@@ -270,6 +269,7 @@
     keyboardHandler     : @getKeyboardHandler()
     scrollPastEnd       : @getScrollPastEnd()
     openRecentFiles     : @getOpenRecentFiles()
+    enableAutocomplete  : @getEnableAutocomplete()

   ###
   SETTERS
@@ -393,6 +393,15 @@

   setOpenRecentFiles:(value, save = yes)->
     @appStorage.setValue 'openRecentFiles', value
+
+  setEnableAutocomplete:(value, save = yes)->
+
+    requirejs ['ace/ext-language_tools'], =>
+      @editor.setOptions
+        enableBasicAutocompletion: value
+        enableSnippets: value
+    return  unless save
+    @appStorage.setValue 'enableAutocomplete', value

   gotoLine: (lineNumber) ->
     @editor.gotoLine lineNumber


Fix suggestions keyup timeout.

Fix suggestions keyup timeout.

The browser was generating one AJAX request per keystroke, ignoring the
timeout. The clearTimeout() function needs to be called with the ID
value returned by setTimeout().

--- app/assets/javascripts/suggest.js.coffee
+++ app/assets/javascripts/suggest.js.coffee
@@ -16,8 +16,10 @@
             js_suggest_selector = $this.data('js-suggest')
             $(js_suggest_selector).html(stHtml)

+      timer = null
+
       $this.on 'keyup', ->
-        window.clearTimeout(callback)
-        window.setTimeout(callback, 1000)
+        window.clearTimeout(timer)
+        timer = window.setTimeout(callback, 1000)

       $this.on 'change', callback


Height of library when dragging. Fixes #394

--- client/library.coffee
+++ client/library.coffee
@@ -30,9 +30,12 @@
     cursor: 'move'
     zIndex: 1
     start: (e, ui) ->
+      collections = $('.library-collections-wrapper')
       # When we start to drag a publication, we display collections fixed so they are in view, ready to be dropped onto.
       # TODO: Make sure this works for people with lots of collections.
-      $('.library-collections-wrapper').addClass('fixed')
+      collections.addClass('fixed')
+      # To prevent footer from moving up, force the containing row to be at least as high as the collections.
+      collections.parent('.row').css('min-height', collections.outerHeight())
     stop: (e, ui) ->
       $('.library-collections-wrapper').removeClass('fixed')


Update slider to dispatch events and have a set method.

--- app/assets/utensils/slider/slider.coffee
+++ app/assets/utensils/slider/slider.coffee
@@ -14,12 +14,16 @@
   options: ->
     @data.handle ?= @el.find('.slider-handle')
     @data.progress ?= @el.find('.slider-progress')
+    @data.namespace ?= 'slider'
     @data.min ?= parseFloat(@data.min, 10) || 0
     @data.max ?= parseFloat(@data.max, 10) || 1
     @data.value ?= parseFloat(@data.value, 10) || 0.5

   initialize: ->
+    @dispatcher = @el
+    @el[0].set = (val) => @setValue(val)
+    @namespace = @data.namespace
     @cursor = new utensils.CursorHand()
     @touchTracker = new utensils.MouseAndTouchTracker(@el[0], @touchCallback, false, "img div")
     @handleWidth = @data.handle.outerWidth() * 0.5
@@ -29,8 +33,13 @@

   touchCallback: (state, touchEvent) =>
     switch state
+      when utensils.MouseAndTouchTracker.state_start
+        @send('focusin')
       when utensils.MouseAndTouchTracker.state_start, utensils.MouseAndTouchTracker.state_move
         @updateInterface(@touchTracker.touchcurrent.x)
+        @send('change')
+      when utensils.MouseAndTouchTracker.state_end
+        @send('focusout')
     utensils.CursorHand.setCursorFromTouchTrackerState(@touchTracker, @cursor, state)

@@ -48,4 +57,9 @@
     @value = @data.min + percent * (@data.max - @data.min)
     @data.callback?.call(@, @value)

+
+  send: (event_type) ->
+    @dispatcher.trigger("#{@namespace}:#{event_type}", {value: Math.round(@value)})
+
+
 utensils.Bindable.register 'slider', utensils.Slider


Remove Logged Info. Place method in Try

Remove Logged Info. Place method in Try

--- app/classifier/world_wide_telescope.cjsx
+++ app/classifier/world_wide_telescope.cjsx
@@ -104,10 +104,6 @@
       yRange = (@findAxis @axisPoints).yAxis.sort( (a, b) -> a.y > b.y )
       xLabel = @findLabels xRange, @axisLabels
       yLabel = @findLabels yRange, @axisLabels
-      console.log xRange
-      console.log yRange
-      console.log xLabel
-      console.log yLabel
       @xAxis = new Axis xRange, xLabel.value
       @yAxis = new Axis yRange, yLabel.value

@@ -305,20 +301,15 @@
       chart.buildAxes()

   render: ->
-    #TODO: this shouldn't be necessary
-    return <div/> unless @props.annotations[1]
-
     subjImage = @props.subject.locations[0]["image/jpeg"]
-
-    @parseClassification()
-
     plates = []
-    for chart in @charts
-      if chart.valid
-        try
+
+    try
+      @parseClassification()
+
+      for chart in @charts
+        if chart.valid
           plates.push(new Plate(chart, subjImage, @props.user_name))
-        catch error
-          console?.error 'Failed to link to Worldwide Telescope. Units must be annotated outside chart.'

     <div>
       {plates.map (plate, idx) ->


Fixing css loads for plugin system

Fixing css loads for plugin system

--- app-loader/app-loader.coffee
+++ app-loader/app-loader.coffee
@@ -28,7 +28,7 @@
     return window._decorators

 loadStylesheet = (path) ->
-    $('head').append('<link rel="stylesheet" href="' + path + '" type="stylesheet" />')
+    $('head').append('<link rel="stylesheet" href="' + path + '" type="text/css" />')

 loadPlugin = (pluginPath) ->
     return new Promise (resolve, reject) ->


Update env.coffee

Update env.coffee

delete useitem in env.coffee

--- views/env.coffee
+++ views/env.coffee
@@ -150,8 +150,6 @@
     _.extend _.clone(window.$ships[ship.api_ship_id]), ship
   extendSlotitem = (item) ->
     _.extend _.clone(window.$slotitems[item.api_slotitem_id]), item
-  extendUseitem = (useitem) ->
-    _.extend _.clone(window.$useitems[useitem.api_id]), useitem
   locked = true
   while responses.length > 0
     [method, path, body, postBody] = responses.shift()
@@ -177,8 +175,6 @@
         $maps[map.api_id] = map for map in body.api_mst_mapinfo
         window.$missions = []
         $missions[mission.api_id] = mission for mission in body.api_mst_mission
-        window.$useitems = []
-        $useitems[useitem.api_id] = useitem for useitem in body.api_mst_useitem
       # User datas prefixed by _
       when '/kcsapi/api_get_member/basic'
         window._teitokuLv = body.api_level
@@ -201,9 +197,6 @@
       when '/kcsapi/api_get_member/slot_item'
         window._slotitems = {}
         _slotitems[item.api_id] = extendSlotitem item for item in body
-      when '/kcsapi/api_get_member/useitem'
-        window._useitems = {}
-        _useitems[useitem.api_id] = extendUseitem useitem for useitem in body
       when '/kcsapi/api_port/port'
         window._ships = {}
         _ships[ship.api_id] = extendShip ship for ship in body.api_ship


added recalculation for person wins/losses and update if different from server dumb ints

--- app/controllers/person_controller.coffee
+++ app/controllers/person_controller.coffee
@@ -13,6 +13,8 @@
   completedGames: ( ->
     @set('games', [])
     person = @get('model')
+    recountWins = 0
+    recountLosses = 0
     @get('store').fetch('completedGame').then ((games) =>
       person = @get('model')
       gameHistory = games.filter (game) =>
@@ -38,6 +40,11 @@
           status = "Won"
           won = true
         game.won = won
+        if game.won
+          recountWins++
+        else
+         recountLosses++
+
         game.isHome = isHome
         game.isAway = isAway
         completed = game.get('completedAt')
@@ -54,6 +61,16 @@
       @set('lastStatus', status)
       @set('lastPlayed', last)
       @set('games', totalGames)
+
+      console.log recountWins
+      console.log recountLosses
+      wins = @get('wins')
+      losses = @get('losses')
+      if recountWins != wins or recountLosses != losses
+        person = @get('model')
+        person.set('wins', recountWins)
+        person.set('losses', recountLosses)
+        person.save()
     )
     ""
   ).property('content', 'controllers.completedGames.games.@each')


Minor refactor

Minor refactor

--- app/authorizers/irene.coffee
+++ app/authorizers/irene.coffee
@@ -14,7 +14,7 @@
 IreneAuthorizer = Base.extend

   authorize: (jqXHR, requestOptions) ->
-    token = getB64Token(@get "session")
+    token = getB64Token @get "session"
     jqXHR.setRequestHeader 'Authorization', "Basic #{token}"

 `export default IreneAuthorizer;`


Tutorials don't need to create preferences

--- app/components/tutorial.cjsx
+++ app/components/tutorial.cjsx
@@ -109,10 +109,6 @@

     if @props.user?
       projectPreferences = @props.preferences
-      projectPreferences ?= apiClient.type('project_preferences').create
-        links:
-          project: project.id
-        preferences: {}
       # Build this manually. Having an index (even as a strings) keys creates an array.
       projectPreferences.preferences ?= {}
       projectPreferences.preferences.tutorials_completed_at ?= {}


index.coffee up

index.coffee up

--- app/controllers/index.coffee
+++ app/controllers/index.coffee
@@ -2,9 +2,9 @@
 ボタンの追加
 ###
 menuBtn = Titanium.UI.createButton
-	image:'/images/menu.png',
-	width:32,
-	height:32,
+	image:'/images/menu.png'
+	width:32
+	height:32
 $.mainWindow.leftNavButton = menuBtn

@@ -42,12 +42,12 @@
 		height:Ti.UI.FILL
 		text:"Menu#{i}"
 		font:{
-			fontFamily:'Arial-BoldMT',
-			fontWeight:'bold',
+			fontFamily:'Arial-BoldMT'
+			fontWeight:'bold'
 			fontSize:17
 		}
 	row.add title
 	$.menuTable.appendRow row

-$.index.open();+$.index.open()


Cleaned up the image controller.

Cleaned up the image controller.

--- app/javascripts/image.coffee
+++ app/javascripts/image.coffee
@@ -2,6 +2,93 @@
   image = ng.module 'qiprofile.image', ['qiprofile.file', 'vr.directives.slider']

   image.factory 'Image', ['$rootScope', '$q', 'File', ($rootScope, $q, File) ->
+
+    # The overlay and color table specification.
+    # TODO - Replace hardcoded temp filepaths with proper references.
+    # The labels are used by the dat gui menu. (This will be replaced
+    #    by the radio button bar.)
+    overlays =
+      [
+        {
+          label: 'None'
+          labelmap:
+            accessor: (parent) -> 'data/QIN_Test/blank.nii.gz'
+          colortable:
+            accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
+        }
+        {
+          label: 'deltaKtrans'
+          labelmap:
+            accessor: (parent) -> 'data/QIN_Test/k_trans_map.nii.gz'
+          colortable:
+            accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
+        }
+        {
+          label: 'FXR Ktrans'
+          labelmap:
+            accessor: (parent) -> 'data/QIN_Test/k_trans_map.nii.gz'
+          colortable:
+            accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
+        }
+        {
+          label: 'FXL Ktrans'
+          labelmap:
+            accessor: (parent) -> 'data/QIN_Test/k_trans_map.nii.gz'
+          colortable:
+            accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
+        }
+        {
+          label: 'v_e'
+          labelmap:
+            accessor: (parent) -> 'data/QIN_Test/v_e_map.nii.gz'
+          colortable:
+            accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
+        }
+        {
+          label: 'tau_i'
+          labelmap:
+            accessor: (parent) -> 'data/QIN_Test/tau_i_map.nii.gz'
+          colortable:
+            accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
+        }
+      ]
+
+    # The available label map types.
+    overlayTypes = []
+    overlayTypes.push overlay.label for overlay in overlays
+
+    # Overlay selection change callback function.
+    selectOverlay = (value, volume) ->
+      # Get the selected index value of the overlay menu.
+      _index = overlayTypes.indexOf(value)
+      # If 'None' is selected, turn the label map off...
+      if _index == 0
+        volume.labelmap.visible = false
+      # ...and if an overlay option is selected, load the new label map
+      # and color table.
+      else
+        # Read each file into an ArrayBuffer. The Coffeescript fat
+        # arrow (=>) binds the this variable to the image object
+        # rather than the $http request.
+        newLabelmapFilename = overlays[_index].labelmap.accessor(parent)
+        @newLabelmapFilename = newLabelmapFilename
+        newColortableFilename = overlays[_index].colortable.accessor(parent)
+        @newColortableFilename = newColortableFilename
+        newLabelmapFile = File.read(newLabelmapFilename, responseType: 'arraybuffer').then (newLabelmapData) =>
+          # Set the data property to the label map file content.
+          @newLabelmapData = newLabelmapData
+        newColortableFile = File.read(newColortableFilename, responseType: 'arraybuffer').then (newColortableData) =>
+          # Set the data property to the color table file content.
+          @newColortableData = newColortableData
+        allNewFilesLoaded = $q.all(newLabelmapFile, newColortableFile)
+        allNewFilesLoaded.then =>
+          # Reload the selected overlay files.
+          volume.labelmap.file = @newLabelmapFilename
+          volume.labelmap.filedata = @newLabelmapData
+          volume.labelmap.colortable.file = @newColortableFilename
+          volume.labelmap.colortable.filedata = @newColortableData
+          volume.labelmap.visible = true
+
     # The root scope {parent id: [Image objects]} cache.
     if not $rootScope.images
       $rootScope.images = {}
@@ -33,53 +120,8 @@
     # @param timePoint the series time point
     # @returns a new image object
     create = (parent, filename, timePoint) ->
-      # TODO - Replace hardcoded temp filepaths with proper references.
-      overlays =
-        [
-          {
-            label: 'None'
-            labelmap:
-              accessor: (parent) -> 'data/QIN_Test/blank.nii.gz'
-            colortable:
-              accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
-          }
-          {
-            label: 'deltaKtrans'
-            labelmap:
-              accessor: (parent) -> 'data/QIN_Test/k_trans_map.nii.gz'
-            colortable:
-              accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
-          }
-          {
-            label: 'FXR Ktrans'
-            labelmap:
-              accessor: (parent) -> 'data/QIN_Test/k_trans_map.nii.gz'
-            colortable:
-              accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
-          }
-          {
-            label: 'FXL Ktrans'
-            labelmap:
-              accessor: (parent) -> 'data/QIN_Test/k_trans_map.nii.gz'
-            colortable:
-              accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
-          }
-          {
-            label: 'v_e'
-            labelmap:
-              accessor: (parent) -> 'data/QIN_Test/v_e_map.nii.gz'
-            colortable:
-              accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
-          }
-          {
-            label: 'tau_i'
-            labelmap:
-              accessor: (parent) -> 'data/QIN_Test/tau_i_map.nii.gz'
-            colortable:
-              accessor: (parent) -> 'data/QIN_Test/generic-colors.txt'
-          }
-        ]
-
+
+      # Set the default overlay and color table.
       labelmapFilename = overlays[0].labelmap.accessor(parent)
       colortableFilename = overlays[0].colortable.accessor(parent)

@@ -135,11 +177,6 @@
       #
       # @param element the Angular jQueryLite element
       open: (element) ->
-
-        # The available label map types.
-        labelmapTypes = []
-        labelmapTypes.push overlay.label for overlay in overlays
-
         # The XTK renderer for this image.
         renderer = new X.renderer3D()
         # The image is rendered within the given element.
@@ -155,9 +192,8 @@
         volume.labelmap.colortable.file = @colortableFilename
         volume.labelmap.colortable.filedata = @colortableData
         renderer.add(volume)
-        # We need to re-load the gui after we change the label map type.
-        gui = null
-        # We need this loader as a container to keep track of the current map.
+
+        # We need this loader as a container to keep track of the current overlay.
         _loader = Type: "None"
         # Display no label map by default.
         volume.labelmap.visible = false
@@ -165,7 +201,6 @@
         # The rendering callback. This function is called after the
         # volume is initialized and prior to the first rendering.
         renderer.onShowtime = ->
-          gui = null
           # The volume display controls. The element is manually
           # placed later in this function.
           gui = new dat.GUI(autoplace: false)
@@ -188,41 +223,16 @@

           # Display the label map (overlay) controls.
           labelmapCtls = gui.addFolder('Overlay')
-          labelmapTypeCtl = labelmapCtls.add(_loader, 'Type', labelmapTypes).name('Type')
+          labelmapTypeCtl = labelmapCtls.add(_loader, 'Type', overlayTypes).name('Type')
           labelmapOpacityCtl = labelmapCtls.add(volume.labelmap, 'opacity', 0, 1).name('Opacity')
           labelmapCtls.open()

           # Label map selection callback.
           labelmapTypeCtl.onChange (value) ->
-            # Get the selected index value of the overlay menu.
-            _index = labelmapTypes.indexOf(value)
-            # If 'None' is selected, turn the label map off...
-            if _index == 0
-              volume.labelmap.visible = false
-            # ...and if an overlay option is selected, load the new label map
-            # and color table.
-            else
-              newLabelmapFilename = overlays[_index].labelmap.accessor(parent)
-              @newLabelmapFilename = newLabelmapFilename
-              newColortableFilename = overlays[_index].colortable.accessor(parent)
-              @newColortableFilename = newColortableFilename
-              newLabelmapFile = File.read(newLabelmapFilename, responseType: 'arraybuffer').then (newLabelmapData) =>
-                # Set the data property to the label map file content.
-                @newLabelmapData = newLabelmapData
-              newColortableFile = File.read(newColortableFilename, responseType: 'arraybuffer').then (newColortableData) =>
-                # Set the data property to the color table file content.
-                @newColortableData = newColortableData
-              # Now we (re-)load the selected label map files.
-              allNewFilesLoaded = $q.all(newLabelmapFile, newColortableFile)
-              allNewFilesLoaded.then =>
-                volume.labelmap.file = @newLabelmapFilename
-                volume.labelmap.filedata = @newLabelmapData
-                volume.labelmap.colortable.file = @newColortableFilename
-                volume.labelmap.colortable.filedata = @newColortableData
-                volume.labelmap.visible = true
-                # Destroy the old control..
-                # ..it will be re-created.
-                ctlElt.remove()
+            selectOverlay(value, volume)
+            # Destroy the old control...
+            # ...it will be re-created.
+            ctlElt.remove()

         # Adjust the camera position.
         renderer.camera.position = [0, 0, 240]


Text style for Label and refactoring from Ozaria (#6521)

--- app/lib/surface/Label.coffee
+++ app/lib/surface/Label.coffee
@@ -1,5 +1,28 @@
 CocoClass = require 'core/CocoClass'
 createjs = require 'lib/createjs-parts'
+
+# Default parameters are using short name (char)
+DEFAULT_STYLE_CHAR = {dialogue: 'D', say: 'S', name: 'N', variable: 'V', text: 'T'}
+DEFAULT_LABEL_OPTIONS  = {
+  marginX: {D: 5, S: 6, N: 3, V: 0, T: 0},
+  marginY: {D: 6, S: 4, N: 3, V: 0, T: 0},
+  fontWeight: {D: 'bold', S: 'bold', N: 'bold', V: 'bold', T: 'bold'},
+  shadow: {D: false, S: true, N: true, V: true, T: false},
+  shadowColor: {D: '#FFF', S: '#000', N: '#000', V: "#000", T: "#FFF"},
+  fontSize: {D: 25, S: 12, N: 24, V: 18, T: 20},
+  lineSpacing: {D: 2, S: 2, N: 2, V: 2, T: 2},
+  fontFamily: {D: 'Arial', S: 'Arial', N: 'Arial', B: 'Arial', V: 'Arial', T: 'Arial'},
+  textAlign: {D: 'left', S: 'left', N: 'left', B: 'left', V: 'left', T: 'left'},
+  fontColor: {D: '#000', S: '#FFF', N: '#6c6', V: '#fff', T: '#000'},
+  backgroundFillColor: {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(0,0,0,0.7)', V: 'rgba(0,0,0,0.7)', T: 'rgba(0,0,0,0)'},
+  backgroundStrokeColor: {D: 'black', S: 'rgba(0,0,0,0.6)', N: 'rgba(0,0,0,0)', V: 'rgba(0,0,0,0)', T: 'rgba(0,0,0,0)'},
+  backgroundStrokeStyle: {D: 2, S: 1, N: 1, V: 1, T: 0},
+  backgroundBorderRadius: {D: 10, S: 3, N: 3, V: 3, T: 0},
+  layerPriority: {D: 10, S: 5, N: 5, V: 5, T: 10},
+  maxWidth: {D: 300, S: 300, N: 180, V: 100, T: 100},
+  maxLength: {D: 100, S: 100, N: 30, V: 30, T: 100}
+}
+

 module.exports = class Label extends CocoClass
   @STYLE_DIALOGUE = 'dialogue'  # A speech bubble from a script
@@ -8,6 +31,7 @@
   # We might want to combine 'say' and 'name'; they're very similar
   # Nick designed 'say' based off of Scott's 'name' back when they were using two systems
   @STYLE_VAR = 'variable'
+  @STYLE_TEXT = 'text' # "Direct" label on the sprite pos without following screen caps

   @BITMAP_SPACE = 20 # Size extenstion for speech bubbles for box pointers

@@ -54,9 +78,10 @@

   update: ->
     return unless @text and @sprite.sprite
-    offset = @sprite.getOffset? (if @style in ['dialogue', 'say'] then 'mouth' else 'aboveHead')
-    offset ?= { x: 0, y: 0 }  # temp (if not Lank)
-    offset.y += 10 if @style is 'variable'
+    offset = {x: 0, y: 0}  # default and for Label.STYLE_TEXT
+    if @sprite.getOffset
+      offset = @sprite.getOffset(if @style in [Label.STYLE_DIALOGUE, Label.STYLE_SAY] then 'mouth' else 'aboveHead')
+    offset.y += 10 if @style is Label.STYLE_VAR
     rotation = @sprite.getRotation()
     offset.x *= -1 if rotation >= 135 or rotation <= -135
     @label.x = @background.x = @sprite.sprite.x + offset.x
@@ -86,32 +111,17 @@

   buildLabelOptions: ->
     o = {}
-    st = {dialogue: 'D', say: 'S', name: 'N', variable: 'V'}[@style]
-    o.marginX = {D: 5, S: 6, N: 3, V: 0}[st]
-    o.marginY = {D: 6, S: 4, N: 3, V: 0}[st]
-    o.fontWeight = {D: 'bold', S: 'bold', N: 'bold', V: 'bold'}[st]
-    o.shadow = {D: false, S: true, N: true, V: true}[st]
-    o.shadowColor = {D: '#FFF', S: '#000', N: '#000', V: "#000"}[st]
-    o.fontSize = {D: 25, S: 12, N: 24, V: 18}[st]
-    o.lineSpacing = 2
-    o.fontFamily = {D: 'Arial', S: 'Arial', N: 'Arial', B: 'Arial', V: 'Arial'}[st]
-    o.textAlign = "left" # it's disabled for customizing on purpose, we don't need it now and need to rework "bubble" forming for that
-    o.fontColor = {D: '#000', S: '#FFF', N: '#6c6', V: '#6c6'}[st]
-    if @style is 'name' and @sprite?.thang?.team is 'humans'
+    st = DEFAULT_STYLE_CHAR[@style]
+    for prop, styleValues of DEFAULT_LABEL_OPTIONS
+      o[prop] = styleValues[st]
+    if @style isnt Label.STYLE_TEXT
+      # 'text' is only non limited
+      o.maxWidth = Math.max(@camera.canvasWidth / 2 - 100, o.maxWidth)
+    if @style is Label.STYLE_NAME and @sprite?.thang?.team is 'humans'
       o.fontColor = '#c66'
-    else if @style is 'name' and @sprite?.thang?.team is 'ogres'
+    else if @style is Label.STYLE_NAME and @sprite?.thang?.team is 'ogres'
       o.fontColor = '#66c'
-    else if @style is 'variable'
-      o.fontColor = '#fff'
-
-    o.backgroundFillColor = {D: 'white', S: 'rgba(0,0,0,0.4)', N: 'rgba(0,0,0,0.7)', V: 'rgba(0,0,0,0.7)'}[st]
-    o.backgroundStrokeColor = {D: 'black', S: 'rgba(0,0,0,0.6)', N: 'rgba(0,0,0,0)', V: 'rgba(0,0,0,0)'}[st]
-    o.backgroundStrokeStyle = {D: 2, S: 1, N: 1, V: 1}[st]
-    o.backgroundBorderRadius = {D: 10, S: 3, N: 3, V: 3}[st]
-    o.layerPriority = {D: 10, S: 5, N: 5, V: 5}[st]
-    o.maxWidth = {D: 300, S: 300, N: 180, V: 100}[st]
-    o.maxWidth = Math.max @camera.canvasWidth / 2 - 100, o.maxWidth
-    o.maxLength = {D: 100, S: 100, N: 30, V: 30}[st]
+    # We allow to override options from thang.sayLabelOptions
     o = _.merge(o, @labelOptions)
     o.fontDescriptor = "#{o.fontWeight} #{o.fontSize}px #{o.fontFamily}"
     multiline = @addNewLinesToText _.string.prune(@text, o.maxLength), o.fontDescriptor, o.maxWidth
@@ -189,7 +199,7 @@
       g.quadraticCurveTo(0, 0, radius, 0)
     else
       # Just draw a rounded rectangle
-      o.hpos ?= "middle"
+      o.hPos ?= "middle"
       o.vPos ?= "middle"
       pointerHeight = 0
       g.drawRoundRect(o.label.x - o.marginX, o.label.y - o.marginY, w, h, o.backgroundBorderRadius)
@@ -206,6 +216,17 @@
       background.regY = h + pointerHeight
     else if o.vPos is "top"
       background.regY = -pointerHeight
+
+    if @style is Label.STYLE_TEXT
+      if o.hPos is 'left'
+        background.regX = 0
+      else if o.hPos is 'right'
+        background.regX = o.textWidth
+      background.regY = h / 2
+      if o.vPos is "bottom"
+        background.regY = h
+      else if o.vPos is "top"
+        background.regY = 0

     o.label.regX = background.regX - o.marginX
     o.label.regY = background.regY - o.marginY


Small fix

Small fix

I just realised that for some reason I didn't translate the PS9 version of Poe. Solved.

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -824,7 +824,7 @@
             name: "Piloto del escuadrón Guja"
             ship: "Defensor TIE"
         "Poe Dameron (PS9)":
-            text: """When attacking or defending, if you have a focus token, you may change 1 of your %FOCUS% results to a %HIT% or %EVADE% result."""
+            text: """Cuando ataques o te defiendas, si tienes una ficha de Concentración, puedes cambiar 1 de tus resultados %FOCUS% por un resultado %HIT% o %EVADE%."""
             ship: "T-70 Ala-X"
         "Resistance Sympathizer":
             name: "Simpatizante de la Resistencia"


socialapicontroller: add test fix to browser focus check

--- client/Main/socialapicontroller.coffee
+++ client/Main/socialapicontroller.coffee
@@ -175,6 +175,13 @@
   # realtime messages to the individual messages
   # if the message is mine and current window has focus.
   isFromThisBrowser = (message) ->
+
+    # selenium doesn't put focus into the
+    # spawned browser, it's causing problems.
+    # Probably a temporary fix.
+    # This flag needs to be set before running
+    # tests. ~Umut
+    return no  if KD.isTesting

     isMyPost  = KD.isMyPost message
     isFocused = KD.singletons.windowController.isFocused()


social: fix xhr request callback return for request status for client and server errors

--- client/Main/socialapicontroller.coffee
+++ client/Main/socialapicontroller.coffee
@@ -540,7 +540,7 @@
       xhr.onreadystatechange = =>
         # 0     - connection failed
         # >=400 - http errors
-        return if xhr.status is 0 or xhr.status >= 400
+        if xhr.status is 0 or xhr.status >= 400
           return callback err

         return if xhr.readyState isnt 4


Client: /pbv command error checking.

Client: /pbv command error checking.

--- client/app/js/concerns/commands.coffee
+++ client/app/js/concerns/commands.coffee
@@ -56,6 +56,7 @@
   messages = []
   total = 0
   for array in pokemon
+    continue  if array.length == 0
     [speciesName, formeName] = array
     if formeName == 'default'
       pokemonName = speciesName
@@ -69,8 +70,11 @@
       name: speciesName, forme: formeName)
     total += pbv
     messages.push("#{pokemonName}: #{pbv}")
-  messages.push("Total: #{total}")  if pokemon.length > 1
-  PokeBattle.chatView.updateChat("PBV: #{messages.join(' | ')}")
+  if messages.length == 0
+    PokeBattle.chatView.updateChat("PBV error: Enter valid Pokemon.")
+  else
+    messages.push("Total: #{total}")  if messages.length > 1
+    PokeBattle.chatView.updateChat("PBV: #{messages.join(' | ')}")

 # Finds the most lenient match possible.
 findPokemon = (pokemonName) ->
@@ -81,5 +85,5 @@
       name += formeName  unless formeName == 'default'
       name = name.toLowerCase().replace(/[^a-zA-Z0-9]+/g, '')
       return [speciesName, formeName]  if pokemonName == name
-  # Return last match
-  [speciesName, formeName]
+  # Return blank match
+  []


Fix event delete from list view, refs #357

--- client/app/views/list_view_item.coffee
+++ client/app/views/list_view_item.coffee
@@ -18,7 +18,7 @@
     deleteModel: ->
         return unless confirm t "are you sure"
         @$el.spin 'tiny'
-        @model.destroy
+        @model.event.destroy
             error: ->
                 alert 'server error'
                 @$el.spin()


remove useless User#isActive method

remove useless User#isActive method

--- app/models/user-model.coffee
+++ app/models/user-model.coffee
@@ -33,8 +33,5 @@
       _.find @memberships(), (membership) ->
         membership.groupId == group.id && membership.isAdmin

-    isActive: ->
-      !@archivedAt
-
     isConfirmed: ->
       !!@confirmedAt


Fixing code

Fixing code

--- source/core/attributes.coffee
+++ source/core/attributes.coffee
@@ -41,11 +41,7 @@
           attributes = item[key]
           if @default?.children?[index]?[key]?
             attributes = Atoms.Core.Helper.mix item[key], @default.children?[index]?[key]
-
-          instance = @instance type, class_name, attributes
-          @children.push instance
-          @[key] = [] unless @[key]?
-          @[key].push instance
+          @children.push @instance type, class_name, attributes

   instance: (type, class_name, attributes) ->
     attributes.parent = @


Stop linting C++ for now.

Stop linting C++ for now.


Bring back linting.

Bring back linting.


Remove no longer used rest api wrapper

Remove no longer used rest api wrapper

--- coffee/turbase.coffee
+++ coffee/turbase.coffee
@@ -1,115 +0,0 @@
-#
-# Nasjonal Turbase MongoDB wrapper
-#
-# @author Hans Skjerpen
-# @author Hans Kristian Flaatten
-#
-
-events   = require 'events'
-util     = require 'util'
-mongodb  = require 'mongodb'
-ObjectID = mongodb.ObjectID
-database = require './database'
-
-#
-# Constructor
-#
-# @param dbUri {code string} database uri ¨
-# @param cb - {code function} callback function (err, data)
-#
-# Thsi method will set up database connection before returning callback. It will
-# allso emmit  'error' and 'ready' events accordingly.
-#
-Turbase = (dbUri, cb) ->
-  events.EventEmitter.call @
-
-  @db = null
-
-  $this = @
-  database.connect null, (err, db) ->
-    cb err if err and typeof cb is 'function'
-    return $this.emit 'error', err if err
-
-    $this.db = db
-    cb db if typeof cb is 'function'
-    return $this.emit 'ready', db
-
-  @
-
-# Initiate event emmitting
-util.inherits Turbase, events.EventEmitter
-
-exports.getTypes = (req, res) ->
-res.jsonp
-  types: ['aktiviteter', 'bilder', 'områder', 'steder', 'turer']
-  count: 5
-
-exports.get = (req, res) ->
-db.collection req.params.object, (err, collection) ->
-  # @TODO move to propper error handling
-  return res.jsonp "{'error':#{err}}" if err
-  return res.jsonp "{'error': 'Mangler id'}" if not req.params.id
-
-  collection.findOne _id: ObjectID.createFromHexString(req.params.id), (err, doc) ->
-    # @TODO move to propper error handling
-
-    delete doc.privat if req.eier.toLowerCase() isnt doc?.eier?.toLowerCase()
-
-    return res.jsonp err if err
-    return res.jsonp doc if doc
-    # @TODO this should 404 Not Found
-    return res.jsonp {}
-
-exports.list = (req, res) ->
-# @TODO parse arguments in server.coffee
-limit  = Math.min(parseInt(req?.query?.limit) || 10, 50)
-offset = parseInt(req?.query?.offset) || 0
-query  = {endret:{$gt:req.query.after}} if req?.query?.after
-
-db.collection req.params.object, (err, collection) ->
-  # @TODO move to propper error handling
-  return res.jsonp "{'error':#{err}}" if err
-
-  opts =
-    limit: limit
-    skip: offset
-    sort: "endret"
-
-  # @TODO add endret paramter
-  # @TODO add support for queries
-  collection.find(query,{eier:1,navn:1,endret:1},opts).toArray (err, result) ->
-      # @TODO move to propper error handling
-      return res.jsonp err if err
-      return res.jsonp documents: result, count: result.length if result
-      # @TODO what if neither?
-
-exports.insert = (req, res) ->
-  #dynamisk collection via req.params.object
-  db.collection req.params.object, (err, collection) ->
-    res.jsonp "{'error':#{err}}" if err
-    collection.insert req.data, {safe: true}, (err, records) ->
-      res.jsonp err if err
-      res.jsonp records
-
-exports.update = (req, res) ->
-  db.collection req.params.object, (err, collection) ->
-    collection.update {'_id':mongodb.ObjectID(req.params.id), 'eier': req.eier}, {$set: req.data}, {safe: true}, (err, records) ->
-      if not err
-        collection.find({'_id':mongodb.ObjectID(req.params.id), 'eier': req.eier}).toArray (err, result) ->
-          res.jsonp result if result
-          res.jsonp err if err
-      else
-        res.jsonp err
-
-exports.updates = (req, res) ->
-  res.jsonp "Updates #{req.params.object} with data: #{req.query.data}"
-
-exports.delete = (req, res) ->
-  # Legg til eier-felt ved spesifisering av hva som skal slettes, slik at kun eier kan slette.
-  # Eier-id bør ikke være api-key, da denne bør kunne byttes.
-  db.collection req.params.object, (err, collection) ->
-    res.jsonp "{'error':#{err}}" if err
-    collection.remove {'_id':mongodb.ObjectID(req.params.id), 'eier': req.eier}, {safe: true}, (err, status) ->
-      res.jsonp "{'_id':#{req.params.id}, 'action':'delete', 'status':#{status}}" if not err
-      res.jsonp JSON.stringify err if err
-


Autofocus on name field for CTA takeover

--- components/artist_page_cta/view.coffee
+++ components/artist_page_cta/view.coffee
@@ -49,6 +49,7 @@
     @$('.main-layout-container').html overlayTemplate
       artist: @artist
       afterAuthPath: @afterAuthPath
+    @$(".artist-page-cta-overlay__register input[name='name']").focus()
     @$('.artist-page-cta-overlay__close').on 'click', @closeOverlay

   closeOverlay: (e) =>


added a null check to getRequestUserAndProject

--- services/web/app/coffee/managers/SecurityManager.coffee
+++ services/web/app/coffee/managers/SecurityManager.coffee
@@ -169,6 +169,9 @@

 getRequestUserAndProject = (req, res, options, callback)->
 	project_id = req.params.Project_id
+	if !project_id?
+		logger.log project_id:project_id, options:options, url:req?.url, "no project_id trying to getRequestUserAndProject"
+		return res.send 422
 	Project.findById project_id, 'name owner_ref readOnly_refs collaberator_refs publicAccesLevel archived', (err, project)=>
 		if err?
 			logger.err err:err, "error getting project for security check"


MINOR WIP meta-data mithril

MINOR WIP meta-data mithril

--- app/static/coffee/Components/meta.coffee
+++ app/static/coffee/Components/meta.coffee
@@ -8,35 +8,34 @@
         return instance

 class Meta_Data
-    headers = []
-    genomes = []
-    meta=(data)->
-        headers = data.head.vars
-        genomes = data.results.bindings
-        {
-            headers: headers
-            genomes: genomes
-        }
+    data = {}
+    console.log("Meta_Data")
+    meta=(response)->
+        console.log("meta")
+        data.headers = response.head.vars
+        data.genomes = response.results.bindings
+        return data

-    controller: ->
-        return m.request(
+    controller: ()->
+        console.log("controller")
+        m.request(
             method: "GET",
             url: 'http://10.139.14.121:5000/mithril/meta'
             datatype: 'json'
             type: meta)
-
-    view: (controller) ->
+    views = {}
+    views.table= () ->
+        console.log("table")
         #console.log(JSON.stringify(controller))
         [
-            header.view()
             m("div", {class:'container', id:'meta'},[
                 m("table",[
                     m("tr", [
-                        m("td", [item]) for item in headers
+                        m("td", [item]) for item in data.headers
                     ])
-                    for binding in genomes
+                    for binding in data.genomes
                         m("tr",[
-                            for item in headers
+                            for item in data.headers
                                 try
                                     m("td", binding[item]["value"])
                                 catch
@@ -44,4 +43,10 @@
                         ])
                 ])
             ])
-        ]+        ]
+    view: () ->
+        console.log("view")
+        [
+            header.view()
+            views.table()
+        ]


Allowing eval with a sandbox, but defaulting to global context

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -10,6 +10,7 @@
 path             = require 'path'
 {Lexer,RESERVED} = require './lexer'
 {parser}         = require './parser'
+vm               = require 'vm'

 # TODO: Remove registerExtension when fully deprecated.
 if require.extensions
@@ -77,21 +78,21 @@
 # The CoffeeScript REPL uses this to run the input.
 exports.eval = (code, options = {}) ->
   return unless code = code.trim()
-  {Script} = require 'vm'
+  Script = vm.Script
   if Script
     if options.sandbox?
-      if options.sandbox instanceof sandbox.constructor
+      if options.sandbox instanceof Script.createContext().constructor
         sandbox = options.sandbox
-        sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
       else
         sandbox = Script.createContext()
         sandbox[k] = v for own k, v of options.sandbox
+      sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
     else
       sandbox = global
     sandbox.__filename = options.filename || 'eval'
     sandbox.__dirname  = path.dirname sandbox.__filename
     # define module/require only if they chose not to specify their own
-    if sandbox isnt global and not (sandbox.module or sandbox.require)
+    unless sandbox isnt global or sandbox.module or sandbox.require
       Module = require 'module'
       sandbox.module  = _module  = new Module(options.modulename || 'eval')
       sandbox.require = _require = (path) ->  Module._load path, _module, true


Add default sanitize options

Add default sanitize options

--- services/web/config/settings.defaults.coffee
+++ services/web/config/settings.defaults.coffee
@@ -484,4 +484,24 @@
 	# 	{imageName: 'texlive-full:2016.1', imageDesc: 'Legacy SL TeXLive 2016'}
 	# 	{imageName: 'texlive-full:2015.1', imageDesc: 'Legacy SL TeXLive 2015'}
 	# 	{imageName: 'texlive-full:2014.2', imageDesc: 'Legacy SL TeXLive 2014.2'}
-	# ]+	# ]
+
+	# module options
+	# ----------
+	modules:
+		sanitize:
+			options:
+				allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe', 'img', 'figure', 'figcaption' ]
+				allowedAttributes:
+					'a': [ 'href', 'name', 'target', 'class' ]
+					'div': [ 'class', 'id', 'style' ]
+					'h1': [ 'class', 'id' ]
+					'h2': [ 'class', 'id' ]
+					'h3': [ 'class', 'id' ]
+					'h4': [ 'class', 'id' ]
+					'h5': [ 'class', 'id' ]
+					'h6': [ 'class', 'id' ]
+					'figure': [ 'class', 'id', 'style']
+					'figcaption': [ 'class', 'id', 'style']
+					'iframe': [ 'allowfullscreen', 'frameborder', 'height', 'src', 'width' ]
+					'img': [ 'alt', 'class', 'src', 'style' ]


Tag::watch() bug with omitting changes on the main tag fixed

--- Element/element/tag/query.coffee
+++ Element/element/tag/query.coffee
@@ -438,12 +438,13 @@
 			return

 		(node) ->
+			node._checkWatchers = CHECK_WATCHERS_THIS
 			tmp = node
-			node._checkWatchers |= CHECK_WATCHERS_THIS
-			while tmp = tmp._parent
+			while tmp
 				if tmp._checkWatchers & CHECK_WATCHERS_CHILDREN
 					break
 				tmp._checkWatchers |= CHECK_WATCHERS_CHILDREN
+				tmp = tmp._parent

 			unless pending
 				setImmediate updateWatchers


Use util's inspect and do not forward if it's not attached to out

--- components/Log.coffee
+++ components/Log.coffee
@@ -1,10 +1,17 @@
 noflo = require("noflo")
+_ = require("underscore")
 _s = require("underscore.string")
 print = require("node-print")
+util = require("util")

 format = "%13s | %s"
+padding = "              | "
 log = []
 count = 0
+options =
+  showHidden: false
+  depth: 2
+  colors: false

 flush = ->
   console.log "---------- NEW STREAM ----------"
@@ -24,6 +31,9 @@

 display = (log) ->
   for packet, i in log
+    packet = util.inspect packet,
+      options.showHidden, options.depth, options.colors
+    packet = packet.replace /\n/g, "\n#{padding}"
     print.pf format, "DATA", packet
     delete log[i]

@@ -40,8 +50,14 @@
   constructor: ->
     @inPorts =
       in: new noflo.Port
+      options: new noflo.Port
     @outPorts =
       out: new noflo.Port
+
+    @inPorts.options.on "data", (options) =>
+      if _.isObject options
+        for own key, value of options
+          options[key] = value

     @inPorts.in.on "connect", =>
       count++
@@ -52,19 +68,19 @@
       here = @locate()
       here[group] = []
       @groups.push group
-      @outPorts.out.beginGroup group
+      @outPorts.out.beginGroup group if @outPorts.out.isAttached()

     @inPorts.in.on "data", (data) =>
       here = @locate()
       here.push data
-      @outPorts.out.send data
+      @outPorts.out.send data if @outPorts.out.isAttached()

     @inPorts.in.on "endgroup", (group) =>
       @groups.pop()
-      @outPorts.out.endGroup()
+      @outPorts.out.endGroup() if @outPorts.out.isAttached()

     @inPorts.in.on "disconnect", =>
-      @outPorts.out.disconnect()
+      @outPorts.out.disconnect() if @outPorts.out.isAttached()

       log.push @cache
       count--


make help show motions

make help show motions

--- assets/js/keyBindings.coffee
+++ assets/js/keyBindings.coffee
@@ -391,12 +391,13 @@

     # build table to visualize hotkeys
     buildTable: (keyMap) ->
-      table = $('<table>').addClass('keybindings-table theme-bg-secondary')
-
-      buildTableContents = (bindings, onto) ->
+      buildTableContents = (bindings, onto, recursed=false) ->
         for k,v of bindings
           if k == 'MOTION'
-            keys = ['<MOTION>']
+            if recursed
+              keys = ['<MOTION>']
+            else
+              keys = []
           else
             keys = keyMap[k]
             if not keys
@@ -412,12 +413,20 @@

           display_cell = $('<td>').css('width', '100%').html v.display
           if v.bindings
-            buildTableContents v.bindings, display_cell
+            buildTableContents v.bindings, display_cell, true
           row.append display_cell

           onto.append row
-      buildTableContents keyDefinitions, table
-      return table
+
+      tables = $('<div>')
+
+      for [label, definitions] in [['Actions', keyDefinitions], ['Motions', motionDefinitions]]
+        tables.append($('<h5>').text(label).css('margin', '5px 10px'))
+        table = $('<table>').addClass('keybindings-table theme-bg-secondary')
+        buildTableContents definitions, table
+        tables.append(table)
+
+      return tables

     renderModeTable: (mode) ->
       if not @modebindingsDiv


Key bindings; rewire vomnibar (tweaked).

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -135,9 +135,6 @@
   new InsertMode permanent: true
   Scroller.init()

-openVomnibar = ({sourceFrameId, registryEntry}) ->
-  Utils.invokeCommandString registryEntry.command, [sourceFrameId, registryEntry] if DomUtils.isTopFrame()
-
 #
 # Complete initialization work that sould be done prior to DOMReady.
 #
@@ -153,7 +150,8 @@
     # A frame has received the focus.  We don't care here (the Vomnibar/UI-component handles this).
     frameFocused: ->
     checkEnabledAfterURLChange: checkEnabledAfterURLChange
-    openVomnibar: openVomnibar
+    openVomnibar: ({sourceFrameId, registryEntry}) ->
+      Utils.invokeCommandString registryEntry.command, [sourceFrameId, registryEntry] if DomUtils.isTopFrame()

   chrome.runtime.onMessage.addListener (request, sender, sendResponse) ->
     # In the options page, we will receive requests from both content and background scripts. ignore those


Add Models.Post.fetch

Add Models.Post.fetch

--- assets/javascripts/models/post.js.coffee
+++ assets/javascripts/models/post.js.coffee
@@ -21,6 +21,31 @@
       @trigger('create:success', post, xhr)
       options.success?(post, xhr)
     , options.middleware
+
+  @fetch: (params, options = {}) ->
+    unless options.client
+      return HTTP.TentClient.find entity: (params.entity || TentClient.config.current_entity), (client) =>
+        @fetch(params, _.extend(options, {client: client}))
+
+    path = if params.entity
+      "/posts/#{encodeURIComponent params.entity}/#{params.id}"
+    else
+      "/posts/#{params.id}"
+
+    _params = _.clone(params)
+    delete _params.id
+    delete _params.entity
+    options.client.get path, _params, (res, xhr) =>
+      if xhr.status != 200
+        @trigger("fetch:failed", params, res, xhr)
+        options.error?(res, xhr)
+        return
+
+      return if @find(params, _.extend(options, {fetch:false}))
+      post = new @(res)
+
+      @trigger("fetch:success", params, post, xhr)
+      options.success?(post, xhr)

   @validate: (attrs, options = {}) ->
     errors = []


activity: double-post issue fixed, .once was buggy

--- client/Framework/core/KDEventEmitter.coffee
+++ client/Framework/core/KDEventEmitter.coffee
@@ -24,7 +24,7 @@
     # listener callbacks.
     else if callback and registry[eventName]
       cbIndex = registry[eventName].indexOf callback
-      registry[eventName].splice 1, cbIndex if cbIndex >= 0
+      registry[eventName].splice cbIndex, 1 if cbIndex >= 0
     else
       registry[eventName] = []

@@ -80,7 +80,7 @@
   once:(eventName, callback) ->
     _callback = =>
       args = [].slice.call arguments
-      @unsubscribe eventName, _callback
+      @off eventName, _callback
       callback.apply @, args

     @on eventName, _callback


Removed coffeescript code for unused buttons.

Tagbox ---

--- client/coffeescript/templates/tagbox.coffee
+++ client/coffeescript/templates/tagbox.coffee
@@ -23,6 +23,18 @@

   tagging_post: ->
     Session.equals('tagging', true) and Session.equals('current_post', @_id)
+
+  add_tag: (id, tag_text) ->
+    Session.set('filter_text', '')
+    if tag_text != ''
+      q = {'$set': {}}
+      q['$set']["my_tags.#{tag_text}"] = 1
+      Posts.update({ '_id': id}, q)
+      #XXX Session is not yet reactive with arrays or objects.
+      temp = Session.get('suggested_tags')
+      temp.remove(tag_text)
+      Session.set('suggested_tags',temp.clone())
+    Meteor.flush()

   events: {

@@ -98,22 +110,5 @@
       if not event.isImmediatePropagationStopped()
         Template.tagbox.add_tag(@_id, Session.get('filter_text'))
       return false
-
-    "click button[name='done_tagging']": (event) ->
-      if not event.isImmediatePropagationStopped()
-        Session.set('tagging', false)
-      return false
   }
-
-  add_tag: (id, tag_text) ->
-    Session.set('filter_text', '')
-    if tag_text != ''
-      q = {'$set': {}}
-      q['$set']["my_tags.#{tag_text}"] = 1
-      Posts.update({ '_id': id}, q)
-      #XXX Session is not yet reactive with arrays or objects.
-      temp = Session.get('suggested_tags')
-      temp.remove(tag_text)
-      Session.set('suggested_tags',temp.clone())
-    Meteor.flush()
 )


Defensive check

Defensive check

--- client/ide/lib/views/tabview/ideview.coffee
+++ client/ide/lib/views/tabview/ideview.coffee
@@ -594,7 +594,7 @@

     selector  = '.kdtabhandle:not(.visible-tab-handle)'
     $target   = $(event.originalEvent.target).closest(selector)
-    index     = $target.index()
+    index     = $target?.index()

     index = null  if index < 0


container should be @context.viewport.lanes

--- client/lanes/components/shared/Tooltip.cjsx
+++ client/lanes/components/shared/Tooltip.cjsx
@@ -1,10 +1,10 @@
 class Lanes.Components.Tooltip extends Lanes.React.Component

-    TTProps: ['placement', 'positionLeft', 'positionTop', 'arrowOffsetLeft', 'arrowOffsetTop']
+    TTProps: ['id', 'placement', 'positionLeft', 'positionTop', 'arrowOffsetLeft', 'arrowOffsetTop']

     render: ->
         ttprops = _.pick(@props, @TTProps...)
         tooltip = <BS.Tooltip {...ttprops}>{@props.content}</BS.Tooltip>
-        <BS.OverlayTrigger overlay={tooltip} container={@context.viewport.root}>
+        <BS.OverlayTrigger overlay={tooltip} placement='left' container={@context.viewport.lanes}>
             {@props.children}
         </BS.OverlayTrigger>