disable extra tests

disable extra tests

--- src/test.coffee
+++ src/test.coffee
@@ -18,8 +18,8 @@
 {test_render} = require './render/test_render'

 run_tests = (rx) ->
-  test_god(test, rx)
-  test_world(test, rx)
+  # test_god(test, rx)
+  # test_world(test, rx)
   test_game(test, rx)
   test_layout(test, rx)
   test_render(test, rx)


Register packageDependencies before cleaning

--- src/cleaner.coffee
+++ src/cleaner.coffee
@@ -19,11 +19,11 @@

   getDependencies: (modulePath, allDependencies) ->
     try
-      {dependencies} = CSON.readFileSync(CSON.resolve(path.join(modulePath, 'package'))) ? {}
+      {dependencies, packageDependencies} = CSON.readFileSync(CSON.resolve(path.join(modulePath, 'package'))) ? {}
     catch error
       return

-    _.extend(allDependencies, dependencies)
+    _.extend(allDependencies, dependencies, packageDependencies)

     modulesPath = path.join(modulePath, 'node_modules')
     for installedModule in fs.list(modulesPath) when installedModule isnt '.bin'


Remove class from log line number to make template simpler (closes #112)

--- assets/scripts/app/views/job.coffee
+++ assets/scripts/app/views/job.coffee
@@ -87,7 +87,7 @@

       target.closest('.fold').toggleClass('open')

-      if target.is('.log-line-number')
+      if target.is('a') && target.attr('id').match(/^L\d+$/)
         path = target.attr 'href'
         Travis.app.get('router').route(path)
         event.stopPropagation()
@@ -183,7 +183,7 @@
         unless payload.append
           pathWithNumber = "#{url}#L#{number}"
           p = document.createElement('p')
-          p.innerHTML = '%@%@'.fmt(pathWithNumber, number, number, line)
+          p.innerHTML = "#{number}#{line}"
           line = p

         if payload.fold && !payload.foldContinuation


Fixed clearing log

Fixed clearing log

--- assets/scripts/app/views/log.coffee
+++ assets/scripts/app/views/log.coffee
@@ -34,7 +34,7 @@
       parts.removeArrayObserver(@, didChange: 'partsDidChange', willChange: 'noop')

     versionDidChange: (->
-      @rerender() if @get('inDOM')
+      @rerender() if @get('state') == 'inDOM'
     ).observes('log.version')

     logDidChange: (->


Remove unused Javascript file

Remove unused Javascript file

--- app/assets/javascripts/application.js.coffee
+++ app/assets/javascripts/application.js.coffee
@@ -4,7 +4,6 @@
 #= require underscore
 #= require turbolinks
 #= require websocket_rails/main
-#= require store
 #= require boards
 #= require swimlanes
 #= require columns


merit order: tipsy detail tooltip

merit order: tipsy detail tooltip

--- app/assets/javascripts/d3/merit_order.coffee
+++ app/assets/javascripts/d3/merit_order.coffee
@@ -82,7 +82,7 @@
         attr("transform", "rotate(270)")

       @svg.append("svg:text").
-              text('Installed Capacity [GW]').
+              text('Installed Capacity [MW]').
               attr("x", @width / 2).
               attr("y", @height + 30).
               attr("text-anchor", "middle").
@@ -105,15 +105,16 @@
         attr("width", (d) => @x d.value_x()).
         on("mouseover", ->
           item = d3.select(this)
-          key = item.attr("data-rel")
-          $("svg.legend text.#{key}").css("text-decoration", "underline")
           item.transition().attr("fill", -> d3.rgb(item.attr("data-original-color")).brighter(1))
         ).
         on("mouseout", ->
-          $("svg.legend text").css("text-decoration", "none")
           item = d3.select(this)
           item.transition().attr("fill", -> item.attr("data-original-color"))
         )
+
+      $('rect.merit_order_node').tipsy
+        gravity: 'se'
+        html: true

       # add legend
       legends = @svg.selectAll("svg.legend").
@@ -168,7 +169,16 @@
           else
             @height - h
         ).
-        attr("x", (d) => @x(d.get 'x_offset'))
+        attr("x", (d) => @x(d.get 'x_offset')).
+        attr("title", (d) ->
+          html = I18n.t "output_element_series.#{d.get('key')}"
+          html += "
"
+          html += "Installed capacity: #{Metric.autoscale_value(d.value_x() * 1000000, 'MW', 2)}"
+          html += "
"
+          html += "Operating costs: #{Metric.autoscale_value d.value_y(), 'euro', 2}"
+          html
+        )
+

 class D3.merit_order.NodeList extends Backbone.Collection
   model: D3.merit_order.Node


make sure remote form is remote

make sure remote form is remote

--- app/assets/javascripts/droom/forms.js.coffee
+++ app/assets/javascripts/droom/forms.js.coffee
@@ -231,6 +231,8 @@
     constructor: (element, opts) ->
       @_form = $(element)
       @_options = $.extend {}, opts
+      @_form.attr('data-remote', true)
+      @_form.attr('data-type', 'html')
       @_form.on 'ajax:beforeSend', @pend
       @_form.on 'ajax:error', @fail
       @_form.on 'ajax:success', @receive


Add unsubscribe method

Add unsubscribe method

--- src/vent.coffee
+++ src/vent.coffee
@@ -59,6 +59,7 @@
         @_queues = {}
         @_exchanges = {}
         @_auto_purge = []
+        @_subscribed_queues = []

         process.on 'SIGINT', @_cleanup

@@ -125,12 +126,27 @@
         @_when_queue(sub_options)
             .then (queue) =>
                 queue.subscribe(listener)
+                @_remember_subscription(event, listener, queue)
                 @emit('bound', {queue})

             .fail (err) ->
                 logger.error({err}, "subscribing") if err
                 @emit('error', err)
         @
+
+    _remember_subscription: (event, listener, queue) ->
+        unless @_subscribed_queues[event]
+            @_subscribed_queues[event] = []
+        @_subscribed_queues[event].push([listener, queue])
+
+    unsubscribe: (event, listener) ->
+        return unless @_subscribed_queues[event]
+        @_subscribed_queues[event] = @_subscribed_queues[event].filter (tuple) ->
+            {l, queue} = tuple
+            if l is listener
+                queue.destroy()
+                return false
+            true

     subscribe_stream: (event, options, cb) ->
         """


Don't reference Node for the type number (for IE incompatabilities).

--- src/view.coffee
+++ src/view.coffee
@@ -48,7 +48,7 @@

     parse = (node) =>
       unless node in skipNodes
-        if node.nodeType is Node.TEXT_NODE
+        if node.nodeType is 3
           parser = Rivets.TextTemplateParser

           if delimiters = @config.templateDelimiters


Fix empty indent problem

Fix empty indent problem

--- src/view.coffee
+++ src/view.coffee
@@ -1088,7 +1088,7 @@
             @minDistanceToBase[line].below = 0

           # The first line of an indent is often empty; this is the desired behavior
-          else if @model.type is 'indent'
+          else if @model.type is 'indent' and line is 0
             @minDistanceToBase[line].above = 0
             @minDistanceToBase[line].below = 0


Fix lint errors

Fix lint errors

--- src/xslt.coffee
+++ src/xslt.coffee
@@ -108,14 +108,14 @@

   stripDuplicateAttributes = (node, nodeName, closeTag) ->
     attrRegex = /([a-zA-Z0-9:\-]+)\s*=\s*("[^"]*")/g
-    collection = {};
-    parts = attrRegex.exec(node);
+    collection = {}
+    parts = attrRegex.exec(node)
     while parts
       collection[parts[1]] = parts[0]
       parts = attrRegex.exec(node)
     newStr = '<' + nodeName
     newStr += (' ' + val) for val of collection
-    newStr += (closeTag || '') + '>';
+    newStr += (closeTag || '') + '>'
     return newStr

   stripNullNamespaces = (node) -> node.replace(/xmlns\s*=\s*""/gi, '')


Table upload link only when results exist

Address #520

Link to the results page for each benchmark only when the benchmark
actually has more than 0 uploads.

--- _includes/coffee/benchmark_table.coffee
+++ _includes/coffee/benchmark_table.coffee
@@ -42,10 +42,9 @@
     the function
   ###
   (data, type, row) ->
-    link_html(
-      'simulations/' + make_id(row)
-      count_uploads_id(make_id(row), sim_data)
-    )
+    cond = (x) ->
+      if x > 0 then link_html('simulations/' + make_id(row), x) else '0'
+    cond(count_uploads_id(make_id(row), sim_data))

 event = (data, type, row) ->


Atom: adjust default fontsize

Atom: adjust default fontsize

--- opt/config.cson
+++ opt/config.cson
@@ -16,7 +16,6 @@
     preferredLineLength: 100
     tabLength: 2
     invisibles: {}
-    fontSize: 17
   "highlight-line": {}
   hashrocket:
     nodeExecutablePath: "/usr/local/bin/node"


guard against signing out

guard against signing out

--- app/lib/controllers/groups_controller.coffee
+++ app/lib/controllers/groups_controller.coffee
@@ -15,7 +15,10 @@
   onRerun: ->
     @next()
   onBeforeAction: ->
-    @next()
+    unless Meteor.userId()
+      @redirect 'home'
+    else
+      @next()

   action: ->
     @render()


s/object/body/g

s/object/body/g

--- app/js/physics.coffee
+++ app/js/physics.coffee
@@ -2,24 +2,24 @@

   window.world = new World "physics"

-  window.rect = new Rectangle 2, 2,
+  window.rect = new Rectangle 5, 2,
     position: [0, -1]
-    inverseMass: 1/4
     inverseMass: 0
-    velocity: [0, 0.0001]
     # angularVelocity: Math.PI/4
     color: "#F00"

   window.rect2 = new Rectangle 1, 1,
     position: [0,0.5]
-    angle: 0.2
     inverseMass: 1/1
-    # velocity: [0, -1.5]
     # angularVelocity: -Math.PI/2
+    acceleration: [0, -2]
     color: "#08F"

-  world.addObject rect
-  world.addObject rect2
+  world.addBody rect
+  world.addBody rect2
+
+window.debug = (msgs...) ->
+  # console.log msgs...

 class World
   constructor: (element) ->
@@ -29,7 +29,7 @@
     @stats = Utils.drawStats()
     @display = new Display @ctx, [0, 0], 50

-    @objects = []
+    @bodies = []
     @paused = true

     _.extend @ctx,
@@ -39,21 +39,21 @@
         if e.keyCode is 32 # space
           @paused = !@paused

-  addObject: (object) ->
-    @objects.push object
-
-  removeObject: (object) ->
-    @objects = _without(@objects, object)
+  addBody: (body) ->
+    @bodies.push body
+
+  removeBody: (body) ->
+    @bodies = _without(@bodies, body)

   update: =>
     return if @paused

     dt = @ctx.dt / 1000

-    for object in @objects
-      object.reset()
-      object.resetDebug()
-      object.integrate dt for object in @objects
+    for body in @bodies
+      body.reset()
+      body.resetDebug()
+      body.integrate dt

     @contacts = @narrowPhaseCollisions @broadPhaseCollisions()

@@ -81,14 +81,14 @@

       # @paused = true

-  # Naive version: returns all unique pairs of objects, nothing more. TODO: use
+  # Naive version: returns all unique pairs of bodies, nothing more. TODO: use
   # (memoized?) AABBs to build quadtree, then iterate and compare bounding boxes
   # for overlap.
   broadPhaseCollisions: ->
     pairs = []
-    for i in [0..(@objects.length-2)]
-      for j in [(i+1)..(@objects.length-1)]
-        pairs.push [@objects[i], @objects[j]]
+    for i in [0..(@bodies.length-2)]
+      for j in [(i+1)..(@bodies.length-1)]
+        pairs.push [@bodies[i], @bodies[j]]
     pairs

   narrowPhaseCollisions: (pairs) ->
@@ -98,9 +98,9 @@
     contacts

   draw: =>
-    for object in @objects
-      object.draw @display
-      object.drawDebug @display
+    for body in @bodies
+      body.draw @display
+      body.drawDebug @display
     @stats.update()

 class Display
@@ -518,7 +518,6 @@
       impulse = Vec.invert impulse
       @to.applyImpulse impulse, @position

-    console.log "sepV is now", @separatingVelocity()

   resolveInterpenetration: ->
     return if @depth <= 0


Move controls onto side

Move controls onto side

--- src/control.coffee
+++ src/control.coffee
@@ -1,13 +1,17 @@

 exports.control = (T, world) ->
   me = world.get("ME")
+  size = world.get("size")
   draw_button = (label, dir, delta) ->
     T.button {
       class: ["control", "dir", label]
       click: -> me.put(dir, me.get(dir) + delta)
     }, label
-  T.div {class: "controls"}, [
-    T.div {class: "controls"}, [
+  T.div {
+    class: "controls"
+    style: "margin-top: -#{size}px; margin-left: #{size}px;"
+  }, [
+    T.div {class: "instructions"}, [
       draw_button("West", "i", -1)
       draw_button("North", "j", -1)
       draw_button("South", "j", 1)


ownerDocument is null if element is document in Firefox

--- src/position.coffee
+++ src/position.coffee
@@ -11,7 +11,7 @@
     return [node, offset]

   @getIndex: (node, index, offsetNode = null) ->
-    while node != offsetNode and node.parentNode != node.ownerDocument.body
+    while node != offsetNode and node.ownerDocument? and node.parentNode != node.ownerDocument.body
       while node.previousSibling?
         node = node.previousSibling
         index += Scribe.Utils.getNodeLength(node)


Removed 0-score fish from search results

--- src/query.js.coffee
+++ src/query.js.coffee
@@ -64,7 +64,8 @@
     result = new Fishpond::Result
     result.score = fish.title.score(string)
     result.fish = fish
-    results.push result
+    if result.score > 0
+      results.push result

   results.sort (result1, result2) ->
     return 0 if result1.score == result2.score


Improved structure of filters in router

Improved structure of filters in router

--- app/lib/router.coffee
+++ app/lib/router.coffee
@@ -1,31 +1,26 @@
 # app/lib/router.coffee

-# client
+Router.configure
+  layoutTemplate: 'layout'
+  loadingTemplate: 'loading'
+  notFoundTemplate: 'notFound'

-if Meteor.isClient

-  Router.configure
-    layoutTemplate:   'layout'
-    notFoundTemplate: 'notFound'
-    loadingTemplate: 'loading'
+# filters

-  # login redirect filter
-
-  loginRedirectKey = 'loginRedirect'
-  Router.onBeforeAction 'loading'
-  Router.onBeforeAction (pause) ->
+Router._filters =
+  isLoggedIn: (pause) ->
+    loginRedirectKey = 'loginRedirect'

     if Meteor.loggingIn()
-      console.log "Logging in"
       pause()

-    else if not Meteor.userId()?
-      console.log "Not logged in"
-
-      if Router.current().path isnt '/'
+    else unless Meteor.userId()?
+      if Router.current().path != '/'
         Session.set loginRedirectKey, Router.current().path

       @redirect 'login'
+      FlashMessages.sendWarning 'Du er ikke logget ind'
       pause()

     else
@@ -33,12 +28,31 @@

       # redirect user to where he came from
       if loginRedirect and loginRedirect != 'logout'
-        console.log "Redirecting"
         Session.set loginRedirectKey, null
         @redirect loginRedirect
         pause()

-  , { except: ['login', 'logout', 'signup', 'game', 'highscores'] }
+  isLoggedOut: (pause) ->
+    if Meteor.userId()?
+      FlashMessages.sendWarning 'Du er allerede logget ind'
+      @redirect 'lobby'
+      pause()
+
+filters = Router._filters
+
+
+# client
+
+if Meteor.isClient
+
+  # before hooks
+
+  Router.onBeforeAction 'loading'
+  Router.onBeforeAction filters.isLoggedIn, only: [ 'logout', 'lobby', 'quiz' ]
+  Router.onBeforeAction filters.isLoggedOut, only: [ 'login', 'signup' ]
+
+
+  # routes

   Router.map ->
     # lobby
@@ -48,13 +62,11 @@
     @route 'highscores'

     #game types
-    @route 'single', path: '/single'
-    @route 'duel', path: '/duel'
-
+    @route 'single'
+    @route 'duel'

     # DEBUG route
-    @route 'quizzes',
-      path: '/quizzes/'
+    @route 'quizzes', path: '/quizzes/'

     # quiz
     @route 'quiz',
@@ -72,7 +84,7 @@

         # Check that the quiz has started and hasn't run out
         now = new Date()
-        if not (quiz.startDate < now and now < quiz.endDate)
+        unless (quiz.startDate < now and now < quiz.endDate)
           @render 'notAvailable'
           pause()
           return
@@ -89,8 +101,8 @@
         else
           Session.set('currentQuizId', quizId)

-        if not Session.get('currentQuestion')
-          Session.set('currentQuestion', 0)
+        unless Session.get 'currentQuestion'
+          Session.set 'currentQuestion', 0

     # game
     @route 'game',
@@ -104,18 +116,9 @@
           @render 'notFound'
           pause()

-        if not this.data()?#or game.state is 'inprogress'
+        unless @data()?
           @render 'notFound'
           pause()
-
-        # gameId = @params._id
-        # game = null
-        # Deps.nonreactive ->
-        #   game = Games.findOne gameId
-        #   console.log game
-        # if not game? #or game.state is 'inprogress'
-        #   @render 'notFound'
-        #   pause()

       waitOn: ->
         Meteor.subscribe 'games'
@@ -130,27 +133,11 @@
         @render @params.action

     # session
-    @route 'signup',
-      onBeforeAction: (pause) ->
-        if Meteor.userId()?
-          FlashMessages.sendWarning 'Du er allerede logget ind'
-          @redirect 'lobby'
-          pause()
+    @route 'login'

-    @route 'login',
-      onBeforeAction: (pause) ->
-        if Meteor.userId()?
-          FlashMessages.sendWarning 'Du er allerede logget ind'
-          @redirect 'lobby'
-          pause()
+    @route 'signup'

     @route 'logout',
-      onBeforeAction: (pause) ->
-        unless Meteor.userId()?
-          FlashMessages.sendError 'Du er ikke logget ind'
-          @redirect 'login'
-          pause()
-
       action: ->
         id = Meteor.userId()


(fix) Do not send an empty exercise id. issue #10

--- app/scripts/pages/exercises/exercises.coffee
+++ app/scripts/pages/exercises/exercises.coffee
@@ -26,7 +26,8 @@
         exercise = m2e @source()
         exercise.activationDate = @activationDate()
         exercise.dueDate = @dueDate()
-        exercise.id = @id()
+        if data
+          exercise.id = @id()
         return exercise

 class ViewModel


Make the field guide text less scary re. insects

--- content/field-examples.coffee
+++ content/field-examples.coffee
@@ -36,7 +36,7 @@
                  '''
     figures: [{
       label: '''
-                  Tag the insect by clicking on it – a text box window appears where you can add your insect species identification (if known). You can use the common or scientific name e.g. ‘Buff-tailed bumblebee’ or Bombus terrestris.
+                  Tag insects by clicking on them – a text box window appears where you can add a name (if known). Don't worry if you can't identify the insects, we just want to know if an insect is present on the plant.
                    '''

       image: "examples/insects.jpg"


Remove debug logging

Remove debug logging

--- src/reloader.coffee
+++ src/reloader.coffee
@@ -48,7 +48,6 @@
   while eqCount < len && comps1[eqCount] == comps2[eqCount]
     ++eqCount

-  console.log "numberOfMatchingSegments('#{path1}', '#{path2}') == #{eqCount}"
   return eqCount

 exports.Reloader = class Reloader
@@ -146,9 +145,6 @@
     # used to detect if reattachImportedRule has been called again on the same rule
     rule.__LiveReload_newHref = href

-    console.log ["rule is ", rule]
-    console.log ["parent is ", parent]
-
     # WORKAROUND FOR WEBKIT BUG: WebKit resets all styles if we add @import'ed
     # stylesheet that hasn't been cached yet. Workaround is to pre-cache the
     # stylesheet by temporarily adding it as a LINK tag.


Update reminder bot to support latest hubot-slack v4

--- src/reminder.coffee
+++ src/reminder.coffee
@@ -32,9 +32,7 @@
     later.setInterval (-> run job ), sched

   run = (job) ->
-    robot.adapter.customMessage
-      channel: job.room
-      text: job.text
+    robot.adapter.send room: job.room, job.text

   getReminders = () ->
     robot.brain.get('reminders') or []
@@ -76,7 +74,7 @@
     else
       return no

-  robot.respond /(?:remind|reminder|reminders) list( all)?/, (msg) ->
+  robot.respond /(?:remind|reminder|reminders) list( all)?/i, (msg) ->
     [ __, all ] = msg.match
     room = if all then undefined else msg.message.room
     message = ""
@@ -85,13 +83,13 @@
       if not room or room and room is reminder.room
         sched = later.parse.text reminder.time
         next = moment later.schedule(sched).next(1, Date.now())
-        message += "#{index}) Reminder to `#{reminder.text}` has been scheduled to run in ##{reminder.room} #{reminder.time} and will next run #{next.fromNow()}\n"
+        message += "#{index}) Reminder to `#{reminder.text}` has been scheduled to run in <##{reminder.room}> #{reminder.time} and will next run #{next.fromNow()}\n"
     message = "No reminders have been scheduled" if not message

     robot.messageRoom msg.message.room, message
     msg.finish()

-  robot.respond /(?:remind|reminder|reminders) (?:remove|delete|cancel) (\d)/, (msg) ->
+  robot.respond /(?:remind|reminder|reminders) (?:remove|delete|cancel) (\d)/i, (msg) ->
     [ __, id ] = msg.match
     if remove id, msg.message.room
       msg.reply "Reminder ##{id} successfully removed"
@@ -99,16 +97,15 @@
       msg.reply "Sorry I'm unable to remove reminder ##{id}"
     msg.finish()

-  regex = /(?:remind|reminder|reminders)(?: me| us)? to\s*`([^]+)`\s*([^]+)/
-  robot.respond regex, (msg) ->
-    [ __, text, time ] = msg.message.rawText.match regex
+  robot.respond /(?:remind|reminder|reminders)(?: me| us)? to\s*`([^]+)`\s*([^]+)/i, (msg) ->
+    [ __, text, time ] = msg.match
     room = msg.message.room

     if isValid time
       reminder = schedule text, room, time
       sched = later.parse.text time
       next = moment later.schedule(sched).next(1, Date.now())
-      msg.reply "Reminder ##{getReminders().length - 1} has been scheduled to run in ##{room} #{time} and will next run #{next.fromNow()}"
+      msg.reply "Reminder ##{getReminders().length - 1} has been scheduled to run in <##{room}> #{time} and will next run #{next.fromNow()}"
     else
       msg.reply "Sorry I don't understand when to set the reminder for :cry:"
     msg.finish()


rtp: Add methods to parse H.264/AAC packets

--- rtp.coffee
+++ rtp.coffee
@@ -10,6 +10,7 @@
 #       DON is to RTP what DTS is to MPEG-TS.

 bits = require './bits'
+aac = require './aac'

 # Number of seconds from 1900-01-01 to 1970-01-01
 EPOCH = 2208988800
@@ -22,6 +23,15 @@
 RTP_HEADER_LEN = 12

 MAX_PAYLOAD_SIZE = 1360
+
+MAX_SEQUENCE_NUMBER = 65535
+UNORDERED_PACKET_BUFFER_SIZE = 10
+
+eventListeners = {}
+packetBuffers = {}
+fragmentedH264PacketBuffer = []
+h264NALUnitBuffer = {}
+aacAccessUnitBuffer = {}

 api =
   # Number of bytes in RTP header
@@ -32,6 +42,26 @@
   RTCP_PACKET_TYPE_SOURCE_DESCRIPTION : 202  # SDES
   RTCP_PACKET_TYPE_GOODBYE            : 203  # BYE
   RTCP_PACKET_TYPE_APPLICATION_DEFINED: 204  # APP
+
+  H264_NAL_UNIT_TYPE_STAP_A: 24
+  H264_NAL_UNIT_TYPE_STAP_B: 25
+  H264_NAL_UNIT_TYPE_MTAP16: 26
+  H264_NAL_UNIT_TYPE_MTAP24: 27
+  H264_NAL_UNIT_TYPE_FU_A  : 28
+  H264_NAL_UNIT_TYPE_FU_B  : 29
+
+  emit: (name, data...) ->
+    if eventListeners[name]?
+      for listener in eventListeners[name]
+        listener data...
+    return
+
+  on: (name, listener) ->
+    if eventListeners[name]?
+      eventListeners[name].push listener
+    else
+      eventListeners[name] = [ listener ]
+

   readRTCPSenderReport: ->
     # RFC 3550 - 6.4.1 SR: Sender Report RTCP Packet
@@ -46,7 +76,8 @@
     info.wordsMinusOne = bits.read_bits 16
     info.totalBytes = (info.wordsMinusOne + 1) * 4
     info.ssrc = bits.read_bits 32
-    info.ntpTimestamp = bits.read_bits 64  # loses some precision
+    info.ntpTimestamp = [ bits.read_bits(32), bits.read_bits(32) ]
+    info.ntpTimestampInMs = api.ntpTimestampToTime info.ntpTimestamp
     info.rtpTimestamp = bits.read_bits 32
     info.senderPacketCount = bits.read_bits 32
     info.senderOctetCount = bits.read_bits 32
@@ -235,6 +266,188 @@
       info.csrc.push bits.read_bits 32
     return info

+  parseAACPacket: (buf, params) ->
+    bits.push_stash()
+    bits.set_data buf
+    packet = {}
+    packet.rtpHeader = api.readRTPFixedHeader()
+    packet.aac = api.readAACPayload params
+    bits.pop_stash()
+    return packet
+
+  onH264NALUnit: (clientId, nalUnit, packet, timestamp) ->
+    if not h264NALUnitBuffer[clientId]?
+      h264NALUnitBuffer[clientId] = []
+    h264NALUnitBuffer[clientId].push nalUnit
+    if packet.rtpHeader.marker
+      api.emit 'h264_nal_units', clientId, h264NALUnitBuffer[clientId], timestamp
+      h264NALUnitBuffer[clientId] = []
+
+  onAACAccessUnits: (clientId, accessUnits, packet, timestamp) ->
+    if not aacAccessUnitBuffer[clientId]?
+      aacAccessUnitBuffer[clientId] = []
+    aacAccessUnitBuffer[clientId] = aacAccessUnitBuffer[clientId].concat accessUnits
+    if packet.rtpHeader.marker
+      api.emit 'aac_access_units', clientId, aacAccessUnitBuffer[clientId], timestamp
+      aacAccessUnitBuffer[clientId] = []
+
+  onOrderedPacket: (tag, packet) ->
+    if (match = /^h264:(.*)$/.exec tag)?
+      clientId = match[1]
+      if packet.h264.fu_a?  # fragmentation unit
+        # startBit and endBit won't both be set to 1 in the same FU header
+        if packet.h264.fu_a.fuHeader.startBit
+          fragmentedH264PacketBuffer = [
+            new Buffer [ (packet.h264.nal_ref_idc << 5) | packet.h264.fu_a.fuHeader.nal_unit_payload_type ]
+            packet.h264.fu_a.nal_unit_fragment
+          ]
+        else if fragmentedH264PacketBuffer?
+          fragmentedH264PacketBuffer.push packet.h264.fu_a.nal_unit_fragment
+        else
+          console.warn "rtp: #{tag}: discarded fragmented buffer: #{packet.rtpHeader.sequenceNumber}"
+          return
+        if packet.h264.fu_a.fuHeader.endBit
+          api.onH264NALUnit clientId, Buffer.concat(fragmentedH264PacketBuffer), packet, packet.rtpHeader.timestamp
+          fragmentedH264PacketBuffer = null
+      else # single nal unit
+        api.onH264NALUnit clientId, packet.h264.nal_unit, packet, packet.rtpHeader.timestamp
+    else if (match = /^aac:(.*)$/.exec tag)?
+      clientId = match[1]
+      api.onAACAccessUnits clientId, packet.aac.accessUnits, packet, packet.rtpHeader.timestamp
+    else
+      throw new Error "Unknown tag: #{tag}"
+
+  feedUnorderedPacket: (tag, packet) ->
+    if not packetBuffers[tag]?
+      packetBuffers[tag] =
+        nextSequenceNumber: packet.rtpHeader.sequenceNumber
+        minSequenceNumberInBuffer: null
+        buffer: []
+    packetBuffer = packetBuffers[tag]
+    if packetBuffer.nextSequenceNumber is packet.rtpHeader.sequenceNumber
+      api.onOrderedPacket tag, packet
+      packetBuffer.nextSequenceNumber++
+      if packetBuffer.nextSequenceNumber > MAX_SEQUENCE_NUMBER
+        packetBuffer.nextSequenceNumber = 0
+    else
+      # stash packet in buffer
+      buffers = packetBuffer.buffer
+      buffers.push packet
+      if buffers.length >= 2
+        buffers.sort (a, b) ->
+          numberA = a.rtpHeader.sequenceNumber
+          numberB = b.rtpHeader.sequenceNumber
+          if numberA - numberB >= 60000  # large enough gap
+            return -1  # a comes first
+          else if numberB - numberA >= 60000  # large enough gap
+            return 1   # b comes first
+          else
+            return numberA - numberB
+        while (buffers.length) > 0 and
+        (buffers[0].rtpHeader.sequenceNumber is packetBuffer.nextSequenceNumber)
+          api.onOrderedPacket tag, buffers.shift()
+          packetBuffer.nextSequenceNumber++
+          if packetBuffer.nextSequenceNumber > MAX_SEQUENCE_NUMBER
+            packetBuffer.nextSequenceNumber = 0
+        while buffers.length >= 2
+          latestSequenceNumber = buffers[buffers.length-1].rtpHeader.sequenceNumber
+          diff = latestSequenceNumber - packetBuffer.nextSequenceNumber
+          if diff < 0
+            diff += MAX_SEQUENCE_NUMBER + 1
+          if diff < UNORDERED_PACKET_BUFFER_SIZE
+            break
+
+          firstPacket = buffers.shift()
+          if packetBuffer.nextSequenceNumber isnt firstPacket.rtpHeader.sequenceNumber
+            discardedSequenceNumber = firstPacket.rtpHeader.sequenceNumber - 1
+            if discardedSequenceNumber < 0
+              discardedSequenceNumber += MAX_SEQUENCE_NUMBER
+            if packetBuffer.nextSequenceNumber isnt discardedSequenceNumber
+              console.warn "rtp: #{tag}: discarded packets #{packetBuffer.nextSequenceNumber}-#{discardedSequenceNumber}"
+            else
+              console.warn "rtp: #{tag}: discarded packet #{discardedSequenceNumber}"
+          # consume the first packet
+          api.onOrderedPacket tag, firstPacket
+
+          packetBuffer.nextSequenceNumber = firstPacket.rtpHeader.sequenceNumber + 1
+          if packetBuffer.nextSequenceNumber > MAX_SEQUENCE_NUMBER
+            packetBuffer.nextSequenceNumber = 0
+
+  feedUnorderedAACBuffer: (buf, clientId, params) ->
+    packet = api.parseAACPacket buf, params
+    api.feedUnorderedPacket "aac:#{clientId}", packet
+
+  feedUnorderedH264Buffer: (buf, clientId) ->
+    packet = api.parseH264Packet buf
+    api.feedUnorderedPacket "h264:#{clientId}", packet
+
+  parseH264Packet: (buf) ->
+    bits.push_stash()
+    bits.set_data buf
+    packet = {}
+    packet.rtpHeader = api.readRTPFixedHeader()
+    packet.h264 = api.readH264Payload()
+    bits.pop_stash()
+    return packet
+
+  readH264Payload: ->
+    info = {}
+    info.forbidden_zero_bit = bits.read_bit()  # 1 indicates error
+    if info.forbidden_zero_bit isnt 0
+      throw new Error "forbidden_zero_bit must be 0 (got #{info.forbidden_zero_bit})"
+    info.nal_ref_idc = bits.read_bits 2  # == 00: not important, > 00: important
+    info.nal_unit_type = bits.read_bits 5
+    if 1 <= info.nal_unit_type <= 23  # Single NAL unit packet
+      bits.push_back_byte()
+      info.nal_unit = bits.remaining_buffer()
+    else if 24 <= info.nal_unit_type <= 29
+      switch info.nal_unit_type
+        when api.H264_NAL_UNIT_TYPE_FU_A  # FU-A
+          info.fu_a = api.readH264FragmentationUnitA()
+        else
+          throw new Error "Not implemented: nal_unit_type=#{info.nal_unit_type}"
+    else
+      throw new Error "Invalid nal_unit_type=#{info.nal_unit_type}"
+    return info
+
+  readH264FragmentationUnitA: ->
+    info = {}
+    info.fuHeader = api.readH264FragmentationUnitHeader()
+    info.nal_unit_fragment = bits.remaining_buffer()
+    return info
+
+  # FU header
+  readH264FragmentationUnitHeader: ->
+    info = {}
+    info.startBit = bits.read_bit()
+    info.endBit = bits.read_bit()
+    reservedBit = bits.read_bit()
+    if reservedBit isnt 0
+      throw new Error "reserved bit must be 0 (got #{reservedBit})"
+    info.nal_unit_payload_type = bits.read_bits 5
+    return info
+
+  parsePacket: (buf) ->
+    bits.push_stash()
+    bits.set_data buf
+    packet = {}
+    payloadValue = bits.get_byte_at 1  # including marker bit
+    switch payloadValue
+      when api.RTCP_PACKET_TYPE_SENDER_REPORT
+        packet.rtcpSenderReport = api.readRTCPSenderReport()
+      when api.RTCP_PACKET_TYPE_RECEIVER_REPORT
+        packet.rtcpReceiverReport = api.readRTCPReceiverReport()
+      when api.RTCP_PACKET_TYPE_SOURCE_DESCRIPTION
+        packet.rtcpSourceDescription = api.readRTCPSourceDescription()
+      when api.RTCP_PACKET_TYPE_GOODBYE
+        packet.rtcpGoodbye = api.readRTCPGoodbye()
+      when api.RTCP_PACKET_TYPE_APPLICATION_DEFINED
+        packet.rtcpApplicationDefined = api.readRTCPApplicationDefined()
+      else  # RTP data transfer protocol - fixed header
+        packet.rtpHeader = api.readRTPFixedHeader()
+    bits.pop_stash()
+    return packet
+
   parsePackets: (buf) ->
     bits.push_stash()
     bits.set_data buf
@@ -242,8 +455,8 @@
     packets = []
     while bits.has_more_data()
       packet = {}
-      packet.payloadValue = bits.get_byte_at 1
-      switch packet.payloadValue
+      payloadValue = bits.get_byte_at 1  # including marker bit
+      switch payloadValue
         when api.RTCP_PACKET_TYPE_SENDER_REPORT
           packet.rtcpSenderReport = api.readRTCPSenderReport()
         when api.RTCP_PACKET_TYPE_RECEIVER_REPORT
@@ -270,6 +483,12 @@
     buf[11] = ssrc & 0xff
     return

+  # ntpTimestamp: [ <32-bit second part>, <32-bit fractional second part> ]
+  ntpTimestampToTime: (ntpTimestamp) ->
+    sec = ntpTimestamp[0] - EPOCH
+    ms = ntpTimestamp[1] / NTP_SCALE_FRAC / 1000
+    return sec * 1000 + ms
+
   # Get NTP timestamp for a time
   # time is expressed the same as Date.now()
   getNTPTimestamp: (time) ->
@@ -278,6 +497,41 @@
     ntp_sec = sec + EPOCH
     ntp_usec = Math.round(ms * 1000 * NTP_SCALE_FRAC)
     return [ntp_sec, ntp_usec]
+
+  readAACPayload: (params) ->
+    info = {}
+    info.auHeadersLengthBits = bits.read_bits 16  # in bits
+    info.numAUHeaders = info.auHeadersLengthBits / 16
+    auHeaders = []
+    for i in [0...info.numAUHeaders]
+      params.index = i
+      auHeaders.push api.readAACAUHeader params
+    info.auHeaders = auHeaders
+    info.accessUnits = []
+    for auHeader in auHeaders
+      info.accessUnits.push bits.read_bytes auHeader.auSize
+      accessUnit = info.accessUnits[info.accessUnits.length-1]
+    return info
+
+  readAACAUHeader: (params) ->
+    if not params.sizelength?
+      throw new Error "sizelength is not defined in params"
+    info = {}
+    # size in octets of the associated Access Unit in the
+    # Access Unit Data Section in the same RTP packet
+    info.auSize = bits.read_bits params.sizelength
+    # serial number of the associated Access Unit (fragment).
+    if not params.index?
+      throw new Error "index is not defined in params"
+    if params.index > 0
+      if not params.indexdeltalength?
+        throw new Error "indexdeltalength is not defined in params"
+      info.auIndexDelta = bits.read_bits params.indexdeltalength
+    else
+      if not params.indexlength?
+        throw new Error "indexlength is not defined in params"
+      info.auIndex = bits.read_bits params.indexlength
+    return info

   # Used for encapsulating AAC audio data
   # opts:
@@ -457,4 +711,12 @@
       octetCount & 0xff,
     ]

+  # Parse config parameter for AAC
+  # see: RFC 3640, 4.1. MIME Type Registration
+  parseAACConfig: (str) ->
+    if str is '""'  # empty string
+      return null
+    buf = new Buffer str, 'hex'
+    return aac.parseAudioSpecificConfig buf
+
 module.exports = api


building a simple tree now works

building a simple tree now works

--- src/resolver.coffee
+++ src/resolver.coffee
@@ -17,7 +17,7 @@
 coffee      = require 'coffee-script'
 fs          = require 'fs'
 path        = require 'path'
-#detective   = require 'detective'
+detective   = require 'detective'
 {compile}   = require './utils'

@@ -51,7 +51,7 @@

   getTree : () ->
-    tree = {name: @basePoint, deps: {}, reqFolders: [], domain: @domainPaths['client']}
+    tree = {name: @basePoint, deps: {}, reqFolders: [], domain: @domainPaths['client'], level: 0}

     clearParentEntries = (treePos) ->
       delete treePos.parent # does not have to exist to be cleared
@@ -73,31 +73,37 @@
       reqFolders = treePos.name.split(path.basename(treePos.name))[0][0...-1].split('/') # array of folders to move into relative to basepoint to get to the file that required dep below
       for dep in @loadDependencies(treePos.name, treePos.reqFolders, treePos.domain) # use detective to get this 'deps' fn
         branchScanUp(treePos, dep) # make sure this dep does not exist above it in the tree
-        treePos.deps[dep] = {name : dep, parent: treePos, deps: {}, reqFolders: reqFolders }
+        treePos.deps[dep] = {name : dep, parent: treePos, deps: {}, reqFolders: reqFolders, level: treePos.level+1 }
         arguments.callee(treePos.deps[dep])
       return

-    recursiveDetective(tree, @basePoint)
+    recursiveDetective(tree, @basePoint) #name of basePoint might have to call path.basename on it first
     clearParentEntries(tree)
     console.log tree

 sortDependencies = (tree) -> # must flatten array into levels to get an ordered list of filenames w/resp. paths
-  #need to:
-  #1. add an index to the tree to indicate what level we are on (do in recursiveDetective step?)
-  #2. create an ARRAY with path+level+$+level (so there will be multiple versions potentially, so remove worse ones as we go)
-  for mod in tree
-    stuff
+  obj = {}
+  ((treePos) ->
+    obj[treePos.name] = Math.max(treePos.level, obj[treePos.name] or 0)
+    arguments.callee(dep) for dep of treePos.deps
+    return
+  )(tree)
+  ([key,val] for key,val of obj).sort((a,b) -> b[1] - a[1]).map((e) -> e[0])

-  #3. then sort by level descending and remove the level from the name so that it can be used by builder verbatim : )
-  arr.sort((a,b) -> b.split('$')[1] - a.split('$')[1]).map((e) -> e.split('$')[0]) # this will give you the inclusion order!
-
+sanitizeTree = (tree) ->
+  m = {'app':{}}
+  ((treePos, mPos) ->
+    arguments.callee(treePos.deps[dep], mPos[dep]={}) for dep of treePos.deps
+    return
+  )(tree,m['app'])
+  m

 module.exports = (o) ->
   tree = (new Resolver(o)).getTree()
   if o.targetTree
-    # write sanitized version of the tree to the target file for code review
+    #fs.writeFileSync(o.treeTarget, sanitizeTree(tree)) if o.treeTarget # write sanitized version of the tree to the target file for code review
+    console.log sanitizeTree(tree)
     return
-
   sortDependencies(tree)

@@ -105,9 +111,21 @@
   reqPoint = 'models/user'
   name = './event'
   reqFolders = reqPoint.split(path.basename(reqPoint))[0][0...-1].split('/') #remove name, last slash and convert to folders
-  console.log reqFolders
-  console.log toAbsPath(name, reqFolders)
+  #console.log reqFolders
+  #console.log toAbsPath(name, reqFolders)

+  tree =
+    name : 'app'
+    deps :
+      'A'  :
+        name : 'A'
+        deps : {}
+      'B'  :
+        name : 'B'
+        deps : {'C': {name:'C', deps: {'E':{name:'E',deps:{}}}  }, 'D' :{name:'D', deps: {}} }
+  console.log JSON.stringify sanitizeTree tree
+
+  return

 ###
 tree = {


ensure multiple ports are respected, support mountpoints

--- src/decking.coffee
+++ src/decking.coffee
@@ -144,31 +144,30 @@
     # of dependency resolution ourselves
     target = getCluster @config, cluster

-    resolveOrder @config, target, (list) ->
-      async.eachSeries list, (details, callback) ->
-
-        name = details.name
-
-        container = docker.getContainer name
-        container.inspect (err, data) ->
-          if not err
-            log "Container #{name} already exists, skipping..."
-            return callback null
-
-          cmdArgs = ["docker", "run", "-d", "-name", "#{name}"]
-          cmdArgs = [].concat cmdArgs, getRunArg key, val, details for key,val of details
-          cmdArgs.push details.image
-
-          cmdString = cmdArgs.join " "
-
-          log "Running container with args #{cmdString}"
-
-          child_process.exec cmdString, (err) ->
-            return callback err if err
-            setTimeout ->
-              child_process.exec "docker stop #{name}", callback
-            , 500
-      , done
+    iterator = (details, callback) ->
+      name = details.name
+
+      container = docker.getContainer name
+      container.inspect (err, data) ->
+        if not err
+          log "Container #{name} already exists, skipping..."
+          return callback null
+
+        cmdArgs = ["docker", "run", "-d", "-name", "#{name}"]
+        cmdArgs = [].concat cmdArgs, getRunArg key, val, details for key,val of details
+        cmdArgs.push details.image
+
+        cmdString = cmdArgs.join " "
+
+        log "Running container with args #{cmdString}"
+
+        child_process.exec cmdString, (err) ->
+          return callback err if err
+          setTimeout ->
+            child_process.exec "docker stop #{name}", callback
+          , 500
+
+    resolveOrder @config, target, (list) -> async.eachSeries list, iterator, done

   build: (image, done) ->

@@ -247,19 +246,27 @@

 getRunArg = (key, val, object) ->
   arg = []
+
   switch key
     when "port"
-      arg = ["-p", "#{val[0]}"]
+      for v in val
+        arg = [].concat arg, ["-p #{v}"]
+
     when "env"
-      for k,v of val
+      for v,k in val
         [key, value] = v.split "="
         if value is "-" then value = process.env[key]
         arg = [].concat arg, ["-e", "#{key}=#{value}"]
+
     when "dependencies"
       for v,k in val
         alias = object.aliases[k]
         arg = [].concat arg, ["-link #{v}:#{alias}"]

+    when "mount"
+      for v in val
+        arg = [].concat arg, ["-v #{v}"]
+
   return arg

 getCluster = (config, cluster) ->


fix admin roles menu selection

fix admin roles menu selection

--- app/coffee/app.coffee
+++ app/coffee/app.coffee
@@ -238,7 +238,7 @@
     $routeProvider.when("/project/:pslug/admin/roles",
         {
             templateUrl: "admin/admin-roles.html",
-            section: "backlog-kanban"
+            section: "admin"
         }
     )


[i18n] Use default language configured in global settings instead en

--- app/coffee/app.coffee
+++ app/coffee/app.coffee
@@ -159,12 +159,12 @@
     $routeProvider.when("/permission-denied",
         {templateUrl: "error/permission-denied.html"})

-    $routeProvider.otherwise({redirectTo: '/not-found'})
+    $routeProvider.otherwise({redirectTo: "/not-found"})
     $locationProvider.html5Mode({enabled: true, requireBase: false})

     defaultHeaders = {
         "Content-Type": "application/json"
-        "Accept-Language": "en"
+        "Accept-Language": window.taigaConfig.defaultLanguage || "en"
         "X-Session-Id": taiga.sessionId
     }

@@ -197,7 +197,7 @@

     $provide.factory("authHttpIntercept", ["$q", "$location", "$tgNavUrls", "lightboxService", authHttpIntercept])

-    $httpProvider.interceptors.push('authHttpIntercept')
+    $httpProvider.interceptors.push("authHttpIntercept")

     # If there is an error in the version throw a notify error.
     # IMPROVEiMENT: Move this version error handler to USs, issues and tasks repository
@@ -217,7 +217,7 @@

     $provide.factory("versionCheckHttpIntercept", ["$q", versionCheckHttpIntercept])

-    $httpProvider.interceptors.push('versionCheckHttpIntercept')
+    $httpProvider.interceptors.push("versionCheckHttpIntercept")

     window.checksley.updateValidators({
         linewidth: (val, width) ->
@@ -233,13 +233,13 @@

     $translateProvider
         .useStaticFilesLoader({
-            prefix: '/locales/locale-',
-            suffix: '.json'
+            prefix: "/locales/locale-",
+            suffix: ".json"
         })
-        .preferredLanguage(window.taigaConfig.defaultLanguage || 'en')
+        .preferredLanguage(window.taigaConfig.defaultLanguage || "en")

     if not window.taigaConfig.debugInfo
-        $translateProvider.fallbackLanguage([window.taigaConfig.defaultLanguage || 'en'])
+        $translateProvider.fallbackLanguage([window.taigaConfig.defaultLanguage || "en"])

 init = ($log, $config, $rootscope, $auth, $events, $analytics, $translate) ->


fixing tag thingy

fixing tag thingy

--- app/coffee/tag.coffee
+++ app/coffee/tag.coffee
@@ -188,6 +188,7 @@
     #$.post "/tags", { entry_id: @entry_id, what_name: @name }, (data) -> alert "Data Loaded: " + data
   destroy: ->
     $.publish 'tags:remove', [@id]
+    @entry_id = $('#showEntry').data('id')
     $.ajax {
       type: 'DELETE'
       url: '/tags/what'


Remove the old main function.

Remove the old main function.

--- save-index.coffee
+++ save-index.coffee
@@ -49,22 +49,3 @@
 			throw err
 		logger.log "done"
 		process.exit()
-
-
-if require.main == module
-	logger.log "beginning package-index build"
-	args = process.argv.slice(2)
-	Indexer.build (err, result) ->
-		if err
-			throw err
-		result_json = JSON.stringify(result, null, 2)
-		fs.writeFileSync(__dirname + '/data/packageIndex.json', result_json)
-		if '--save' in args
-			Indexer.save_index_to_mongo result, (err) ->
-				if err
-					throw err
-				logger.log "done"
-				process.exit()
-		else
-			logger.log "done"
-			process.exit()


Make layouts work again

Make layouts work again

--- src/enfield.coffee
+++ src/enfield.coffee
@@ -237,7 +237,7 @@

   layouts = {}
   for name, content of rawLayouts
-    layouts[name] = tinyliquid.compile load(name, content), liquidOptions
+    layouts[name] = tinyliquid.compile content, liquidOptions

   # Run generators
   async.forEachSeries(
@@ -395,15 +395,19 @@
   # substitute the {{ content }} portion of the layout, and leave the rest of
   # the liquid directives for later.
   layoutContents = {}
-  load = (name, content) ->
+  loadLayout = (name, content) ->
     unless name of layoutContents
       if layout = fileData[name]?.layout
-        layout = load layout, fileContents[layout]
+        layout = loadLayout layout, fileContents[layout]
         content = layout.replace /\{\{\s*content\s*\}\}/, content

       layoutContents[name] = content

     layoutContents[name]
+
+  for name, content of fileContents
+    continue if name of layoutContents
+    layoutContents[name] = loadLayout name, content

   return layoutContents


Throw on type error during CLI run

Throw on type error during CLI run

--- src/shem-cli.coffee
+++ src/shem-cli.coffee
@@ -8,9 +8,10 @@
   if result.request
     console.error options
     console.error result
+  else if result.errors
+    throw new Error result.errors[0].message
   else
     result.js
-

 # Compile and execute a string of CoffeeScript (on the server), correctly
 # setting `__filename`, `__dirname`, and relative `require()`.


delete 'WEBGL' parameter in environ.coffee

--- src/environ.coffee
+++ src/environ.coffee
@@ -16,7 +16,6 @@
 GRAVITY_X       = 0.0                       # box2d用水平方向重力
 GRAVITY_Y       = 9.8                       # box2d用垂直方向重力
 GAMEPADMIX      = false                     # ゲームパッドとアナログコントローラーの値を合算
-WEBGL           = false                     # WebGLフラグ

 # preloading image list ***********************************************************************************
 MEDIALIST =


rearranged file order on includes file

rearranged file order on includes file

--- client/includes.coffee
+++ client/includes.coffee
@@ -398,6 +398,7 @@
   "app/Applications/Home.kdapplication/AppController.coffee",
   "app/Applications/Topics.kdapplication/AppController.coffee",
   "app/Applications/Feeder.kdapplication/AppController.coffee",
+  "app/Applications/Environments.kdapplication/AppController.coffee",
   "app/Applications/Apps.kdapplication/AppController.coffee",
   "app/Applications/Inbox.kdapplication/AppController.coffee",
   "app/Applications/Demos.kdapplication/AppController.coffee",
@@ -553,7 +554,6 @@
   "app/Applications/Environments.kdapplication/views/NewDNSRecordFormView.coffee",
   "app/Applications/Environments.kdapplication/views/DNSRecordListItemView.coffee",
   "app/Applications/Environments.kdapplication/AppView.coffee",
-  "app/Applications/Environments.kdapplication/AppController.coffee",
   # "app/Applications/Environments.kdapplication/Controllers/VMListViewController.coffee",
   "app/Applications/Environments.kdapplication/Controllers/DomainsListViewController.coffee",
   "app/Applications/Environments.kdapplication/Controllers/FirewallFilterListController.coffee"


Add production survey IDs

Add production survey IDs

--- app/lib/config.coffee
+++ app/lib/config.coffee
@@ -34,7 +34,7 @@
     quiz:
       invitationId: '502bfa73516bcb3c600003e9'
       workflowId: '502a701e516bcb0001000002'
-
+
   developmentLocal:
     quizzesActive: false
     apiHost: 'http://localhost:3000'
@@ -70,7 +70,7 @@
     quiz:
       invitationId: '502bfa73516bcb3c600003e9'
       workflowId: '502a701e516bcb0001000002'
-
+
   developmentRemote:
     quizzesActive: false
     apiHost: 'https://dev.zooniverse.org'
@@ -133,11 +133,11 @@
         id: '5514521e2f0eef2012000001'
         workflowId: '5514521f2f0eef2012000002'
       decals:
-        id: ''
-        workflowId: ''
+        id: '55db7cf01766276e7b000001'
+        workflowId: '55db7cf01766276e7b000002'
       illustris:
-        id: ''
-        workflowId: ''
+        id: '55db71251766276613000001'
+        workflowId: '55db71251766276613000002'
     subjectCache: 5
     quiz:
       invitationId: '502bfa73516bcb3c600003e9'


Some changes which may or may not fix what happened in #37.

--- app/lib/object.coffee
+++ app/lib/object.coffee
@@ -83,6 +83,8 @@
       @name = nameStr

   updateAliases: (aliases) ->
+    if not Array.isArray aliases
+      throw new Error "`aliases` must be an array."
     for alias in aliases
       if not (alias? and alias.toString?)
         throw new Error "Invalid alias '#{alias}'"
@@ -116,6 +118,9 @@
   # array of all descendants of this object
   descendants: ->
     (o for id, o of @db.objects).filter (o) => o.inheritsFrom @id
+
+  send: ->
+    # stub

   ####################
   # property methods #


Use lstatSyncNoException when available

Use lstatSyncNoException when available

--- src/fs-plus.coffee
+++ src/fs-plus.coffee
@@ -95,9 +95,9 @@
   # Public: Returns true if the specified path is a symbolic link.
   isSymbolicLinkSync: (symlinkPath) ->
     return false unless symlinkPath?.length > 0
-    try
-      fs.lstatSync(symlinkPath).isSymbolicLink()
-    catch error
+    if stat = lstatSyncNoException(symlinkPath)
+      stat.isSymbolicLink()
+    else
       false

   # Public: Returns true if the specified path is executable.
@@ -456,11 +456,17 @@
         catch parseError
           done(parseError)

-{statSyncNoException} = fs
+{statSyncNoException, lstatSyncNoException} = fs
 statSyncNoException ?= (args...) ->
   try
     fs.statSync(args...)
   catch error
     false

+lstatSyncNoException ?= (args...) ->
+  try
+    fs.lstatSync(args...)
+  catch error
+    false
+
 module.exports = _.extend({}, fs, fsExtensions)


Use TomDoc for documentation

Use TomDoc for documentation

--- src/fs-plus.coffee
+++ src/fs-plus.coffee
@@ -24,12 +24,12 @@
   # Public: Make the given path absolute by resolving it against the current
   # working directory.
   #
-  # * relativePath:
-  #   The String containing the relative path. If the path is prefixed with
-  #   '~', it will be expanded to the current user's home directory.
-  #
-  # Returns the absolute path or the relative path if it's unable to determine
-  # it's realpath.
+  # relativePath - The {String} containing the relative path. If the path is
+  #                prefixed with '~', it will be expanded to the current user's
+  #                home directory.
+  #
+  # Returns the {String} absolute path or the relative path if it's unable to
+  # determine its realpath.
   absolute: (relativePath) ->
     return null unless relativePath?

@@ -47,10 +47,9 @@

   # Public: Is the given path absolute?
   #
-  # * pathToCheck:
-  #   The relative or absolute path to check.
-  #
-  # Returns true if the path is absolute, false otherwise.
+  # pathToCheck - The relative or absolute {String} path to check.
+  #
+  # Returns a {Boolean}, true if the path is absolute, false otherwise.
   isAbsolute: (pathToCheck='') ->
     if process.platform is 'win32'
       pathToCheck[1] is ':' # C:\ style
@@ -124,11 +123,9 @@
   # Public: Returns an Array with the paths of the files and directories
   # contained within the directory path. It is not recursive.
   #
-  # * rootPath:
-  #   The absolute path to the directory to list.
-  # * extensions:
-  #   An array of extensions to filter the results by. If none are given, none
-  #   are filtered (optional).
+  # rootPath - The absolute {String} path to the directory to list.
+  # extensions - An {Array} of extensions to filter the results by. If none are
+  #              given, none are filtered (optional).
   listSync: (rootPath, extensions) ->
     return [] unless fsPlus.isDirectorySync(rootPath)
     paths = fs.readdirSync(rootPath)
@@ -139,13 +136,10 @@
   # Public: Asynchronously lists the files and directories in the given path.
   # The listing is not recursive.
   #
-  # * rootPath:
-  #   The absolute path to the directory to list.
-  # * extensions:
-  #   An array of extensions to filter the results by. If none are given, none
-  #   are filtered (optional)
-  # * callback:
-  #   The function to call
+  # rootPath - The absolute {String} path to the directory to list.
+  # extensions - An {Array} of extensions to filter the results by. If none are
+  #              given, none are filtered (optional).
+  # callback - The {Function} to call.
   list: (rootPath, rest...) ->
     extensions = rest.shift() if rest.length > 1
     done = rest.shift()
@@ -157,7 +151,7 @@
         paths = paths.map (childPath) -> path.join(rootPath, childPath)
         done(null, paths)

-  # Private: Returns only the paths which end with one of the given extensions.
+  # Returns only the paths which end with one of the given extensions.
   filterExtensions: (paths, extensions) ->
     extensions = extensions.map (ext) ->
       if ext is ''
@@ -167,7 +161,11 @@
     paths.filter (pathToCheck) ->
       _.include(extensions, path.extname(pathToCheck))

-  # Deprecated: No one currently uses this.
+  # Public: Get all paths under the given path.
+  #
+  # rootPath - The {String} path to start at.
+  #
+  # Return an {Array} of {String}s under the given path.
   listTreeSync: (rootPath) ->
     paths = []
     onPath = (childPath) ->
@@ -247,14 +245,11 @@
   # Public: Recursively walk the given path and execute the given functions
   # synchronously.
   #
-  # * rootPath:
-  #   The String containing the directory to recurse into.
-  # * onFile:
-  #   The function to execute on each file, receives a single argument the
-  #   absolute path.
-  # * onDirectory:
-  #   The function to execute on each directory, receives a single argument the
-  #   absolute path (defaults to onFile)
+  # rootPath - The {String} containing the directory to recurse into.
+  # onFile - The {Function} to execute on each file, receives a single argument
+  #          the absolute path.
+  # onDirectory - The {Function} to execute on each directory, receives a single
+  #               argument the absolute path (defaults to onFile).
   traverseTreeSync: (rootPath, onFile, onDirectory=onFile) ->
     return unless fsPlus.isDirectorySync(rootPath)

@@ -275,14 +270,11 @@
   # Public: Recursively walk the given path and execute the given functions
   # asynchronously.
   #
-  # * rootPath:
-  #   The String containing the directory to recurse into.
-  # * onFile:
-  #   The function to execute on each file, receives a single argument the
-  #   absolute path.
-  # * onDirectory:
-  #   The function to execute on each directory, receives a single argument the
-  #   absolute path (defaults to onFile)
+  # rootPath - The {String} containing the directory to recurse into.
+  # onFile - The {Function} to execute on each file, receives a single argument
+  #          the absolute path.
+  # onDirectory - The {Function} to execute on each directory, receives a single
+  #               argument the absolute path (defaults to onFile).
   traverseTree: (rootPath, onFile, onDirectory, onDone) ->
     fs.readdir rootPath, (error, files) ->
       if error
@@ -312,8 +304,7 @@

   # Public: Hashes the contents of the given file.
   #
-  # * pathToDigest:
-  #   The String containing the absolute path.
+  # pathToDigest - The {String} containing the absolute path.
   #
   # Returns a String containing the MD5 hexadecimal hash.
   md5ForPath: (pathToDigest) ->
@@ -322,13 +313,11 @@

   # Public: Finds a relative path among the given array of paths.
   #
-  # * loadPaths:
-  #   An Array of absolute and relative paths to search.
-  # * pathToResolve:
-  #   The string containing the path to resolve.
-  # * extensions:
-  #   An array of extensions to pass to {resolveExtensions} in which case
-  #   pathToResolve should not contain an extension (optional).
+  # loadPaths - An {Array} of absolute and relative paths to search.
+  # pathToResolve - The {String} containing the path to resolve.
+  # extensions - An {Array} of extensions to pass to {resolveExtensions} in
+  #              which case pathToResolve should not contain an extension
+  #              (optional).
   #
   # Returns the absolute path of the file to be resolved if it's found and
   # undefined otherwise.
@@ -352,7 +341,8 @@
         return fsPlus.absolute(candidatePath) if fsPlus.existsSync(candidatePath)
     undefined

-  # Deprecated:
+  # Public: Like {.resolve} but uses node's modules paths as the load paths to
+  # search.
   resolveOnLoadPath: (args...) ->
     loadPaths = Module.globalPaths.concat(module.paths)
     fsPlus.resolve(loadPaths..., args...)
@@ -360,11 +350,9 @@
   # Public: Finds the first file in the given path which matches the extension
   # in the order given.
   #
-  # * pathToResolve:
-  #   The String containing relative or absolute path of the file in question
-  #   without the extension or '.'.
-  # * extensions:
-  #   The ordered Array of extensions to try.
+  # pathToResolve - The {String} containing relative or absolute path of the
+  #                 file in question without the extension or '.'.
+  # extensions - The ordered {Array} of extensions to try.
   #
   # Returns the absolute path of the file if it exists with any of the given
   # extensions, otherwise it's undefined.
@@ -418,7 +406,7 @@
     base = path.basename(readmePath, extension).toLowerCase()
     base is 'readme' and (extension is '' or fsPlus.isMarkdownExtension(extension))

-  # Private: Used by isReadmePath.
+  # Public: Returns true for extensions associated with Markdown files.
   isMarkdownExtension: (ext) ->
     _.indexOf([
       '.markdown'


Api fix.

Api fix.

--- src/telegram.coffee
+++ src/telegram.coffee
@@ -65,7 +65,7 @@
     reply: (envelope, strings...) ->
         self = @

-        @telegram.invoke 'sendMessage', { chat_id: envelope.room, text: strings.join(), reply_to_message_id: envelope.message.id }, (err, message) =>
+        @api.invoke 'sendMessage', { chat_id: envelope.room, text: strings.join(), reply_to_message_id: envelope.message.id }, (err, message) =>

             if (err)
                 self.emit 'error', err


Moved {morgan} middleware to after assets and static

--- app/middleware.coffee
+++ app/middleware.coffee
@@ -18,11 +18,6 @@
   # getting a shit-ton of useless lines in logs
   #app.use serve_favicon "#{__dirname}/assets/favicon.ico"

-  # Morgan logs requests from browser. Used ONLY if logging level
-  # is info (super verbose).
-  if app.get('log level') is 'info'
-    app.use morgan 'combined'
-
   # These are quite obvious
   app.use serve_static "#{__dirname}/static"
   app.use connect_assets paths: [
@@ -35,6 +30,11 @@
   app.use '/assets/bootswatch-dist/fonts',
     serve_static "#{__dirname}/../bower_components/bootswatch-dist/fonts"

+  # Morgan logs requests from browser. Used ONLY if logging level
+  # is info (super verbose).
+  if app.get('log level') is 'info'
+    app.use morgan 'combined'
+
   app.use express_session
     secret: app.get 'session secret'
     resave: false


Removed unused WAITFOR.

Removed unused WAITFOR.

--- src/grammar.coffee
+++ src/grammar.coffee
@@ -621,7 +621,7 @@
   ['nonassoc',  'INDENT', 'OUTDENT']
   ['right',     '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS']
   ['right',     'FORIN', 'FOROF', 'BY', 'WHEN']
-  ['right',     'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS', 'AWAIT', 'WAITFOR']
+  ['right',     'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS', 'AWAIT']
   ['left',      'POST_IF']
 ]


add smooth scroll to nav links in header

--- components/layout/index.coffee
+++ components/layout/index.coffee
@@ -38,6 +38,17 @@

   $('body').on 'click', '.js-language-toggle', toggleLanguageOnClick

+  $ ->
+    $('a[href*=#]:not([href=#])').click ->
+      if location.pathname.replace(/^\//, '') == @pathname.replace(/^\//, '') and location.hostname == @hostname
+        target = $(@hash)
+        target = if target.length then target else $('[name=' + @hash.slice(1) + ']')
+        if target.length
+          $('html,body').animate { scrollTop: target.offset().top - 140 }, 1000
+          return false
+      return
+    return
+

   initSlideShow()
   initExpandContent()


Remove unused require

Remove unused require

--- lib/incompatible-packages-status-view.coffee
+++ lib/incompatible-packages-status-view.coffee
@@ -1,5 +1,5 @@
 _ = require 'underscore-plus'
-{$, View} = require 'atom-space-pen-views'
+{View} = require 'atom-space-pen-views'

 module.exports =
 class IncompatiblePackagesStatusView extends View


Fix heroku issues

Fix heroku issues

by adding `vendor/node` directory to `conventions.ignored`
Resolves brunch/uglify-js-brunch#15

--- src/helpers.coffee
+++ src/helpers.coffee
@@ -220,8 +220,10 @@

   conventions          = config.conventions   ?= {}
   conventions.assets  ?= /assets[\\/]/
-  conventions.ignored ?= paths.ignored ? (path) ->
-    sysPath.basename(path)[0] is '_'
+  conventions.ignored ?= paths.ignored ? [
+    (path) -> sysPath.basename(path)[0] is '_'
+    /vendor[\\/]node[\\/]/
+  ]
   conventions.vendor  ?= /(^bower_components|vendor)[\\/]/

   config.notifications ?= true


Simpler toString

Simpler toString

--- nodejs/MicroFloComponent.coffee
+++ nodejs/MicroFloComponent.coffee
@@ -101,7 +101,7 @@
       return unless event is 'data'

       # Assumes microcontroller is ready to accept commands (ie: magic string has been sent)
-      b = microflo.commandstream.dataLiteralToCommand packet.toString(), nodeId, portId
+      b = microflo.commandstream.dataLiteralToCommand '' + packet, nodeId, portId
       @transport.write b

     # TODO: Detect type information from graph


add status link to toolbar

add status link to toolbar


Bump to version 1.2.0

Bump to version 1.2.0

--- index.cjsx
+++ index.cjsx
@@ -37,7 +37,7 @@
   priority: 8
   displayName: <FontAwesome key={0} name='heart' />{' ' + __("Map HP")}
   description: __("Map HP")
-  version: '1.0.0'
+  version: '1.2.0'
   author: 'Chiba'
   link: 'https://github.com/Chibaheit'
   reactClass: React.createClass


fix zero damage

fix zero damage


fix intercom details

fix intercom details

--- lineman/app/services/intercom_service.coffee
+++ lineman/app/services/intercom_service.coffee
@@ -30,7 +30,9 @@
           id: group.id
           key: group.key
           name: group.name
-          description: group.description
+          subscription_kind: group.subscriptionKind
+          subscription_plan: group.subscriptionPlan
+          subscription_expires_at: group.subscriptionExpiresAt
           creator_id: group.creatorId
           visible_to: group.visibleTo
           cohort_id: group.cohortId


rm consoles

rm consoles

--- apps/channel/client.coffee
+++ apps/channel/client.coffee
@@ -30,9 +30,7 @@
     super

   checkUserAbilities: (collaborators) ->
-    console.log 'mediator.shared.current_user.id', mediator.shared.current_user.id, "collaborators.pluck('id')", collaborators.pluck('id')
     if _.contains collaborators.pluck('id'), mediator.shared.current_user.id
-      console.log 'should add new block view'
       new NewBlockView
         el: $ ".grid__block--new-block"
         container: @$el


Update Atom config

Update Atom config

--- atom/atom/config.cson
+++ atom/atom/config.cson
@@ -1,9 +1,8 @@
 "*":
   "atom-alignment": {}
   "atom-beautify":
-    _analyticsUserId: "016e9f8c-043c-4df4-b21b-8532dd9d8856"
     general:
-      _analyticsUserId: "b34d75b0-43ef-49e3-973a-21111427aa02"
+      _analyticsUserId: "016e9f8c-043c-4df4-b21b-8532dd9d8856"
   "atom-color-highlight": {}
   "atom-ctags":
     autoBuildTagsWhenActive: true
@@ -46,9 +45,6 @@
       "file-type-icons"
       "autocomplete-bibtex"
       "wrap-guide"
-      "atom-ctags"
-      "tidy-markdown"
-      "markdown-scroll-sync"
     ]
     ignoredNames: [
       ".git"
@@ -60,7 +56,7 @@
     ]
     projectHome: "/Users/Felix/Development"
     themes: [
-      "one-dark-ui"
+      "dracula-ui"
       "dracula-theme"
     ]
   docblockr: {}
@@ -143,6 +139,7 @@
     position: "Left"
   "tree-view":
     hideIgnoredNames: true
+    showOnRightSide: true
   "vim-mode": {}
   "vim-surround":
     surroundKey: "S"


Remove shortcute ctrl+space bound on window. Related to #584 and #647

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -287,15 +287,9 @@
             # One callback to rule them all
             $(window).mousemove @handle_mousemove
             $(window).mouseup @handle_mouseup
-            $(window).keydown  @handle_keydown
             @id_execution = 0

             @render()
-        handle_keydown: (event) =>
-            if event?.type is 'keydown' and event?.which is 32 and (event.ctrlKey is true or event.metaKey is true)
-                @options_view.toggle_suggestions()
-                @hide_suggestion_and_description()
-

         handle_mousemove: (event) =>
             @results_view.handle_mousemove event
@@ -2412,8 +2406,6 @@
             $(window).off 'resize', @display_full
             $(document).unbind 'mousemove', @handle_mousemove
             $(document).unbind 'mouseup', @handle_mouseup
-            $(document).unbind 'keydown', @handle_keydown
-

             clearTimeout @timeout_driver_connect
             # We do not destroy the cursor, because the user might come back and use it.


make sure save does not write over changes made by other instances

--- src/learner.coffee
+++ src/learner.coffee
@@ -10,12 +10,27 @@
   load: ->
     try
       @stats = JSON.parse(localStorage.getItem this.storage_key)
+      @stats.lastLoaded
     catch error
       console.log 'Init @stats: caused by'
       @stats = {
         terms: {}
-        lastSaved: undefined
+        lastSaved: undefined,
+        lastLoaded: new Date
       }

   save: ->
-    localStorage.setItem this.storage_key, JSON.stringify(@stats)
+    if @getLastSaved() <= @stats.lastLoaded
+      @stats.lastSaved = new Date
+      localStorage.setItem this.storage_key, JSON.stringify(@stats)
+    else
+      console.log 'The stats has been updated since load'
+
+  getLastSaved: ->
+    try
+      return JSON.parse(localStorage.getItem this.storage_key).lastSaved
+    catch error
+      return undefined
+
+root = exports ? window
+root.Learner = Learner


Fix behavior with curly bracket. Related to #764

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -649,7 +649,7 @@
                                     @insert_next char_to_insert
                             else # Else we'll just insert one quote
                                 return true
-                    else if last_element_incomplete_type isnt 'string' and last_element_incomplete_type isnt 'object_key'
+                    else if last_element_incomplete_type isnt 'string'
                         next_char = @get_next_char()

                         if char_to_insert of @matching_opening_bracket


added more ignoring errors

added more ignoring errors

--- src/lib/bot.coffee
+++ src/lib/bot.coffee
@@ -179,7 +179,7 @@
                 timer = null

             stderr = stderr.trim()
-            if stderr.length > 0 and stderr.indexOf('ETIMEDOUT') is -1 and stderr.indexOf('socket hang up') is -1
+            if stderr.length > 0 and stderr.indexOf('TIMEDOUT') is -1 and stderr.indexOf('socket hang up') is -1 and stderr.indexOf('CONNRESET') is -1

                 Mail.send
                     subject: '[SH-ENL-BOT] Error when fetching'


basic playout stuff

basic playout stuff

--- javascripts/lib/random_playouts.coffee
+++ javascripts/lib/random_playouts.coffee
@@ -25,3 +25,14 @@
     move = create_ramdom_move(size, color, tries)
     tries += 1
   move
+
+
+is_finished_game = (board)->
+  if board.moves.length > 2
+    is_pass_move(get_last_move(board)) && (is_pass_move(board.moves[board.moves.length - 2]))
+
+playout_for_board = (board)->
+  until is_finished_game(board)
+    move = generate_random_move_for(board)
+    play_stone(move, board)
+  board


Add an explicit soak to avoid deleting undefined progress bar

Part of #3496

--- admin/static/coffee/tables/table.coffee
+++ admin/static/coffee/tables/table.coffee
@@ -836,6 +836,5 @@
             @$('.alert_confirm_delete').hide()

         remove: =>
-            if @progress_bar?
-                @progress_bar.destroy()
+            @progress_bar?.destroy()
             super()


Fix rejoin issue

Fix rejoin issue

--- public/js/wireroom.coffee
+++ public/js/wireroom.coffee
@@ -164,8 +164,12 @@

     notificationCenter = new WRNotificationCenter(self)

+  _joined: {}
+
   joinChannel: (room) ->
     self = this
+    return if _joined[ room ]
+    _joined[ room ] = true

     # create a new tab for the channel


addClass isntead of toggle

addClass isntead of toggle

--- app/assets/javascripts/builds.js.coffee
+++ app/assets/javascripts/builds.js.coffee
@@ -36,7 +36,7 @@
     img.onload = imageToCanvas

 hideBugged = ->
-  $('.bugged').toggleClass 'hide'
+  $('.bugged').addClass 'hide'

 readyCalls = ->
   removeAA()


sankey: tooltip with the total node value

--- app/assets/javascripts/d3/sankey.coffee
+++ app/assets/javascripts/d3/sankey.coffee
@@ -264,6 +264,9 @@
         attr("transform", "translate(#{@margin.left}, #{@margin.top})")
       @links = @draw_links()
       @nodes = @draw_nodes()
+      $("g.node rect").tipsy
+        gravity: 'sw'
+

     draw_links: =>
       # links are treated as a group made of a link path and label text element
@@ -289,9 +292,7 @@
       attr("y", (d) => @y d.right_y()).
       attr("dx", -55).
       attr("dy", 2).
-      style("opacity", 0).
-      text((d) => @format_value d.value())
-
+      style("opacity", 0)
       return links

     draw_nodes: =>
@@ -325,9 +326,6 @@

       return nodes

-    # formats the value shown in the link labels
-    format_value: (x) -> "#{x.toFixed(2)} PJ"
-
     # callbacks
     #
     node_mouseover: ->
@@ -372,6 +370,7 @@
         selectAll("rect").
         transition().duration(500).
         attr("height", (d) => @y d.value()).
+        attr("title", (d) => Metric.autoscale_value d.value(), 'PJ', 2).
         attr("y", (d) => @y(d.y_offset()))

       # then move the node label
@@ -392,7 +391,7 @@
         transition().duration(500).
         selectAll("text.link_label").
         attr("y", (link) => @y link.right_y()).
-        text((d) => @format_value d.value())
+        text((d) => Metric.autoscale_value d.value(), 'PJ', 2)

 class D3.sankey.NodeList extends Backbone.Collection
   model: D3.sankey.Node


sankey: tooltip clean-up

sankey: tooltip clean-up

--- app/assets/javascripts/d3/sankey.coffee
+++ app/assets/javascripts/d3/sankey.coffee
@@ -343,6 +343,9 @@

       @link_list = _.map D3.sankey.charts[k].data.links, (l) =>
         new D3.sankey.Link(_.extend l, view: this)
+
+      @value_formatter = (x) -> Metric.autoscale_value x, 'PJ', 2
+
       @initialize_defaults()

     # this method is called when we first render the chart. It is called if we
@@ -437,6 +440,7 @@
     setup_tooltips: ->
         $("g.node").qtip
           content:
+            title: -> $(this).attr('data-tooltip-title')
             text: -> $(this).attr('data-tooltip')
           position:
             target: 'mouse'
@@ -480,15 +484,15 @@
       # update the node label
       @nodes.data(@node_list, (d) -> d.get('id'))
         .attr("data-tooltip", (d) =>
-          h = "
-            #{d.label()}: #{Metric.autoscale_value d.value(), 'PJ', 2}
-
"
+          h = ""
           for l in d.right_links
-            h += "-> #{l.right.label()}: #{Metric.autoscale_value l.value(), 'PJ', 2}
"
+            h += "-> #{l.right.label()}: #{@value_formatter l.value()}
"
           for l in d.left_links
-            h += "#{l.left.label()} <-: #{Metric.autoscale_value l.value(), 'PJ', 2}
"
+            h += "<- #{l.left.label()}: #{@value_formatter l.value()}
"
           h
           )
+        .attr("data-tooltip-title", (d) =>
+          "#{d.label()}: #{@value_formatter d.value()}")

       # move the rectangles
       @nodes.selectAll("rect")


Use non-american date format for event since ruby 1.9+ Time.prase does not support

--- app/assets/javascripts/events.js.coffee
+++ app/assets/javascripts/events.js.coffee
@@ -14,7 +14,7 @@

     setPickers = ()->
       $('.timepicker').timepicker({  'step': 15, 'showDuration': true, 'timeFormat': 'g:ia', 'scrollDefaultNow': true })
-      $('.datepicker').datepicker({ 'autoclose': true, 'dateFormat': 'm/d/yy', 'format': 'mm/dd/yyyy' })
+      $('.datepicker').datepicker({ 'autoclose': true, 'dateFormat': 'd/m/yy', 'format': 'dd/mm/yyyy' })

     runPermanentHooks = ()->
       $(document).on 'keydown', '.silence-input', ->


Small fixes in FullCalendar

Small fixes in FullCalendar

--- app/assets/javascripts/events.js.coffee
+++ app/assets/javascripts/events.js.coffee
@@ -67,26 +67,17 @@
       FullCalendar.Events.send_request(url, data, revertFunc)

     show: (event) ->
-      $.get event.path, (data) ->
+      url = event.path + "/edit"
+      $.get url, (data) ->
         $('#modal-holder').html(data).find('.modal').modal()

-    showPeriodAndFrequency: (value) ->
-      switch (value)
-        when 'Daily'
-          $('#period').html('day')
-          $('#frequency').show()
-        when 'Weekly'
-          $('#period').html('week')
-          $('#frequency').show()
-        when 'Monthly'
-          $('#period').html('month')
-          $('#frequency').show()
-        when 'Yearly'
-          $('#period').html('year')
-          $('#frequency').show()
-        else
-          $('#period').html('')
-          $('#frequency').hide()
+    toggle_period_and_frequency: (value) ->
+      if value == 'no_repeat'
+        $('#frequency').hide()
+        $('#period').html('')
+      else
+        $('#period').html(value)
+        $('#frequency').show()

   Form:


updating images coffeescript

updating images coffeescript

--- app/assets/javascripts/images.js.coffee
+++ app/assets/javascripts/images.js.coffee
@@ -1,13 +1,13 @@
 ready = ->
-  Dropzone.autoDiscover = false
-  $('#new_image').dropzone
-  dropzone = new Dropzone('.dropzone',
-    maxFilesize: 500
-    paramName: 'image[image]'
-    addRemoveLinks: true
-    uploadMultiple: true
-    enqueueForUpload: true
-    clickable: true)
+  if $('.dropzone').length > 0
+    Dropzone.autoDiscover = false
+    dropzone = new Dropzone('.dropzone',
+      maxFilesize: 500
+      paramName: 'image[image]'
+      addRemoveLinks: true
+      uploadMultiple: true
+      enqueueForUpload: true
+      clickable: true)

 $(document).ready(ready)
 $(document).on('page:load', ready)


Fix user-defined volume memorization

Fix user-defined volume memorization

--- app/assets/javascripts/player.js.coffee
+++ app/assets/javascripts/player.js.coffee
@@ -45,6 +45,10 @@
     ready: ->
       DEBUG('jPlayer: ready')
       $this = $(this)
+      user_volume = memcache.get('volume')
+      user_muted = memcache.get('muted')
+      $this.jPlayer('volume', user_volume) unless isNaN(user_volume)
+      $this.jPlayer('mute', user_muted) unless isNaN(user_muted)
       $this.trigger('reload')

     play: ->


One more effect in pwnfx

One more effect in pwnfx

--- app/assets/javascripts/pwn-fx.js.coffee
+++ app/assets/javascripts/pwn-fx.js.coffee
@@ -186,8 +186,8 @@
 #       with a random string; useful for generating unique IDs
 #   data-pwnfx-render-target: set on the element(s) receiving the rendered HTML;
 #       set to the identifier in data-pwnfx-render
-#   data-pwnfx-render-source: set on the <script> tag containing the source HTML
-#       to be rendered; set to the identifier in data-pwnfx-render
+#   data-pwnfx-render-source: set to the identifier in data-pwnfx-render, on the
+#       <script> tag containing the source HTML
 class PwnFxRender
   constructor: (element, identifier, scopeId) ->
     sourceSelector = "script[data-pwnfx-render-source=\"#{identifier}\"]"
@@ -393,6 +393,46 @@
 PwnFx.registerEffect 'hide', PwnFxHide

+# Shows / hides elements in a DOM depending on an input's value.
+#
+# Attributes:
+#   data-pwnfx-showif: an identifier that connects the source <input>
+#   data-pwnfx-showif-class: the CSS class that is added to hidden elements;
+#       (default: hidden)
+#   data-pwnfx-showif-is: the value that the <input> has to match for this
+#       element to be shown
+#   data-pwnfx-showif-source: set to the identifier in data-pwnfx-showif
+#       on the <input> whose value determines if this element is shown or not
+class PwnFxShowIf
+  constructor: (element, identifier, scopeId) ->
+    hiddenClass = element.getAttribute('data-pwnfx-showif-class') || 'hidden'
+    showValue = element.getAttribute 'data-pwnfx-showif-is'
+    sourceSelector = "[data-pwnfx-showif-source=\"#{identifier}\"]"
+
+    oldValue = null
+    onChange = (event) ->
+      console.log ['change', event.target]
+      value = event.target.value
+      return true if value == oldValue
+      oldValue = value
+
+      console.log [value, showValue]
+      if value == showValue
+        element.classList.remove hiddenClass
+      else
+        element.classList.add hiddenClass
+      true
+
+    scope = PwnFx.resolveScope scopeId, element
+    for sourceElement in PwnFx.queryScope(scope, sourceSelector)
+      sourceElement.addEventListener 'change', onChange, false
+      sourceElement.addEventListener 'keydown', onChange, false
+      sourceElement.addEventListener 'keyup', onChange, false
+      onChange target: sourceElement
+
+PwnFx.registerEffect 'showif', PwnFxShowIf
+
+
 # Removes elements from the DOM when an element is clicked.
 #
 # Attributes:


ventas.new route

ventas.new route

--- app/assets/javascripts/router.js.coffee
+++ app/assets/javascripts/router.js.coffee
@@ -11,6 +11,7 @@
     @route "new"
   @resource "ventas", ->
     @route "index", path: "/"
+    @route "new"
   @resource "clientes", ->
     @route "index", path: "/"
     @route "new"


[backport] Fix broken links of tasks in user story detail page

--- app/coffee/modules/related-tasks.coffee
+++ app/coffee/modules/related-tasks.coffee
@@ -77,16 +77,12 @@
                 renderView($model.$modelValue)

         renderView = (task) ->
-            @childScope.$destroy()
-            @childScope = $scope.$new()
-            $el.off()
-
             perms = {
                 modify_task: $scope.project.my_permissions.indexOf("modify_task") != -1
                 delete_task: $scope.project.my_permissions.indexOf("delete_task") != -1
             }

-            $el.html($compile(templateView({task: task, perms: perms}))(childScope))
+            $el.html($compile(templateView({task: task, perms: perms}))($scope))

             $el.on "click", ".edit-task", ->
                 renderEdit($model.$modelValue)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -133,7 +133,7 @@
     nameColor - Color of the property name displayed above the meter
     font - Size and style of the meter's font
     height - Height of the meter
-    position -
+    position - Position of the
   ###
   meter: (options={}) ->
     Object.reverseMerge options,


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/metered.coffee
+++ src/metered.coffee
@@ -131,7 +131,7 @@
     borderWidth - Width of the meter's border
     color - Color of the meter's inner rectangle
     nameColor - Color of the property name displayed above the meter
-    font - Size and style of the meter
+    font - Size and style of the meter title
   ###
   meter: (options={}) ->
     Object.reverseMerge options,


Fixing tests

Fixing tests

--- javascripts/lib.coffee
+++ javascripts/lib.coffee
@@ -86,9 +86,10 @@
       subject instanceof @type
   class_mixer RefinementByType

-  ttm.defaults = (provided, defaults)->
-    _.extend({}, defaults, provided)
+
+  return Refinement

+ttm.defaults = (provided, defaults)->
+  _.extend({}, defaults, provided)

-  return Refinement


change the layout of comprehension

change the layout of comprehension

--- mechanics/003-twilight-zone/simulator.coffee
+++ mechanics/003-twilight-zone/simulator.coffee
@@ -48,18 +48,16 @@
             if time - Math.floor(time) < 0.1
                 lng = (i) -> 2 * Math.PI / 128 * i
                 lat = (j) -> Math.PI / 128 * (64 - j)
-                cut = (val) ->
-                        if val > 0
-                               val
-                        else
-                               0
+                cut = (val) -> val > 0 ? val : 0
                 u1 = vec3.unify([x1 - x3, y1 - y3, 0])
                 u2 = vec3.unify([x2 - x3, y2 - y3, 0])
                 twilight = (
-                  [
-                      lum1 * cut(vec3.inner(p.zenith(lng(i), lat(j), time), u1))
-                      lum2 * cut(vec3.inner(p.zenith(lng(i), lat(j), time), u2))
-                  ] for i in [0...128] for j in [0...128]
+                    (
+                        [
+                           lum1 * cut(vec3.inner(p.zenith(lng(i), lat(j), time), u1))
+                           lum2 * cut(vec3.inner(p.zenith(lng(i), lat(j), time), u2))
+                        ] for j in [0...128]
+                    ) for i in [0...128]
                 )
                 self.postMessage({twlt: twilight})


Update rest.coffee

Update rest.coffee

--- src/rest.coffee
+++ src/rest.coffee
@@ -1,6 +1,26 @@
 # rest api
 path = require 'path'
 Nedb = require 'nedb'
+
+# export express app plugin
+module.exports = (app) ->
+	# collections api
+	app.get ':db', ls
+	app.get ':db/:collection', get_collection
+
+	# docs api
+	app.get ':db/:collection/:id', get_doc
+	app.del ':db/:collection/:id', del_doc
+	app.update ':db/:collection/:id', update_doc
+	app.post ':db/:collection', add_doc
+
+	# metadata
+	app.get ':db/:collection/:id/metadata', get_meta
+	app.update ':db/:collection/:id/metadata', update_meta
+
+	# TODO permissions
+	# app.get ':db/:collection/:id/permissions', get_permissions
+	# app.update ':db/:collection/:id/permissions', update_permissions

 # dirs
 root = path.dirname __dirname
@@ -29,14 +49,14 @@
 	# todo get list of databases from data dir
 	res.send(["docs"])

-# GET :db/:collection handler
+# GET db/collection handler
 # TODO support ?selector={}
 get_collection = (req, res) ->
 	db = load_db req
 	db.find {}, (err, docs) ->
 		send_docs(res, err, docs)

-# GET :db/:collection/:id handler
+# GET db/collection/id handler
 get_doc = (req, res, doc_handler) ->
 	db = load_db req
 	id = req.params.id
@@ -45,7 +65,7 @@
 		json = if doc_handler then doc_handler(doc) else serialize_doc(doc)
 		res.send json

-# DELETE :db/:collection/:id handler
+# DELETE db/collection/id handler
 del_doc = (req, res) ->
 	db = load_db req
 	id = req.params.id
@@ -53,7 +73,7 @@
 		return res.send {error: err} if err
 		res.send {ok: numRemoved}

-# POST :db/:collection handler
+# POST db/collection handler
 add_doc = (req, res) ->
 	db = load_db req
 	doc = req.body
@@ -70,7 +90,7 @@
 		newDoc.$metadata.id = newDoc._id
 		res.send serialize_doc(newDoc)

-# UPDATE :db/:collection/:id
+# UPDATE db/collection/id
 update_doc = (req, res, update_handler) ->
 	db = load_db req
 	id = req.params.id
@@ -87,7 +107,7 @@
 			return res.send {error: err} if err
 			res.send serialize_doc(doc)

-# GET :db/:collection/:id/metadata
+# GET db/collection/id/metadata
 get_meta = (req, res) ->
 	get_doc req, res, (doc) -> doc.$metadata

@@ -95,18 +115,3 @@
 update_meta = (req, res) ->
 	update_doc req, res, (doc) -> doc.$metadata

-# export express app plugin
-module.exports = (app) ->
-	# collections api
-	app.get ':db', ls
-	app.get ':db/:collection', get_collection
-
-	# docs api
-	app.get ':db/:collection/:id', get_doc
-	app.del ':db/:collection/:id', del_doc
-	app.update ':db/:collection/:id', update_doc
-	app.post ':db/:collection', add_doc
-
-	# metadata
-	app.get ':db/:collection/:id/metadata', get_meta
-	app.update ':db/:collection/:id/metadata', update_meta


MAJOR REVAMP

MAJOR REVAMP

--- paginate.coffee
+++ paginate.coffee
@@ -1,4 +1,3 @@
-Meteor.Paginations = {}
 Meteor.Pagination = (collection, settings = {}) ->
   @setCollection collection
   @setId collection
@@ -30,7 +29,7 @@
   pageSizeLimit: 60 #Unavailable to the client
   paginationMargin: 3
   perPage: 10
-  requestTimeout: 4
+  requestTimeout: 2
   route: "/page/"
   router: false
   routerTemplate: "pages"
@@ -106,12 +105,12 @@
         @route "home",
             path: "/"
             template: t
-            onBeforeRun: ->
+            before: ->
                 self.sess "currentPage", 1
         @route "page",
             path: pr
             template: t
-            onBeforeRun: ->
+            before: ->
                 self.sess "currentPage", parseInt(@params.n)
   setTemplates: ->
     name = if @templateName is "" then @name else @templateName
@@ -144,8 +143,10 @@
   loading: (p) ->
     unless @infinite
       for k, v of @subscriptions
-        @subscriptions[k].stop()
-        delete @subscriptions[k]
+        try
+          @subscriptions[k].stop()
+          delete @subscriptions[k]
+        catch e
       @_ready = false
       if p is @currentPage() and Session?
         @sess "ready", false
@@ -203,11 +204,16 @@
     @pagesRequested = []
     @pagesReceived = []
   onData: (page) ->
-    #console.log page, 'READY'
+    #console.log @name, page, 'READY'
     @currentSubscription = page
     @logResponse page
     @ready(page)
     @cache[page] = @_getPage(1).fetch()
+    @checkQueue()
+  checkQueue: ->
+    if @queue.length
+      i = @queue.shift() until i in @neighbors(@currentPage()) or not @queue.length
+      @recvPage i, false
   _getPage: (page) ->
     #console.log(page, ((page - 1) * @perPage), @perPage, @sort) if Meteor.isServer
     #console.log "Fetching last result"
@@ -247,10 +253,10 @@
     Run again if something goes wrong and the page is still needed
     """
     @timeouts[page] = setTimeout ((page) ->
-      if (not page in @pagesReceived or not page in @pagesRequested) and page in @neighbors @currentPage()
-        #console.log "Again #{page}"
+      if (page not in @pagesReceived or page not in @pagesRequested) and page in @neighbors @currentPage()
+        console.log "Again #{page}"
         @recvPage page
-    ).bind(@, page), 2000
+    ).bind(@, page), @requestTimeout * 1000
     """
     Subscription may block unless deferred.
     """
