Dynamic Rails routing based on database - ruby-on-rails

I'm building a CMS with various modules (blog, calendar, etc.) using Rails 2.3. Each module is handled by a different controller and that works just fine.
The only problem I have is with the root URL. Depending on the configuration chosen by the user, this default URL should show a different module i.e. a different controller, but the only way I have to determine the correct controller is by checking the database for what "default" module is to be shown.
For the moment I'm using a specific "root" controller which checks the database and redirects to the correct controller. However I'd prefer the URL not to be changed, which means I want to invoke the correct controller from the very same request.
I've tried using Rails Metal to fetch this info and manually calling the controller I want but I'm thinking I may be reinventing the wheel (identify the request path to choose the controller, manage session, etc.).
Any idea? Thanks a lot in advance!

This problem can be solved with some Rack middleware:
This code in lib/root_rewriter.rb:
module DefV
class RootRewriter
def initialize(app)
#app = app
end
def call(env)
if env['REQUEST_URI'] == '/' # Root is requested!
env['REQUEST_URI'] = Page.find_by_root(true).uri # for example /blog/
end
#app.call(env)
end
end
end
Then in your config/environment.rb at the bottom
require 'root_rewriter'
ActionController::Dispatcher.middleware.insert_after ActiveRecord::QueryCache, DefV::RootRewriter
This middleware will check if the requested page (REQUEST_URI) is '/' and then do a lookup for the actual path (Implementation to this is up to you ;-)). You might do good on caching this info somewhere (Cache.fetch('root_path') { Page.find... })
There are some problems with checking REQUEST_URI, since not all webservers pass this correctly. For the whole implementation detail in Rails see http://api.rubyonrails.org/classes/ActionController/Request.html#M000720 (Click "View source")

In Rails 3.2 this was what I came up with (still a middleware):
class RootRewriter
def initialize(app)
#app = app
end
def call(env)
if ['', '/'].include? env['PATH_INFO']
default_thing = # Do your model lookup here to determine your default item
env['PATH_INFO'] = # Assemble your new 'internal' path here (a string)
# I found useful methods to be: ActiveModel::Naming.route_key() and to_param
end
#app.call(env)
end
end
This tells Rails that the path is different from what was requested (the root path) so references to link_to_unless_current and the like still work well.
Load the middleware in like so in an initialiser:
MyApp::Application.config.middleware.use RootRewriter

Related

How do I modify the request object before routing in Rails in a testable way?

