extended_inherited_resources 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,220 @@
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) responder searches for a template at people/index.xml;
18
+ #
19
+ # 2) if the template is not available, it will invoke :to_xml in the given resource;
20
+ #
21
+ # 3) if the responder does not respond_to :to_xml, call :to_format on it.
22
+ #
23
+ # === Builtin HTTP verb semantics
24
+ #
25
+ # Rails default responder holds semantics for each HTTP verb. Depending on the
26
+ # content type, verb and the resource status, it will behave differently.
27
+ #
28
+ # Using Rails default responder, a POST request for creating an object could
29
+ # be written as:
30
+ #
31
+ # def create
32
+ # @user = User.new(params[:user])
33
+ # flash[:notice] = 'User was successfully created.' if @user.save
34
+ # respond_with(@user)
35
+ # end
36
+ #
37
+ # Which is exactly the same as:
38
+ #
39
+ # def create
40
+ # @user = User.new(params[:user])
41
+ #
42
+ # respond_to do |format|
43
+ # if @user.save
44
+ # flash[:notice] = 'User was successfully created.'
45
+ # format.html { redirect_to(@user) }
46
+ # format.xml { render :xml => @user, :status => :created, :location => @user }
47
+ # else
48
+ # format.html { render :action => "new" }
49
+ # format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
50
+ # end
51
+ # end
52
+ # end
53
+ #
54
+ # The same happens for PUT and DELETE requests.
55
+ #
56
+ # === Nested resources
57
+ #
58
+ # You can given nested resource as you do in form_for and polymorphic_url.
59
+ # Consider the project has many tasks example. The create action for
60
+ # TasksController would be like:
61
+ #
62
+ # def create
63
+ # @project = Project.find(params[:project_id])
64
+ # @task = @project.comments.build(params[:task])
65
+ # flash[:notice] = 'Task was successfully created.' if @task.save
66
+ # respond_with(@project, @task)
67
+ # end
68
+ #
69
+ # Giving an array of resources, you ensure that the responder will redirect to
70
+ # project_task_url instead of task_url.
71
+ #
72
+ # Namespaced and singleton resources requires a symbol to be given, as in
73
+ # polymorphic urls. If a project has one manager which has many tasks, it
74
+ # should be invoked as:
75
+ #
76
+ # respond_with(@project, :manager, @task)
77
+ #
78
+ # Check polymorphic_url documentation for more examples.
79
+ #
80
+ class Responder
81
+ attr_reader :controller, :request, :format, :resource, :resources, :options
82
+
83
+ ACTIONS_FOR_VERBS = {
84
+ :post => :new,
85
+ :put => :edit
86
+ }
87
+
88
+ def initialize(controller, resources, options={})
89
+ @controller = controller
90
+ @request = controller.request
91
+ @format = controller.formats.first
92
+ @resource = resources.is_a?(Array) ? resources.last : resources
93
+ @resources = resources
94
+ @options = options
95
+ @action = options.delete(:action)
96
+ @default_response = options.delete(:default_response)
97
+ end
98
+
99
+ delegate :head, :render, :redirect_to, :to => :controller
100
+ delegate :get?, :post?, :put?, :delete?, :to => :request
101
+
102
+ # Undefine :to_json and :to_yaml since it's defined on Object
103
+ undef_method(:to_json) if method_defined?(:to_json)
104
+ undef_method(:to_yaml) if method_defined?(:to_yaml)
105
+
106
+ # Initializes a new responder an invoke the proper format. If the format is
107
+ # not defined, call to_format.
108
+ #
109
+ def self.call(*args)
110
+ new(*args).respond
111
+ end
112
+
113
+ # Main entry point for responder responsible to dispatch to the proper format.
114
+ #
115
+ def respond
116
+ method = :"to_#{format}"
117
+ respond_to?(method) ? send(method) : to_format
118
+ end
119
+
120
+ # HTML format does not render the resource, it always attempt to render a
121
+ # template.
122
+ #
123
+ def to_html
124
+ default_render
125
+ rescue ActionView::MissingTemplate => e
126
+ navigation_behavior(e)
127
+ end
128
+
129
+ # All others formats follow the procedure below. First we try to render a
130
+ # template, if the template is not available, we verify if the resource
131
+ # responds to :to_format and display it.
132
+ #
133
+ def to_format
134
+ default_render
135
+ rescue ActionView::MissingTemplate => e
136
+ raise unless resourceful?
137
+ api_behavior(e)
138
+ end
139
+
140
+ protected
141
+
142
+ # This is the common behavior for "navigation" requests, like :html, :iphone and so forth.
143
+ def navigation_behavior(error)
144
+ if get?
145
+ raise error
146
+ elsif has_errors? && default_action
147
+ render :action => default_action
148
+ else
149
+ redirect_to resource_location
150
+ end
151
+ end
152
+
153
+ # This is the common behavior for "API" requests, like :xml and :json.
154
+ def api_behavior(error)
155
+ if get?
156
+ display resource
157
+ elsif has_errors?
158
+ display resource.errors, :status => :unprocessable_entity
159
+ elsif post?
160
+ display resource, :status => :created, :location => resource_location
161
+ else
162
+ head :ok
163
+ end
164
+ end
165
+
166
+ # Checks whether the resource responds to the current format or not.
167
+ #
168
+ def resourceful?
169
+ resource.respond_to?(:"to_#{format}")
170
+ end
171
+
172
+ # Returns the resource location by retrieving it from the options or
173
+ # returning the resources array.
174
+ #
175
+ def resource_location
176
+ options[:location] || resources
177
+ end
178
+
179
+ # If a given response block was given, use it, otherwise call render on
180
+ # controller.
181
+ #
182
+ def default_render
183
+ @default_response.call
184
+ end
185
+
186
+ # display is just a shortcut to render a resource with the current format.
187
+ #
188
+ # display @user, :status => :ok
189
+ #
190
+ # For xml request is equivalent to:
191
+ #
192
+ # render :xml => @user, :status => :ok
193
+ #
194
+ # Options sent by the user are also used:
195
+ #
196
+ # respond_with(@user, :status => :created)
197
+ # display(@user, :status => :ok)
198
+ #
199
+ # Results in:
200
+ #
201
+ # render :xml => @user, :status => :created
202
+ #
203
+ def display(resource, given_options={})
204
+ controller.send :render, given_options.merge!(options).merge!(format => resource)
205
+ end
206
+
207
+ # Check if the resource has errors or not.
208
+ #
209
+ def has_errors?
210
+ resource.respond_to?(:errors) && !resource.errors.empty?
211
+ end
212
+
213
+ # By default, render the :edit action for html requests with failure, unless
214
+ # the verb is post.
215
+ #
216
+ def default_action
217
+ @action ||= ACTIONS_FOR_VERBS[request.method]
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,10 @@
1
+ en:
2
+ flash:
3
+ actions:
4
+ create:
5
+ notice: '{{resource_name}} was successfully created.'
6
+ update:
7
+ notice: '{{resource_name}} was successfully updated.'
8
+ destroy:
9
+ notice: '{{resource_name}} was successfully destroyed.'
10
+ alert: '{{resource_name}} could not be destroyed.'
@@ -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,6 @@
1
+ module InheritedResources
2
+ class Responder < ActionController::Responder
3
+ include Responders::FlashResponder
4
+ include Responders::HttpCacheResponder
5
+ end
6
+ end
@@ -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