@@ -260,13 +266,13 @@
           @.onData page
         ).bind(@, page)
         onError: (e) ->
-          console.log e
+          console.log 'Error', e
     ).bind(@, page)
   recvPages: (page) ->
     #console.log "recvPages called for #{page}"
     #console.log "Neighbors list: ", @neighbors page
     for p in @neighbors page
-      unless p in @pagesRequested and p in @pagesReceived or p of @cache
+      unless p in @pagesReceived or p of @cache
         #console.log "Requesting #{p}" + (if p is page then " (current)" else " (not current)")
         @recvPage p
   watchman: ->
@@ -275,50 +281,7 @@
         Meteor.Pagination.prototype.watch.call v
     , 1000
   watch: ->
-    p = @currentPage()
-    """
-    Sometimes (when page changes are too frequent), stopping
-    a subscription before getting data for a new page fails.
-    In such cases, the local collection has to be cleaned up again
-    to make place for a new chunk and all data have to be reloaded.
-    Otherwise, the same data shows up on every newly requested page.
-    """
-    if @_ready and (
-      (not @infinite and @Collection.find().count() > @perPage) or
-      (not @cache[p]? or (@cache[p].length is 0 and p <= @sess "totalPages"))
-      )
-      console.log @name, 'rl1'
-      try
-        for i in @Collection._collection.find().fetch()
-          @Collection._collection.remove i
-      catch e
-      @reload()
-      """
-      Make sure the current page is loaded. If so,
-      make sure the status is set to ready.
-      """
-    else if p in @pagesReceived
-      @ready(true)
-    else if @_ready
-      """
-      Stop any background requests that may be running and get
-      the current page immediately.
-      """
-      #console.log @name, 'rl2', p
-      try
-        @clearQueue()
-        @recvPages p
-      catch e
-    else if (@now() - @timeLastRequest) / 1000 > @requestTimeout
-      #console.log @name, 'rl3', p
-      @reload()
-      @recvPages p
-    """
-    If all previous requests are complete, proceed to the next one in the queue.
-    """
-    if @queue.length and @_ready
-      """Current page is never in the queue, so isCurrent = false"""
-      @recvPage @queue.shift(), false
+    @checkQueue @currentPage()
   neighbors: (page) ->
     @n = [page]
     for d in [1 .. @dataMargin]


