Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/branches/1-2-stable/actionpack/lib/action_controller/resources.rb

Revision 7713, 21.3 kB (checked in by nzkoz, 9 months ago)

Change the resource seperator from ; to / change the generated routes to use the new-style named routes. e.g. new_group_user_path(@group) instead of group_new_user_path(@group). [pixeltrix] Closes #8558

Line 
1 module ActionController
2   module Resources
3     class Resource #:nodoc:
4       attr_reader :collection_methods, :member_methods, :new_methods
5       attr_reader :path_prefix, :new_name_prefix
6       attr_reader :plural, :singular
7       attr_reader :options
8
9       def initialize(entities, options)
10         @plural   = entities
11         @singular = options[:singular] || plural.to_s.singularize
12
13         @options = options
14
15         arrange_actions
16         add_default_actions
17         set_prefixes
18       end
19
20       def controller
21         @controller ||= (options[:controller] || plural).to_s
22       end
23
24       def path
25         @path ||= "#{path_prefix}/#{plural}"
26       end
27
28       def new_path
29         @new_path ||= "#{path}/new"
30       end
31
32       def member_path
33         @member_path ||= "#{path}/:id"
34       end
35
36       def nesting_path_prefix
37         @nesting_path_prefix ||= "#{path}/:#{singular}_id"
38       end
39
40       def deprecate_name_prefix?
41         @name_prefix.blank? && !@new_name_prefix.blank?
42       end
43
44       def name_prefix
45         deprecate_name_prefix? ? @new_name_prefix : @name_prefix
46       end
47
48       def old_name_prefix
49         @name_prefix
50       end
51
52       def nesting_name_prefix
53         "#{new_name_prefix}#{singular}_"
54       end
55
56       def action_separator
57         @action_separator ||= Base.resource_action_separator
58       end
59
60       protected
61         def arrange_actions
62           @collection_methods = arrange_actions_by_methods(options.delete(:collection))
63           @member_methods     = arrange_actions_by_methods(options.delete(:member))
64           @new_methods        = arrange_actions_by_methods(options.delete(:new))
65         end
66
67         def add_default_actions
68           add_default_action(member_methods, :get, :edit)
69           add_default_action(new_methods, :get, :new)
70         end
71
72         def set_prefixes
73           @path_prefix = options.delete(:path_prefix)
74           @name_prefix = options.delete(:name_prefix)
75           @new_name_prefix = options.delete(:new_name_prefix)
76         end
77
78         def arrange_actions_by_methods(actions)
79           (actions || {}).inject({}) do |flipped_hash, (key, value)|
80             (flipped_hash[value] ||= []) << key
81             flipped_hash
82           end
83         end
84
85         def add_default_action(collection, method, action)
86           (collection[method] ||= []).unshift(action)
87         end
88     end
89
90     class SingletonResource < Resource #:nodoc:
91       def initialize(entity, options)
92         @plural = @singular = entity
93         @options = options
94         arrange_actions
95         add_default_actions
96         set_prefixes
97       end
98
99       alias_method :member_path,         :path
100       alias_method :nesting_path_prefix, :path
101     end
102
103     # Creates named routes for implementing verb-oriented controllers. This is
104     # useful for implementing REST API's, where a single resource has different
105     # behavior based on the HTTP verb (method) used to access it.
106     #
107     # Example:
108     #
109     #   map.resources :messages
110     #
111     #   class MessagesController < ActionController::Base
112     #     # GET messages_url
113     #     def index
114     #       # return all messages
115     #     end
116     #
117     #     # GET new_message_url
118     #     def new
119     #       # return an HTML form for describing a new message
120     #     end
121     #
122     #     # POST messages_url
123     #     def create
124     #       # create a new message
125     #     end
126     #
127     #     # GET message_url(:id => 1)
128     #     def show
129     #       # find and return a specific message
130     #     end
131     #
132     #     # GET edit_message_url(:id => 1)
133     #     def edit
134     #       # return an HTML form for editing a specific message
135     #     end
136     #
137     #     # PUT message_url(:id => 1)
138     #     def update
139     #       # find and update a specific message
140     #     end
141     #
142     #     # DELETE message_url(:id => 1)
143     #     def destroy
144     #       # delete a specific message
145     #     end
146     #   end
147     #
148     # The #resources method sets HTTP method restrictions on the routes it generates. For example, making an
149     # HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in
150     # <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for resource routes.
151     #
152     # Along with the routes themselves, #resources generates named routes for use in
153     # controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers:
154     #
155     #   Named Route   Helpers
156     #   messages      messages_url, hash_for_messages_url,
157     #                 messages_path, hash_for_messages_path
158     #   message       message_url(id), hash_for_message_url(id),
159     #                 message_path(id), hash_for_message_path(id)
160     #   new_message   new_message_url, hash_for_new_message_url,
161     #                 new_message_path, hash_for_new_message_path
162     #   edit_message  edit_message_url(id), hash_for_edit_message_url(id),
163     #                 edit_message_path(id), hash_for_edit_message_path(id)
164     #
165     # You can use these helpers instead of #url_for or methods that take #url_for parameters:
166     #
167     #   redirect_to :controller => 'messages', :action => 'index'
168     #   # becomes
169     #   redirect_to messages_url
170     #
171     #   <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %>
172     #   # becomes
173     #   <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically
174     #
175     # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your
176     # form tags. The form helpers make this a little easier. For an update form with a <tt>@message</tt> object:
177     #
178     #   <%= form_tag message_path(@message), :method => :put %>
179     #   
180     # or
181     #   
182     #   <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %>
183     #
184     # The #resources method accepts various options, too, to customize the resulting
185     # routes:
186     # * <tt>:controller</tt> -- specify the controller name for the routes.
187     # * <tt>:singular</tt> -- specify the singular name used in the member routes.
188     # * <tt>:path_prefix</tt> -- set a prefix to the routes with required route variables.
189     #   Weblog comments usually belong to a post, so you might use resources like:
190     #
191     #     map.resources :articles
192     #     map.resources :comments, :path_prefix => '/articles/:article_id'
193     #
194     #   You can nest resources calls to set this automatically:
195     #
196     #     map.resources :articles do |article|
197     #       article.resources :comments
198     #     end
199     #
200     #   The comment resources work the same, but must now include a value for :article_id.
201     #   
202     #     article_comments_url(@article)
203     #     article_comment_url(@article, @comment)
204     #
205     #     article_comments_url(:article_id => @article)
206     #     article_comment_url(:article_id => @article, :id => @comment)
207     #
208     # * <tt>:name_prefix</tt> -- define a prefix for all generated routes, usually ending in an underscore.
209     #   Use this if you have named routes that may clash.
210     #
211     #     map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_'
212     #     map.resources :tags, :path_prefix => '/toys/:toy_id',   :name_prefix => 'toy_'
213     #
214     # * <tt>:collection</tt> -- add named routes for other actions that operate on the collection.
215     #   Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>
216     #   or <tt>:any</tt> if the method does not matter.  These routes map to a URL like /messages/rss, with a route of rss_messages_url.
217     # * <tt>:member</tt> -- same as :collection, but for actions that operate on a specific member.
218     # * <tt>:new</tt> -- same as :collection, but for actions that operate on the new resource action.
219     #
220     # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied.
221     #
222     # Examples:
223     #
224     #   map.resources :messages, :path_prefix => "/thread/:thread_id"
225     #   # --> GET /thread/7/messages/1
226    
227     #   map.resources :messages, :collection => { :rss => :get }
228     #   # --> GET /messages/rss (maps to the #rss action)
229     #   #     also adds a named route called "rss_messages"
230     #
231     #   map.resources :messages, :member => { :mark => :post }
232     #   # --> POST /messages/1/mark (maps to the #mark action)
233     #   #     also adds a named route called "mark_message"
234     #
235     #   map.resources :messages, :new => { :preview => :post }
236     #   # --> POST /messages/new/preview (maps to the #preview action)
237     #   #     also adds a named route called "preview_new_message"
238     #
239     #   map.resources :messages, :new => { :new => :any, :preview => :post }
240     #   # --> POST /messages/new/preview (maps to the #preview action)
241     #   #     also adds a named route called "preview_new_message"
242     #   # --> /messages/new can be invoked via any request method
243     #
244     #   map.resources :messages, :controller => "categories",
245     #         :path_prefix => "/category/:category_id",
246     #         :name_prefix => "category_"
247     #   # --> GET /categories/7/messages/1
248     #   #     has named route "category_message"
249     def resources(*entities, &block)
250       options = entities.last.is_a?(Hash) ? entities.pop : { }
251       entities.each { |entity| map_resource entity, options.dup, &block }
252     end
253
254     # Creates named routes for implementing verb-oriented controllers for a singleton resource.
255     # A singleton resource is global to the current user visiting the application, such as a user's
256     # /account profile.
257     #
258     # See map.resources for general conventions.  These are the main differences:
259     #   - A singular name is given to map.resource.  The default controller name is taken from the singular name.
260     #   - There is no <tt>:collection</tt> option as there is only the singleton resource.
261     #   - There is no <tt>:singular</tt> option as the singular name is passed to map.resource.
262     #   - No default index route is created for the singleton resource controller.
263     #   - When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1')
264     #
265     # Example:
266     #
267     #   map.resource :account
268     #
269     #   class AccountController < ActionController::Base
270     #     # POST account_url
271     #     def create
272     #       # create an account
273     #     end
274     #
275     #     # GET new_account_url
276     #     def new
277     #       # return an HTML form for describing the new account
278     #     end
279     #
280     #     # GET account_url
281     #     def show
282     #       # find and return the account
283     #     end
284     #
285     #     # GET edit_account_url
286     #     def edit
287     #       # return an HTML form for editing the account
288     #     end
289     #
290     #     # PUT account_url
291     #     def update
292     #       # find and update the account
293     #     end
294     #
295     #     # DELETE account_url
296     #     def destroy
297     #       # delete the account
298     #     end
299     #   end
300     #
301     # Along with the routes themselves, #resource generates named routes for use in
302     # controllers and views. <tt>map.resource :account</tt> produces the following named routes and helpers:
303     #
304     #   Named Route   Helpers
305     #   account       account_url, hash_for_account_url,
306     #                 account_path, hash_for_account_path
307     #   edit_account  edit_account_url, hash_for_edit_account_url,
308     #                 edit_account_path, hash_for_edit_account_path
309     def resource(*entities, &block)
310       options = entities.last.is_a?(Hash) ? entities.pop : { }
311       entities.each { |entity| map_singleton_resource entity, options.dup, &block }
312     end
313
314     private
315       def map_resource(entities, options = {}, &block)
316         resource = Resource.new(entities, options)
317
318         with_options :controller => resource.controller do |map|
319           map_collection_actions(map, resource)
320           map_default_collection_actions(map, resource)
321           map_new_actions(map, resource)
322           map_member_actions(map, resource)
323
324           if block_given?
325             with_options(:path_prefix => resource.nesting_path_prefix, :new_name_prefix => resource.nesting_name_prefix, &block)
326           end
327         end
328       end
329
330       def map_singleton_resource(entities, options = {}, &block)
331         resource = SingletonResource.new(entities, options)
332
333         with_options :controller => resource.controller do |map|
334           map_collection_actions(map, resource)
335           map_default_singleton_actions(map, resource)
336           map_new_actions(map, resource)
337           map_member_actions(map, resource)
338
339           if block_given?
340             with_options(:path_prefix => resource.nesting_path_prefix, :new_name_prefix => resource.nesting_name_prefix, &block)
341           end
342         end
343       end
344
345       def map_collection_actions(map, resource)
346         resource.collection_methods.each do |method, actions|
347           actions.each do |action|
348             action_options = action_options_for(action, resource, method)
349
350             unless resource.old_name_prefix.blank?
351               map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.old_name_prefix}#{action}_#{resource.plural}")
352               map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "formatted_#{resource.old_name_prefix}#{action}_#{resource.plural}")
353             end
354
355             if resource.deprecate_name_prefix?
356               map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{action}_#{resource.plural}")
357               map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "formatted_#{action}_#{resource.plural}")
358             end
359
360             map.named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options)
361             map.connect("#{resource.path};#{action}", action_options)
362             map.connect("#{resource.path}.:format;#{action}", action_options)
363             map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}.:format", action_options)
364           end
365         end
366       end
367
368       def map_default_collection_actions(map, resource)
369         index_action_options = action_options_for("index", resource)
370         map.named_route("#{resource.name_prefix}#{resource.plural}", resource.path, index_action_options)
371         map.named_route("formatted_#{resource.name_prefix}#{resource.plural}", "#{resource.path}.:format", index_action_options)
372
373         if resource.deprecate_name_prefix?
374           map.deprecated_named_route("#{resource.name_prefix}#{resource.plural}", "#{resource.plural}")
375           map.deprecated_named_route("formatted_#{resource.name_prefix}#{resource.plural}", "formatted_#{resource.plural}")
376         end
377
378         create_action_options = action_options_for("create", resource)
379         map.connect(resource.path, create_action_options)
380         map.connect("#{resource.path}.:format", create_action_options)
381       end
382
383       def map_default_singleton_actions(map, resource)
384         create_action_options = action_options_for("create", resource)
385         map.connect(resource.path, create_action_options)
386         map.connect("#{resource.path}.:format", create_action_options)
387       end
388
389       def map_new_actions(map, resource)
390         resource.new_methods.each do |method, actions|
391           actions.each do |action|
392             action_options = action_options_for(action, resource, method)
393             if action == :new
394
395               unless resource.old_name_prefix.blank?
396                 map.deprecated_named_route("new_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}new_#{resource.singular}")
397                 map.deprecated_named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}new_#{resource.singular}")
398               end
399
400               if resource.deprecate_name_prefix?
401                 map.deprecated_named_route("new_#{resource.name_prefix}#{resource.singular}", "new_#{resource.singular}")
402                 map.deprecated_named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "formatted_new_#{resource.singular}")
403               end
404
405               map.named_route("new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options)
406               map.named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}.:format", action_options)
407
408             else
409
410               unless resource.old_name_prefix.blank?
411                 map.deprecated_named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}#{action}_new_#{resource.singular}")
412                 map.deprecated_named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}#{action}_new_#{resource.singular}")
413               end
414
415               if resource.deprecate_name_prefix?
416                 map.deprecated_named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{action}_new_#{resource.singular}")
417                 map.deprecated_named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{action}_new_#{resource.singular}")
418               end
419
420               map.named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options)
421               map.connect("#{resource.new_path};#{action}", action_options)
422               map.connect("#{resource.new_path}.:format;#{action}", action_options)
423               map.named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}.:format", action_options)
424
425             end
426           end
427         end
428       end
429
430       def map_member_actions(map, resource)
431         resource.member_methods.each do |method, actions|
432           actions.each do |action|
433             action_options = action_options_for(action, resource, method)
434
435             unless resource.old_name_prefix.blank?
436               map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}#{action}_#{resource.singular}")
437               map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}#{action}_#{resource.singular}")
438             end
439
440             if resource.deprecate_name_prefix?
441               map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{action}_#{resource.singular}")
442               map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "formatted_#{action}_#{resource.singular}")
443             end
444
445             map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}", action_options)
446             map.connect("#{resource.member_path};#{action}", action_options)
447             map.connect("#{resource.member_path}.:format;#{action}", action_options)
448             map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}.:format", action_options)
449
450           end
451         end
452
453         show_action_options = action_options_for("show", resource)
454         map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options)
455         map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options)
456
457         if resource.deprecate_name_prefix?
458           map.deprecated_named_route("#{resource.name_prefix}#{resource.singular}", "#{resource.singular}")
459           map.deprecated_named_route("formatted_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.singular}")
460         end
461
462         update_action_options = action_options_for("update", resource)
463         map.connect(resource.member_path, update_action_options)
464         map.connect("#{resource.member_path}.:format", update_action_options)
465
466         destroy_action_options = action_options_for("destroy", resource)
467         map.connect(resource.member_path, destroy_action_options)
468         map.connect("#{resource.member_path}.:format", destroy_action_options)
469       end
470
471       def conditions_for(method)
472         { :conditions => method == :any ? {} : { :method => method } }
473       end
474
475       def action_options_for(action, resource, method = nil)
476         default_options = { :action => action.to_s }
477         require_id = resource.kind_of?(SingletonResource) ? {} : { :requirements => { :id => Regexp.new("[^#{Routing::SEPARATORS.join}]+") } }
478         case default_options[:action]
479           when "index", "new" : default_options.merge(conditions_for(method || :get))
480           when "create"       : default_options.merge(conditions_for(method || :post))
481           when "show", "edit" : default_options.merge(conditions_for(method || :get)).merge(require_id)
482           when "update"       : default_options.merge(conditions_for(method || :put)).merge(require_id)
483           when "destroy"      : default_options.merge(conditions_for(method || :delete)).merge(require_id)
484           else                  default_options.merge(conditions_for(method))
485         end
486       end
487   end
488 end
489
490 ActionController::Routing::RouteSet::Mapper.send :include, ActionController::Resources
Note: See TracBrowser for help on using the browser.