views:

278

answers:

3

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)

+1  A: 

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
madlep
This seems like the way to go but I wouldn't worry about having routing/controller logic in the model and trying to separate it out. The fact is that in your system, the model *does* depend on routing - it's simply not a valid model if it clashes with another route.
hopeless
A: 

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)
jdl
Unfortunately that's not an option in this case. The URLs are already set up, and have related SEO value, and other system configuration that depends on them etc, so they can't be changed easily.Everything is working as is, it's just that if a user creates a name that overlaps, that product gets ignored effectively by the system.
madlep
You could also take a page out of the typical "to_param" examples and prepend the ID to the name. You end up with permalinks that look like "01-foo-bar".
jdl
A: 

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.

Wahnfrieden