Commenting tests which break with validation

To be uncommented when we implement TP #1105 properly

--- jsapp/test/unit/SkipLogic.Tests.coffee
+++ jsapp/test/unit/SkipLogic.Tests.coffee
@@ -452,14 +452,19 @@

   describe 'integer response model', () ->
     _response_model = new $mRdsl.IntegerResponseModel()
-    it 'sets state to valid when passed value is integer', () ->
-      _response_model.set('value', 123, validate:true)
-
-      expect(_response_model.isValid()).toBeTruthy()
-    it 'sets state to invalid when passed value is decimal', () ->
-      _response_model.set('value', 123.1234, validate:true)
-
-      expect(_response_model.isValid()).toBeFalsy()
+    # until we handle validation properly,
+    # the SkipLogicValidatingTextResponseView will prevent the view from
+    # saving
+
+    # it 'sets state to valid when passed value is integer', () ->
+    #   _response_model.set('value', 123, validate:true)
+
+    #   expect(_response_model.isValid()).toBeTruthy()
+    # it 'sets state to invalid when passed value is decimal', () ->
+    #   _response_model.set('value', 123.1234, validate:true)
+
+    #  expect(_response_model.isValid()).toBeFalsy()
+
     it 'sets state to invalid when passed value is text', () ->
       _response_model.set('value', 'asdf', validate:true)