So, I have a situation where I need to determine something about a request before it is dispatched to any of the routes. Currently, this is implemented using several constraints that all hit the database, and I want to reduce the database hit to one. Unfortunately, doing it inline in routes.rb doesn't work, because the local variables within routes.rb don't get refreshed between requests; so if I do:
# Database work occurs here, and is then used to create comparator lambdas.
request_determinator = RequestDeterminator.new(request)
constraint(request_determinator.lambda_for(:ninja_requests)) do
# ...
end
constraint(request_determinator.lambda_for(:pirate_requests)) do
# ...
end
This works great on the first request, but then subsequent requests get routed as whatever the first request was. (D'oh.)
My next thought was to write a Rack middleware to add the "determinator" to the env hash, but there are two problems with this: first, it doesn't seem to be sticking in the hash at all, and specs don't even go through the Rack middleware, so there's no way to really test it.
Is there a simple mechanism I'm overlooking where I can insert, say, a hook for ActionDispatch to add something to the request, or just to say to Rails routing: "Do this before routing?"
I am using Rails 3.2 and Ruby 1.9.
One way to do this would be to store your determinator on the request's env object (which you have since ActionDispatch::Request is a subclass of Rack::Request):
class RequestDeterminator
def initialize(request)
#request = request
end
def self.for_request(request)
request.env[:__determinator] ||= new(request)
end
def ninja?
query_db
# Verify ninjaness with #request
end
def pirate?
query_db
# Verify piratacity with #request
end
def query_db
#result ||= begin
# Some DB lookup here
end
end
end
constraint lambda{|req| RequestDeterminator.for_request(req).ninja? } do
# Routes
end
constraint lambda{|req| RequestDeterminator.for_request(req).pirate? } do
# Routes
end
That way, you just instantiate a single determinator which caches your DB request across constraint checks.
if you really want to intercept the request,try rack as it is the first one to handle request in any Rails app...refer http://railscasts.com/episodes/151-rack-middleware to understand how rack works....
hope it helps.

Determine if Journey::Path::Pattern matches current page

I'm trying to use the method outlined this post in conjunction with url_for to determine if the current path is in a mounted engine, but I'm having a hard time figuring out how exactly to use Journey::Path::Pattern (which is what is returned by the mounted_path method outlined in the other post).
class Rails::Engine
def self.mounted_path
route = Rails.application.routes.routes.detect do |route|
route.app == self
end
route && route.path
end
end
There doesn't seem to be too much discussion on it anywhere, aside from the official documentation, which wasn't particularly helpful. I'm sure the solution is relatively simple, and the gist of the helper method I'm trying to write is below:
def in_engine? engine
current_url.include?(engine.mounted_path)
end
Edit:
Some of my engines are mounted as subdomains and some are mounted within the app itself, preventing me from simply checking if the current subdomain is the same as the mounted path, or using path_for.
Not exactly a solution, but maybe a useful lead.
I found your question interesting, so I started delving deep inside rails source... what a scary, yet instructive trip :D
Turns out that Rails' router has a recognize method that accepts a request as argument, and yields the routes that match the request.
As the routes have an app method you can compare to your engine, and as you can have access to the request object (which takes into account the http method, subdomain, etc), if you find out how to have direct access to the router instance, you should be able to do something along the lines of :
def in_engine?(engine)
router.recognize(request) do |route,*|
return true if route.app == engine
end
false
end
EDIT
I think i found out, but it's late here in I have no rails app at hand to test this :(
def in_engine?(engine)
# get all engine routes.
# (maybe possible to do this directly with the engine, dunno)
engine_routes = Rails.application.routes.set.select do |route|
route.app == engine
end
!!engine_routes.detect{ |route| route.matches?(request) }
end
EDIT
also, maybe a simpler workaround would be to do this :
in your main app
class ApplicationController < ActionController::Base
def in_engine?(engine)
false
end
helper_method :in_engine?
end
then in your engine's application controller
def in_engine?(engine)
engine == ::MyEngine
end
helper_method :in_engine?

Help with Rack middleware

I have an uploader, which for the moment includes a Flash uploading option as a fallback. In order to make the flash uploader work I have to use this middleware to preserve the session cookie.
I don't know beans about rack, or middleware, I'm guilt of copying this code from a tutorial on how to fix flash uploading without understanding what it does. Here's the code:
require 'rack/utils'
class FlashSessionCookieMiddleware
def initialize(app, session_key = '_session_id')
#app = app
#session_key = session_key
end
def call(env)
if env['HTTP_USER_AGENT'] =~ /^(Adobe|Shockwave) Flash/
req = Rack::Request.new(env)
env['HTTP_COOKIE'] = [ #session_key, ::Rack::Utils.escape(req.params[#session_key]) ].join('=').freeze unless req.params[#session_key].nil?
env['HTTP_ACCEPT'] = "#{req.params['_http_accept']}".freeze unless req.params['_http_accept'].nil?
end
#app.call(env)
end
end
This gets include in the session store initializer:
#initializers/session_store.rb
Rails.application.config.middleware.insert_before(
Rails.application.config.session_store,
FlashSessionCookieMiddleware,
Rails.application.config.session_options[:key])
Now I've run into a problem: I'm setting up an admin namespace to group a bunch of administrator-only controller actions throughout the site. The first thing I tried to do is setup the root of the admin namespace, like so:
namespace :admin do
root :to => 'queues#index'
end
But this crashes in the middleware, with the following error message in the server log:
ActionController::RoutingError (uninitialized constant Admin):
app/uploaders/flash_session_cookie_middleware.rb:16:in `call'
So, the middleware (that I don't understand) is trying to call Admin (which doesn't exist), I suppose because that's the prefix of the route? I would have been less surprised if it tried to call AdminController or AdminsController.
This really baffles me because other namespaces in my app work. For instance, this works fine:
namespace :account do
resource :billing, :except => [:edit,:update]
resource :subscription
end
So something about the middleware and defining the root of a namespace is causing this issue, and I don't comprehend it at all to be honest.
If anyone could explain what's going on with this middleware, why it's causing a conflict with my routing, and how to fix it, I would be very grateful. Thanks!
Kinda bugged right now because I've had this issue before, and now that I'm trying to reproduce it in a test app, I find that I can't.
But anyway. The error doesn't have anything to do with the middleware. It just happens that the wrapping #call in the flash middleware is getting stuck with the exception which is happening inside.
I wish I could say what the actual problem is, but it likely has to do with that "Admin" module namespace. Have you tried say, "Administrator" to see if changing to a different module clears it up?
Rather than namespace :admin, you could also use scope "/admin", the latter accomplishing the same thing minus the need to namespace the controller, which will probably solve your problem.
Speaking of, I'm assuming that the 'queues' controller is in a subfolder named "admin", and namespaced Admin::QueuesController?

Possible to use route helpers to redirect from a Rack app in Rails 3?

I have a simple url shortener that base 62 encodes my Developer model's id number and returns something like this as a url:
http://example.com/d/dYbZ
I've mounted a rack app in my routes.rb file thusly:
match '/d/:token' => DeveloperRedirectApp
... and my simple Rack app looks like this:
class DeveloperRedirectApp
# no worky:
#def initialize(app)
# #app = app
#end
def self.call(env)
request = Rack::Request.new(env)
token = request.path_info.sub("/d/", "")
dev_id = token.b(62).to_s(10)
if dev = Developer.find_by_id(dev_id)
# developer_path also doesn't work since #app is not defined
location = #app.developer_path(dev)
else
# same here
location = #app.root_path
end
[301, {"Location" => location}, self]
end
def self.each(&block)
end
end
The problem is... apparently the initialize call is only sent a rails app instance if it's an actual middleware, not simply a rack app mounted in the routes file. A middleware doesn't make sense to me since this only needs to run if a url of the form /d/:token is requested, not on every request.
I'm just trying to do a simple base 62 decode, then redirect to the decoded developer id (if it exists, redirect to root_url otherwise). Is there a way to access the route helpers (ie, developer_path) or simply a better way to do this?
It might simply be easier to do it with a rails controller that does the redirect. eg:
routes:
get "/d/:token" => "developers#redirect"
in the developers controller:
def redirect
#magic goes here, use params[:token]
redirect_to some_url
end

How do I access the Rack environment from within Rails?

I have a Rack application that looks like this:
class Foo
def initialize(app)
#app = app
end
def call(env)
env["hello"] = "world"
#app.call(env)
end
end
After hooking my Rack application into Rails, how do I get access to env["hello"] from within Rails?
Update: Thanks to Gaius for the answer. Rack and Rails let you store things for the duration of the request, or the duration of the session:
# in middleware
def call(env)
Rack::Request.new(env)["foo"] = "bar" # sticks around for one request
env["rack.session"] ||= {}
env["rack.session"]["hello"] = "world" # sticks around for duration of session
end
# in Rails
def index
if params["foo"] == "bar"
...
end
if session["hello"] == "world"
...
end
end
I'm pretty sure you can use the Rack::Request object for passing request-scope variables:
# middleware:
def call(env)
request = Rack::Request.new(env) # no matter how many times you do 'new' you always get the same object
request[:foo] = 'bar'
#app.call(env)
end
# Controller:
def index
if params[:foo] == 'bar'
...
end
end
Alternatively, you can get at that "env" object directly:
# middleware:
def call(env)
env['foo'] = 'bar'
#app.call(env)
end
# controller:
def index
if request.env['foo'] == 'bar'
...
end
end
Short answer: Use request.env or env inside a controller.
Long answer:
According to the Rails Guide on Rails controllers, ActionController provides a request method that you can use to access information about the current HTTP request your controller is responding to.
Upon further inspection of the docs for ActionController::Base#request, we see that it "Returns an ActionDispatch::Request instance that represents the current request."
If we look at the docs for ActionDispatch::Request, we see that it inherits from Rack::Request. Aha! Here we go.
Now, in case you're not familiar with the docs for Rack::Request, it's basically a wrapper around the Rack environment. So for most cases, you should just be able to use it as-is. If you really do want the raw environment hash though, you can get it with Rack::Request#env. So within the Rails controller, that would just be request.env.
Digging deeper:
After further examining the instance methods of ActionController::Base, I noticed there's not a whole lot there to look at. In particular, I noticed the params and session variables seem to be missing. So, I moved up one level to ActionController::Metal, which ActionController::Base inherits from.
In ActionController::Metal, I discovered a method env which had no documentation as to what it did - but I could guess. Turns out I was right. That variable was being assigned to request.env.
ActionController::Metal also contained the params method, which, according to the source, was set to request.parameters by default. As it turns out, request.parameters isn't from Rack::Request, but ActionDispatch::Http::Parameters, which is included by ActionDispatch::Request. This method is very similar to the Rack::Request#params method, except that altering it modifies a Rails-specific Rack environment variable (and therefore changes will remain persistent across instances of ActionDispatch::Request).
However, I still couldn't seem to find the session method. Turns out, it's not in the documentation at all. After searching the source code for ActionController::Metal, I finally found it on this line. That's right, it's just a shortcut for request.session.
To summarize:
In the controller...
Use request.env or env to get at the raw environment object
Use params to read Rack query strings and post data from the rack input stream. (E.g. Rack::Request#params)
Use session to access the value of rack.session in the rack environment
In the middleware...
Access properties of the environment the usual way through the environment hash
Access the Rails session through the rack.session property on the environment hash
Read params through Rack::Request#params
Update params through Rack::Request#update_param and Rack::Request#delete_param (as stated in the docs for Rack::Request#params)
Update params in a Rails specific way using ActionDispatch::Http::Parameters#params through ActionDispatch::Request

Resources