Validate no routing overlap when creating new resources in Ruby on Rails - 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.

Related

Rails "pretty URLs", using entries/23 or 2011/07/some-post-slug-here for creating URLs via helpers

I'm attempting to create "pretty URLs" for linking to posts in a blog. I want to maintain access to the blog entries via entries/23 and 2011/07/some-post-slug-here as I only generate a slug once an entry has been published (just in case the title of the posts changes, and, though not a strict requirement, I would prefer to be able to edit/delete posts via the entries/23 style URL. Currently, the appropriate part of what I have in my config/routes.rb:
root :to => 'entries#index'
resources :entries
match ':year/:month/:slug' => 'entries#show', :constraints => {
:year => /[0-9][0-9][0-9][0-9]/,
:month => /[0-9][0-9]/,
:slug => /[a-zA-Z0-9\-]+/
}, :as => :vanity_entry
and I use this (in my application helper) function for creating the links:
def appropriate_entry_path entry
if entry.published
vanity_entry_path entry.published_on.year.to_s, entry.published_on.month.to_s, entry.slug
else
entries_path entry
end
end
def appropriate_entry_url entry
if entry.published
vanity_entry_url entry.published_on.year.to_s, entry.published_on.month.to_s, entry.slug
else
entries_url entry
end
end
Basically, I check if the article is published (and therefore has a slug) and then use that URL/path helper, or otherwise use the default one.
However, when trying to use this, I get the following from Rails:
No route matches {:slug=>"try-this-one-on-for", :month=>"7", :controller=>"entries", :year=>"2011", :action=>"show"}
I have tried a few different solutions, including overriding to_param in my Entry model, however then I would have to create match routes for the edit/delete actions, and I would like to keep my routes.rb file as clean as possible. Ideally, I would also love to lose the appropriate_entry_path/appropriate_entry_url helper methods, but I'm not sure that this is possible?
Is there any thing I am missing regarding routing that might make this easier and/or is there any specific way of doing this that is the cleanest?
Thanks in advance.
You might want to take a look at friendly_id. It's a gem for creating seo friendly slugs :)
I found the issue with what I had been doing, the regex for :month in the route wanted two numbers, whereas I was only passing in one number. Anyways, I decided that the URLs look nicer (in my opinion) without the month padded to 2 digits, so I updated my route accordingly.

Rails: Difference between List and Index

I appreciate this is an incredibly noob question, but having Googled multiple combinations of search terms I regretfully am still in the dark. These things can be difficult when one doesn't know and so obvious when one does.
I have semi-home page where incoming customers can choose to see a queried list or do a handful of other things. This isn't a home page but a sort of mini 'switchboard' within the site.
The seven standard RESTful Rails controller methods are (as I understand them):
List # shows a list of records generated with .find(:all)
Show # shows details on one record
New # initiates new record
Create # saves and renders/redirects
Edit # finds and opens existing record for edit
Update # updates attributes
Delete # deletes record
What to use when some users need to see a selected 'list' of records that isn't literally .find(:all)? How would this work given I still need a list function that gives me .find(:all) for other purposes?
I've heard of 'index' being used in Rails controllers, but I don't know the difference between index and list.
For best practice and best design, what controller methods would you use for a mini-switchboard (and other intermediate pages such as 'About Us')?
Any specific answers would be a bit more useful than links to http://guides.rubyonrails.org/action_controller_overview.html etc. :) Thanks very much.
First, I think it's important to note that the "standard methods" are neither standard nor methods in a sense. These are considered actions, and are only standard in that they are the conventions used with scaffolding. You can create any number of actions and group them logically with a controller.
If you open up [Project]/config/routes.rb and read through the comments, I think you'll understand a little better how controllers and actions map to a specific route. For instance, you can create a named route to the login controller's login action and call it authenticate by adding to the top of your routes.rb:
# ex: http://localhost/authenticate
map.authenticate 'authenticate', :controller => 'login', :action => 'login'
# map.[route] '[pattern]', :controller => '[controller]', :action => '[action]'
# ex: http://localhost/category/1
map.category 'category/:id', :controller => 'categories', :action => 'list'
# ex: http://localhost/product_group/electronics
map.search 'product_group/:search', :controller => 'products', :action => 'list'
To partially answer your question, you may want to consider adding a category model and associating all products to a category. then, you can add a named route to view items by category as in the code block above.
The major benefit to using named routes is that you can call them in your views as category_url or category_path. Most people don't want to do this and rely on the default route mappings (at the end of the routes.rb):
# ex: http://localhost/category/view/1
map.connect ':controller/:action/:id'
# ex: http://localhost/category/view/1.xml
# ex: http://localhost/category/view/1.json
map.connect ':controller/:action/:id.:format'
The key thing to mention here is that when a URI matches a route, the parameter that matches against a symbol (:id, or :search) is passed into the params hash. For instance, the search named route above would match a search term into params[:search], so if your products have a string column called 'type' that you plan to search against, your products controller might look like:
class Products < ApplicationController
def list
search_term = params[:search]
#products = Product.find(:all, :conditions => ["type = ?", search_term])
end
end
Then, the view [Project]/app/views/products/list.html.erb will have direct access to #products
If you'd really like an in-depth view into Ruby on Rails (one that is probably 10 times easier to follow than the guide in the link that you posted) you should check out
Agile Web Development with Rails: Second Edition, 2nd Edition