Refacoring headerbar

Refacoring headerbar

--- app/scripts/directives/headerbar.coffee
+++ app/scripts/directives/headerbar.coffee
@@ -7,20 +7,8 @@
     replace: true,
     link: (scope, element, attrs) ->
       scope.model = scope.model or {}
-
       scope.model.addPageFormVisible = false
-      scope.toggleAddPage = ->
-        scope.model.addPageFormVisible = !scope.model.addPageFormVisible
-        scope.model.searchFormVisible = false
-        if scope.model.addPageFormVisible
-          scope.toggleBar()
-
       scope.model.searchFormVisible = false
-      scope.toggleSearch = ->
-        scope.model.addPageFormVisible = false
-        scope.model.searchFormVisible = !scope.model.searchFormVisible
-        if scope.model.searchFormVisible
-          scope.toggleBar()

       scope.toggleBar = ->
         if scope.model.addPageFormVisible or scope.model.searchFormVisible
@@ -30,12 +18,29 @@
           $window.onclick = null
           scope.$apply()

-      _hideBar = (event, element, obj) ->
-        Ythreshold = Math.abs($(element).position().top) + $(element).height() - $($window).scrollTop()
-        if event.y > Ythreshold
-          scope.model[obj] = false
+      _toggle = (obj) ->
+        headers = ['searchFormVisible', 'addPageFormVisible']
+        headers.splice(headers.indexOf(obj), 1)
+        scope.model[obj] = !scope.model[obj]
+
+        for x in headers
+            scope.model[x] = false
+
+        if scope.model[obj]
+          scope.toggleBar()
+
+      scope.toggleAddPage = ->
+        _toggle('addPageFormVisible')
+
+      scope.toggleSearch = ->
+        _toggle('searchFormVisible')

       scope.hideBarIfClickOutside = (event, callback) ->
