josevalim-inherited_resources 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/CHANGELOG +4 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +362 -0
  4. data/Rakefile +19 -0
  5. data/init.rb +1 -0
  6. data/lib/inherited_resources.rb +4 -0
  7. data/lib/inherited_resources/base.rb +272 -0
  8. data/lib/inherited_resources/base_helpers.rb +199 -0
  9. data/lib/inherited_resources/belongs_to.rb +227 -0
  10. data/lib/inherited_resources/belongs_to_helpers.rb +89 -0
  11. data/lib/inherited_resources/class_methods.rb +155 -0
  12. data/lib/inherited_resources/polymorphic_helpers.rb +19 -0
  13. data/lib/inherited_resources/respond_to.rb +324 -0
  14. data/lib/inherited_resources/singleton_helpers.rb +53 -0
  15. data/lib/inherited_resources/url_helpers.rb +147 -0
  16. data/test/aliases_test.rb +71 -0
  17. data/test/base_helpers_test.rb +130 -0
  18. data/test/base_test.rb +219 -0
  19. data/test/belongs_to_base_test.rb +268 -0
  20. data/test/belongs_to_test.rb +109 -0
  21. data/test/class_methods_test.rb +73 -0
  22. data/test/fixtures/en.yml +9 -0
  23. data/test/nested_belongs_to_test.rb +138 -0
  24. data/test/polymorphic_base_test.rb +282 -0
  25. data/test/respond_to_test.rb +282 -0
  26. data/test/singleton_base_test.rb +226 -0
  27. data/test/test_helper.rb +37 -0
  28. data/test/url_helpers_test.rb +284 -0
  29. data/test/views/cities/edit.html.erb +1 -0
  30. data/test/views/cities/index.html.erb +1 -0
  31. data/test/views/cities/new.html.erb +1 -0
  32. data/test/views/cities/show.html.erb +1 -0
  33. data/test/views/comments/edit.html.erb +1 -0
  34. data/test/views/comments/index.html.erb +1 -0
  35. data/test/views/comments/new.html.erb +1 -0
  36. data/test/views/comments/show.html.erb +1 -0
  37. data/test/views/employees/edit.html.erb +1 -0
  38. data/test/views/employees/index.html.erb +1 -0
  39. data/test/views/employees/new.html.erb +1 -0
  40. data/test/views/employees/show.html.erb +1 -0
  41. data/test/views/managers/edit.html.erb +1 -0
  42. data/test/views/managers/new.html.erb +1 -0
  43. data/test/views/managers/show.html.erb +1 -0
  44. data/test/views/pets/edit.html.erb +1 -0
  45. data/test/views/professors/edit.html.erb +1 -0
  46. data/test/views/professors/index.html.erb +1 -0
  47. data/test/views/professors/new.html.erb +1 -0
  48. data/test/views/professors/show.html.erb +1 -0
  49. data/test/views/projects/index.html.erb +1 -0
  50. data/test/views/projects/respond_to_with_resource.html.erb +1 -0
  51. data/test/views/students/edit.html.erb +1 -0
  52. data/test/views/students/new.html.erb +1 -0
  53. data/test/views/users/edit.html.erb +1 -0
  54. data/test/views/users/index.html.erb +1 -0
  55. data/test/views/users/new.html.erb +1 -0
  56. data/test/views/users/show.html.erb +1 -0
  57. metadata +108 -0
