Update cards-es.coffee

Update cards-es.coffee

-Fixed Fleet Officer name translation.

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -1263,7 +1263,7 @@
             name: "Mara Jade"
             text: """Al final de la fase de Combate, toda nave enemiga situada a alcance 1 que no tenga 1 ficha de Tensión recibe 1 ficha de Tensión."""
         "Fleet Officer":
-            name: "Oficial de Flota"
+            name: "Oficial de la Flota"
             text: """Acción: Elige un máximo de 2 naves aliadas que tengas a alcance 1-2 y asigna 1 ficha de Concentración a cada una de ellas. Luego recibes 1 ficha de Tensión."""
         "Lone Wolf":
             name: "Lobo solitario"


remove unused junk

remove unused junk

--- bokehjs/src/coffee/main.coffee
+++ bokehjs/src/coffee/main.coffee
@@ -60,38 +60,6 @@
   # glyphs
   Bokeh.Glyph = require("renderer/glyph/glyph")
   glyph_factory = require("renderer/glyph/glyph_factory")
-  Bokeh.AnnularWedge     = glyph_factory.annular_wedge
-  Bokeh.Annulus          = glyph_factory.annulus
-  Bokeh.Arc              = glyph_factory.arc
-  Bokeh.Asterisk         = glyph_factory.asterisk
-  Bokeh.Bezier           = glyph_factory.bezier
-  Bokeh.Circle           = glyph_factory.circle
-  Bokeh.CircleCross      = glyph_factory.circle_cross
-  Bokeh.CircleX          = glyph_factory.circle_x
-  Bokeh.Cross            = glyph_factory.cross
-  Bokeh.Diamond          = glyph_factory.diamond
-  Bokeh.DiamondCross     = glyph_factory.diamond_cross
-  Bokeh.Image            = glyph_factory.image
-  Bokeh.ImageRGBA        = glyph_factory.image_rgba
-  Bokeh.ImageURL         = glyph_factory.image_url
-  Bokeh.InvertedTriangle = glyph_factory.inverted_triangle
-  Bokeh.Line             = glyph_factory.line
-  Bokeh.MultiLine        = glyph_factory.multi_line
-  Bokeh.Oval             = glyph_factory.oval
-  Bokeh.Patch            = glyph_factory.patch
-  Bokeh.Patches          = glyph_factory.patches
-  Bokeh.Quad             = glyph_factory.quad
-  Bokeh.Quadratic        = glyph_factory.quadratic
-  Bokeh.Ray              = glyph_factory.ray
-  Bokeh.Rect             = glyph_factory.rect
-  Bokeh.Segment          = glyph_factory.segment
-  Bokeh.Square           = glyph_factory.square
-  Bokeh.SquareCross      = glyph_factory.square_cross
-  Bokeh.SquareX          = glyph_factory.square_x
-  Bokeh.Text             = glyph_factory.text
-  Bokeh.Triangle         = glyph_factory.triangle
-  Bokeh.Wedge            = glyph_factory.wedge
-  Bokeh.X                = glyph_factory.x

   # guides
   Bokeh.CategoricalAxis = require("renderer/guide/categorical_axis")


added un-added things

added un-added things

--- browser/coffee/mainpage.coffee
+++ browser/coffee/mainpage.coffee
@@ -11,11 +11,15 @@
             forward: 'control 8'
             back: 'control 7'

+    click: (which) -> ->
+        elm.ports.changing.send which
+
     render: ->
         div null,
             h1 null, "Change Ur Hotkeys"
             for name, setting of @state.settings
-                p null, "#{name}: ", span(null, setting), button(null, 'change')
+                p null, "#{name}: ", span(null, setting), button
+                    onClick: @click name

 hotkeys = Hotkeys()


Send new fullscale url

Send new fullscale url

--- components/ToFullscale.coffee
+++ components/ToFullscale.coffee
@@ -108,8 +108,10 @@
     .end (err, res) ->
       return callback err if err
       # If the newUrl exists, send it
-      return newUrl if res and res.statusCode is 200
+      if res and res.statusCode is 200
+        out.send newUrl
       # Otherwise, keep the original one
-      out.send url
+      else
+        out.send url
       callback null
   c


Do not redirect to a fullscale URL if is unavailable from flickr

--- components/ToFullscale.coffee
+++ components/ToFullscale.coffee
@@ -50,8 +50,14 @@
   parts.query = URI.buildQuery q
   return URI.build parts

-tryFindingFullscale = (url, out, callback) ->
+tryFindingFullscale = (url) ->
   return url.replace /[-_](small|thumbnail|thumb|tm)/, ''
+
+# Flickr redirects to a photo_unavailable image if the new URL do not exists
+tryRedirect = (original, redirected) ->
+  return redirected unless redirected.indexOf('staticflickr.com') isnt -1
+  return original if redirected.match /photo_unavailable/
+  return redirected

 exports.getComponent = ->
   c = new noflo.Component
@@ -98,13 +104,16 @@
     superagent.head newUrl
     .redirects(1)
     .end (err, res) ->
-      newUrl = res.request?.uri?.href ? newUrl
       return callback err if err
       console.log 'URL', url
       console.log 'new URL', newUrl
       console.log 'status', res.statusCode
+      console.log 'res.redirects', res.redirects
       # If the newUrl exists, send it
       if res and res.statusCode is 200
+        # Use redirection URL
+        if res.redirects?
+          newUrl = tryRedirect res.redirects[0], url
         out.send newUrl
       # Otherwise, keep the original one
       else


Fix regression: exception when cancelling draft

--- client/app/actions/message_action_creator.coffee
+++ client/app/actions/message_action_creator.coffee
@@ -313,7 +313,7 @@

             newMailboxIds = {}
             newMailboxIds[trashMailbox] = -1
-            updated.push message.set 'mailboxIDs', newMailboxIds
+            updated.push(message.set('mailboxIDs', newMailboxIds).toJS())

     # immediately apply change to refresh UI
     # Update datastore


Profile cleanup

Profile cleanup

--- src/scripts/controllers/Profile.coffee
+++ src/scripts/controllers/Profile.coffee
@@ -2,6 +2,7 @@
   Spine = require 'Spine'
   $ = require 'jQuery'

+  {delay} = require 'zooniverse/util'
   config = require 'zooniverse/config'

   User = require 'zooniverse/models/User'
@@ -52,7 +53,9 @@

         query = "SELECT * FROM #{config.cartoTable} WHERE user_id='#{User.current.id}'"
         url = "http://#{config.cartoUser}.cartodb.com/tiles/#{config.cartoTable}/{z}/{x}/{y}.png?sql=#{query}"
-        @userLayer = @map.addLayer url
+
+        delay =>
+          @userLayer = @map.addLayer url

     signOut: (e) =>
       e.preventDefault()


Sample Net is now a Petri net

Sample Net is now a Petri net

--- src/scripts/services/netStorage.coffee
+++ src/scripts/services/netStorage.coffee
@@ -2,7 +2,7 @@
 	constructor: (converterService, $localStorage, $state) ->

 		getDefaultNet = ->
-			new TransitionSystem({name: "Sample Net"})
+			new PetriNet({name: "Sample Net"})

 		getNetIdByName = (name) ->
 			return id for net, id in storage.nets when net.name is name


Working on get_data.

Working on get_data.

--- start.coffee
+++ start.coffee
@@ -34,14 +34,20 @@
 		# Note that the arguments 'filter' and 'attributes' are
 		# required, otherwise an error is returned.
 		@app.get "/pca", ( req, res ) ->
-
+
+			# Force filter and attributes to be specified.
+			for required in [ "filter", "attributes" ]
+				if not req.body[required]?
+					return _error_out res, "Required field '#{required}' not specified."
+
+			#

 	get_attributes: ( cb ) ->
 		# Helper function so that code makes logical sense
 		# when reading.
 		get_data null, null, cb

-	get_data: ( filter, attributes, cb ) ->
+	get_data: ( filters, attributes, cb ) ->

 		# If the data that has been specified is a function, simply
 		# pass off to it.
@@ -52,14 +58,30 @@

 		# If both filter and attributes are null,
 		# return a list of attributes.
-		if not filter and not attributes
+		if not filters and not attributes
 			_r = [ ]
 			for data_obj in data
 				_r.push key for key, val of data_obj when _r.indexOf( key ) < 0
 			return cb null, _r

-		# If we've gotten here, there is at least a filter or attributes.
+		# If we've gotten here, there is at least a set of filters and attributes.
 		# As well we're dealing with an array of objects in @options['data'].
+		_r = [ ]
+		for data_obj in @options['data']
+
+			skip = false
+			for key, val of filters
+				if data_obj[key] isnt val
+					skip = true
+			if skip
+				continue
+
+			_o = { }
+			for attr in attributes
+				_o[attr] = data_obj[attr]
+			_r.push _o
+
+		cb null, _r

 	_error_out: ( res, err ) ->
 		# Helper function for sending an error back.


Add report to end of process

Add report to end of process

--- tourney.coffee
+++ tourney.coffee
@@ -47,7 +47,7 @@
 get_cmd = (p, game_state, player_state) ->
   "coffee botResponder.coffee --bot '#{p.file}'"

-run_match = (p1, p2) ->
+run_match = (p1, p2, callback) ->
   p1.state = {}
   p2.state = {}

@@ -55,6 +55,9 @@

   g = new Game([p1.name, p2.name], true)
   g.run((result) ->
+    p1.score += result[0]
+    p2.score += result[1]
+
     if (result[0] == 0.5)
       say("results", "Tie!")
     else if (result[0] == 1)
@@ -63,6 +66,7 @@
       say("results", "#{p2.name} vanquished #{p1.name}")
     else
       say("results", "nobody wins!")
+    callback()
   )

 Models.Team.findAll()