+        _hideBar = (event, element, obj) ->
+          Ythreshold = Math.abs($(element).position().top) + $(element).height() - $($window).scrollTop()
+          if event.y > Ythreshold
+            scope.model[obj] = false
+
         if scope.model.addPageFormVisible
           _hideBar(event, '.add-page-form', 'addPageFormVisible')
         else if scope.model.searchFormVisible


added dustjs-runtime helper desctiption.

--- src/helpers/dustjs-runtime.coffee
+++ src/helpers/dustjs-runtime.coffee
@@ -1,4 +1,6 @@
 module.exports.init = ( grunt ) ->
+  # Resolves dustjs runtime path and version
+  # ---
   fs = require "fs"
   semver = require "semver"


change constructor return value handling

--- src/util/obj.coffee
+++ src/util/obj.coffee
@@ -9,7 +9,9 @@
     else
       # Create function which forwards to given constructor function (if given).
       F = if not constructor? then -> else
-        (args) -> (constructor.apply @, args) || @ # if constructor returns truthy value: use it instead of object
+        (args) ->
+          ret = constructor.apply @, args
+          if ret != undefined then ret else @ # if constructor returns not undefined value: use it instead of object
       # Set functions prototype field.
       F.prototype = prototype
       # Create function which creates a new object with the given prototype and initializes with the given constructor.