Using named routes vs. using url_for()

When should one use named routes vs. using url_for with a {:controller => "somecontroller", :action => "someaction"} hash?
Is one preferred over the other and why? Is one more maintainable or more efficient w.r.t. performance?
It might help to understand what named routes are doing.
Defining a Named route creates a wrapper around url_for providing the options required for the created route. Routing resources creates many named routes.
With that in mind, the overhead of calling a named route as opposed to url_for with the options needed is negligible. So if you're linking to a specific resource, named routes are the way to go. They're easier to read, type and maintain.
However, don't discount url_for. It has many creative uses thanks to the way it handles missing options. It is very useful when it comes to keeping views DRY that are used from multiple nested sources. Ie: when you have a blog_posts controller and posts_controller sharing the same views.
I strongly encourage you to read the url_for documentation. To help figure out where those places it makes sense to use url_for are.
I would prefer named routes as it's shorter and does the same thing.
named route is very neat.
map.with_options :controller => "company", :action => "show", :panel => "name" do |m|
m.company '/company/:action/:id/:panel'
end
Then you can call
company_url :id => 1
If you set up your routes and resources carefully, you shouldn't need any hash routes, only named ones (either built-in via map.resource or custom map.<something> ). Hash routes are useful, if you have to create links based on dynamic content. Something like:
link_to #post.title, :controller => (#user.is_admin ? 'admin/posts' : 'public/posts'), :action => 'show', :id => #post
(This is just a forced example, but you should get the gist of it :)

Rails - RESTful Routing - Add a POST for Member i.e(tips/6)