@@ -114,9 +118,10 @@

   _.each(PARTICIPANTS, (p) ->
     p.strikes = 0
+    p.score = 0

     cmd = get_cmd(p)
-    proc = exec(cmd, {silent: true, async: true})
+    p.proc = exec(cmd, {silent: true, async: true})

     Bot.register(p.name, (game_state, player_state, move) ->
       if (p.strikes < 3)
@@ -163,13 +168,36 @@

   say('play', "Waiting for processes to settle")

+  def = Q.defer()
+
+  d = _.after(MATCHES.length, () ->
+    def.resolve()
+  )
+
   setTimeout(() ->
     say('play', "Starting all matches")
     _.each(MATCHES, (match) ->
       p1 = PARTICIPANTS[match[0]]
       p2 = PARTICIPANTS[match[1]]

-      run_match(p1, p2)
+      run_match(p1, p2, d)
     )
   , 1000)
+
+  def.promise
+).then(() ->
+  winners = _.chain(PARTICIPANTS)
+             .each((p) -> p.proc.kill('SIGKILL') )
+             .sortBy((p) -> -p.score)
+             .map((p) -> [p.name, p.score])
+             .value()
+  console.log()
+  console.log()
+
+  console.log("Results:")
+  _.each(winners, (w) ->
+    console.log("#{w[0]} won #{w[1]} matches")
+  )
+
+  process.exit(0)
 )


formalize colors

formalize colors

--- httpdd.fsm.html.coffee
+++ httpdd.fsm.html.coffee
@@ -21,6 +21,15 @@
 fontSize = gridMultiplier * .5
 # http://www.w3schools.com/cssref/css_websafe_fonts.asp
 fontFamily = '"Trebuchet MS", Helvetica, sans-serif'
+colorGrid = '#EEEEEE'
+colorCoord = '#AAAAAA'
+colorStatusCodeText = '#000000'
+colorStatusCodeBlock = '#EEEEEE'
+colorDecisionText = '#000000'
+colorDecisionBlock = '#EEEEEE'
+colorTrue = '#00FF00'
+colorFalse = '#FF0000'
+colorAnything = '#0000FF'

 render = () ->
   paperWidth = gridMultiplier * gridCellWidth * gridCellsH
@@ -40,11 +49,11 @@

   V(paper.svg).defs().append V """
   <pattern id="smallGrid" width="#{gridMultiplier}" height="#{gridMultiplier}" patternUnits="userSpaceOnUse">
-    <path d="M #{gridMultiplier} 0 L 0 0 0 #{gridMultiplier}" fill="none" stroke="#EEEEEE" stroke-width="0.5"/>
+    <path d="M #{gridMultiplier} 0 L 0 0 0 #{gridMultiplier}" fill="none" stroke="#{colorGrid}" stroke-width="0.5"/>
   </pattern>
   <pattern id="grid" width="#{gridMultiplier * gridCellWidth}" height="#{gridMultiplier * gridCellHeight}" patternUnits="userSpaceOnUse">
     <rect width="#{gridMultiplier * gridCellWidth}" height="#{gridMultiplier * gridCellHeight}" fill="url(#smallGrid)"/>
-    <path d="M #{gridMultiplier * gridCellWidth} 0 L 0 0 0 #{gridMultiplier * gridCellHeight}" fill="none" stroke="#EEEEEE" stroke-width="1"/>
+    <path d="M #{gridMultiplier * gridCellWidth} 0 L 0 0 0 #{gridMultiplier * gridCellHeight}" fill="none" stroke="#{colorGrid}" stroke-width="1"/>
   </pattern>
   """
   V(paper.svg).prepend V """
@@ -69,7 +78,7 @@
             'font-size': fontSize
             'font-family': fontFamily
             'font-weight': 'bold'
-            fill: '#AAAAAA'
+            fill: colorCoord
       }
       cells.push new joint.shapes.basic.Text {
         position:
@@ -85,7 +94,7 @@
             'font-size': fontSize
             'font-family': fontFamily
             'font-weight': 'bold'
-            fill: '#AAAAAA'
+            fill: colorCoord
       }

     for i in [1..gridCellsV - 1]
@@ -103,7 +112,7 @@
             'font-size': fontSize
             'font-family': fontFamily
             'font-weight': 'bold'
-            fill: '#AAAAAA'
+            fill: colorCoord
       }
       cells.push new joint.shapes.basic.Text {
         position:
@@ -119,7 +128,7 @@
             'font-size': fontSize
             'font-family': fontFamily
             'font-weight': 'bold'
-            fill: '#AAAAAA'
+            fill: colorCoord
       }

   # CELLS
@@ -188,7 +197,7 @@
           d: "M #{height / 2} 0 L #{height} #{width / 2} #{height / 2} #{width} 0 #{width / 2} z"
           transform: 'translate(' + (-height / 2) + ', ' + (-width / 2) + ')'
           'stroke-width': 0
-          fill: '#EEEEEE'
+          fill: colorDecisionBlock
         '.decision':
           'font-size': fontSize
           'font-weight': 'bold'
@@ -196,7 +205,7 @@
           'text-anchor': 'middle'
           ref: 'path'
           'y-alignment': 'middle'
-          fill: 'black'
+          fill: colorDecisionText
           'font-family': fontFamily
           'ref-x': .5
           'ref-y': .2
@@ -209,7 +218,7 @@
           'ref-y': .5
           ref: 'path'
           'y-alignment': 'middle'
-          fill: '#AAAAAA'
+          fill: colorCoord
           'font-family': fontFamily
     }, joint.dia.Element::defaults
   }
@@ -247,7 +256,7 @@
           d: "M #{height} 0 L #{height} #{width} L 0 #{width} L 0 0 z"
           transform: 'translate(' + (-height / 2) + ', ' + (-width / 2) + ')'
           'stroke-width': 0
-          fill: '#EEEEEE'
+          fill: colorStatusCodeBlock
         '.status':
           'font-size': fontSize
           'font-weight': 'bold'
@@ -255,7 +264,7 @@
           'text-anchor': 'middle'
           ref: 'path'
           'y-alignment': 'middle'
-          fill: 'black'
+          fill: colorStatusCodeText
           'font-family': fontFamily
           'ref-x': .5
           'ref-y': .5
@@ -325,13 +334,13 @@
   switch transition.message
     when 'true'
       message = 'T'
-      color = '#00FF00'
+      color = colorTrue
     when 'false'
       message = 'F'
-      color = '#FF0000'
+      color = colorFalse
     when 'anything'
       message = ''
-      color = '#0000FF'
+      color = colorAnything
   cell = new joint.shapes.fsa.Arrow {
     smooth: false
     source:


invitations: minor name, format fixes

invitations: minor name, format fixes

--- workers/social/lib/social/models/invitation.coffee
+++ workers/social/lib/social/models/invitation.coffee
@@ -27,7 +27,7 @@
     sharedMethods   :
       instance      : ['modifyMultiuse', 'remove']
       static        : ['inviteFriend', 'byCode', 'suggestCode', 'createMultiuse',
-                       'createBetaUse']
+                       'createForBetaUse']
     sharedEvents    :
       static        : []
       instance      : []
@@ -215,7 +215,7 @@
             return callback err  if err
             groupObj.addInvitation invite, callback

-  @createBetaUse = permit 'send invitations',
+  @createForBetaUse = permit 'send invitations',
     success: ({connection:{delegate}}, username, callback)->
       JUser = require './user'
       JUser.one {username}, (err, user)=>
@@ -224,13 +224,13 @@
         {email} = user
         code    = @generateInvitationCode "group", email, "resurrection"
         invite  = new JInvitation {
-          username
           code
           email
-          maxUses : 100
-          group   : "resurrection"
-          type    : "multiuse"
-          origin  : ObjectRef(delegate)
+          type       : "multiuse"
+          group      : "resurrection"
+          origin     : ObjectRef(delegate)
+          maxUses    : 100
+          createdFor : username
         }
         invite.save callback


JUser: dont allow to unregister guestuser

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -192,6 +192,9 @@
     if delegate.type is 'unregistered'
       return callback createKodingError "You are not registered!"

+    if toBeDeletedUsername is "guestuser"
+      return callback createKodingError "It's not allowed to delete this user!"
+
     # only owner and the dummy admins can delete a user
     unless toBeDeletedUsername is delegate.profile.nickname or
            delegate.can 'administer accounts'


Existence check

Existence check

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -510,7 +510,7 @@
     , =>
       # temp - sy
       # add loggedin user to the group
-      user.fetchAccount 'koding', (err, account) =>
+      user?.fetchAccount 'koding', (err, account) =>
         @addToGroup account, groupName, null, null, ->
           queue.next()
     ]


Images are now shown, resolves #156

Images are now shown, resolves #156

--- wzk/ui/ac/NativeDataProvider.coffee
+++ wzk/ui/ac/NativeDataProvider.coffee
@@ -3,7 +3,7 @@
 class wzk.ui.ac.NativeDataProvider

   @DATA:
-    PHOTO: 'photo'
+    PHOTO: 'image'

   constructor: ->

@@ -34,6 +34,10 @@
     data = {
       _obj_name: @dom.getTextContent option
       id: id
-      photo: goog.dom.dataset.get option, wzk.ui.ac.NativeDataProvider.DATA.PHOTO
     }
+
+    photo = goog.dom.dataset.get option, wzk.ui.ac.NativeDataProvider.DATA.PHOTO
+    if !!photo
+      data["photo"] = photo
+
     new wzk.resource.Model(data)


fix getdomain, refs #559

fix getdomain, refs #559

--- commands.coffee
+++ commands.coffee
@@ -75,15 +75,15 @@
             break

         when 'getdomain'
-            CozyInstance.all (err, instances) ->
+            client.get 'api/instances', (err, res, instances) ->
                 if err
                     console.log '{domain:"error"}'
                     process.exit 1
-                else if instances.length is 0
+                else if instances.rows.length is 0
                     console.log '{domain:null}'
                     process.exit 0
                 else
-                    console.log "{domain:\"#{instances[0].domain}}\""
+                    console.log "{domain:\"#{instances.rows[0].domain}}\""
                     process.exit 0
             break


async

async

--- lib/importers/geonames.coffee
+++ lib/importers/geonames.coffee
@@ -3,8 +3,6 @@
 __ = require "../../vendor/underscore"

 module.exports =
-  storeCountry: (line) ->
-
   storePopulatedPlace: (line) ->
     data = line.split("\t")
     id = data[0]
@@ -15,11 +13,6 @@
     state_id = data[10]
     population = data[14]
     country_iso = data[8]
-  waiter: ->
-    setTimeout((->
-      console.log("jdsak")
-      return false
-    ),4000)

   importData: (filename, importfunction,callback) ->
     csv().fromPath filename,
@@ -27,17 +20,14 @@
       escape: ""
     .transform (data) ->
       importfunction data
-    .on "data", (data, index) ->
-      importfunction data
     .on "end", (count) ->
       console.log "Number of lines: " + count
       callback(count)
-      return true
     .on "error", (error) ->
       console.log error.message
   storeCountryAndAdminDivision: (countrycode = "DE",callback = ->) ->
+    redis.set countrycode, "foo"
     importfunction = (data) ->
       doo = "DE"+":"+data[1]
-      #console.log(doo)
       redis.set doo, "foo"
-    return @importData("./spec/fixtures/admin1CodesASCII.txt", importfunction, callback)
+    @importData("./spec/fixtures/admin1CodesASCII.txt", importfunction, callback)


add missing space

add missing space

--- lib/langs/php-beautify.coffee
+++ lib/langs/php-beautify.coffee
@@ -12,7 +12,7 @@
   if level
     levelOption = " --level=#{level} "
   if fixers
-    fixerOption = " --fixers=#{fixers}"
+    fixerOption = " --fixers=#{fixers} "

   cmd = "#{levelOption} #{fixerOption} \"#{inputPath}\") || (mv \"#{inputPath}\" \"#{outputPath}\")"


slightly better error handling

slightly better error handling

--- lib/runtime/evaluation.coffee
+++ lib/runtime/evaluation.coffee
@@ -36,6 +36,10 @@
         evaluate({text, line: line+1, mod, path: edpath})
           .catch -> r?.destroy()
           .then (result) =>
+            if not result?
+              r?.destroy()
+              console.error 'Error: Something went wrong while evaluating.'
+              return
             error = result.type == 'error'
             view = if error then result.view else result
             if not r? or r.isDestroyed then r = new @ink.Result editor, [start, end], {type: rtype, scope: 'julia'}


fix the beacon

fix the beacon

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -210,10 +210,10 @@
     # implement the appropriate asynchronous hook. This mechanism is
     # intended as a heartbeat that can be leveraged by each service.
     setupKernelBeacon: ->
+        assert _.isFunction u = -> try moment.unix()
         msg = "Setting up the kernel beacon at %s ms"
         interval = nconf.get("beacon:interval") or null
         noInterval = "no beacon interval has been given"
-        u = -> ts = moment.unix(); assert _.isNumber ts; ts
         eua = (s) => (n) => s.downstream(beacon: n)(@, u())
         assert _.isNumber(interval), noInterval.toString()
         logger.info msg.magenta, interval.toString().bold


Better wording

Better wording

--- library/nucleus/kernel.coffee
+++ library/nucleus/kernel.coffee
@@ -126,7 +126,7 @@
         @broker = new content.JsonBroker this
         assert _.isObject(options), "no options"
         message = "Booted up the kernel instance"
-        identica = "Using %s as application identica"
+        identica = "Using %s as instance identica"
         sigint = "Received the SIGINT (interrupt signal)"
         sigterm = "Received the SIGTERM (terminate signal)"
         process.on "SIGINT", => @shutdownKernel sigint


a bunch of code reorderings to the scaled kernel

--- library/nucleus/scaled.coffee
+++ library/nucleus/scaled.coffee
@@ -327,13 +327,18 @@
     # the scope is being dispersed and some events are being emited.
     # This implementaton cleans up some of the scalability resources.
     shutdownKernel: (reason, eol=yes) ->
-        try util.puts require("os").EOL if eol
+        terminator = (proxy) -> try proxy.close()
+        util.puts require("os").EOL if (eol or false)
         this.emit "killed-scaled-kernel", arguments...
         message = "Graceful shutdown of Scaled kernel"
-        try @spserver.close() if @spserver.close?
-        try @serverProxy.close() if @serverProxy?
-        try @secureProxy.close() if @secureProxy?
-        terminator = (proxy) -> try proxy.close()
+        log = (message) -> logger.silly message.yellow
+        log "Closing the Seaport servers" if @spserver
+        log "Closing the server (HTTP) proxy instance"
+        log "Closing the secure (HTTPS) proxy instance"
+        log "Terminating remote proxies in the queues"
+        try @spserver.close() if (@spserver) # Seaport
+        try @serverProxy.close() if this.serverProxy?
+        try @secureProxy.close() if this.secureProxy?
         _.each @queueOfHttps or Array(), terminator
         _.each @queueOfHttp or Array(), terminator
         logger.warn message.red; super reason, no


Display department name for joint municipal authorities

--- src/util/organization_hierarchy.coffee
+++ src/util/organization_hierarchy.coffee
@@ -1,9 +1,14 @@
 define (require) ->
     { Department } = require 'cs!app/models'
+    p13n           = require 'cs!app/p13n'

     generateDepartmentDescription: (department) ->
         rootDepartment = department.get('hierarchy')[0]  # city
         unitDepartment = department # unit
+
+        if rootDepartment.organization_type == 'JOINT_MUNICIPAL_AUTHORITY' and
+                unitDepartment.get('organization_type') == 'JOINT_MUNICIPAL_AUTHORITY'
+            return p13n.getTranslatedAttr rootDepartment.name

         if rootDepartment.organization_type != 'MUNICIPALITY'
             return null


Fix single style binding definition construction.

--- src/view/bindings/style_binding.coffee
+++ src/view/bindings/style_binding.coffee
@@ -40,7 +40,7 @@
       @bindSingleAttribute(key, "#{@keyPath}.#{key}")

   bindSingleAttribute: (attr, keyPath) ->
-    definition = new Batman.DOM.AttrReaderBindingDefinition(@node, attr, keyPath, @renderContext, @renderer)
+    definition = new Batman.DOM.AttrReaderBindingDefinition(@node, attr, keyPath, @view)
     @styleBindings[attr] = new Batman.DOM.StyleBinding.SingleStyleBinding(definition, this)

   setStyle: (key, value) =>


user project tracker: only register one at a time properly; this is also a major bugfix...

--- src/smc-hub/postgres-synctable.coffee
+++ src/smc-hub/postgres-synctable.coffee
@@ -183,6 +183,8 @@
                     @_feed = feed
                     @_feed.on 'change', @_handle_change
                     cb()
+    _dbg: (f) =>
+        return @_db._dbg("Tracker.#{f}")

     close: =>
         @emit('close')
@@ -277,7 +279,9 @@
         delete @_users[project_id][account_id]
         delete @_projects[account_id][project_id]

-    # TODO: only register one at a time!!
+    # Register the given account so that this client watches the database
+    # in order to be aware of all projects and collaborators of the
+    # given account.
     register: (opts) =>
         opts = defaults opts,
             account_id : required
@@ -286,32 +290,59 @@
             # already registered
             opts.cb()
             return
-        if not @_register_cbs?
-            @_register_cbs = [opts.cb]
+        @_register_todo ?= {}
+        if misc.len(@_register_todo) == 0
+            # no registration is currently happening
+            @_register_todo[opts.account_id] = [opts.cb]
+            # kick things off -- this will keep registering accounts
+            # until everything is done, then set @_register_todo to undefined
+            @_register()
         else
-            @_register_cbs.push(opts.cb)
-        if @_register_cbs.length > 1
-            return
+            # Accounts are being registered right now.  Add to the todo list.
+            v = @_register_todo[opts.account_id]
+            if v?
+                v.push(opts.cb)
+            else
+                @_register_todo[opts.account_id] = [opts.cb]
+
+    # Call _register to completely clear the work @_register_todo work queue.
+    # NOTE: _register does each account, *one after another*, rather than doing
+    # everything in parallel.   WARNING: DO NOT rewrite this to do everything in parallel,
+    # unless you think you thoroughly understand the algorithm, since I think
+    # doing things in parallel would horribly break!
+    _register: =>
+        account_id = misc.keys(@_register_todo)?[0]
+        if not account_id?
+            # no work
+            return
+        # Register this account
+        dbg = @_dbg("_register")
+        dbg("registering account='#{account_id}'...")
         @_db._query
             query  : "SELECT project_id, json_agg(o) as users FROM (select project_id, jsonb_object_keys(users) AS o FROM projects WHERE users ? $1::TEXT) s group by s.project_id"
-            params : [opts.account_id]
+            params : [account_id]
             cb     : all_results (err, x) =>
+                if not err
+                    @_accounts[account_id] = true
+                    for a in x
+                        if @_users[a.project_id]?
+                            # already have data about this project
+                            continue
+                        else
+                            for collab_account_id in a.users
+                                @_add_user_to_project(collab_account_id, a.project_id)
+                # call the callbacks
                 if err
-                    for cb in @_register_cbs
-                        cb(err)
-                    delete @_register_cbs
-                    return
-                @_accounts[opts.account_id] = true
-                for a in x
-                    if @_users[a.project_id]?
-                        # already have data about this project
-                        continue
-                    else
-                        for account_id in a.users
-                            @_add_user_to_project(account_id, a.project_id)
-                for cb in @_register_cbs
-                    cb()
-                delete @_register_cbs
+                    dbg("error registering '#{account_id}' -- err=#{err}")
+                else
+                    dbg("successfully registered '#{account_id}'")
+                for cb in @_register_todo[account_id]
+                    cb?(err)
+                # We are done (trying to) register account_id, for good or ill.
+                delete @_register_todo[account_id]
+                if misc.len(@_register_todo) > 0
+                    # Deal with next account that needs to be registered
+                    @_register()

     unregister: (opts) =>
         opts = defaults opts,
@@ -350,7 +381,7 @@

 class Changes extends EventEmitter
     constructor: (@_db, @_table, @_select, @_watch, @_where, cb) ->
-        @dbg = @_db._dbg("ChangeFeed(table='#{@_table}')")
+        @dbg = @_dbg("constructor")
         @dbg("select=#{misc.to_json(@_select)}, watch=#{misc.to_json(@_watch)}, @_where=#{misc.to_json(@_where)}")
         try
             @_init_where()
@@ -369,6 +400,9 @@
             # requests pile up!).
             @_db.once('connect', @close)
             cb(undefined, @)
+
+    _dbg: (f) =>
+        return @_db._dbg("Changes(table='#{@_table}').#{f}")

     close: (cb) =>
         @emit('close', {action:'close'})


Update CommitsTable component

Update CommitsTable component

[#94794346]

--- review_front/app/components/commits-table.coffee
+++ review_front/app/components/commits-table.coffee
@@ -1,7 +1,7 @@
-`import Ember from 'ember';`
+`import Ember from 'ember'`

-Component = Ember.Component.extend
+CommitsTable = Ember.Component.extend
   tagName:    'article'
   classNames: ['commits']

-`export default Component;`
+`export default CommitsTable`


onBeforeRemove, update remove() method

onBeforeRemove, update remove() method

 - New collection callback `onBeforeRemove`, allows you to check user
and file itself before removing it
 - `remove()` method now accepts second argument - `callback` function
 - Important security updates

--- files.coffee
+++ files.coffee
@@ -9,6 +9,7 @@
   request      = Npm.require 'request'
   Throttle     = Npm.require 'throttle'
   fileType     = Npm.require 'file-type'
+  nodePath     = Npm.require 'path'

   ###
   @var {object} bound - Meteor.bindEnvironment (Fiber wrapper)
@@ -32,14 +33,11 @@
   o =
     currentFile:    obj.currentFile
     search:         obj.search
-    storagePath:    obj.storagePath
     collectionName: obj.collectionName
     downloadRoute:  obj.downloadRoute
     chunkSize:      obj.chunkSize
     debug:          obj.debug
     _prefix:        obj._prefix
-    cacheControl:   obj.cacheControl
-    versions:       obj.versions
   return o

 ###
@@ -52,14 +50,11 @@
 cp = (to, from) ->
   to.currentFile    = from.currentFile
   to.search         = from.search
-  to.storagePath    = from.storagePath
   to.collectionName = from.collectionName
   to.downloadRoute  = from.downloadRoute
   to.chunkSize      = from.chunkSize
   to.debug          = from.debug
   to._prefix        = from._prefix
-  to.cacheControl   = from.cacheControl
-  to.versions       = from.versions
   return to

 ###
@@ -88,6 +83,7 @@
 @param config.onBeforeUpload {Function}- [Both]   Function which executes on server after receiving each chunk and on client right before beginning upload. Function context is `File` - so you are able to check for extension, mime-type, size and etc.
 return `true` to continue
 return `false` or `String` to abort upload
+@param config.onBeforeRemove {Function} - [Server] Executes before removing file on server, so you can check permissions. Return `true` to allow action and `false` to deny.
 @param config.allowClientCode  {Boolean}  - [Both]   Allow to run `remove` from client
 @param config.downloadCallback {Function} - [Server] Callback triggered each time file is requested, return truthy value to continue download, or falsy to abort
 @param config.interceptDownload {Function} - [Server] Intercept download request, so you can serve file from third-party resource, arguments {http: {request: {...}, response: {...}}, fileRef: {...}}
@@ -101,7 +97,7 @@
       events.EventEmitter.call @
     else
       EventEmitter.call @
-    {@storagePath, @collectionName, @downloadRoute, @schema, @chunkSize, @namingFunction, @debug, @onbeforeunloadMessage, @permissions, @allowClientCode, @onBeforeUpload, @integrityCheck, @protected, @public, @strict, @downloadCallback, @cacheControl, @throttle, @onAfterUpload, @interceptDownload} = config if config
+    {@storagePath, @collectionName, @downloadRoute, @schema, @chunkSize, @namingFunction, @debug, @onbeforeunloadMessage, @permissions, @allowClientCode, @onBeforeUpload, @integrityCheck, @protected, @public, @strict, @downloadCallback, @cacheControl, @throttle, @onAfterUpload, @interceptDownload, @onBeforeRemove} = config if config

     self               = @
     cookie             = new Cookies()
@@ -131,6 +127,8 @@
       delete @integrityCheck
       delete @downloadCallback
       delete @interceptDownload
+      delete @onBeforeRemove
+
       if @protected
         if not cookie.has('meteor_login_token') and Meteor._localStorage.getItem('Meteor.loginToken')
           cookie.set 'meteor_login_token', Meteor._localStorage.getItem('Meteor.loginToken'), null, '/'
@@ -141,6 +139,7 @@
       @throttle         ?= false
       @permissions      ?= 0o755
       @cacheControl     ?= 'public, max-age=31536000, s-maxage=31536000'
+      @onBeforeRemove   ?= false
       @onAfterUpload    ?= false
       @integrityCheck   ?= true
       @downloadCallback ?= false
@@ -148,6 +147,7 @@
         throw new Meteor.Error 500, "[FilesCollection.#{@collectionName}] \"storagePath\" must be set on \"public\" collections! Note: \"storagePath\" must be equal on be inside of your web/proxy-server (absolute) root."
       @storagePath      ?= "assets/app/uploads/#{@collectionName}"
       @storagePath       = @storagePath.replace /\/$/, ''
+      @storagePath       = nodePath.normalize @storagePath

       fs.mkdirsSync @storagePath

@@ -158,6 +158,7 @@
       check @cacheControl, String
       check @onAfterUpload, Match.OneOf false, Function
       check @integrityCheck, Boolean
+      check @onBeforeRemove, Match.OneOf false, Function
       check @downloadCallback, Match.OneOf false, Function
       check @interceptDownload, Match.OneOf false, Function

@@ -224,7 +225,7 @@
         user = user()

         if _.isFunction self.protected
-          result = if http then self.protected.call(_.extend(http, userFuncs), self.currentFile or null) else self.protected.call userFuncs, self.currentFile or null
+          result = if http then self.protected.call(_.extend(http, userFuncs), (self.currentFile or null)) else self.protected.call userFuncs, (self.currentFile or null)
         else
           result = !!user

@@ -316,9 +317,23 @@
         check inst, Object
         console.info '[FilesCollection] [Unlink Method]' if self.debug
         if self.allowClientCode
-          self.remove.call cp(_insts[inst._prefix], inst), inst.search
+          __instData = cp _insts[inst._prefix], inst
+          if self.onBeforeRemove and _.isFunction self.onBeforeRemove
+            user = false
+            userFuncs = {
+              userId: @userId
+              user: -> Meteor.users.findOne @userId
+            }
+
+            __inst = self.find.call __instData, inst.search
+            unless self.onBeforeRemove.call userFuncs, (__inst.cursor or null)
+              throw new Meteor.Error 403, '[FilesCollection] [remove] Not permitted!'
+
+          self.remove.call __instData, inst.search
+          return true
         else
           throw new Meteor.Error 401, '[FilesCollection] [remove] Run code from client is not allowed!'
+        return

       _methods[self.methodNames.MeteorFileWrite] = (opts) ->
         @unblock()
@@ -340,7 +355,13 @@
         console.info "[FilesCollection] [Write Method] Got ##{opts.chunkId}/#{opts.fileLength} chunks, dst: #{opts.file.name or opts.file.fileName}" if self.debug

         if self.onBeforeUpload and _.isFunction self.onBeforeUpload
-          isUploadAllowed = self.onBeforeUpload.call {file: opts.file}, opts.file
+          isUploadAllowed = self.onBeforeUpload.call(_.extend({
+            file: opts.file
+          }, {
+            userId: @userId,
+            user: -> Meteor.users.findOne @userId
+          }), opts.file)
+
           if isUploadAllowed isnt true
             throw new Meteor.Error(403, if _.isString(isUploadAllowed) then isUploadAllowed else '@onBeforeUpload() returned false')

@@ -816,7 +837,7 @@
     check search, Match.Optional Match.OneOf Object, String
     @srch search

-    if @checkAccess
+    if @checkAccess()
       @currentFile = null
       @cursor = @collection.find @search
     return @
@@ -990,6 +1011,7 @@
       @config.onUploaded and @config.onUploaded.call @result, error, data
       if error
         console.warn "[FilesCollection] [insert] [end] Error: ", error if @collection.debug
+        @result.abort()
         @result.state.set 'aborted'
         @result.emitEvent 'error', [error, @fileData]
         @config.onError and @config.onError.call @result, error, @fileData
@@ -1130,12 +1152,12 @@
     start: ->
       self = @
       if @config.onBeforeUpload and _.isFunction @config.onBeforeUpload
-        isUploadAllowed = @config.onBeforeUpload.call @result, @fileData
+        isUploadAllowed = @config.onBeforeUpload.call _.extend(@result, @collection.getUser()), @fileData
         if isUploadAllowed isnt true
           return @end new Meteor.Error(403, if _.isString(isUploadAllowed) then isUploadAllowed else 'config.onBeforeUpload() returned false')

       if @collection.onBeforeUpload and _.isFunction @collection.onBeforeUpload
-        isUploadAllowed = @collection.onBeforeUpload.call @result, @fileData
+        isUploadAllowed = @collection.onBeforeUpload.call _.extend(@result, @collection.getUser()), @fileData
         if isUploadAllowed isnt true
           return @end new Meteor.Error(403, if _.isString(isUploadAllowed) then isUploadAllowed else 'collection.onBeforeUpload() returned false')

@@ -1225,7 +1247,8 @@
       @config._onEnd()
       @state.set 'aborted'
       console.timeEnd('insert ' + @config.file.name) if @config.debug
-      Meteor.call @config.MeteorFileAbort, {fileId: @config.fileId, fileLength: @config.fileLength, fileData: @config.fileData}
+      if @config.fileLength
+        Meteor.call @config.MeteorFileAbort, {fileId: @config.fileId, fileLength: @config.fileLength, fileData: @config.fileData}
       return
   else undefined

@@ -1234,24 +1257,28 @@
   @memberOf FilesCollection
   @name remove
   @param {String|Object} search - `_id` of the file or `Object` like, {prop:'val'}
+  @param {Function} cb - Callback with one `error` argument
   @summary Remove file(s) on cursor or find and remove file(s) if search is set
   @returns {undefined}
   ###
-  remove: (search) ->
+  remove: (search, cb) ->
     console.info "[FilesCollection] [remove(#{JSON.stringify(search)})]" if @debug
     check search, Match.Optional Match.OneOf Object, String
+    check cb, Match.Optional Function

     if @checkAccess()
       @srch search
       if Meteor.isClient
-        Meteor.call @methodNames.MeteorFileUnlink, rcp(@)
+        Meteor.call @methodNames.MeteorFileUnlink, rcp(@), cb

       if Meteor.isServer
         files = @collection.find @search
         if files.count() > 0
           self = @
           files.forEach (file) -> self.unlink file
-        @collection.remove @search
+        @collection.remove @search, cb
+    else
+      cb and cb new Meteor.Error 401, '[FilesCollection] [remove] Access denied!'
     return @

   ###
@@ -1499,17 +1526,16 @@
   @name link
   @param {Object}   fileRef - File reference object
   @param {String}   version - [Optional] Version of file you would like to request
-  @param {Boolean}  pub     - [Optional] is file located in publicity available folder?
   @summary Returns downloadable URL
   @returns {String} Empty string returned in case if file not found in DB
   ###
-  link: (fileRef, version = 'original', pub = false) ->
+  link: (fileRef, version = 'original') ->
     console.info '[FilesCollection] [link()]' if @debug
     if _.isString fileRef
       version = fileRef
       fileRef = null
     return '' if not fileRef and not @currentFile
-    return formatFleURL (fileRef or @currentFile), version, @public
+    return formatFleURL (fileRef or @currentFile), version

 ###
 @locus Anywhere


End only not finished responses on error

--- files.coffee
+++ files.coffee
@@ -2384,9 +2384,9 @@
       responseType = '200'

     streamErrorHandler = (error) ->
-      http.response.writeHead 500
-      http.response.end error.toString()
       console.error "[FilesCollection] [serve(#{vRef.path}, #{version})] [500]", error if self.debug
+      if !http.response.finished
+        http.response.end error.toString()
       return

     headers = if _.isFunction(self.responseHeaders) then self.responseHeaders(responseType, fileRef, vRef, version) else self.responseHeaders


added tile inventory management and double-click zoom

--- bokehjs/src/coffee/renderer/geo/tile_layer.coffee
+++ bokehjs/src/coffee/renderer/geo/tile_layer.coffee
@@ -1,5 +1,23 @@
 _ = require "underscore"
 Glyph = require "../glyph/glyph"
+
+class ImagePool
+
+  constructor: () ->
+    @images = []
+
+  pop: () ->
+    img = @images.pop()
+    if img?
+      return img
+    else
+      return new Image()
+
+  push: (img) ->
+    if img.constructor == Array
+      Array::push.apply(@images, img)
+    else
+      @images.push(img)

 class ProjectionUtils

@@ -30,18 +48,53 @@
     [xmax, ymax] = @meters_to_geographic(xmax, ymax)
     return [xmin, ymin, xmax, ymax]

-class MercatorTileProvider
+class GridLayer
+
+  tile_xyz_to_key: (x, y, z) ->
+    return x.toString() + ":" + y.toString() + ":" + z.toString()
+
+  key_to_tile_xyz: (key) ->
+    return (parseInt(c) for c in key.split(':'))
+
+class MercatorTileProvider extends GridLayer
+
+

   constructor: (@url, @tile_size=256) ->
     @utils = new ProjectionUtils()
+    @pool = new ImagePool()
     @origin_shift = 2 * Math.PI * 6378137 / 2.0
     @initial_resolution = 2 * Math.PI * 6378137 / @tile_size
+    @resolutions = (@get_resolution(z) for z in [0..30])
+    @tiles = {}

   get_image_url: () ->
     throw Error "Unimplemented Method"

   get_resolution: (level) ->
     return @initial_resolution / Math.pow(2, level)
+
+  get_level_by_extent: (extent, height, width) ->
+    x_rs = (extent[2] - extent[0]) / width
+    y_rs = (extent[3] - extent[1]) / height
+    res = Math.max(x_rs, y_rs)
+
+    closest = @resolutions.reduce (previous, current) ->
+      return current if (Math.abs(current - res) < Math.abs(previous - res))
+      return previous
+
+    return @resolutions.indexOf(closest)
+
+  snap_to_zoom: (extent, height, width, level) ->
+    desired_res = @resolutions[level]
+    desired_x_delta = width * desired_res
+    desired_y_delta = height * desired_res
+
+    [xmin, ymin, xmax, ymax] = extent
+    x_adjust = (desired_x_delta - (xmax - xmin)) / 2
+    y_adjust = (desired_y_delta - (ymax - ymin)) / 2
+
+    return [xmin - x_adjust, ymin - y_adjust, xmax + x_adjust, ymax + y_adjust]

   tms_to_wmts: (x, y, z) ->
     '''Note this works both ways'''
@@ -83,14 +136,25 @@
     return [minLon, minLat, maxLon, maxLat]

   get_tiles_by_extent: (extent, level) ->
+
     [xmin, ymin, xmax, ymax] = extent
     [txmin, tymin] = @meters_to_tile(xmin, ymin, level)
     [txmax, tymax] = @meters_to_tile(xmax, ymax, level)

+    center_tile_x = (txmax - txmin) // 2
+    center_tile_y = (tymax - tymin) // 2
+
     tiles = []
-    for ty in [tymin..tymax] by 1
+    for ty in [tymax..tymin] by -1
       for tx in [txmin..txmax] by 1
-        tiles.push(@get_image_url(tx, ty, level))
+        tx = tx %% 2 ** level
+        tiles.push([tx, ty, level, @get_tile_meter_bounds(tx, ty, level)])
+
+    #TOOD: sort tiles by distance from center doesn't seem to be working
+    tiles.sort (a, b) ->
+      a_distance = Math.sqrt(Math.pow(center_tile_x - a[0], 2) + Math.pow(center_tile_y - a[1], 2))
+      b_distance = Math.sqrt(Math.pow(center_tile_x - b[0], 2) + Math.pow(center_tile_y - b[1], 2))
+      return a_distance - b_distance

     return tiles

@@ -100,10 +164,6 @@
     return @url.replace("{X}", x).replace('{Y}', y).replace("{Z}", z)

 class WMTSTileProvider extends MercatorTileProvider
-
-  get_tile_meter_bounds: (x, y, z) ->
-    [x, y, z] = @tms_to_wmts(x, y, z)
-    return super(x, y, z)

   get_image_url: (x, y, z) ->
     [x, y, z] = @tms_to_wmts(x, y, z)
@@ -129,7 +189,7 @@
       else if value == '1'
         tileX |= mask

-      else if value == '2'
+      else if value == '2 '
         tileY |= mask

       else if value == '3'
@@ -161,89 +221,166 @@
     quadKey = @tile_xyz_to_quadkey(x, y, z)
     return @url.replace("{QUADKEY}", quadKey)

-
 class TileLayerView extends Glyph.View

+  trace: (msg) ->
+    console.warn "TileLayerView :: " + msg.toString()
+
+  _create_tile_provider: (provider_type, service_url, tile_size) ->
+    return new QUADKEYTileProvider(service_url, tile_size) if(provider_type.toLowerCase() == 'quadkeytileprovider')
+    return new TMSTileProvider(service_url, tile_size) if(provider_type.toLowerCase() == 'tmstileprovider')
+    return new WMTSTileProvider(service_url, tile_size)
+
   _index_data: () ->
+    @trace("._index_data()")
     @_xy_index()

+  get_extent: () ->
+    return [@x_range.get('start'), @y_range.get('start'), @x_range.get('end'), @y_range.get('end')]
+
+  _on_dblclick: (e) =>
+    extent = @get_extent()
+    [xmin, xmax, ymin, ymax] = extent
+    zoom_level = @tile_provider.get_level_by_extent(extent, @map_frame.get('height'), @map_frame.get('width'))
+
+    rect = @map_canvas.canvas.getBoundingClientRect()
+    x = e.clientX - rect.left
+    #TODO: y-dim is not perfect.  zooming doesn't seem completely correct
+    y = e.clientY - rect.top - (@map_frame.get('top') - @map_frame.get('height'))
+    y = @map_frame.get("height") - y
+
+    x_meters = @x_mapper.map_from_target(x)
+    y_meters = @y_mapper.map_from_target(y)
+
+    x_offset = Math.abs((@x_range.get('end') - @x_range.get('start')) / 2)
+    y_offset = Math.abs((@y_range.get('end') - @y_range.get('start')) / 2)
+
+    updated_extent =  [x_meters - x_offset, y_meters - y_offset, x_meters + x_offset, y_meters + y_offset]
+
+    if e.shiftKey
+      zoom_level = zoom_level - 1
+    else
+      zoom_level = zoom_level + 1
+
+    new_extent = @tile_provider.snap_to_zoom(updated_extent, @map_frame.get('height'), @map_frame.get('width'), zoom_level)
+    @x_range.set('start', new_extent[0])
+    @y_range.set('start', new_extent[1])
+    @x_range.set('end', new_extent[2])
+    @y_range.set('end', new_extent[3])
+
   _set_data: () ->
-    @need_load = (true for img in @url)
-    @loaded = (false for img in @url)
-
-    @_xy_index()
+    @trace("._set_data()")
+    @tile_provider = @_create_tile_provider(@provider_type[0], @url[0], @tile_size[0])
+    @pool = new ImagePool()
+    @initial_extent = [-20037508.34, -20037508.34, 20037508.34, 20037508.34]
+    @map_plot = @renderer.plot_view.model
+    @map_canvas = @renderer.plot_view.canvas_view.ctx
+    @map_frame = @renderer.plot_view.frame
+
+    @x_range = @map_plot.get('x_range')
+    @x_mapper = this.map_frame.get('x_mappers')['default']
+
+    @y_range = @map_plot.get('y_range')
+    @y_mapper = this.map_frame.get('y_mappers')['default']
+
+    @x_range.set('start', @initial_extent[0])
+    @y_range.set('start', @initial_extent[1])
+    @x_range.set('end', @initial_extent[2])
+    @y_range.set('end', @initial_extent[3])
+
+    @renderer.plot_view.$el.dblclick(@_on_dblclick)

   _map_data: () ->
-    @sw = @sdist(@renderer.xmapper, @x, @tile_size, 'edge', @mget('dilate'))
-    @sh = @sdist(@renderer.ymapper, @y, @tile_size, 'edge', @mget('dilate'))
+    @trace("._map_data()")
+    @sw = @sdist(@renderer.xmapper, @x, @mget('tile_size'), 'edge', @mget('dilate'))
+    @sh = @sdist(@renderer.ymapper, @y, @mget('tile_size'), 'edge', @mget('dilate'))
+
+  _on_tile_load: (e) =>
+    @tile_provider.tiles[e.target.cache_key] = {img: e.target, tile_coords: e.target.tile_coords, bounds: e.target.bounds, current: true}
+    @_render_tile(e.target.cache_key)
+
+  _on_tile_error: (e) =>
+    @trace("._on_tile_error()")
+    return ''
+
+  _is_valid_tile: (x, y, z) ->
+
+    #is tile currently on screen?
+    return true
+
+  _create_tile: (x, y, z, bounds) ->
+    tile = @pool.pop()
+    tile.onload = @_on_tile_load
+    tile.onerror = @_on_tile_error
+    tile.alt = ''
+    tile.src = @tile_provider.get_image_url(x, y, z)
+    tile.tile_coords = [x, y, z]
+    tile.bounds = bounds
+    tile.x_coord = bounds[0]
+    tile.y_coord = bounds[3]
+    tile.cache_key = @tile_provider.tile_xyz_to_key(x, y, z)
+    return tile

   _render: (ctx, indices, {url, image, need_load, sx, sy, sw, sh, angle}) ->
-    for i in indices
-
-      if isNaN(sx[i]+sy[i]+angle[i])
-        continue
-
-      if need_load[i]
-        img = new Image()
-        img.onload = do (img, i) =>
-          return () =>
-            @loaded[i] = true
-            @renderer.request_render()
-
-        img.src = url[i]
-        need_load[i] = false
-      else if @loaded[i]
-        @_render_image(ctx, i, image[i], sx, sy, @tile_size, @tile_size, angle)
-
-  _final_sx_sy: (anchor, sx, sy, sw, sh) ->
-    switch anchor
-      when "top_left"      then [sx       , sy       ]
-      when "top_center"    then [sx - sw/2, sy       ]
-      when "top_right"     then [sx - sw  , sy       ]
-      when "right_center"  then [sx - sw  , sy - sh/2]
-      when "bottom_right"  then [sx - sw  , sy - sh  ]
-      when "bottom_center" then [sx - sw/2, sy - sh  ]
-      when "bottom_left"   then [sx       , sy - sh  ]
-      when "left_center"   then [sx       , sy - sh/2]
-      when "center"        then [sx - sw/2, sy - sh/2]
-
-  _render_image: (ctx, i, image, sx, sy, sw, sh, angle) ->
-
-    anchor = @mget('anchor') or "top_left"
-    [sx, sy] = @_final_sx_sy(anchor, sx[i], sy[i], 256, 256)
-
-    ctx.save()
-
-    ctx.globalAlpha = @mget("global_alpha")
-
-    if angle[i]
-      ctx.translate(sx, sy)
-      ctx.rotate(angle[i])
-      ctx.drawImage(image, 0, 0, 256, 256)
-      ctx.rotate(-angle[i])
-      ctx.translate(-sx, -sy)
-    else
-      ctx.drawImage(image, sx, sy, 256, 256)
-    ctx.restore()
+    @_update()
+
+  _render_tile: (tile_key) ->
+    tile_obj = @tile_provider.tiles[tile_key]
+    if tile_obj?
+      @map_canvas.save()
+      [sx, sy] = @renderer.plot_view.frame.map_to_screen([tile_obj.bounds[0]], [tile_obj.bounds[3]], @renderer.plot_view.canvas)
+      @map_canvas.rect(
+        @map_frame.get('left')+1, @map_frame.get('bottom')+2,
+        @map_frame.get('width')-2, @map_frame.get('height'),
+      )
+      @map_canvas.clip()
+      @map_canvas.drawImage(tile_obj.img, sx[0], sy[0], 256, 256)
+      @map_canvas.restore()
+
+  _update: () =>
+    if window.stop?
+      window.stop()
+    else if document.execCommand?
+      document.execCommand("Stop", false)
+
+    @trace("._render()")
+    extent = @get_extent()
+    zoom_level = @tile_provider.get_level_by_extent(extent, @map_frame.get('height'), @map_frame.get('width'))
+    new_extent = @tile_provider.snap_to_zoom(extent, @map_frame.get('height'), @map_frame.get('width'), zoom_level)
+
+    @x_range.set('start', new_extent[0])
+    @y_range.set('start', new_extent[1])
+    @x_range.set('end', new_extent[2])
+    @y_range.set('end', new_extent[3])
+    tiles = @tile_provider.get_tiles_by_extent(new_extent, zoom_level)
+
+    for t in tiles
+      [x, y, z, bounds] = t
+      if @_is_valid_tile(x, y, z)
+        key = @tile_provider.tile_xyz_to_key(x, y, z)
+        if key of @tile_provider.tiles
+          @_render_tile(key)
+        else
+          @_create_tile(x, y, z, bounds)

 class TileLayer extends Glyph.Model
   default_view: TileLayerView
   type: 'TileLayer'
-  tile_width: 256
-  tile_height: 256
   visuals: []
   distances: ['w', 'h']
   angles: ['angle']
-  fields: ['url:string']
+  fields: ['url:string', 'provider_type:string', 'tile_size']

   defaults: ->
     return _.extend {}, super(), {
       angle: 0
+      tile_size: 256
       global_alpha: 1.0
     }

   display_defaults: ->
     return _.extend {}, super(), {
+      level: 'glyph'
     }

 module.exports =


test(core): refactor tmpdir

test(core): refactor tmpdir

--- packages/core/test/plugins/metadata/tmpdir.coffee
+++ packages/core/test/plugins/metadata/tmpdir.coffee
@@ -21,37 +21,7 @@
           "got {}"
         ].join ' '

-  describe 'cascade', ->
-
-    they 'not available in children', ({ssh}) ->
-      nikita.call $ssh: ssh, $tmpdir: true, ->
-        @call -> @call ({metadata: {tmpdir}}) ->
-          should(tmpdir).be.undefined()
-
-    they 'current action', ({ssh}) ->
-      nikita
-        $ssh: ssh
-      , ({ssh}) ->
-        tmpdir = await @call
-          $tmpdir: true
-        , ({metadata: {tmpdir}}) ->
-          await fs.exists(ssh, tmpdir).should.be.resolvedWith true
-          tmpdir
-        fs.exists(ssh, tmpdir).should.be.resolvedWith false
-
-    they 'remove directory with files and foders inside', ({ssh}) ->
-      nikita
-        $ssh: ssh
-      , ({ssh}) ->
-        tmpdir = await @call
-          $tmpdir: true
-        , ({metadata: {tmpdir}}) ->
-          await fs.mkdir ssh, "#{tmpdir}/a_dir"
-          await fs.writeFile ssh, "#{tmpdir}/a_dir/a_file", ''
-          tmpdir
-        fs.exists(ssh, tmpdir).should.be.resolvedWith false
-
-  describe 'option tmpdir', ->
+  describe 'accepted values', ->

     they 'is a boolean', ({ssh}) ->
       nikita
@@ -85,14 +55,53 @@
       , ({metadata}) -> metadata.tmpdir
       .then (tmpdir) ->
         tmpdir.should.match /ok$/
+
+  describe 'disposal', ->
+
+    they 'remove left directory', ({ssh}) ->
+      nikita
+        $ssh: ssh
+      , ({ssh}) ->
+        tmpdir = await @call
+          $tmpdir: true
+        , ({metadata: {tmpdir}}) ->
+          await fs.exists(ssh, tmpdir).should.be.resolvedWith true
+          tmpdir
+        fs.exists(ssh, tmpdir).should.be.resolvedWith false
+
+    they 'remove directory with files and foders inside', ({ssh}) ->
+      nikita
+        $ssh: ssh
+      , ({ssh}) ->
+        tmpdir = await @call
+          $tmpdir: true
+        , ({metadata: {tmpdir}}) ->
+          await fs.mkdir ssh, "#{tmpdir}/a_dir"
+          await fs.writeFile ssh, "#{tmpdir}/a_dir/a_file", ''
+          tmpdir
+        fs.exists(ssh, tmpdir).should.be.resolvedWith false
+
+  describe 'cascade', ->
+
+    they 'not available in children', ({ssh}) ->
+      nikita.call $ssh: ssh, $tmpdir: true, ->
+        @call -> @call ({metadata: {tmpdir}}) ->
+          should(tmpdir).be.undefined()
+
+    they 'several true dont change value', ({ssh}) ->
+      nikita.call $ssh: ssh, $tmpdir: true, ->
+        @call -> @call ({metadata: {tmpdir}}) ->
+          should(tmpdir).be.undefined()
+
+  describe 'metadata', ->

-    they 'ssh and tmpdir in same child', -> ({ssh}) ->
+    they 'with ssh in same child action', -> ({ssh}) ->
       # Fix bug where the ssh connection was not discoved when
       # ssh was created in the same child and tmpdir
       nikita.call $ssh: ssh, $tmpdir: true, (->)
       .should.be.resolved()

-    it 'honors templated', () ->
+    it 'with templated', () ->
       nikita.call
         $tmpdir: '/tmp/nikita'
         $templated: true
@@ -100,7 +109,7 @@
       , ({config}) ->
         config.target.should.eql 'a value with /tmp/nikita'

-  describe 'option dirty', ->
+  describe 'config.dirty', ->

     they 'is true', ({ssh}) ->
       nikita


Delete stray file

Delete stray file


[atom-reason] Add try-catch snippet

[atom-reason] Add try-catch snippet

--- editorSupport/language-reason/snippets/reason.cson
+++ editorSupport/language-reason/snippets/reason.cson
@@ -20,20 +20,20 @@
   "Reason: Type alias":
     prefix: "lt"
     body: """
-       type ${1:t} = ${2:typ};
+      type ${1:t} = ${2:typ};
     """

   "Reason: Let Binding":
     prefix: "lt"
     body: """
-       let ${1:name} = ${2:value};
+      let ${1:name} = ${2:value};
     """
   "Reason: Let Function":
     prefix: "lf"
     body: """
-       let ${1:funcName} ${2:args} => {
-         $0
-       };
+      let ${1:funcName} ${2:args} => {
+        $0
+      };
     """

   "Reason: Let Module":
@@ -47,7 +47,15 @@
   "Reason: Switch Pattern Match":
     prefix: "sw"
     body: """
-       switch ${1:expression} {
-         | ${2:firstCase} => ${3:result}
-       }
+      switch ${1:expression} {
+      | ${2:firstCase} => ${3:result}
+      }
     """
+
+  "Reason: Try-catch":
+    prefix: "try"
+    body: """
+      try (${1:expression}) {
+      | ${2:firstException} => ${3:firstHandler}
+      };
+    """


log for profiling

log for profiling

--- src/renderer/components/route-component.coffee
+++ src/renderer/components/route-component.coffee
@@ -89,7 +89,9 @@
   render: ->
     <div style={@props.style}>
       {
-        console.log 'route', (+new Date()).toString()[-4..-1], @props.children.type?.displayName || @props.children.type?.name || @props.children[0]?.type?.displayName || @props.children[0]?.type?.name
+        _children_arr = []
+        React.Children.forEach @props.children, (c) -> _children_arr.push c
+        console.log 'route', (+new Date()).toString()[-4..-1], _children_arr.map (c) -> c.type?.displayName || c.type?.name || c.type
         @router.route @state.fragment, @props.logined, (route, argu, default_route, fragment, default_fragment) =>
           if default_route? && default_fragment?
             @context.ctx.routeAction.navigate(fragment, {replace: true, silent: true})


Use new CSON format for menus

Use new CSON format for menus

--- menus/decoration-example.cson
+++ menus/decoration-example.cson
@@ -1,7 +1,8 @@
 # See https://atom.io/docs/latest/creating-a-package#menus for more details
 'context-menu':
-  '.overlayer':
-      'Enable decoration-example': 'decoration-example:toggle'
+  'atom-text-editor, .overlayer': [
+    {label: 'Enable decoration-example', command: 'decoration-example:toggle'}
+  ]

 'menu': [
   {


counter: bugfix: upsert should not be used if a query is passed along (otherwise that'll throw an error.)

--- node_modules_koding/koding-counter/lib/koding-counter/index.coffee
+++ node_modules_koding/koding-counter/lib/koding-counter/index.coffee
@@ -21,10 +21,10 @@
         [query, update, callback] = arguments

     callback  ?= ->
+    options   = { new: yes, upsert: query? }
     query     ?= {}
     query     = extend { _id: @counterName }, query
     sort      = [['_id', 'desc']]
-    options   = { new: yes, upsert: yes }

     countersCollection = @db.collection @collectionName
     countersCollection.findAndModify query, sort, update, options, callback


Rename electron executable on Linux and Windows

--- build/tasks/build-task.coffee
+++ build/tasks/build-task.coffee
@@ -21,6 +21,11 @@
       fs.renameSync path.join(shellAppDir, 'Contents', 'Frameworks', 'Atom Helper.app', 'Contents', 'MacOS', 'Electron Helper'), path.join(shellAppDir, 'Contents', 'Frameworks', 'Atom Helper.app', 'Contents', 'MacOS', 'Atom Helper')
     else
       cp 'electron', shellAppDir, filter: /default_app/
+
+      if process.platform is 'win32'
+        fs.renameSync path.join(shellAppDir, 'electron.exe'), path.join(shellAppDir, 'atom.exe')
+      else
+        fs.renameSync path.join(shellAppDir, 'electron'), path.join(shellAppDir, 'atom')

     mkdir appDir


Remove dead code

Remove dead code

--- src/scripts/modules/media/header/header.coffee
+++ src/scripts/modules/media/header/header.coffee
@@ -62,12 +62,6 @@
         model: @model
         owner: @$el.find('.info .btn')

-    isEditable: () ->
-      if @model.isBook()
-        return @model.get('currentPage')?.isEditable()
-
-      return @model.isEditable()
-
     toggleSummary: (e) ->
       $summary = @$el.find('.summary')


Enable generating compressed artifacts.

Enable generating compressed artifacts.

--- intel-corei7-64.coffee
+++ intel-corei7-64.coffee
@@ -91,6 +91,7 @@
 		fstype: 'resin-sdcard'
 		version: 'yocto-jethro'
 		deployArtifact: 'resin-image-flasher-intel-corei7-64.resin-sdcard'
+		compressed: true

 	configuration:
 		config:


relative urls

relative urls

the original url is needed in order to parse relative urls that are found in
the html, or else JSDOM will thrown errors.

--- unshrtn.coffee
+++ unshrtn.coffee
@@ -101,7 +101,7 @@

 canonical = (url, html) ->
-  dom = new jsdom.JSDOM(html)
+  dom = new jsdom.JSDOM(html, url: url)
   link = dom.window.document.querySelector('head link[rel=canonical]')
   if link and link.attributes and link.attributes.href
     url = link.attributes.href.value


fix(test): Fix import test

fix(test): Fix import test

--- src/spec/integration/importArchive.spec.coffee
+++ src/spec/integration/importArchive.spec.coffee
@@ -17,7 +17,7 @@

 createImporter = (format) ->
-  config = _.cloneDeep(Config)
+  config = JSON.parse(JSON.stringify(Config)) # cloneDeep
   config.importFormat = format || "csv"
   im = new Import config
   im.matchBy = 'sku'


LEV-80 probability fix

LEV-80 probability fix

--- modules/levelcreator/app/assets/javascripts/level_creator/plugins/segment_importer.coffee
+++ modules/levelcreator/app/assets/javascripts/level_creator/plugins/segment_importer.coffee
@@ -493,4 +493,6 @@
   setProbability: (segments, mission) ->

     for segment in segments
-      segment.probability = _.find(mission.possibleEnds, (m) => m.id is segment.value).probability +      es = _.find(mission.possibleEnds, (m) => m.id is segment.value)
+      if es?
+        segment.probability = es.probability


Open calendar when selected.

Open calendar when selected.

--- app/assets/javascripts/crm.js.coffee
+++ app/assets/javascripts/crm.js.coffee
@@ -164,7 +164,7 @@
       if value is "specific_time"
         $("#task_bucket").toggle() # Hide dropdown.
         $("#task_calendar").toggle() # Show editable date field.
-        $("#task_calendar").focus() # Focus to invoke calendar popup.
+        $("#task_calendar").datepicker().focus() # Focus to invoke calendar popup.

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


Remove NYI alert

Remove NYI alert

--- app/js/lib/views/report/frame.coffee
+++ app/js/lib/views/report/frame.coffee
@@ -11,7 +11,6 @@
     LedgerWeb.app.set(showMenu: true)

   captureTransaction: =>
-    alert('Not yet implemented!')

   render: ->
     @$el.html(@template(report: @model.toJSON()))


Label for wait for replacement tasks now fits on one line - the word 'this' was removed

--- SingularityUI/app/views/taskOverviewSubview.coffee
+++ SingularityUI/app/views/taskOverviewSubview.coffee
@@ -74,7 +74,7 @@
                         input: """
                             <label name="wait-for-replacement-task-label" id="wait-for-replacement-task-label" for="wait-for-replacement-task">
                                 <input name="wait-for-replacement-task" id="wait-for-replacement-task" type="checkbox" #{checked} />
-                                Wait for replacement task to start before killing this task
+                                Wait for replacement task to start before killing task
                             </label>
                             <input name="message" type="text" placeholder="Message (optional)" />
                         """


tab component set tab without url if it's active

--- client/app/components/basic_components.coffee
+++ client/app/components/basic_components.coffee
@@ -51,9 +51,14 @@
     render: ->
         ul className: "nav nav-tabs", role: "tablist",
             for tab in @props.tabs
+                if tab.class?.indexOf('active') > 0
+                    url = null
+                else
+                    url = tab.url
+
                 li className: tab.class,
                     a
-                        href: tab.url
+                        href: url
                     ,
                         tab.text


Decouple function calls

Decouple function calls

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -792,10 +792,11 @@
     socialHelpers.leaveChannel @socialChannel, (err) =>
       throwError err  if err

-    @removeWorkspaceSnapshot().then =>
-      @setMachineUser [nick()], no, =>
-        throwError err  if err
-        @broadcastMessage { type: 'ParticipantWantsToLeave' }
+    @removeWorkspaceSnapshot()
+
+    @setMachineUser [nick()], no, (err) =>
+      throwError err  if err
+      @broadcastMessage type: 'ParticipantWantsToLeave'

     callback()


use lanes-field className

use lanes-field className

--- client/lanes/components/shared/FormGroup.cjsx
+++ client/lanes/components/shared/FormGroup.cjsx
@@ -14,7 +14,7 @@
         model: "invalid-fields invalid-field:#{@getInvalidFieldName()}"

     render: ->
-        className = _.classnames(className, @props.className, "field"
+        className = _.classnames(className, @props.className, "lanes-field"
             editing: @props.editing
             "align-#{@props.align}": @props.align
             display: false == @props.editing


fix error in multipleTags helper

fix error in multipleTags helper

--- client/views/pages/dashboard/dashboard.coffee
+++ client/views/pages/dashboard/dashboard.coffee
@@ -161,7 +161,7 @@

 	multipleProjects: -> Projects.find({}, fields: _id: 1).count() > 1

-	multipleTags: -> @tags.length > 1
+	multipleTags: -> if @tags then @tags.length > 1

 	getTagPath: (tagId) -> FlowRouter.path 'shifts', { projectId:@_id, language:TAPi18n.getLanguage() }, showTags: tagId


Don't add a newline for extra BR element

--- src/trix/utilities/html_parser.coffee
+++ src/trix/utilities/html_parser.coffee
@@ -57,7 +57,8 @@
   processElementNode: (node) ->
     switch node.tagName.toLowerCase()
       when "br"
-        @appendString("\n", getAttributes(node))
+        unless nodeIsExtraBR(node)
+          @appendString("\n", getAttributes(node))
       when "img"
         attributes = { contentType: "image", url: node.getAttribute("src") }

@@ -115,3 +116,9 @@
           element.removeAttribute(name) unless name in allowedAttributes

     container
+
+  nodeIsExtraBR = (node) ->
+    node.tagName.toLowerCase() is "br" and
+      node.tagName is node.previousElementSibling?.tagName and
+      node is node.parentNode.lastElementChild and
+      window.getComputedStyle(node.parentNode).display is "block"


For @all send push notifications to online and offline users.

--- packages/rocketchat-lib/server/sendMessage.coffee
+++ packages/rocketchat-lib/server/sendMessage.coffee
@@ -105,17 +105,21 @@
 					# the mentioned user if mention isn't for all
 					RocketChat.models.Subscriptions.incUnreadForRoomIdAndUserIds message.rid, mentionIds, 1

+				# Get ids of all mentioned users.
 				userIdsToNotify = _.pluck(usersOfMention, '_id')
+				userIdsToPushNotify = userIdsToNotify

-				# If the message is @all, send a notification to all online room users except for the sender.
+				# If the message is @all, notify all room users except for the sender.
 				if toAll and room.usernames?.length > 0
-					onlineUsersOfRoom = RocketChat.models.Users.find({
+					usersOfRoom = RocketChat.models.Users.find({
 							username: {$in: room.usernames},
-							_id: {$ne: user._id}
-							status: {$in: ['online', 'away', 'busy']}},
-						{fields: {_id: 1, username: 1}})
+							_id: {$ne: user._id}},
+						{fields: {_id: 1, username: 1, status: 1}})
 						.fetch()
+					onlineUsersOfRoom = _.filter usersOfRoom, (user) ->
+						user.status in ['online', 'away', 'busy']
 					userIdsToNotify = _.union userIdsToNotify, _.pluck(onlineUsersOfRoom, '_id')
+					userIdsToPushNotify = _.union userIdsToPushNotify, _.pluck(usersOfRoom, '_id')

 				if userIdsToNotify.length > 0
 					for usersOfMentionId in userIdsToNotify
@@ -128,6 +132,7 @@
 								type: room.t
 								name: room.name

+				if userIdsToPushNotify.length > 0
 					if Push.enabled is true
 						Push.send
 							from: 'push'
@@ -143,7 +148,7 @@
 								type: room.t
 								name: room.name
 							query:
-								userId: $in: userIdsToNotify
+								userId: $in: userIdsToPushNotify

 		###
 		Update all other subscriptions to alert their owners but witout incrementing


Set alert, open room and increate unread for all users in room if there is a mention for 'all'

--- packages/rocketchat-lib/server/sendMessage.coffee
+++ packages/rocketchat-lib/server/sendMessage.coffee
@@ -79,12 +79,18 @@
 				Update all other subscriptions of mentioned users to alert their owners and incrementing
 				the unread counter for mentions and direct messages
 				###
-				ChatSubscription.update
+				query =
 					# only subscriptions to the same room
 					rid: message.rid
-					# the mentioned user
-					'u._id': mention._id
-				,
+
+				if mention._id is 'all'
+					# all users except sender if mention is for all
+					query['u._id'] = $ne: user._id
+				else
+					# the mentioned user if mention isn't for all
+					query['u._id'] = mention._id
+
+				ChatSubscription.update query,
 					$set:
 						# alert de user
 						alert: true
@@ -93,6 +99,8 @@
 					# increment unread couter
 					$inc:
 						unread: 1
+				,
+					multi: true

 		###
 		Update all other subscriptions to alert their owners but witout incrementing


change controller name

change controller name

--- app/assets/javascripts/app/controllers/stops.js.coffee
+++ app/assets/javascripts/app/controllers/stops.js.coffee
@@ -1,4 +1,4 @@
-class App.Stops.index extends Spine.Controller
+class App.Stops extends Spine.Controller
 	className: 'stops'

 	elements:


Removes unwanted 'console.log' in backend/interventions.js.coffee

--- app/assets/javascripts/backend/interventions.js.coffee
+++ app/assets/javascripts/backend/interventions.js.coffee
@@ -180,7 +180,6 @@
       E.interventions.refresh $(this)

   $(document).on "keyup change dp.change", ".nested-fields.working-period:first-child input.intervention-started-at", (e) ->
-    console.log e
     $(this).each ->
       E.interventions.updateAvailabilityInstant($(this).val())


Fixed link jumping bug.

Fixed link jumping bug.

--- picoCTF-web/web/coffee/management-problems.coffee
+++ picoCTF-web/web/coffee/management-problems.coffee
@@ -246,6 +246,7 @@
     .done @props.onProblemChange

   handleExpand: (e) ->
+    e.preventDefault()
     if !$(e.target).hasClass "btn"
       @setState {expanded: !@state.expanded}

@@ -260,6 +261,7 @@

       {@props.category} - {@props.name}

+        <Glyphicon className="pad" color="green" glyph="ok"/>
         {statusButton}

@@ -333,7 +335,7 @@

   render: ->
     bundleDisplay = @props.bundles.map ((bundle, i) ->
-      switchText = if bundle.dependencies_enabled then "Disable" else "Enable"
+      switchText = if bundle.dependencies_enabled then "Unlock Problems" else "Lock Problems"
       <ListGroupItem key={i} className="clearfix">

{bundle.name}


missed this. was inserting dupes when loading more

--- app/assets/javascripts/discourse/models/user.js.coffee
+++ app/assets/javascripts/discourse/models/user.js.coffee
@@ -88,9 +88,11 @@
       cache: 'false'
       success: (result)=>
         if result and result.user_actions and result.user_actions.each
+          copy = Em.A()
           result.user_actions.each (i)=>
-            stream.pushObject(Discourse.UserAction.create(i))
-          stream = Discourse.UserAction.collapseStream(stream)
+            copy.pushObject(Discourse.UserAction.create(i))
+          copy = Discourse.UserAction.collapseStream(copy)
+          stream.pushObjects(copy)
           @set('stream', stream)
         callback() if callback


applied PR feedback #895

applied PR feedback #895

--- app/assets/javascripts/main/non_backbone_router.coffee
+++ app/assets/javascripts/main/non_backbone_router.coffee
@@ -63,12 +63,6 @@

     app.router.setReloadFlag()

-    $('[href="/dashboard"]').click(->
-      # Temporary hack to circumvent backbones routing.
-      # This ensures that the tracing view is completely unloaded.
-      location.href = "/dashboard"
-    )
-
     require [
       "oxalis/controller"
       "slider"


Meta overrides the standard event actions.

Meta implies reloading a record.  In that case we shouldn't try to
push in the event-streamed data because it will be immediately replaced
by the reloaded data.

--- app/assets/javascripts/services/event_stream.js.coffee
+++ app/assets/javascripts/services/event_stream.js.coffee
@@ -21,9 +21,10 @@
       meta = esData.meta
       delete esData.meta
       delete esData.action
-      (@eventStreamActions[action]||->).call(@, esData)
       if meta
         @eventStreamActions["meta"].call(@, meta.model_name, meta.id)
+      else
+        (@eventStreamActions[action]||->).call(@, esData)

   eventStreamActions:
     created: (esData) ->
@@ -60,7 +61,8 @@
           task.triggerLater('didDelete')

     meta: (modelName, id) ->
-      if model = @store.getById(modelName, id)
-        model.reload()
-      else
-        store.find(modelName, id)
+      Ember.run =>
+        if model = @store.getById(modelName, id)
+          model.reload()
+        else
+          store.find(modelName, id)


add individual quiz breadcrumb for fabulous quizzes

fixes CNVS-12748

test plan
  - enable fabulous quizzes
  - visit the quizzes index page
  - navigate to an individual quiz
  - a link to the individual quiz should be added to the breadcrumb navigation
  - hit the back button to go back to quizzes#index
  - the link to the individual quiz should be removed from the breadcrumbs

Change-Id: I9b972c6d3a4ab1441e85eb6e20a35de45e099867
Reviewed-on: https://gerrit.instructure.com/35081
Reviewed-by: Jason Madsen <bcb5fff571a20f07c35d17ba9c364855c776dce7@instructure.com>
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
QA-Review: Caleb Guanzon <54fa1f30d451e4f3c341a8f3c47c9215fd0be9c6@instructure.com>
Product-Review: Derek DeVries <ff87a0776152510efd53bb1f4ea089dbbc90e730@instructure.com>

--- app/coffeescripts/ember/quizzes/views/quiz_view.coffee
+++ app/coffeescripts/ember/quizzes/views/quiz_view.coffee
@@ -4,6 +4,19 @@
   'compiled/jquery/ModuleSequenceFooter'
 ], (Ember, environment) ->
   QuizView = Ember.View.extend
+    addBreadCrumb: (->
+      quizUrl = @controller.get('htmlURL')
+      breadcrumb = $(
+        "
" +
+          @controller.get("title") +
+        "
")
+      $("#breadcrumbs ul").append(breadcrumb)
+    ).on('didInsertElement')
+
+    removeBreadcrumb: (->
+      $("#breadcrumbs li").last().remove()
+    ).on('willDestroyElement')
+
     addModuleSequenceFooter: (->
       this.$('#module_sequence_footer').moduleSequenceFooter(
         courseID: environment.get('courseId')


Remove prototype cruft

Remove prototype cruft

--- js/inner/container.coffee
+++ js/inner/container.coffee
@@ -33,14 +33,6 @@
     @position_style =
       left: (@container_index * 100) + "%"

-    @container_class = @komp =>
-      # Everything about this function is icky
-      #   - It shouldn't need to be a ko.computed, the value is static
-      #   - The dynamic property syntax is ugly, coffeescript supports interpolation but not "foo#{name}": value
-      o = {}
-      o["container_" + @container_id] = true
-      return o
-
   success: () ->
     for action in @actions()
       if not action.success()


Move zombies.coffee into the correct directory

--- zombies.coffee
+++ zombies.coffee
@@ -1,30 +0,0 @@
-# Description:
-#   Bring forth zombies
-#
-# Dependencies:
-#   None
-#
-# Configuration:
-#   None
-#
-# Commands:
-#   (zombie) - Call in a zombie
-#
-# Author:
-#   solap
-
-
-
-images = [
-  "http://24.media.tumblr.com/tumblr_m35jnyjTco1qikhvso1_100.gif",
-  "http://www.netanimations.net/head2.gif",
-  "http://www.netanimations.net/Animated-Zombie-Reverse.gif",
-  "http://www.freewebs.com/echoeyy/zombie%20getting%20shot.gif",
-  "https://i.chzbgr.com/maxW500/6360720640/h487AE90F/",
-  "https://i.chzbgr.com/maxW500/5912815872/h8AB29CB2/",
-  "https://i.chzbgr.com/maxW500/5299680512/h5120FD0B/"
-  ]
-
-module.exports = (robot) ->
-  robot.hear /zombi(e|es)/i, (msg) ->
-    msg.send msg.random images


Fix https://github.com/poooi/poi/issues/447

--- views/layout.horizontal.cjsx
+++ views/layout.horizontal.cjsx
@@ -84,6 +84,9 @@
   if webviewWidth != -1
     $('kan-game').style.flex = webviewWidth
     $('poi-app').style.flex = window.innerWidth - webviewWidth
+  else
+    $('kan-game').style.flex = (if window.doubleTabbed then 4.0 else 5.0)
+    $('poi-app').style.flex = (if window.doubleTabbed then 3.0 else 2.0)
   if !window._delay
     adjustSize()
   else


client: mainview removed legacy code

client: mainview removed legacy code

--- client/app/lib/mainview.coffee
+++ client/app/lib/mainview.coffee
@@ -38,8 +38,6 @@

   viewAppended: ->

-    @bindPulsingRemove()
-    @bindTransitionEnd()
     @createSidebar()
     @createHeader()
     @createPanelWrapper()
@@ -370,28 +368,6 @@
       @enableFullscreen()

-  bindPulsingRemove:->
-
-    router     = kd.getSingleton 'router'
-    appManager = kd.getSingleton 'appManager'
-
-    appManager.once 'AppCouldntBeCreated', removePulsing
-
-    appManager.on 'AppCreated', (appInstance)->
-      options = appInstance.getOptions()
-      {title, name, appEmitsReady} = options
-      routeArr = global.location.pathname.split('/')
-      routeArr.shift()
-      checkedRoute = if routeArr.first is "Develop" \
-                     then routeArr.last else routeArr.first
-
-      if checkedRoute is name or checkedRoute is title
-        if appEmitsReady
-          appView = appInstance.getView()
-          appView.ready removePulsing
-        else removePulsing()
-
-
   _logoutAnimation: ->

     {body}      = global.document
@@ -405,32 +381,3 @@
     @setClass "logout-tv"

-  removePulsing = ->
-
-    loadingScreen = global.document.getElementById 'main-loading'
-
-    return unless loadingScreen
-
-    logo = loadingScreen.children[0]
-    logo.classList.add 'out'
-
-    kd.utils.wait 750, ->
-
-      loadingScreen.classList.add 'out'
-
-      kd.utils.wait 750, ->
-
-        loadingScreen.parentElement.removeChild loadingScreen
-
-        return if isLoggedIn()
-
-        cdc      = kd.singleton('display')
-        mainView = kd.getSingleton 'mainView'
-
-        return unless Object.keys(cdc.displays).length
-
-        for own id, display of cdc.displays
-          top      = display.$().offset().top
-          duration = 400
-          KDScrollView::scrollTo.call mainView, {top, duration}
-          break


test gloabl

test gloabl

--- coffeescript/004-webworkers/multiplication.coffee
+++ coffeescript/004-webworkers/multiplication.coffee
@@ -10,12 +10,12 @@
     define [
       'underscore'
     ], (_) ->
-        postMessage('before binding')
+        global.postMessage('before binding')
         global.onmessage = (e) ->
             postMessage('hello from worker!')
             result = _.reduce(_.values(e.data), ((memo, elem) -> memo * elem), 1)
             postMessage(result)
             false
-
+        false
 )()


find url list fix

find url list fix

--- noddy/service/v2/find.coffee
+++ noddy/service/v2/find.coffee
@@ -15,10 +15,11 @@
   finder = ''
   for tid in ['doi','pmid','pmcid','url','title']
     if metadata[tid]
+      mt = metadata[tid][0] if _.isArray metadata[tid]
       finder += ' OR ' if finder isnt ''
       if tid is 'title'
         finder += 'ftitle:' + _ftitle(metadata.title) + '~ OR '
-      finder += 'metadata.' + tid + (if tid is 'url' or tid is 'title' then '' else '.exact') + ':"' + metadata[tid] + '"'
+      finder += 'metadata.' + tid + (if tid is 'url' or tid is 'title' then '' else '.exact') + ':"' + mt + '"'
   return finder

 oab_catalogue.finder = (metadata) ->


Building with tests bug fixed

Building with tests bug fixed

--- cli/tasks/build/bundle.coffee
+++ cli/tasks/build/bundle.coffee
@@ -38,6 +38,7 @@
     # disable app
     globalNeft = Neft
     mockNeft = ->
+        onReady: ->
     utils.merge mockNeft, Neft
     global.Neft = mockNeft


Abstract isImporter()

Abstract isImporter()

--- client/code/model/tool.coffee
+++ client/code/model/tool.coffee
@@ -7,6 +7,9 @@
   isBasic: ->
     return @get('name') in ['spreadsheet-download', 'datatables-view-tool']

+  isImporter: ->
+    return @get('type') is 'importer'
+
 Cu.Model.Tool.setup()

 class Cu.Collection.Tools extends Backbone.Collection
@@ -14,11 +17,11 @@
   url: "/api/tools/"

   importers: ->
-    importers = @filter (t) -> t.get('type') is 'importer'
+    importers = @filter (t) -> t.isImporter()
     new Cu.Collection.Tools importers

   nonimporters: ->
-    nonimporters = @filter (t) -> t.get('type') isnt 'importer'
+    nonimporters = @filter (t) -> not t.isImporter()
     new Cu.Collection.Tools nonimporters

   basics: ->


Add tab, comma for tag confirm keys (#77)

- Comma does not seem to work

--- client/views/admin/edit.coffee
+++ client/views/admin/edit.coffee
@@ -87,7 +87,9 @@
   @$('.html-editor').height(@$('.editable').height())

 Template.blogAdminEdit.rendered = ->
-  @$('input[data-role="tagsinput"]').tagsinput()
+  @$('input[data-role="tagsinput"]').tagsinput(
+    confirmKeys: [13, 44, 9]
+  )
   @$('input[data-role="tagsinput"]').tagsinput('input').typeahead(
     highlight: true,
     hint: false


a 405 is better when a method isn't allowed

--- web/app.coffee
+++ web/app.coffee
@@ -76,7 +76,7 @@
           next()
         else
           # TODO: handle this with a reusable, custom 404, function
-          res.statusCode = 404
+          res.statusCode = 405
           res.end err.message

   # Cloud9's vfs for static files


Add emacs-style subword movement bindings

--- keymaps/emacs.cson
+++ keymaps/emacs.cson
@@ -1,7 +1,13 @@
 'atom-text-editor':
   'alt-f': 'editor:move-to-end-of-word'
+  'alt-ctrl-f': 'editor:move-to-next-subword-boundary'
   'alt-F': 'editor:select-to-end-of-word'
+  'alt-ctrl-F': 'editor:select-to-next-subword-boundary'
   'alt-b': 'editor:move-to-beginning-of-word'
+  'alt-ctrl-b': 'editor:move-to-previous-subword-boundary'
   'alt-B': 'editor:select-to-beginning-of-word'
+  'alt-ctrl-B': 'editor:select-to-previous-subword-boundary'
   'alt-h': 'editor:delete-to-beginning-of-word'
+  'alt-ctrl-h': 'editor:delete-to-beginning-of-subword'
   'alt-d': 'editor:delete-to-end-of-word'
+  'alt-ctrl-d': 'editor:delete-to-end-of-subword'


Keyboard shortcut to open folder on Windows

Based on Linux behaviour

--- keymaps/win32.cson
+++ keymaps/win32.cson
@@ -14,6 +14,7 @@
   'ctrl-alt-i': 'window:toggle-dev-tools'
   'ctrl-alt-p': 'window:run-package-specs'
   'ctrl-alt-s': 'application:run-all-specs'
+  'ctrl-shift-o': 'application:open-folder'
   'F11': 'window:toggle-full-screen'

   # Sublime Parity


setAudioEnabled

setAudioEnabled

--- lib/assets/javascripts/webrtc_rails/main.js.coffee
+++ lib/assets/javascripts/webrtc_rails/main.js.coffee
@@ -85,6 +85,20 @@
       @_videoEnabled = enabled
       return
     for track in @_localStream.getVideoTracks()
+      track.enabled = enabled
+
+  enableAudio: ->
+    @setAudioEnabled(true)
+
+  disableAudio: ->
+    @setAudioEnabled(false)
+
+  setAudioEnabled: (enabled) ->
+    unless @_localStream?
+      @_wantSetAudioEnabled = true
+      @_audioEnabled = enabled
+      return
+    for track in @_localStream.getAudioTracks()
       track.enabled = enabled

   hangUp: ->
@@ -228,6 +242,8 @@
           @localOutput.volume = 0
           if @_wantSetVideoEnabled
             @setVideoEnabled(@_videoEnabled)
+          if @_wantSetAudioEnabled
+            @setAudioEnabled(@_audioEnabled)
       (error) =>
         console.error('An error occurred: [CODE ' + error.code + ']')
     )


remove clashing subject values as they are not necessary

--- noddy/service/v2/find.coffee
+++ noddy/service/v2/find.coffee
@@ -253,7 +253,7 @@
         try metadata.volume = cr.volume if cr.volume?
         try metadata.page = cr.page.toString() if cr.page?
         try metadata.issn ?= cr.ISSN[0] if cr.ISSN?
-        try metadata.subject = cr.subject if cr.subject? # not sure if this is present in crossref... check anyway
+        #try metadata.subject = cr.subject if cr.subject? # not sure if this is present in crossref... check anyway - commented out because getting clashes with the mapping
         try metadata.publisher = cr.publisher if cr.publisher?
         try metadata.year ?= cr['published-print']['date-parts'][0][0] if cr['published-print']['date-parts'][0][0].length
         try metadata.year ?= cr.created['date-time'].split('-')[0]
@@ -285,7 +285,7 @@
         try metadata.volume = cr.journalInfo.volume if cr.journalInfo.volume?
         try metadata.page = cr.pageInfo.toString() if cr.pageInfo?
         try metadata.issn = cr.journalInfo.journal.issn if cr.journalInfo.journal.issn?
-        try metadata.subject = cr.subject if cr.subject? # not sure if epmc has subject
+        #try metadata.subject = cr.subject if cr.subject? # not sure if epmc has subject
         #try metadata.publisher = cr.publisher #epmc does not appear to have publisher
         try metadata.year = cr.journalInfo.yearOfPublication if cr.journalInfo.yearOfPublication?
         try metadata.year ?= cr.journalInfo.printPublicationDate.split('-')[0]


fix to find

fix to find

--- noddy/service/v2/find.coffee
+++ noddy/service/v2/find.coffee
@@ -108,23 +108,6 @@
   options.metadata = if options.metadata is true then ['title','doi','author','journal','issn','volume','issue','page','published','year'] else if _.isArray(options.metadata) then options.metadata else []
   content ?= options.dom if options.dom?

-  # other possible options are permissions
-  # TODO what other options values should we be sure to collect based on old behaviour? from,uid...
-  res.plugin = options.plugin if options.plugin?
-  res.from = options.from if options.from?
-  res.all = options.all ? false
-  res.parallel = options.parallel ? true
-  res.find = options.find ? true
-  # other possible sources are ['base','dissemin','share','core','openaire','bing']
-  res.sources = options.sources ? ['oabutton','crossref','epmc','reverse','scrape','oadoi','figshare','doaj']
-  res.sources.push('bing') if options.plugin in ['widget','oasheet'] or options.from in ['illiad','clio'] or options.url.indexOf('alma.exlibrisgroup.com') isnt -1
-  try res.refresh = if options.refresh is false then 30 else if options.refresh is true then 0 else parseInt options.refresh
-  res.refresh = 30 if typeof res.refresh isnt 'number' or isNaN res.refresh
-  res.embedded ?= options.embedded if options.embedded?
-  res.pilot = options.pilot if options.pilot?
-  res.live = options.live if options.live?
-  res.found = {}
-
   if metadata.url
     options.url ?= metadata.url
   if metadata.id
@@ -132,12 +115,6 @@
     delete metadata.id
   options.url = options.url[0] if _.isArray options.url
   res.checked = []
-
-  _got = (obj=metadata) ->
-    for w in options.metadata
-      if not obj[w]?
-        return false
-    return true

   metadata.doi ?= options.doi.replace(/doi\:/i,'').trim() if typeof options.doi is 'string'
   metadata.title ?= options.title.trim() if typeof options.title is 'string'
@@ -186,6 +163,28 @@
           metadata.title = options.citation.split('"')[0].split("'")[0].trim()
         metadata.title = metadata.title.replace(/(<([^>]+)>)/g,'').trim()

+  # other possible options are permissions
+  res.plugin = options.plugin if options.plugin?
+  res.from = options.from if options.from?
+  res.all = options.all ? false
+  res.parallel = options.parallel ? true
+  res.find = options.find ? true
+  # other possible sources are ['base','dissemin','share','core','openaire','bing']
+  res.sources = options.sources ? ['oabutton','crossref','epmc','reverse','scrape','oadoi','figshare','doaj']
+  res.sources.push('bing') if options.plugin in ['widget','oasheet'] or options.from in ['illiad','clio'] or (options.url? and options.url.indexOf('alma.exlibrisgroup.com') isnt -1)
+  try res.refresh = if options.refresh is false then 30 else if options.refresh is true then 0 else parseInt options.refresh
+  res.refresh = 30 if typeof res.refresh isnt 'number' or isNaN res.refresh
+  res.embedded ?= options.embedded if options.embedded?
+  res.pilot = options.pilot if options.pilot?
+  res.live = options.live if options.live?
+  res.found = {}
+
+  _got = (obj=metadata) ->
+    for w in options.metadata
+      if not obj[w]?
+        return false
+    return true
+
   # special cases for instantill demo and exlibris - dev and live demo accounts that always return a fixed answer
   if options.plugin is 'instantill' and metadata.doi is '10.1234/567890' and options.from in ['qZooaHWRz9NLFNcgR','eZwJ83xp3oZDaec86']
     res.ill = {openurl: ""}


Fix a spacing issue that was encoding an incorrect URL

--- scripts/jira-linkifier.coffee
+++ scripts/jira-linkifier.coffee
@@ -86,6 +86,6 @@

 issue_summary = (issue) ->
   if issue.fields
-    process.env.HUBOT_JIRA_INSTANCE_URL + "/browse/" + issue.key + "|" + issue.key + ": " + issue.fields.summary + " (status: " + issue.fields.status.name + ")"
+    process.env.HUBOT_JIRA_INSTANCE_URL + "/browse/" + issue.key + " | " + issue.key + ": " + issue.fields.summary + " (status: " + issue.fields.status.name + ")"
   else
     null


update wysiwyg image width js

update wysiwyg image width js

--- app/assets/javascripts/storytime/wysiwyg.js.coffee
+++ app/assets/javascripts/storytime/wysiwyg.js.coffee
@@ -53,7 +53,7 @@
       if !target.hasClass("medium-image-controls") && !(target.closest(".medium-image-controls").length > 0) && $('.medium-image-controls').is(":visible")
         self.closeImageControls()
         mediumEditor.activate()
-      if target.is("img") && !target.hasClass("medium-active-image")
+      if target.is("img") && !target.hasClass("medium-active-image") # TODO: SCOPE THIS TO THE POST EDITING PAGE ONLY....
         self.openImageControls(target)
         mediumEditor.deactivate()

@@ -61,6 +61,7 @@
       if $(this).val() == "" || $(this).val() == "auto"
         $(".medium-active-image").css("width", "")
         $("#medium-image-height").val("")
+        $("#medium-image-width").val("")
       else
         $(".medium-active-image").css("width", $(this).val())
         $(".medium-active-image").css("height", "")


remove console.log

remove console.log

--- components/layout/index.coffee
+++ components/layout/index.coffee
@@ -27,7 +27,6 @@
   render(DEFAULT_LANGUAGE)

   toggleLanguageOnClick = (e) ->
-    console.log this
     language = $(e.currentTarget).data('language')
     render(language)


Always _cleanupTimeout in _onTimeout

Always _cleanupTimeout in _onTimeout

--- source/server/infrastructure/commit_publisher.coffee
+++ source/server/infrastructure/commit_publisher.coffee
@@ -80,7 +80,7 @@
     })
     if(failedCommit)
       @log.error(@_logMsg("#{commitId} TIMED OUT"), changes)
-      @_cleanupTimeout(commitId)
+    @_cleanupTimeout(commitId)

   _clearTimeout: (commitId) ->
     @meteor.clearTimeout(@_inProgress[commitId])


Pull blog title, description, and url from package.json

--- config/plugins/markdown.coffee
+++ config/plugins/markdown.coffee
@@ -21,9 +21,9 @@
     markdown:
       options:
         author: "FirstName LastName"
-        title: "my lineman blog"
-        description: "where I post all my brilliant ideas"
-        url: "http://www.mylinemanblog.com"
+        title: "<%= pkg.title %>"
+        description: "<%= pkg.description %>"
+        url: "<%= pkg.homepage %>"
         rssCount: 10 #<-- remove, comment, or set to zero to disable RSS generation
         #disqus: "my_disqus_name" #<-- uncomment and set your disqus account name to enable disqus support
         layouts:


doube to single quote

doube to single quote

--- src/Bu.dev.coffee
+++ src/Bu.dev.coffee
@@ -36,4 +36,4 @@
 	]

 	for lib in libs
-		document.write '<script src='../../src/' + lib + '.js'></script>')()
+		document.write '<script src="../../src/' + lib + '.js"></script>')()


Add API for checking if we can make a feed post, support bailing out if a feed post can't be made

--- src/Carrot.coffee
+++ src/Carrot.coffee
@@ -132,6 +132,15 @@
     params['object_instance_id'] = objectInstanceId if objectInstanceId
     @postSignedRequest("/me/actions.json", params, @callbackHandler(callback))

+  canMakeFeedPost: (objectInstanceId, callback) ->
+    params = {
+      'object_instance_id': objectInstanceId
+    }
+    @postSignedRequest("/me/can_post.json", params, (jqXHR) =>
+      carrotResponse = $.parseJSON(jqXHR.responseText)
+      callback(carrotResponse.code == 200) if callback
+    )
+
   popupFeedPost: (objectInstanceId, objectProperties, callback, postMethod) ->
     if !postMethod? && FB?
       postMethod = FB.ui
@@ -145,13 +154,16 @@
       params['object_instance_id'] = objectInstanceId if objectInstanceId
       @postSignedRequest("/me/feed_post.json", params, (jqXHR) =>
         carrotResponse = $.parseJSON(jqXHR.responseText)
-        postMethod(carrotResponse.fb_data,
-          (fbResponse) =>
-            if fbResponse and fbResponse.post_id
-              @ajaxPost("#{@scheme}://parsnip.gocarrot.com/feed_dialog_post", {platform_id: carrotResponse.post_id})
-
-            callback(carrotResponse, fbResponse) if callback
-        )
+        if carrotResponse.code == 200
+          postMethod(carrotResponse.fb_data,
+            (fbResponse) =>
+              if fbResponse and fbResponse.post_id
+                @ajaxPost("#{@scheme}://parsnip.gocarrot.com/feed_dialog_post", {platform_id: carrotResponse.post_id})
+
+              callback(carrotResponse, fbResponse) if callback
+          )
+        else
+          callback(carrotResponse) if callback
       )

   reportFeedClick: (postId, callback) ->


WIP; small fixes & improvements

WIP; small fixes & improvements

--- src/Casket.coffee
+++ src/Casket.coffee
@@ -2,6 +2,12 @@
 http =			require 'http'
 url =			require 'url'
 accepts =		require 'accepts'
+
+onOPTIONS =		require './methods/options'
+onGET =			require './methods/get'
+onPOST =		require './methods/post'
+onDELETE =		require './methods/delete'
+Context =		require './Context'

@@ -11,20 +17,23 @@

-	_options:	require './methods/options'
-	_get:		require './methods/get'
-	_post:		require './methods/post'
-	_delete:	require './methods/delete'
+	onOPTIONS:	onOPTIONS
+	onGET:		onGET
+	onPOST:		onPOST
+	onDELETE:	onDELETE

-	_methods: {}
+	Context:	Context

-	cors: false
+	dir:		null
+	name:		null
+	log:		null
+	cors:		false

-	init: (dir, name, logger) ->
+	init: (dir, name, log) ->
 		if not dir? then throw new Error 'Missing `dir` parameter'
 		@dir = path.normalize dir
 		@relative = path.join '.', path.relative process.cwd(), @dir
@@ -32,11 +41,13 @@
 		if not name? then throw new Error 'Missing `name` parameter'
 		@name = name

-		if not logger? then throw new Error 'Missing `logger` parameter'
-		@logger = logger
+		if not log? then throw new Error 'Missing `log` parameter'
+		@log = log

-		for method in ['options', 'get', 'post', 'delete']
-			@_methods[method] = @['_' + method] this
+		@onOPTIONS = @onOPTIONS this
+		@onGET = @onGET this
+		@onPOST = @onPOST this
+		@onDELETE = @onDELETE this

 		thus = this
 		@server = http.createServer (req, res) ->
@@ -47,13 +58,13 @@

 	listen: (port, callback) ->
 		@server.listen port, () =>
-			@logger.info "Serving #{@relative} as '#{@name}' on port #{port}."
+			@log.info "Serving #{@relative} as '#{@name}' on port #{port}."
 			callback?()
 		return this

 	close: (callback) ->
 		@server.close () ->
-			@logger.info "Stopped serving '#{@name}'."
+			@log.info "Stopped serving '#{@name}'."
 			callback?()
 		return this