Disable periodic serialization for buffers > 2MB

Writing so much data to IndexedDB is blocking the main thread for
perceptible amounts of time. A patch-based representation of the
modified state could allows us to pay only for what has changed, but is
too complex to justify implementing right now to support full crash
recovery for large files.

Signed-off-by: Max Brunsfeld <78036c9b69b887700d5846f26a788d53b201ffbb@github.com>

--- src/project.coffee
+++ src/project.coffee
@@ -70,7 +70,15 @@
   serialize: (options={}) ->
     deserializer: 'Project'
     paths: @getPaths()
-    buffers: _.compact(@buffers.map (buffer) -> buffer.serialize({markerLayers: options.isUnloading is true}) if buffer.isRetained())
+    buffers: _.compact(@buffers.map (buffer) ->
+      if buffer.isRetained()
+        state = buffer.serialize({markerLayers: options.isUnloading is true})
+        # Skip saving large buffer text unless unloading to avoid blocking main thread
+        if not options.isUnloading and state.text.length > 2 * 1024 * 1024
+          delete state.text
+          delete state.digestWhenLastPersisted
+        state
+    )

   ###
   Section: Event Subscription


Fix typo s/binded/bound

Fix typo s/binded/bound

--- src/promise.coffee
+++ src/promise.coffee
@@ -31,11 +31,11 @@
       for promise in promises
         results[promise._id] = {order: i++}

