extended_inherited_resources 0.1.0

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,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