Is it a bad idea to reload routes dynamically in Rails? - ruby-on-rails

I have an application I'm writing where I'm allowing the administrators to add aliases for pages, categories, etc, and I would like to use a different controller/action depending on the alias (without redirecting, and I've found that render doesn't actually call the method. I just renders the template). I have tried a catch all route, but I'm not crazy about causing and catching a DoubleRender exception that gets thrown everytime.
The solution for this I've come up with is dynamically generated routes when the server is started, and using callbacks from the Alias model to reload routes when an alias is created/updated/destroyed.
Here is the code from my routes.rb:
Alias.find(:all).each do |alias_to_add|
map.connect alias_to_add.name,
:controller => alias_to_add.page_type.controller,
:action => alias_to_add.page_type.action,
:navigation_node_id => alias_to_add.navigation_node.id
end
I am using callbacks in my Alias model as follows:
after_save :rebuild_routes
after_destroy :rebuild_routes
def rebuild_routes
ActionController::Routing::Routes.reload!
end
Is this against Rails best practices? Is there a better solution?

Ben,
I find the method you're already using to be the best. Using Rails 3, you'd have to change the code a bit, to:
MyNewApplication::Application.reload_routes!
That's all.

Quick Solution
Have a catch-all route at the bottom of routes.rb. Implement any alias lookup logic you want in the action that route routes you to.
In my implementation, I have a table which maps defined URLs to a controller, action, and parameter hash. I just pluck them out of the database, then call the appropriate action and then try to render the default template for the action. If the action already rendered something, that throws a DoubleRenderError, which I catch and ignore.
You can extend this technique to be as complicated as you want, although as it gets more complicated it makes more sense to implement it by tweaking either your routes or the Rails default routing logic rather than by essentially reimplementing all the routing logic yourself.
If you don't find an alias, you can throw the 404 or 500 error as you deem appropriate.
Stuff to keep in mind:
Caching: Not knowing your URLs a priori can make page caching an absolute bear. Remember, it caches based on the URI supplied, NOT on the url_for (:action_you_actually_executed). This means that if you alias
/foo_action/bar_method
to
/some-wonderful-alias
you'll get some-wonderful-alias.html living in your cache directory. And when you try to sweep foo's bar, you won't sweep that file unless you specify it explicitly.
Fault Tolerance: Check to make sure someone doesn't accidentally alias over an existing route. You can do this trivially by forcing all aliases into a "directory" which is known to not otherwise be routable (in which case, the alias being textually unique is enough to make sure they never collide), but that isn't a maximally desirable solution for a few of the applications I can think of of this.

First, as other have suggested, create a catch-all route at the bottom of routes.rb:
map.connect ':name', :controller => 'aliases', :action => 'show'
Then, in AliasesController, you can use render_component to render the aliased action:
class AliasesController < ApplicationController
def show
if alias = Alias.find_by_name(params[:name])
render_component(:controller => alias.page_type.controller,
:action => alias.page_type.action,
:navigation_node_id => alias.navigation_node.id)
else
render :file => "#{RAILS_ROOT}/public/404.html", :status => :not_found
end
end
end

I'm not sure I fully understand the question, but you could use method_missing in your controllers and then lookup the alias, maybe like this:
class MyController
def method_missing(sym, *args)
aliased = Alias.find_by_action_name(sym)
# sanity check here in case no alias
self.send( aliased.real_action_name )
# sanity check here in case the real action calls a different render explicitly
render :action => aliased.real_action_name
end
def normal_action
#thing = Things.find(params[:id])
end
end
If you wanted to optimize that, you could put a define_method in the method_missing, so it would only be 'missing' on the first invocation, and would be a normal method from then on.

Related

Rails controller action duplication