-        binded_success = ((success, promise)->
+        bound_success = ((success, promise)->
           return -> success.apply promise, arguments
         )(success, promise)

-        promise.then(binded_success, fail)
+        promise.then(bound_success, fail)

       promise_all


Fix infinite loop for rename conflicts

Fix infinite loop for rename conflicts

--- src/sync.coffee
+++ src/sync.coffee
@@ -138,7 +138,7 @@
         (err) =>
             if err
                 log.error err
-                @updateErrors change.doc, callback
+                @updateErrors change, callback
             else
                 log.debug "Applied #{change.seq}"
                 @pouch.setLocalSeq change.seq, (err) =>
@@ -149,13 +149,18 @@
                         @updateRevs change.doc, side, callback

     # Increment the counter of errors for this document
-    updateErrors: (doc, callback) ->
+    updateErrors: (change, callback) ->
+        doc = change.doc
         doc.errors = (doc.errors or 0) + 1
         # Don't try more than 10 times for the same operation
         return callback() if doc.errors >= 10
-        @pouch.db.put doc, (err) ->
-            # It's not important if the number of errors can't be saved
-            log.debug err if err
+        @pouch.db.put doc, (err) =>
+            # If the doc can't be saved, it's because of a new revision.
+            # So, we can skip this revision
+            if err
+                log.debug err
+                log.debug "Ignored #{change.seq}"
+                @pouch.setLocalSeq change.seq, callback
             # The sync error may be due to the remote cozy being overloaded.
             # So, it's better to wait a bit before trying the next operation.
             setTimeout callback, 3000


Beginnings of series tag popup menu thing, getting acclimated to coffeescript

--- static/books.coffee
+++ static/books.coffee
@@ -8,15 +8,25 @@
   base = document if not base?
   base.querySelectorAll sel

+HTMLElement::on = (evt, func, bubble) ->
+  if evt.constructor.name is 'Array'
+    this.addEventListener e, func, bubble for e in evt
+  else
+    # add a touchend event automatically for click events
+    if evt is 'click'
+      this.addEventListener 'touchend', func, bubble
+    this.addEventListener evt, func, bubble
+
 ajaxPost = (path, data, async, callbacks) ->
   x = new XMLHttpRequest()

-  switch typeof(callbacks)
-    when 'object'
-      x.addEventListener(evt, callback, false) for evt, callback of callbacks
-
-    when 'function'
-      x.addEventListener 'load', callbacks, true
+  if callbacks?
+    switch typeof(callbacks)
+      when 'object'
+        x.addEventListener(evt, callback, false) for evt, callback of callbacks
+
+      when 'function'
+        x.addEventListener 'load', callbacks, true

   x.open 'post', path, async

@@ -109,6 +119,85 @@
       else
         resp = JSON.parse x.response
         alert resp.msg
+
+class Series
+  constructor: () ->
+    @tags = $ '#tags li a'
+    tag.addEventListener 'click', showTagInfo, false for tag in @tags
+
+    @tagShown = null
+    @tagInfo = null
+
+showTagInfo = (e) ->
+  a = e.targetElement
+
+  if THIS.tagShown? and THIS.tagShown is a.innerText
+    return
+
+  info = THIS.tagInfo
+  info.style.display = 'none'
+
+  if not info?
+    ajaxPost '/ajax/tag-info', 'tagName': a.innerText, false, (e) ->
+      resp = JSON.parse e.response
+      if resp.err isnt null
+        alert resp.msg
+        return
+
+      info = make 'div', 'id': 'tag-info'
+      info.innerHTML = resp.msg
+
+      $('#tag-upvote', info).addEventListener 'click', (e) ->
+        # ajax post...
+        false
+      , false
+
+      $('#tag-downvote', info).addEventListener 'click', (e) ->
+        # ajax post...
+        false
+      , false
+
+      THIS.tagInfo = info
+      document.body.appendChild info
+
+  if not info?
+    alert 'Something went wrong getting tag info'
+    return
+
+  desc = $ '#tag-desc', info
+  ajaxPost '/ajax/tag-desc', 'tagName': a.innerText, false, (e) ->
+    resp = JSON.parse e.response
+    if resp.err isnt null
+      alert resp.msg
+      return
+
+    desc.innerText = resp.msg
+
+  info.style.display = 'block'
+
+  info.style.left = a.offsetWidth + a.offsetLeft + 8 + 'px'
+  info.style.top = a.offsetHeight/2 + a.offsetTop - info.offsetHeight
+  THIS.tagShown = a.innerText
+
+  # if user clicks outside the popup, close it
+  document.body.addEventListener 'click', (e) ->
+    el = e.targetElement
+    if el isnt info and el.parentElement isnt info
+      hideTagInfo()
+    true # keep going down the click events.
+  , true
+
+hideTagInfo = ->
+  if not THIS.tagInfo?
+    return
+
+  tagInfo.style.display = 'none'
+  THIS.tagShown = ''
+
+pageObjects =
+  'series': Series
+
+THIS = null

 main = ->
   pairs = [
@@ -123,6 +212,9 @@
       console.log "Element: #{pair[0]}"
       console.log e.stack

+  if (page = $('body').getAttribute 'id').length > 0
+    THIS = new pageObjects[page]()
+
 window.addEventListener 'load', main, true
 window.addEventListener 'load', ->
   $('head').appendChild make 'script',