josevalim-inherited_resources 0.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.
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