Understanding Routes in Rails - ruby-on-rails

I actually have two questions. I've read the Rails guide and a couple of other articles, but I haven't been able to translate what I read into working routes. I have an application that allows the uploading of images from several different contexts. I'd like the URI to express the proper context so that the following URIs access the same page:
/images/upload
/photos/upload
In this example, I've overridden the new_image_path to use upload for descriptive purposes. I have the override working, but using :as to map images to photos only appears to work one way (with :as => 'photos' in place, the /images routes don't work). Is there a way to make multiple routes point to the same place?
I also have a couple of different ways to upload images/photos/etc. The standard method with a single image per form or a batch method where the user uploads a zip file and that archive is extracted and each of its images is saved.
It seems like the most semantic way to do this is by adding a handler component to the URI (e.g. /images/upload/batch), but I'm not sure how to handle this. The default route path seems pretty general for something that would only be required for images, but I also don't want to be so specific with a named route for the entire bit. What's the best way to do something like this?
Thanks.
Update: Based on jonnii's answer to my first question, I've added the following to my routes.rb file:
map.resources :images, :path_names => { :new => 'upload' }
map.resources :photos, :controller => 'Images', :path_names => { :new => 'upload' }
That seems to do the trick for allowing me to use /images/ and /photos/ interchangeably.

I'm assuming you're doing your photos routes using resources:
map.resources :photos
If that's the case you should be able to define a second resource pointing to the same controller as the photos resource:
map.resources :uploads, :controller => 'PhotosController'
I haven't tested this, but I don't see why something like this wouldn't work..
Question 2:
There are a few different ways you can do batch uploads, I think the best way is to have a separate resource as you're most likely going to have a different UI for it. For example you might do:
map.resources :batch_uploads
This would probably be enough if you were going to take batch uploads as a zip.
An option that's slightly more complicated but takes advantage of the rails niceties (and lets be honest, who doesn't want to take advantage of that??) is something with nested child forms and accepts_nested_attributes_for. This would be useful if you wanted to allow a user to attach more than one image to a form at time.
For example, if your model was something like:
class User < AR:B
has_many :photos
end
You could have a route like:
map.resources :users do |u|
u.resources :photos, :collection => {:get => :new_batch, :post => create_batch}
end
In your new_batch view you would have a form_for #user with a user_form.fields_for :photos. You can add a new form using ajax, whatever and post it all at once.
If you wanted to keep the same semantics as you have now and didn't want to add any more routes you could extend your model to do something different based on the filename of what is being uploaded.
For example if you were using paperclip for attachments you could stop processing the attachment if the filename ends with .zip (this code is not guaranteed to work, I'm doing it from memory):
def is_zip?
attachment.filename.ends_with?('.zip')
end
before_attachment_process do |attachment|
false if is_zip?
end
before_filter :process_bulk_attachment, :if => :is_zip?
def process_bulk_attachment
... extract the zip and save each image in it ...
false
end
The beauty of this is that it's part of the model. You should always aim for fat models and skinny controllers!
I hope this gives you a few ideas and/or points you in the right direction.

I've gotten a little closer to what I'm going for:
map.resources :images, :path_names => { :new => 'upload' }
map.resources :images, :new => { :batch => :get }
The former allows me to use /images/upload instead of /images/new, as shown in jonnii's answer to my first question. The latter allows me to specify a second route to "new" functionality via /images/new/batch which calls ImagesController#batch. I was hoping to be able to use /images/upload/batch, but this may have to do.
Clearly, I still have a long way to go before I really understand routing in Rails. jonnii, if I'm just rehashing part of what you've already said, I apologize. I may have to plead ignorance with respect to much of your answer to question 2.

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.

Implicit creation of helpers - routes.rb and 'match' statements

I am reading Obie Fernandez' "The Rails 3 Way", and there is a bit of it that I am not sure I understand correctly. I am new to rails, and want to make sure I understand it correctly. I have some experience with vanilla Ruby. Not much, but some.
The text in question is as follows: (regarding routing and the config/routes.rb file)
"...
By creating a route like
match 'auctions/:id' => "auction#show", :as => 'auction'
you gain the ability to use nice helper methods in situations like
link_to item.description, auction_path(item.auction)
..."
My question is, specifically what part of match 'auctions/:id' => "auction#show", :as => 'auction' creates the helper functions? (such as link_to auction and auction_path() ) Is it the :as => 'auction' part? Would any helpers be created without appending :as => 'auction'?
My confusion stems from other guides I have seen where this is omitted, and yet helpers seem to be created regardless. What specifically does rails use in match statements in the routes.rb file to create helpers? If it isn't the :as => 'auction' part, then what is the specific purpose of appending this to the match statement?
I know this seems like a super basic question, but this detail seems to get glossed over in the texts I have read thus far. Thanks in advance for any light you can shed on this.
I just tried this:
match "alfa/beta", to: 'users#new'
In this case, even without an :as => 'named_route', I got for free the following helper
alfa_beta_path
which, as expected, points to users#new.
So, it seems that helpers are also automagically generated by parsing the route's string, in case there is no :as specification.
Yes, it is the :as => 'named_route' part that creates the named route (which in turn creates the helpers). As for leaving it off, are you referring to instances of resources :something in routes.rb? The resources method generates a set of URL helpers based on the name of the resource automagically.

Rails routing of a controller's functions query

So I've got a Users controller, and it has (amongst others) a function called details.
The idea is that a user can go to
localhost:3000/user/:user_id/details
and be able to view the details of :user_id.
For example, I have a user called "tester".
When I go to the uri: http://localhost:3000/users/tester/details
I'd want the details function to be called up, to render the details view, and to display the information for the user tester.
But instead I get an error saying that
No action responded to tester. Actions: change_password, create, current_user, details, forgot_password, index, login_required, new, redirect_to_stored, show, and update_attributes
And I understand that to basically mean that if I wanted to access details, I should really be using
http://localhost:3000/users/details
Except that that isn't really working either... >.<
That is instead bringing me to http://localhost:3000/users/details/registries
(which is the default path that I'd stipulated for anybody trying to view users/:user_id, so again, that's working the way I wanted it to)
Point is: Can anybody help and tell me how I can go about getting
users/:user_id/details to work the way I want it to and display the details of :user_id?
Thanks!
Are you using resources? If your routes look like:
map.resources :users
You could make it:
map.resources :users, :member => { :details => :get }
That would allow GET requests for the URL /users/:id/details
More info here: http://guides.rubyonrails.com/routing.html#customizing-resources
I think that your problem is with setting routes in such way, that instead of :user_id you have :login (or whatever) in url /users/tester instead of /users/34. Probably you should take a look at to_param (1st example, 2nd example, 3rd example).
If you want to have another option in routes (besides default REST routes), you can add :member => {:details => :get} if you are using map.resources (#dylanfm answer) or just map it like in #Salil answer.
In order to get routes like "users/:user_id/details" change following in routes.rb
map.users 'users/:user_id/details', :controller => 'users', :action=>'details'

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.

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.

Resources