I'm trying to create some nice RESTful structure for my app in rails but now I'm stuck on a conception that unfortunately I'm not sure if its correct, but if someone could help me on this it would be very well appreciated.
If noticed that for RESTful routes we have (the uncommented ones)
collection
:index => 'GET'
:create => 'POST'
#:? => 'PUT'
#:? => 'DELETE'
member
:show => 'GET'
#:? => 'POST'
:update => 'PUT'
:destroy => 'DELETE'
in this case I'm only talking about base level action or the ones that occur directly inside i.e http://domain.com/screename/tips or http://domain.com/screename/tips/16
but at the same time I notice that there's no POST possibility for the members, anybody knows why?
What if I'm trying to create a self contained item that clones itself with another onwer?
I'm almost sure that this would be nicely generated by a POST method inside the member action, but unfortunately it looks like that there's no default methods on the map.resources on rails for this.
I tried something using :member, or :new but it doesn't work like this
map.resources :tips, :path_prefix => ':user', :member => {:add => :post}
so this would be accessed inside http://domain.com/screename/tips/16/add and not http://domain.com/screename/tips/16.
So how would it be possible to create a "default" POST method for the member in a RESTful route?
I was thinking that maybe this isn't in there because it's not part of REST declaration, but as a quick search over it I found:
POST
for collections :
Create a new entry in the collection where the ID is assigned automatically by the collection. The ID created is usually included as part of the data returned by this operation.
for members :
Treats the addressed member as a collection in its own right and creates a new subordinate of it.
So this concept still the same if you think about the DELETE method or PUT for the collection. What if I want to delete all the collection instead just one member? or even replace them(PUT)?
So how could I create this specific methods that seems to be missing on map.resources?
That's it, I hope its easy to understand.
Cheers
The reason they aren't included by is that they're dangerous unless until secured. Member POST not so much as collection PUT/DELETE. The missing member POST is more a case of being made redundant by the default collection POST action.
If you still really want to add these extra default actions, the only way you're going to be able to do it, is it to rewrite bits of ActionController::Resources.
However this is not hard to do. Really you only need to rewrite two methods. Even then you don't have to rewrite them fully. The methods bits that you'll need to add to those methods don't really on complex processing of the arguments to achieve your goal. So you can get by with a simple pair of alias_method_chain.
Assuming I haven't made any errors, including the following will create your extra default routes as described below. But do so at your own risk.
module ActionController
module Resources
def map_member_actions_with_extra_restfulness(map, resource)
map_member_actions_without_extra_restfulness(map, resource)
route_path = "#{resource.shallow_name_prefix}#{resource.singular}"
map_resource_routes(map, resource, :clone, resource.member_path, route_path, :post, :force_id => true)
end
alias_method_chain :map_member_actions, :extra_restfulness
def map_default_collection_actions_with_extra_restfullness(map, resource)
map_default_collection_actions_without_extra_restfullness(map,resource
index_route_name = "#{resource.name_prefix}#{resource.plural}"
if resource.uncountable?
index_route_name << "_index"
end
map_resource_routes(map, resource, :rip, resource.path, index_route_name, :put)
map_resource_routes(map, resource, :genocide, resource.path, index_route_name, :delete)
end
alias_method_chain :map_default_collection_actions, :extra_restfulness
end
end
You'll have to mess around with generators to ensure that script/generate resource x will create meaningful methods for these new actions.
Now that we've covered the practical part, lets talk about the theory. Part of the problem is coming up with words to describe the missing actions:
The member action described for POST in the question, although technically correct does not hold up when applied to ActionController and the underlying ActiveRecord. At best it is ambiguous, at, worst it's not possible. It makes sense for resources with a recursive nature (like trees,) or resources that have many of a different kind of resource. however this second case is ambiguous and already covered by Rails. Instead I chose clone for the collection POST. It made the most sense for default post on an existing record. Here are the rest of the default actions I decided on:
collection
:index => 'GET'
:create => 'POST'
:rip => 'PUT'
:genocide => 'DELETE'
member
:show => 'GET'
:clone => 'POST'
:update => 'PUT'
:destroy => 'DELETE'
I chose genocide for collection DELETE because it just sounded right. I chose rip for the collection PUT because that was the term a company I used to work for would describe the act of a customer replacing all of one vendor's gear with another's.
I'm not quite following, but to answer your last question there, you can add collection routes for update_multiple or destroy_multiple if you want to update or delete multiple records, rather than a single record one at a time.
I answered that question earlier today actually, you can find that here.
The reason that there's no POST to a particular member is because that member record already exists in the database, so the only thing you can do to it is GET (look at), PUT (update), or DELETE (destroy). POST is designed only for creating new records.
If you were trying to duplicate an existing member, you would want to GET the original member in a "duplicate" member action and POST to the resource root with its contents.
Please let me know if I'm missing what you're asking.

Is it a bad idea to reload routes dynamically in 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.

Resources