emmanuel-inherited_resources 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ module ActionController #:nodoc:
2
+ # Responder is responsible to expose a resource for different mime requests,
3
+ # usually depending on the HTTP verb. The responder is triggered when
4
+ # respond_with is called. The simplest case to study is a GET request:
5
+ #
6
+ # class PeopleController < ApplicationController
7
+ # respond_to :html, :xml, :json
8
+ #
9
+ # def index
10
+ # @people = Person.find(:all)
11
+ # respond_with(@people)
12
+ # end
13
+ # end
14
+ #
15
+ # When a request comes, for example with format :xml, three steps happen:
16
+ #
17
+ # 1) respond_with searches for a template at people/index.xml;
18
+ #
19
+ # 2) if the template is not available, it will create a responder, passing
20
+ # the controller and the resource and invoke :to_xml on it;
21
+ #
22
+ # 3) if the responder does not respond_to :to_xml, call to_format on it.
23
+ #
24
+ # === Builtin HTTP verb semantics
25
+ #
26
+ # Rails default responder holds semantics for each HTTP verb. Depending on the
27
+ # content type, verb and the resource status, it will behave differently.
28
+ #
29
+ # Using Rails default responder, a POST request for creating an object could
30
+ # be written as:
31
+ #
32
+ # def create
33
+ # @user = User.new(params[:user])
34
+ # flash[:notice] = 'User was successfully created.' if @user.save
35
+ # respond_with(@user)
36
+ # end
37
+ #
38
+ # Which is exactly the same as:
39
+ #
40
+ # def create
41
+ # @user = User.new(params[:user])
42
+ #
43
+ # respond_to do |format|
44
+ # if @user.save
45
+ # flash[:notice] = 'User was successfully created.'
46
+ # format.html { redirect_to(@user) }
47
+ # format.xml { render :xml => @user, :status => :created, :location => @user }
48
+ # else
49
+ # format.html { render :action => "new" }
50
+ # format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
51
+ # end
52
+ # end
53
+ # end
54
+ #
55
+ # The same happens for PUT and DELETE requests.
56
+ #
57
+ # === Nested resources
58
+ #
59
+ # You can given nested resource as you do in form_for and polymorphic_url.
60
+ # Consider the project has many tasks example. The create action for
61
+ # TasksController would be like:
62
+ #
63
+ # def create
64
+ # @project = Project.find(params[:project_id])
65
+ # @task = @project.comments.build(params[:task])
66
+ # flash[:notice] = 'Task was successfully created.' if @task.save
67
+ # respond_with(@project, @task)
68
+ # end
69
+ #
70
+ # Giving an array of resources, you ensure that the responder will redirect to
71
+ # project_task_url instead of task_url.
72
+ #
73
+ # Namespaced and singleton resources requires a symbol to be given, as in
74
+ # polymorphic urls. If a project has one manager which has many tasks, it
75
+ # should be invoked as:
76
+ #
77
+ # respond_with(@project, :manager, @task)
78
+ #
79
+ # Check polymorphic_url documentation for more examples.
80
+ #
81
+ class Responder
82
+ attr_reader :controller, :request, :format, :resource, :resource_location, :options
83
+
84
+ def initialize(controller, resources, options={})
85
+ @controller = controller
86
+ @request = controller.request
87
+ @format = controller.formats.first
88
+ @resource = resources.is_a?(Array) ? resources.last : resources
89
+ @resource_location = options[:location] || resources
90
+ @options = options
91
+ end
92
+
93
+ delegate :head, :render, :redirect_to, :to => :controller
94
+ delegate :get?, :post?, :put?, :delete?, :to => :request
95
+
96
+ # Undefine :to_json since it's defined on Object
97
+ undef_method :to_json
98
+
99
+ # Initializes a new responder an invoke the proper format. If the format is
100
+ # not defined, call to_format.
101
+ #
102
+ def self.call(*args)
103
+ responder = new(*args)
104
+ method = :"to_#{responder.format}"
105
+ responder.respond_to?(method) ? responder.send(method) : responder.to_format
106
+ end
107
+
108
+ # HTML format does not render the resource, it always attempt to render a
109
+ # template.
110
+ #
111
+ def to_html
112
+ if get?
113
+ render
114
+ elsif has_errors?
115
+ render :action => default_action
116
+ else
117
+ redirect_to resource_location
118
+ end
119
+ end
120
+
121
+ # All others formats try to render the resource given instead. For this
122
+ # purpose a helper called display as a shortcut to render a resource with
123
+ # the current format.
124
+ #
125
+ def to_format
126
+ return render unless resourceful?
127
+
128
+ if get?
129
+ display resource
130
+ elsif has_errors?
131
+ display resource.errors, :status => :unprocessable_entity
132
+ elsif post?
133
+ display resource, :status => :created, :location => resource_location
134
+ else
135
+ head :ok
136
+ end
137
+ end
138
+
139
+ protected
140
+
141
+ # Checks whether the resource responds to the current format or not.
142
+ #
143
+ def resourceful?
144
+ resource.respond_to?(:"to_#{format}")
145
+ end
146
+
147
+ # display is just a shortcut to render a resource with the current format.
148
+ #
149
+ # display @user, :status => :ok
150
+ #
151
+ # For xml request is equivalent to:
152
+ #
153
+ # render :xml => @user, :status => :ok
154
+ #
155
+ # Options sent by the user are also used:
156
+ #
157
+ # respond_with(@user, :status => :created)
158
+ # display(@user, :status => :ok)
159
+ #
160
+ # Results in:
161
+ #
162
+ # render :xml => @user, :status => :created
163
+ #
164
+ def display(resource, given_options={})
165
+ render given_options.merge!(options).merge!(format => resource)
166
+ end
167
+
168
+ # Check if the resource has errors or not.
169
+ #
170
+ def has_errors?
171
+ resource.respond_to?(:errors) && !resource.errors.empty?
172
+ end
173
+
174
+ # By default, render the :edit action for html requests with failure, unless
175
+ # the verb is post.
176
+ #
177
+ def default_action
178
+ request.post? ? :new : :edit
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,155 @@
1
+ module InheritedResources
2
+
3
+ # = polymorphic associations
4
+ #
5
+ # In some cases you have a resource that belongs to two different resources
6
+ # but not at the same time. For example, let's suppose you have File, Message
7
+ # and Task as resources and they are all commentable.
8
+ #
9
+ # Polymorphic associations allows you to create just one controller that will
10
+ # deal with each case.
11
+ #
12
+ # class Comment < InheritedResources::Base
13
+ # belongs_to :file, :message, :task, :polymorphic => true
14
+ # end
15
+ #
16
+ # Your routes should be something like:
17
+ #
18
+ # m.resources :files, :has_many => :comments #=> /files/13/comments
19
+ # m.resources :tasks, :has_many => :comments #=> /tasks/17/comments
20
+ # m.resources :messages, :has_many => :comments #=> /messages/11/comments
21
+ #
22
+ # When using polymorphic associations, you get some free helpers:
23
+ #
24
+ # parent? #=> true
25
+ # parent_type #=> :task
26
+ # parent_class #=> Task
27
+ # parent #=> @task
28
+ #
29
+ # This polymorphic controllers thing is a great idea by James Golick and he
30
+ # built it in resource_controller. Here is just a re-implementation.
31
+ #
32
+ # = optional polymorphic associations
33
+ #
34
+ # Let's take another break from ProjectsController. Let's suppose we are
35
+ # building a store, which sell products.
36
+ #
37
+ # On the website, we can show all products, but also products scoped to
38
+ # categories, brands, users. In this case case, the association is optional, and
39
+ # we deal with it in the following way:
40
+ #
41
+ # class ProductsController < InheritedResources::Base
42
+ # belongs_to :category, :brand, :user, :polymorphic => true, :optional => true
43
+ # end
44
+ #
45
+ # This will handle all those urls properly:
46
+ #
47
+ # /products/1
48
+ # /categories/2/products/5
49
+ # /brands/10/products/3
50
+ # /user/13/products/11
51
+ #
52
+ # = nested polymorphic associations
53
+ #
54
+ # You can have polymorphic associations with nested resources. Let's suppose
55
+ # that our File, Task and Message resources in the previous example belongs to
56
+ # a project.
57
+ #
58
+ # This way we can have:
59
+ #
60
+ # class CommentsController < InheritedResources::Base
61
+ # belongs_to :project {
62
+ # belongs_to :file, :message, :task, :polymorphic => true
63
+ # }
64
+ # end
65
+ #
66
+ # Or:
67
+ #
68
+ # class CommentsController < InheritedResources::Base
69
+ # nested_belongs_to :project
70
+ # nested_belongs_to :file, :message, :task, :polymorphic => true
71
+ # end
72
+ #
73
+ # Choose the syntax that makes more sense to you. :)
74
+ #
75
+ # Finally your routes should be something like:
76
+ #
77
+ # map.resources :projects do |m|
78
+ # m.resources :files, :has_many => :comments #=> /projects/1/files/13/comments
79
+ # m.resources :tasks, :has_many => :comments #=> /projects/1/tasks/17/comments
80
+ # m.resources :messages, :has_many => :comments #=> /projects/1/messages/11/comments
81
+ # end
82
+ #
83
+ # The helpers work in the same way as above.
84
+ #
85
+ module PolymorphicHelpers
86
+
87
+ protected
88
+
89
+ # Returns the parent type. A Comments class can have :task, :file, :note
90
+ # as parent types.
91
+ #
92
+ def parent_type
93
+ @parent_type
94
+ end
95
+
96
+ def parent_class
97
+ parent.class if @parent_type
98
+ end
99
+
100
+ # Returns the parent object. They are also available with the instance
101
+ # variable name: @task, @file, @note...
102
+ #
103
+ def parent
104
+ instance_variable_get("@#{@parent_type}") if @parent_type
105
+ end
106
+
107
+ # If the polymorphic association is optional, we might not have a parent.
108
+ #
109
+ def parent?
110
+ if resources_configuration[:polymorphic][:optional]
111
+ parents_symbols.size > 1 || !@parent_type.nil?
112
+ else
113
+ true
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ # Maps parents_symbols to build association chain.
120
+ #
121
+ # If the parents_symbols find :polymorphic, it goes through the
122
+ # params keys to see which polymorphic parent matches the given params.
123
+ #
124
+ # When optional is given, it does not raise errors if the polymorphic
125
+ # params are missing.
126
+ #
127
+ def symbols_for_association_chain #:nodoc:
128
+ polymorphic_config = resources_configuration[:polymorphic]
129
+
130
+ parents_symbols.map do |symbol|
131
+ if symbol == :polymorphic
132
+ params_keys = params.keys
133
+
134
+ key = polymorphic_config[:symbols].find do |poly|
135
+ params_keys.include? resources_configuration[poly][:param].to_s
136
+ end
137
+
138
+ if key.nil?
139
+ raise ScriptError, "Could not find param for polymorphic association. The request" <<
140
+ "parameters are #{params.keys.inspect} and the polymorphic " <<
141
+ "associations are #{polymorphic_config[:symbols].inspect}." unless polymorphic_config[:optional]
142
+
143
+ nil
144
+ else
145
+ @parent_type = key.to_sym
146
+ end
147
+ else
148
+ symbol
149
+ end
150
+ end.compact
151
+ end
152
+
153
+ end
154
+ end
155
+
@@ -0,0 +1,95 @@
1
+ module InheritedResources
2
+
3
+ # = singleton
4
+ #
5
+ # Singletons are usually used in associations which are related through has_one
6
+ # and belongs_to. You declare those associations like this:
7
+ #
8
+ # class ManagersController < InheritedResources::Base
9
+ # belongs_to :project, :singleton => true
10
+ # end
11
+ #
12
+ # But in some cases, like an AccountsController, you have a singleton object
13
+ # that is not necessarily associated with another:
14
+ #
15
+ # class AccountsController < InheritedResources::Base
16
+ # defaults :singleton => true
17
+ # end
18
+ #
19
+ # Besides that, you should overwrite the methods :resource and :build_resource
20
+ # to make it work properly:
21
+ #
22
+ # class AccountsController < InheritedResources::Base
23
+ # defaults :singleton => true
24
+ #
25
+ # protected
26
+ # def resource
27
+ # @current_user.account
28
+ # end
29
+ #
30
+ # def build_resource(attributes = {})
31
+ # Account.new(attributes)
32
+ # end
33
+ # end
34
+ #
35
+ # When you have a singleton controller, the action index is removed.
36
+ #
37
+ module SingletonHelpers
38
+
39
+ protected
40
+
41
+ # Singleton methods does not deal with collections.
42
+ #
43
+ def collection
44
+ nil
45
+ end
46
+
47
+ # Overwrites how singleton deals with resource.
48
+ #
49
+ # If you are going to overwrite it, you should notice that the
50
+ # end_of_association_chain here is not the same as in default belongs_to.
51
+ #
52
+ # class TasksController < InheritedResources::Base
53
+ # belongs_to :project
54
+ # end
55
+ #
56
+ # In this case, the association chain would be:
57
+ #
58
+ # Project.find(params[:project_id]).tasks
59
+ #
60
+ # So you would just have to call find(:all) at the end of association
61
+ # chain. And this is what happened.
62
+ #
63
+ # In singleton controllers:
64
+ #
65
+ # class ManagersController < InheritedResources::Base
66
+ # belongs_to :project, :singleton => true
67
+ # end
68
+ #
69
+ # The association chain will be:
70
+ #
71
+ # Project.find(params[:project_id])
72
+ #
73
+ # So we have to call manager on it, not find.
74
+ #
75
+ def resource
76
+ get_resource_ivar || set_resource_ivar(end_of_association_chain.send(resource_instance_name))
77
+ end
78
+
79
+ private
80
+
81
+ # Returns the appropriated method to build the resource.
82
+ #
83
+ def method_for_association_build #:nodoc:
84
+ :"build_#{resource_instance_name}"
85
+ end
86
+
87
+ # Sets the method_for_association_chain to nil. See <tt>resource</tt>
88
+ # above for more information.
89
+ #
90
+ def method_for_association_chain #:nodoc:
91
+ nil
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,173 @@
1
+ module InheritedResources
2
+ # = URLHelpers
3
+ #
4
+ # When you use InheritedResources it creates some UrlHelpers for you.
5
+ # And they handle everything for you.
6
+ #
7
+ # # /posts/1/comments
8
+ # resource_url # => /posts/1/comments/#{@comment.to_param}
9
+ # resource_url(comment) # => /posts/1/comments/#{comment.to_param}
10
+ # new_resource_url # => /posts/1/comments/new
11
+ # edit_resource_url # => /posts/1/comments/#{@comment.to_param}/edit
12
+ # collection_url # => /posts/1/comments
13
+ #
14
+ # # /projects/1/tasks
15
+ # resource_url # => /products/1/tasks/#{@task.to_param}
16
+ # resource_url(task) # => /products/1/tasks/#{task.to_param}
17
+ # new_resource_url # => /products/1/tasks/new
18
+ # edit_resource_url # => /products/1/tasks/#{@task.to_param}/edit
19
+ # collection_url # => /products/1/tasks
20
+ #
21
+ # # /users
22
+ # resource_url # => /users/#{@user.to_param}
23
+ # resource_url(user) # => /users/#{user.to_param}
24
+ # new_resource_url # => /users/new
25
+ # edit_resource_url # => /users/#{@user.to_param}/edit
26
+ # collection_url # => /users
27
+ #
28
+ # The nice thing is that those urls are not guessed during runtime. They are
29
+ # all created when you inherit.
30
+ #
31
+ module UrlHelpers
32
+
33
+ # This method hard code url helpers in the class.
34
+ #
35
+ # We are doing this because is cheaper than guessing them when our action
36
+ # is being processed (and even more cheaper when we are using nested
37
+ # resources).
38
+ #
39
+ # When we are using polymorphic associations, those helpers rely on
40
+ # polymorphic_url Rails helper.
41
+ #
42
+ def create_resources_url_helpers!
43
+ resource_segments, resource_ivars = [], []
44
+ resource_config = self.resources_configuration[:self]
45
+
46
+ singleton = self.resources_configuration[:self][:singleton]
47
+ polymorphic = self.parents_symbols.include?(:polymorphic)
48
+
49
+ # Add route_prefix if any.
50
+ unless resource_config[:route_prefix].blank?
51
+ if polymorphic
52
+ resource_ivars << resource_config[:route_prefix].to_s.inspect
53
+ else
54
+ resource_segments << resource_config[:route_prefix]
55
+ end
56
+ end
57
+
58
+ # Deal with belongs_to associations and polymorphic associations.
59
+ # Remember that we don't have to build the segments in polymorphic cases,
60
+ # because the url will be polymorphic_url.
61
+ #
62
+ self.parents_symbols.each do |symbol|
63
+ if symbol == :polymorphic
64
+ resource_ivars << :parent
65
+ else
66
+ config = self.resources_configuration[symbol]
67
+ resource_segments << config[:route_name]
68
+ resource_ivars << :"@#{config[:instance_name]}"
69
+ end
70
+ end
71
+
72
+ collection_ivars = resource_ivars.dup
73
+ collection_segments = resource_segments.dup
74
+
75
+ # This is the default route configuration, later we have to deal with
76
+ # exception from polymorphic and singleton cases.
77
+ #
78
+ collection_segments << resource_config[:route_collection_name]
79
+ resource_segments << resource_config[:route_instance_name]
80
+ resource_ivars << :"@#{resource_config[:instance_name]}"
81
+
82
+ # In singleton cases, we do not send the current element instance variable
83
+ # because the id is not in the URL. For example, we should call:
84
+ #
85
+ # project_manager_url(@project)
86
+ #
87
+ # Instead of:
88
+ #
89
+ # project_manager_url(@project, @manager)
90
+ #
91
+ # Another exception in singleton cases is that collection url does not
92
+ # exist. In such cases, we create the parent collection url. So in the
93
+ # manager case above, the collection url will be:
94
+ #
95
+ # project_url(@project)
96
+ #
97
+ # If the singleton does not have a parent, it will default to root_url.
98
+ #
99
+ # Finally, polymorphic cases we have to give hints to the polymorphic url
100
+ # builder. This works by attaching new ivars as symbols or records.
101
+ #
102
+ if singleton
103
+ collection_segments.pop
104
+ resource_ivars.pop
105
+
106
+ if polymorphic
107
+ resource_ivars << resource_config[:instance_name].inspect
108
+ new_ivars = resource_ivars
109
+ end
110
+ elsif polymorphic
111
+ collection_ivars << '(@_resource_class_new ||= resource_class.new)'
112
+ end
113
+
114
+ generate_url_and_path_helpers nil, :collection, collection_segments, collection_ivars
115
+ generate_url_and_path_helpers :new, :resource, resource_segments, new_ivars || collection_ivars
116
+ generate_url_and_path_helpers nil, :resource, resource_segments, resource_ivars
117
+ generate_url_and_path_helpers :edit, :resource, resource_segments, resource_ivars
118
+ end
119
+
120
+ def generate_url_and_path_helpers(prefix, name, resource_segments, resource_ivars) #:nodoc:
121
+ ivars = resource_ivars.dup
122
+
123
+ singleton = self.resources_configuration[:self][:singleton]
124
+ polymorphic = self.parents_symbols.include?(:polymorphic)
125
+
126
+ # If it's not a singleton, ivars are not empty, not a collection or
127
+ # not a "new" named route, we can pass a resource as argument.
128
+ #
129
+ unless singleton || ivars.empty? || name == :collection || prefix == :new
130
+ ivars.push "(given_args.first || #{ivars.pop})"
131
+ end
132
+
133
+ # In collection in polymorphic cases, allow an argument to be given as a
134
+ # replacemente for the parent.
135
+ #
136
+ if name == :collection && polymorphic
137
+ index = ivars.index(:parent)
138
+ ivars.insert index, "(given_args.first || parent)"
139
+ ivars.delete(:parent)
140
+ end
141
+
142
+ # When polymorphic is true, the segments must be replace by :polymorphic
143
+ # and ivars should be gathered into an array, which is compacted when
144
+ # optional.
145
+ #
146
+ if polymorphic
147
+ segments = :polymorphic
148
+ ivars = "[#{ivars.join(', ')}]"
149
+ ivars << '.compact' if self.resources_configuration[:polymorphic][:optional]
150
+ else
151
+ segments = resource_segments.empty? ? 'root' : resource_segments.join('_')
152
+ ivars = ivars.join(', ')
153
+ end
154
+
155
+ prefix = prefix ? "#{prefix}_" : ''
156
+ ivars << (ivars.empty? ? 'given_options' : ', given_options')
157
+
158
+ class_eval <<-URL_HELPERS, __FILE__, __LINE__
159
+ protected
160
+ def #{prefix}#{name}_path(*given_args)
161
+ given_options = given_args.extract_options!
162
+ #{prefix}#{segments}_path(#{ivars})
163
+ end
164
+
165
+ def #{prefix}#{name}_url(*given_args)
166
+ given_options = given_args.extract_options!
167
+ #{prefix}#{segments}_url(#{ivars})
168
+ end
169
+ URL_HELPERS
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,23 @@
1
+ # respond_to is the only file that should be loaded before hand. All others
2
+ # are loaded on demand.
3
+ #
4
+ unless defined?(ActionController::Responder)
5
+ require File.join(File.dirname(__FILE__), 'inherited_resources', 'legacy', 'responder')
6
+ require File.join(File.dirname(__FILE__), 'inherited_resources', 'legacy', 'respond_to')
7
+ end
8
+
9
+ module InheritedResources
10
+ ACTIONS = [ :index, :show, :new, :edit, :create, :update, :destroy ] unless self.const_defined?(:ACTIONS)
11
+ end
12
+
13
+ class ActionController::Base
14
+ # If you cannot inherit from InheritedResources::Base you can call
15
+ # inherit_resource in your controller to have all the required modules and
16
+ # funcionality included.
17
+ #
18
+ def self.inherit_resources
19
+ InheritedResources::Base.inherit_resources(self)
20
+ initialize_resources_class_accessors!
21
+ create_resources_url_helpers!
22
+ end
23
+ end