@@ -0,0 +1,155 @@
1
+ # = singleton
2
+ #
3
+ # Singletons are usually used in associations which are related through has_one
4
+ # and belongs_to. You declare those associations like this:
5
+ #
6
+ # class ManagersController < InheritedResources::Base
7
+ # belongs_to :project, :singleton => true
8
+ # end
9
+ #
10
+ # But in some cases, like an AccountsController, you have a singleton object
11
+ # that is not necessarily associated with another:
12
+ #
13
+ # class AccountsController < InheritedResources::Base
14
+ # defaults :singleton => true
15
+ # end
16
+ #
17
+ # Besides that, you should overwrite the methods :resource and :build_resource
18
+ # to make it work properly:
19
+ #
20
+ # class AccountsController < InheritedResources::Base
21
+ # defaults :singleton => true
22
+ #
23
+ # protected
24
+ # def resource
25
+ # @current_user.account
26
+ # end
27
+ #
28
+ # def build_resource(attributes = {})
29
+ # Account.new(attributes)
30
+ # end
31
+ # end
32
+ #
33
+ # When you have a singleton controller, the action index is removed.
34
+ #
35
+ module InheritedResources #:nodoc:
36
+ RESOURCES_CLASS_ACCESSORS = [ :resource_class, :resources_configuration, :parents_symbols, :singleton, :polymorphic_symbols ]
37
+
38
+ module ClassMethods #:nodoc:
39
+
40
+ protected
41
+
42
+ # When you inherit from InheritedResources::Base, we make some assumptions on
43
+ # what is your resource_class, instance_name and collection_name.
44
+ #
45
+ # You can change those values by calling the class method defaults:
46
+ #
47
+ # class PeopleController < InheritedResources::Base
48
+ # defaults :resource_class => User, :instance_name => 'user', :collection_name => 'users'
49
+ # end
50
+ #
51
+ # You can also provide :class_name, which is the same as :resource_class
52
+ # but accepts string (this is given for ActiveRecord compatibility).
53
+ #
54
+ def defaults(options)
55
+ raise ArgumentError, 'Class method :defaults expects a hash of options.' unless options.is_a? Hash
56
+
57
+ options.symbolize_keys!
58
+ options.assert_valid_keys(:resource_class, :collection_name, :instance_name, :class_name, :singleton)
59
+
60
+ # Checks for special argument :resource_class and :class_name and sets it right away.
61
+ self.resource_class = options.delete(:resource_class) if options[:resource_class]
62
+ self.resource_class = options.delete(:class_name).constantize if options[:class_name]
63
+
64
+ acts_as_singleton! if options.delete(:singleton)
65
+
66
+ options.each do |key, value|
67
+ self.resources_configuration[:self][key] = value.to_sym
68
+ end
69
+
70
+ InheritedResources::UrlHelpers.create_resources_url_helpers!(self)
71
+ end
72
+
73
+ # Defines wich actions to keep from the inherited controller.
74
+ # Syntax is borrowed from resource_controller.
75
+ #
76
+ # actions :index, :show, :edit
77
+ # actions :all, :except => :index
78
+ #
79
+ def actions(*actions_to_keep)
80
+ raise ArgumentError, 'Wrong number of arguments. You have to provide which actions you want to keep.' if actions_to_keep.empty?
81
+
82
+ options = actions_to_keep.extract_options!
83
+ actions_to_keep.map!{ |a| a.to_s }
84
+
85
+ actions_to_remove = Array(options[:except])
86
+ actions_to_remove.map!{ |a| a.to_s }
87
+
88
+ actions_to_remove += RESOURCES_ACTIONS.map{|a| a.to_s } - actions_to_keep unless actions_to_keep.first == 'all'
89
+ actions_to_remove.uniq!
90
+
91
+ # Undefine actions that we don't want
92
+ (instance_methods & actions_to_remove).each do |action|
93
+ undef_method action, "#{action}!"
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ # Defines this controller as singleton.
100
+ # You can call this method to define your controller as singleton.
101
+ #
102
+ def acts_as_singleton!
103
+ unless self.singleton
104
+ self.singleton = true
105
+ include SingletonHelpers
106
+ actions :all, :except => :index
107
+ end
108
+ end
109
+
110
+ # Defines this controller as polymorphic.
111
+ # Do not call this method on your own.
112
+ #
113
+ def acts_as_polymorphic!
114
+ if self.polymorphic_symbols.empty?
115
+ include PolymorphicHelpers
116
+ end
117
+ end
118
+
119
+ # Initialize resources class accessors by creating the accessors
120
+ # and setting their default values.
121
+ #
122
+ def initialize_resources_class_accessors!(base)
123
+ # Add and protect class accessors
124
+ base.class_eval do
125
+ RESOURCES_CLASS_ACCESSORS.each do |cattr|
126
+ cattr_accessor "#{cattr}", :instance_writer => false
127
+
128
+ # Protect instance methods
129
+ self.send :protected, cattr
130
+
131
+ # Protect class writer
132
+ metaclass.send :protected, "#{cattr}="
133
+ end
134
+ end
135
+
136
+ # Initialize resource class
137
+ base.resource_class = base.controller_name.classify.constantize rescue nil
138
+
139
+ # Initialize resources configuration hash
140
+ base.resources_configuration = {}
141
+ config = base.resources_configuration[:self] = {}
142
+ config[:collection_name] = base.controller_name.to_sym
143
+ config[:instance_name] = base.controller_name.singularize.to_sym
144
+
145
+ # Initialize polymorphic, singleton and belongs_to parameters
146
+ base.singleton = false
147
+ base.parents_symbols = []
148
+ base.polymorphic_symbols = []
149
+
150
+ # Create helpers
151
+ InheritedResources::UrlHelpers.create_resources_url_helpers!(base)
152
+ end
153
+
154
+ end
155
+ end
@@ -0,0 +1,19 @@
1
+ module InheritedResources #:nodoc:
2
+ module PolymorphicHelpers #:nodoc:
3
+
4
+ protected
5
+
6
+ def parent_type
7
+ @parent_type
8
+ end
9
+
10
+ def parent_class
11
+ parent_instance.class
12
+ end
13
+
14
+ def parent_instance
15
+ instance_variable_get("@#{@parent_type}")
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,324 @@
1
+ # Provides an extension for Rails respond_to by expading MimeResponds::Responder
2
+ # and adding respond_to class method and respond_with instance method.
3
+ #
4
+ module ActionController #:nodoc:
5
+ class Base #:nodoc:
6
+
7
+ protected
8
+ # Defines respond_to method to store formats that are rendered by default.
9
+ #
10
+ # Examples:
11
+ #
12
+ # respond_to :html, :xml, :json
13
+ #
14
+ # All actions on your controller will respond to :html, :xml and :json.
15
+ # But if you want to specify it based on your actions, you can use only and
16
+ # except:
17
+ #
18
+ # respond_to :html
19
+ # respond_to :xml, :json, :except => [ :edit ]
20
+ #
21
+ # The definition above explicits that all actions respond to :html. And all
22
+ # actions except :edit respond to :xml and :json.
23
+ #
24
+ # You can specify also only parameters:
25
+ #
26
+ # respond_to :rjs, :only => :create
27
+ #
28
+ # Which would be the same as:
29
+ #
30
+ # respond_to :rjs => :create
31
+ #
32
+ def self.respond_to(*formats)
33
+ options = formats.extract_options!
34
+ formats_hash = {}
35
+
36
+ only_actions = Array(options.delete(:only))
37
+ except_actions = Array(options.delete(:except))
38
+
39
+ only_actions.map!{ |a| a.to_sym }
40
+ except_actions.map!{ |a| a.to_sym }
41
+
42
+ formats.each do |format|
43
+ formats_hash[format.to_sym] = {}
44
+ formats_hash[format.to_sym][:only] = only_actions unless only_actions.empty?
45
+ formats_hash[format.to_sym][:except] = except_actions unless except_actions.empty?
46
+ end
47
+
48
+ options.each do |format, actions|
49
+ formats_hash[format.to_sym] = {}
50
+ next if actions == :all || actions == 'all'
51
+
52
+ actions = Array(actions)
53
+ actions.map!{ |a| a.to_sym }
54
+
55
+ formats_hash[format.to_sym][:only] = actions unless actions.empty?
56
+ end
57
+
58
+ write_inheritable_hash(:formats_for_respond_to, formats_hash)
59
+ end
60
+ class_inheritable_reader :formats_for_respond_to
61
+
62
+ # Define defaults respond_to
63
+ respond_to :html
64
+ respond_to :xml, :except => [ :edit ]
65
+
66
+ # Method to clear all respond_to declared until the current controller.
67
+ # This is like freeing the controller from the inheritance chain. :)
68
+ #
69
+ def self.clear_respond_to!
70
+ formats = formats_for_respond_to
71
+ formats.each { |k,v| formats[k] = { :only => [] } }
72
+ write_inheritable_hash(:formats_for_respond_to, formats)
73
+ end
74
+
75
+ # respond_with accepts an object and tries to render a view based in the
76
+ # controller and actions that called respond_with. If the view cannot be
77
+ # found, it will try to call :to_format in the object.
78
+ #
79
+ # class ProjectsController < ApplicationController
80
+ # respond_to :html, :xml
81
+ #
82
+ # def show
83
+ # @project = Project.find(:id)
84
+ # respond_with(@project)
85
+ # end
86
+ # end
87
+ #
88
+ # When the client request a xml, we will check first for projects/show.xml
89
+ # if it can't be found, we will call :to_xml in the object @project. If the
90
+ # object eventually doesn't respond to :to_xml it will render 404.
91
+ #
92
+ # If you want to overwrite the formats specified in the class, you can
93
+ # send your new formats using the options :to.
94
+ #
95
+ # def show
96
+ # @project = Project.find(:id)
97
+ # respond_with(@project, :to => :json)
98
+ # end
99
+ #
100
+ # That means that this action will ONLY reply to json requests.
101
+ #
102
+ # All other options sent will be forwarded to the render method. So you can
103
+ # do:
104
+ #
105
+ # def create
106
+ # # ...
107
+ # if @project.save
108
+ # respond_with(@project, :status => :ok, :location => @project)
109
+ # else
110
+ # respond_with(@project.errors, :status => :unprocessable_entity)
111
+ # end
112
+ # end
113
+ #
114
+ # respond_with does not accept blocks, if you want advanced configurations
115
+ # check respond_to method sending :with => @object as option.
116
+ #
117
+ # Returns true if anything is rendered. Returns false otherwise.
118
+ #
119
+ def respond_with(object, options = {})
120
+ attempt_to_respond = false
121
+
122
+ # You can also send a responder object as parameter.
123
+ #
124
+ responder = options.delete(:responder) || Responder.new(self)
125
+
126
+ # Check for given mime types
127
+ #
128
+ mime_types = Array(options.delete(:to))
129
+ mime_types.map!{ |mime| mime.to_sym }
130
+
131
+ # If :skip_not_acceptable is sent, it will not render :not_acceptable
132
+ # if the mime type sent by the client cannot be found.
133
+ #
134
+ skip_not_acceptable = options.delete(:skip_not_acceptable)
135
+
136
+ for priority in responder.mime_type_priority
137
+ if priority == Mime::ALL && template_exists?
138
+ render options.merge(:action => action_name)
139
+ return true
140
+
141
+ elsif responder.action_respond_to_format?(priority.to_sym, mime_types)
142
+ attempt_to_respond = true
143
+ response.template.template_format = priority.to_sym
144
+ response.content_type = priority.to_s
145
+
146
+ if template_exists?
147
+ render options.merge(:action => action_name)
148
+ return true
149
+ elsif object.respond_to?(:"to_#{priority.to_sym}")
150
+ render options.merge(:text => object.send(:"to_#{priority.to_sym}"))
151
+ return true
152
+ end
153
+ end
154
+ end
155
+
156
+ # If we got here we could not render the object. But if attempted to
157
+ # render (this means, the format sent by the client was valid) we should
158
+ # render a 404.
159
+ #
160
+ # If we even didn't attempt to respond, we respond :not_acceptable
161
+ # unless is told otherwise.
162
+ #
163
+ if attempt_to_respond
164
+ render :text => '404 Not Found', :status => 404
165
+ return true
166
+ elsif !skip_not_acceptable
167
+ head :not_acceptable
168
+ return false
169
+ end
170
+
171
+ return false
172
+ end
173
+
174
+ # Extends respond_to behaviour.
175
+ #
176
+ # You can now pass objects using the options :with.
177
+ #
178
+ # respond_to(:html, :xml, :rjs, :with => @project)
179
+ #
180
+ # If you pass an object and send any block, it's exactly the same as:
181
+ #
182
+ # respond_with(@project, :to => [:html, :xml, :rjs])
183
+ #
184
+ # But the main difference of respond_to and respond_with is that the first
185
+ # allows further customizations:
186
+ #
187
+ # respond_to(:html, :with => @project) do |format|
188
+ # format.xml { render :xml => @project.errors }
189
+ # end
190
+ #
191
+ # It's the same as:
192
+ #
193
+ # 1. When responding to html, execute respond_with(@object).
194
+ # 2. When accessing a xml, execute the block given.
195
+ #
196
+ # Formats defined in blocks have precedence to formats sent as arguments.
197
+ # In other words, if you pass a format as argument and as block, the block
198
+ # will always be executed.
199
+ #
200
+ # And as in respond_with, all extra options sent will be forwarded to
201
+ # the render method:
202
+ #
203
+ # respond_to(:with => @projects.errors, :status => :unprocessable_entity) do |format|
204
+ # format.html { render :template => 'new' }
205
+ # end
206
+ #
207
+ def respond_to(*types, &block)
208
+ options = types.extract_options!
209
+ object = options.delete(:with)
210
+ responder = Responder.new(self)
211
+
212
+ # This is the default respond_to behaviour, when no object is given.
213
+ if object.nil?
214
+ block ||= lambda { |responder| types.each { |type| responder.send(type) } }
215
+ block.call(responder)
216
+ responder.respond
217
+ return true # we are done here
218
+
219
+ else
220
+ # If a block is given, it checks if we can perform the requested format.
221
+ #
222
+ # Even if Mime::ALL is sent by the client, we do not respond_to it now.
223
+ # This is done using calling :respond_to_block instead of :respond.
224
+ #
225
+ # It's worth to remember that responder_to_block does not respond
226
+ # :not_acceptable also.
227
+ #
228
+ if block_given?
229
+ block.call(responder)
230
+ responder.respond_to_block
231
+ return true if responder.responded? || performed?
232
+ end
233
+
234
+ # Let's see if we get lucky rendering with :respond_with.
235
+ # At the end, respond_with checks for Mime::ALL if any template exist.
236
+ #
237
+ # Notice that we are sending the responder (for performance gain) and
238
+ # sending :skip_not_acceptable because we don't want to respond
239
+ # :not_acceptable yet.
240
+ #
241
+ if respond_with(object, options.merge(:to => types, :responder => responder, :skip_not_acceptable => true))
242
+ return true
243
+
244
+ # Since respond_with couldn't help us, our last chance is to reply to
245
+ # any block given if the user send all as mime type.
246
+ #
247
+ elsif block_given?
248
+ return true if responder.respond_to_all
249
+ end
250
+ end
251
+
252
+ # If we get here it means that we could not satisfy our request.
253
+ # Now we finally return :not_acceptable.
254
+ #
255
+ head :not_acceptable
256
+ return false
257
+ end
258
+ end
259
+
260
+ module MimeResponds #:nodoc:
261
+ class Responder #:nodoc:
262
+
263
+ # Create an attr_reader for @mime_type_priority
264
+ attr_reader :mime_type_priority
265
+
266
+ # Stores if this Responder instance called any block.
267
+ def responded?; @responded; end
268
+
269
+ # Similar as respond but if we can't find a valid mime type,
270
+ # we do not send :not_acceptable message as head.
271
+ #
272
+ # It does not respond to Mime::ALL in priority as well.
273
+ #
274
+ def respond_to_block
275
+ for priority in @mime_type_priority
276
+ next if priority == Mime::ALL
277
+
278
+ if @responses[priority]
279
+ @responses[priority].call
280
+ return (@responded = true) # mime type match found, be happy and return
281
+ end
282
+ end
283
+
284
+ if @order.include?(Mime::ALL)
285
+ @responses[Mime::ALL].call
286
+ return (@responded = true)
287
+ else
288
+ return (@responded = false)
289
+ end
290
+ end
291
+
292
+ # Respond to the first format given if Mime::ALL is included in the
293
+ # mime type priorites. This is the behaviour expected when the client
294
+ # sends "*/*" as mime type.
295
+ #
296
+ def respond_to_all
297
+ if @mime_type_priority.include?(Mime::ALL) && first = @responses[@order.first]
298
+ first.call
299
+ return (@responded = true)
300
+ end
301
+ end
302
+
303
+ # Receives an format and checks if the current action responds to
304
+ # the given format. If additional mimes are sent, only them are checked.
305
+ #
306
+ def action_respond_to_format?(format, additional_mimes = [])
307
+ if !additional_mimes.blank?
308
+ additional_mimes.include?(format.to_sym)
309
+ elsif formats = @controller.formats_for_respond_to[format.to_sym]
310
+ if formats[:only]
311
+ formats[:only].include?(@controller.action_name.to_sym)
312
+ elsif formats[:except]
313
+ !formats[:except].include?(@controller.action_name.to_sym)
314
+ else
315
+ true
316
+ end
317
+ else
318
+ false
319
+ end
320
+ end
321
+
322
+ end
323
+ end
324
+ end