I have a controller show action which does some stuff and renders a view but due to some custom routing, I need a totally different action in a totally different controller to perform the same stuff and render the same view.
I don't really wish to duplicate the code. Is there somewhere I can put it and call it from both locations?
Edit:
I basically need to run the following from Collection#Show AND also from Gallery#SplitUrl:
#collection = Collection.find_by_id(params[:id])
if #collection.has_children?
#collections = #collection.children
else
redirect_to [#collection, :albums]
end
I cannot just redirect_to Collection#Show at the end of Gallery#SplitUrl as the redirect causes my custom URL's to be lost as it's a new request.
You could put the view content into a partial (/app/views/home/_example.html.erb) and then use the render "example" command in the HomeController.
The "some stuff" code you talk about could be put into a helper file /app/helpers/... to save you from duplicating code in the two separate controllers although it could depend on what the code is trying to do in the first place.
http://guides.rubyonrails.org/layouts_and_rendering.html
This might provide more information on the subject in general.
I think the simplest approach would be to add a route definition for your new url and map that to your existing controller's action.
Something like follows:
# config/routes.rb
# Existing resource
resources :foos
# The resource in question
resources :bars do
get :show, controller: 'foos', action: 'show', as: bar_foo_common_show
end
Note that this will give you /bar/:id, where id represents a bar resource. So in your foo#show action, your finder needs to be executed on appropriate class. This is when a few lines of hacky codes get added, for e.g. checking for request referer.
I think moving the common code to a function in possibly application_controller and calling that function from both show action would be cleaner approach, but based on my understanding you already have a similar scenario except for the common code in application_controller, and would like to try out a different approach!

How to create a route without an associated View?

I want to create a route such as
get '/referrals/send_invite/:email_address'
I will be calling this route via remote: true and GET. However if I issue the GET request in my browser, the route will still try to find a View, thus leading me to:
Template is missing
Is there a way that I could tell rails that the send_invite method in Referrals Controller doesn't have a view associated?
I would hope that this could be accomplished by just using rails routes.
Thanks.
Not completely sure, but I am guessing you want to start the action send_invite so you are not interested in an actual result, correct?
You could do something like
def send_invite
SomeMailer.mail(:email => params[:email_address])
# or queue it or whatever
head :ok
end
Note that that is not the only option, you could also do something like
render :text => "The mail has been sent to #{params[:email_address]}"
or
render :json => {:result => 'ok', :email_adress => params[:email_address]}
Also note this should be a POST, since this action is not idempotent (a GET should not have side-effects).
Hope this helps.
More likely than not your template missing is coming from not having a sendinvite.js.erb. Try putting a blank one in your view folder to see that sorts it out.

Accessing URL helpers in routes.rb

I would like to redirect a path in routes using the following lines:
get 'privacy_policy', :controller => :pages, :as => 'privacy_policy'
get 'privacypolicy.php' => redirect(privacy_policy_url)
So that /privacypolicy.php gets redirected to the correct page defined right above it.
However, it's throwing the following error:
undefined local variable or method `privacy_policy_url'
So I'm guessing one cannot use URL helpers in routes.rb. Is there a way to use URL helpers in the route file, and is it advisable to do so?
I know I'm a little late here, but this question is one of the top hits when googling "use url_helpers in routes.rb", and I initially found it when I had stumbled upon this problem, so I'd like to share my solution.
As #martinjlowm mentioned in his answer, one cannot use URL helpers when drawing new routes. However, there is one way to define a redirecting route rule using URL helpers. The thing is, ActionDispatch::Routing::Redirection#redirect can take a block (or a #call-able), which is later (when the user hits the route) invoked with two parameters, params and request, to return a new route, a string. And because the routes are properly drawn at that moment, it is completely valid to call URL helpers inside the block!
get 'privacypolicy.php', to: redirect { |_params, _request|
Rails.application.routes.url_helpers.privacy_policy_path
}
Furthermore, we can employ Ruby metaprogramming facilities to add some sugar:
class UrlHelpersRedirector
def self.method_missing(method, *args, **kwargs) # rubocop:disable Style/MethodMissing
new(method, args, kwargs)
end
def initialize(url_helper, args, kwargs)
#url_helper = url_helper
#args = args
#kwargs = kwargs
end
def call(_params, _request)
url_helpers.public_send(#url_helper, *#args, **#kwargs)
end
private
def url_helpers
Rails.application.routes.url_helpers
end
end
# ...
Rails.application.routes.draw do
get 'privacypolicy.php', to: redirect(UrlHelperRedirector.privacy_policy_path)
end
URL Helpers are created from the routes. Therefore they won't be usable when drawing new routes.
You will have to use gayavat's approach.
-- or --
Redirect using the exact URL like http://guides.rubyonrails.org/routing.html does.
edit:
If it's more than just the one '...php' route, you might want to consider making a redirect controller. Take a look here, how to se it up: http://palexander.posterous.com/provide-valid-301-redirects-using-rails-route
Inside your routes file, you should add this at the bottom, so it doesn't interfere with other routes:
get '/:url' => 'redirect#index'
Something like:
get 'privacypolicy.php' => "privacy_policy#show"

Validate no routing overlap when creating new resources in Ruby on Rails

I've got a RESTful setup for the routes in a Rails app using text permalinks as the ID for resources.
In addition, there are a few special named routes as well which overlap with the named resource e.g.:
# bunch of special URLs for one off views to be exposed, not RESTful
map.connect '/products/specials', :controller => 'products', :action => 'specials'
map.connect '/products/new-in-stock', :controller => 'products', :action => 'new_in_stock'
# the real resource where the products are exposed at
map.resources :products
The Product model is using permalink_fu to generate permalinks based on the name, and ProductsController does a lookup on the permalink field when accessing. That all works fine.
However when creating new Product records in the database, I want to validate that the generated permalink does not overlap with a special URL.
If a user tries to create a product named specials or new-in-stock or even a normal Rails RESTful resource method like new or edit, I want the controller to lookup the routing configuration, set errors on the model object, fail validation for the new record, and not save it.
I could hard code a list of known illegal permalink names, but it seems messy to do it that way. I'd prefer to hook into the routing to do it automatically.
(controller and model names changed to protect the innocent and make it easier to answer, the actual setup is more complicated than this example)
Well, this works, but I'm not sure how pretty it is. Main issue is mixing controller/routing logic into the model. Basically, you can add a custom validation on the model to check it. This is using undocumented routing methods, so I'm not sure how stable it'll be going forward. Anyone got better ideas?
class Product < ActiveRecord::Base
#... other logic and stuff here...
validate :generated_permalink_is_not_reserved
def generated_permalink_is_not_reserved
create_unique_permalink # permalink_fu method to set up permalink
#TODO feels really ugly having controller/routing logic in the model. Maybe extract this out and inject it somehow so the model doesn't depend on routing
unless ActionController::Routing::Routes.recognize_path("/products/#{permalink}", :method => :get) == {:controller => 'products', :id => permalink, :action => 'show'}
errors.add(:name, "is reserved")
end
end
end
You can use a route that would not otherwise exist. This way it won't make any difference if someone chooses a reserved word for a title or not.
map.product_view '/product_view/:permalink', :controller => 'products', :action => 'view'
And in your views:
product_view_path(:permalink => #product.permalink)
It's a better practice to manage URIs explicitly yourself for reasons like this, and to avoid accidentally exposing routes you don't want to.

renaming routes (map, link_to, to_param) in rails

I'm having a little issue...I setup a rails application that is to serve a german website. To make use of Rails' internal pluralization features, I kept all my models in english (e.g. the model "JobDescription").
Now, if I call "http://mysite.com/job_descriptions/", I get all my job_descriptions....so far, so good. Because I didn't want the english term "job_descriptions" in my url, I put the following into my routes.rb
map.german_term '/german_term', :controller => 'job_descriptions', :action => 'index'
map.german_term '/german_term/:id', :controller => 'job_descriptions', :action => 'show'
If I call "http://mysite.com/german_term/" or "http://mysite.com/german_term/283" I get all my job_descriptions, which is fine.
However, to make the URL more SEO friendly, I'd like to exchange the id for a more userfriendly slug in the URL. Thus, I put the following in my job_description.rb:
def to_param
"#{id}-#{name.gsub(/[^a-z0-9]+/i, '-')}"
end
which, whenever I use "job_description_path" in any link_to method, renders my URLs out to something like "http://mysite/job_descriptions/13-my-job-description-title".
However, and this is where I'm stuck, I'd like to get "http://mysite/german_term/13-my-job-description-title". I already tried to exchange the "job_description_path" with "german_term_path" in the link_to code, but that only generates "http://mysite/german_term/13". Obviously, to_param isn't called.
One workaround I found is to build the link with:
<%= link_to job_description.name, german_term_path(job_description.to_param) %>
But that's rather tedious to change all the link_to calls in my code. What I want is to replace "job_description" by "german_term" whenever it occurs in a URL.
Any thoughts?!?
Regards,
Sebastian
I think you're going to need to use the restful route helpers to get what you want.
In that case, it wouldn't take much re-factoring (assuming you've mapped JobDescriptions as a resource). Leave your to_param as is and change your JobDescriptions route to something like the following:
map.resources :job_descriptions, :as => 'german_term'
Hope this helps!
Rails only utilizes the
def to_params
end
URL builder when you are using a restful route/link helper. The only way I am aware of is to do it similar to how you did, unless you are willing to just scrap your english language links and do it all in German. In that case, just get rid of the named route lines and change the to_params to use the correct name field from the database. At that point, the REST routes should behave correctly.

Resources