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,199 @@
1
+ module InheritedResources #:nodoc:
2
+ module BaseHelpers #:nodoc:
3
+
4
+ # Protected helpers. You might want to overwrite some of them.
5
+ protected
6
+ # This is how the collection is loaded.
7
+ #
8
+ # You might want to overwrite this method if you want to add pagination
9
+ # for example. When you do that, don't forget to cache the result in an
10
+ # instance_variable:
11
+ #
12
+ # def collection
13
+ # @projects ||= end_of_association_chain.paginate(params[:page]).all
14
+ # end
15
+ #
16
+ def collection
17
+ get_collection_ivar || set_collection_ivar(end_of_association_chain.find(:all))
18
+ end
19
+
20
+ # This is how the resource is loaded.
21
+ #
22
+ # You might want to overwrite this method when you are using permalink.
23
+ # When you do that, don't forget to cache the result in an
24
+ # instance_variable:
25
+ #
26
+ # def resource
27
+ # @project ||= end_of_association_chain.find_by_permalink!(params[:id])
28
+ # end
29
+ #
30
+ # You also might want to add the exclamation mark at the end of the method
31
+ # because it will raise a 404 if nothing can be found. Otherwise it will
32
+ # probably render a 500 error message.
33
+ #
34
+ def resource
35
+ get_resource_ivar || set_resource_ivar(end_of_association_chain.find(params[:id]))
36
+ end
37
+
38
+ # This method is responsable for building the object on :new and :create
39
+ # methods. You probably won't need to change it. Again, if you overwrite
40
+ # don't forget to cache the result in an instance_variable.
41
+ #
42
+ def build_resource(attributes = {})
43
+ get_resource_ivar || set_resource_ivar(end_of_association_chain.send(method_for_build, attributes))
44
+ end
45
+
46
+ # This class allows you to set a instance variable to begin your
47
+ # association chain. For example, usually your projects belongs to users
48
+ # and that means that they belong to the current logged in user. So you
49
+ # could do this:
50
+ #
51
+ # def begin_of_association_chain
52
+ # @current_user
53
+ # end
54
+ #
55
+ # So every time we instantiate a project, we will do:
56
+ #
57
+ # @current_user.projects.build(params[:project])
58
+ # @current_user.projects.find(params[:id])
59
+ #
60
+ # The variable set in begin_of_association_chain is not sent when building
61
+ # urls, so this is never going to happen:
62
+ #
63
+ # project_url(@current_user, @project)
64
+ #
65
+ # If the user actually scopes the url, you should user belongs_to method
66
+ # and declare that projects belong to user.
67
+ #
68
+ def begin_of_association_chain
69
+ nil
70
+ end
71
+
72
+ # Private helpers, you probably don't have to worry with them.
73
+ private
74
+
75
+ # Fast accessor to resource_collection_name
76
+ #
77
+ def resource_collection_name
78
+ resources_configuration[:self][:collection_name]
79
+ end
80
+
81
+ # Fast accessor to resource_instance_name
82
+ #
83
+ def resource_instance_name
84
+ resources_configuration[:self][:instance_name]
85
+ end
86
+
87
+ # Returns if the object has a parent. This means, if it has an object
88
+ # set at begin_of_association_chain is not nil.
89
+ #
90
+ def parent?
91
+ !begin_of_association_chain.nil?
92
+ end
93
+
94
+ # This methods gets your begin_of_association_chain and returns the
95
+ # scoped association.
96
+ #
97
+ def end_of_association_chain
98
+ if parent?
99
+ begin_of_association_chain.send(resource_collection_name)
100
+ else
101
+ resource_class
102
+ end
103
+ end
104
+
105
+ # Returns the appropriated method to build the resource.
106
+ #
107
+ def method_for_build
108
+ parent? ? :build : :new
109
+ end
110
+
111
+ # Get resource ivar based on the current resource controller.
112
+ #
113
+ def get_resource_ivar
114
+ instance_variable_get("@#{resource_instance_name}")
115
+ end
116
+
117
+ # Set resource ivar based on the current resource controller.
118
+ #
119
+ def set_resource_ivar(resource)
120
+ instance_variable_set("@#{resource_instance_name}", resource)
121
+ end
122
+
123
+ # Get collection ivar based on the current resource controller.
124
+ #
125
+ def get_collection_ivar
126
+ instance_variable_get("@#{resource_collection_name}")
127
+ end
128
+
129
+ # Set collection ivar based on the current resource controller.
130
+ #
131
+ def set_collection_ivar(collection)
132
+ instance_variable_set("@#{resource_collection_name}", collection)
133
+ end
134
+
135
+ # Helper to set flash messages. It's powered by I18n API.
136
+ # It checks for messages in the following order:
137
+ #
138
+ # flash.controller_name.action_name.status
139
+ # flash.actions.action_name.status
140
+ #
141
+ # If none is available, a default message is set. So, if you have
142
+ # a CarsController, create action, it will check for:
143
+ #
144
+ # flash.cars.create.status
145
+ # flash.actions.create.status
146
+ #
147
+ # The statuses can be :notice (when the object can be created, updated
148
+ # or destroyed with success) or :error (when the objecy cannot be created
149
+ # or updated).
150
+ #
151
+ # Those messages are interpolated by using the resource class human name.
152
+ # This means you can set:
153
+ #
154
+ # flash:
155
+ # actions:
156
+ # create:
157
+ # notice: "Hooray! {{resource}} was successfully created!"
158
+ #
159
+ # But sometimes, flash messages are not that simple. Going back
160
+ # to cars example, you might want to say the brand of the car when it's
161
+ # updated. Well, that's easy also:
162
+ #
163
+ # flash:
164
+ # cars:
165
+ # update:
166
+ # notice: "Hooray! You just tuned your {{car_brand}}!"
167
+ #
168
+ # Since :car_name is not available for interpolation by default, you have
169
+ # to overwrite interpolation_options.
170
+ #
171
+ # def interpolation_options
172
+ # { :car_brand => @car.brand }
173
+ # end
174
+ #
175
+ # Then you will finally have:
176
+ #
177
+ # 'Hooray! You just tuned your Aston Martin!'
178
+ #
179
+ def set_flash_message!(status, default_message = '')
180
+ options = {
181
+ :default => [ :"flash.actions.#{action_name}.#{status}", default_message ],
182
+ :resource => resource_class.human_name
183
+ }.merge(interpolation_options)
184
+
185
+ message = I18n.t "flash.#{controller_name}.#{action_name}.#{status}", options
186
+
187
+ flash[status] = message unless message.blank?
188
+ end
189
+
190
+ # Overwrite this method to provide other interpolation options when
191
+ # the flash message is going to be set. Check set_flash_message! for
192
+ # more information.
193
+ #
194
+ def interpolation_options
195
+ { }
196
+ end
197
+
198
+ end
199
+ end
@@ -0,0 +1,227 @@
1
+ # = belongs_to
2
+ #
3
+ # This allows you to specify to belongs_to in your controller. You might use
4
+ # this when you are having nested resources in your routes:
5
+ #
6
+ # class TasksController < InheritedResources::Base
7
+ # belongs_to :project
8
+ # end
9
+ #
10
+ # This will do all magic assuming some defaults. It assumes that your URL to
11
+ # access those tasks are:
12
+ #
13
+ # /projects/:project_id/tasks
14
+ #
15
+ # But all defaults are configurable. The options are:
16
+ #
17
+ # * :parent_class => Allows you to specify what is the parent class.
18
+ #
19
+ # belongs_to :project, :parent_class => AdminProject
20
+ #
21
+ # * :class_name => Also allows you to specify the parent class, but you should
22
+ # give a string. Added for ActiveRecord belongs to compatibility.
23
+ #
24
+ # * :instance_name => How this object will appear in your views. In this case
25
+ # the default is @project. Overwrite it with a symbol.
26
+ #
27
+ # belongs_to :project, :instance_name => :my_project
28
+ #
29
+ # * :finder => Specifies which method should be called to instantiate the
30
+ # parent. Let's suppose you are using slugs ("this-is-project-title") in URLs
31
+ # so your tasks url would be: "projects/this-is-project-title/tasks". Then you
32
+ # should do this in your TasksController:
33
+ #
34
+ # belongs_to :project, :finder => :find_by_title!
35
+ #
36
+ # This will make your projects be instantiated as:
37
+ #
38
+ # Project.find_by_title!(params[:project_id])
39
+ #
40
+ # Instead of:
41
+ #
42
+ # Project.find(params[:project_id])
43
+ #
44
+ # * param => Allows you to specify params key used to instantiate the parent.
45
+ # Default is :parent_id, which in this case is :project_id.
46
+ #
47
+ # * route_name => Allows you to specify what is the route name in your url
48
+ # helper. By default is 'project'. But if your url helper should be
49
+ # "admin_project_task_url" instead of "project_task_url", just do:
50
+ #
51
+ # belongs_to :project, :route_name => "admin_project"
52
+ #
53
+ # = nested_belongs_to
54
+ #
55
+ # If for some reason you need to nested more than two resources, you can do:
56
+ #
57
+ # class TasksController
58
+ # belongs_to :company, :project
59
+ # end
60
+ #
61
+ # ATTENTION! This DOES NOT mean polymorphic associations as in resource_controller.
62
+ # Polymorphic associations are not supported yet.
63
+ #
64
+ # It means that companies have many projects which have many tasks. You URL
65
+ # should be:
66
+ #
67
+ # /companies/:company_id/projects/:project_id/tasks/:id
68
+ #
69
+ # Everything will be handled for you again. And all defaults will describe above
70
+ # will be assumed. But if you have to change the defaults. You will have to
71
+ # specify one association by one:
72
+ #
73
+ # class TasksController
74
+ # belongs_to :company, :finder => :find_by_name!, :param => :company_name
75
+ # belongs_to :project
76
+ # end
77
+ #
78
+ # belongs_to is aliased as nested_belongs_to, so this provides a nicer syntax:
79
+ #
80
+ # class TasksController
81
+ # nested_belongs_to :company, :finder => :find_by_name!, :param => :company_name
82
+ # nested_belongs_to :project
83
+ # end
84
+ #
85
+ # In this case the association chain would be:
86
+ #
87
+ # Company.find_by_name!(params[:company_name]).projects.find(params[:project_id]).tasks.find(:all)
88
+ #
89
+ # When you are using nested resources, you have one more option to config.
90
+ # Let's suppose that to get all projects from a company, you have to do:
91
+ #
92
+ # Company.admin_projects
93
+ #
94
+ # Instead of:
95
+ #
96
+ # Company.projects
97
+ #
98
+ # In this case, you can set the collection_name in belongs_to:
99
+ #
100
+ # nested_belongs_to :project, :collection_name => 'admin_projects'
101
+ #
102
+ # = polymorphic associations
103
+ #
104
+ # In some cases you have a resource that belongs to two different resources
105
+ # but not at the same time. For example, let's suppose you have File, Message
106
+ # and Task as resources and they are all commentable.
107
+ #
108
+ # Polymorphic associations allows you to create just one controller that will
109
+ # deal with each case.
110
+ #
111
+ # class Comment < InheritedResources::Base
112
+ # belongs_to :file, :message, :task, :polymorphic => true
113
+ # end
114
+ #
115
+ # Your routes should be something like:
116
+ #
117
+ # m.resources :files, :has_many => :comments #=> /files/13/comments
118
+ # m.resources :tasks, :has_many => :comments #=> /tasks/17/comments
119
+ # m.resources :messages, :has_many => :comments #=> /messages/11/comments
120
+ #
121
+ # When using polymorphic associations, you get some free helpers:
122
+ #
123
+ # parent? #=> true
124
+ # parent_type #=> :task
125
+ # parent_class #=> Task
126
+ # parent_instance #=> @task
127
+ #
128
+ # This polymorphic controllers thing is a great idea by James Golick and he
129
+ # built it in resource_controller. Here is just a re-implementation.
130
+ #
131
+ # = nested polymorphic associations
132
+ #
133
+ # You can have polymorphic associations with nested resources. Let's suppose
134
+ # that our File, Task and Message resources in the previous example belongs to
135
+ # a project.
136
+ #
137
+ # This way we can have:
138
+ #
139
+ # class Comment < InheritedResources::Base
140
+ # belongs_to :project {
141
+ # belongs_to :file, :message, :task, :polymorphic => true
142
+ # }
143
+ # end
144
+ #
145
+ # Or:
146
+ #
147
+ # class Comment < InheritedResources::Base
148
+ # nested_belongs_to :project
149
+ # nested_belongs_to :file, :message, :task, :polymorphic => true
150
+ # end
151
+ #
152
+ # Choose the syntax that makes more sense to you. :)
153
+ #
154
+ # Finally your routes should be something like:
155
+ #
156
+ # map.resources :projects do |m|
157
+ # m.resources :files, :has_many => :comments #=> /projects/1/files/13/comments
158
+ # m.resources :tasks, :has_many => :comments #=> /projects/1/tasks/17/comments
159
+ # m.resources :messages, :has_many => :comments #=> /projects/1/messages/11/comments
160
+ # end
161
+ #
162
+ # The helpers work in the same way as above.
163
+ #
164
+ # = singleton
165
+ #
166
+ # If you have singleton resources, in other words, if your controller resource
167
+ # associates to another through a has_one association, you can pass the option
168
+ # :singleton to it. It will deal with all the details and automacally remove
169
+ # the :index action.
170
+ #
171
+ # class ManagersController < InheritedResources::Base
172
+ # belongs_to :project, :singleton => true # a project has one manager
173
+ # end
174
+ #
175
+ module InheritedResources #:nodoc:
176
+ module BelongsTo #:nodoc:
177
+
178
+ protected
179
+ def belongs_to(*symbols, &block)
180
+ options = symbols.extract_options!
181
+
182
+ options.symbolize_keys!
183
+ options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param, :finder, :route_name, :collection_name, :singleton, :polymorphic)
184
+
185
+ acts_as_singleton! if singleton = options.delete(:singleton)
186
+ acts_as_polymorphic! if polymorphic = options.delete(:polymorphic)
187
+
188
+ raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty?
189
+ raise ArgumentError, 'You cannot define multiple associations with the options: #{options.keys.inspect}.' unless symbols.size == 1 || options.empty?
190
+
191
+ # Add BelongsToHelpers if we haven't yet.
192
+ include BelongsToHelpers if self.parents_symbols.empty?
193
+
194
+ # Set configuration default values
195
+ symbols.each do |symbol|
196
+ symbol = symbol.to_sym
197
+
198
+ if polymorphic
199
+ self.parents_symbols << :polymorphic unless self.parents_symbols.include? :polymorphic
200
+ self.polymorphic_symbols << symbol
201
+ else
202
+ self.parents_symbols << symbol
203
+ end
204
+
205
+ config = self.resources_configuration[symbol] = {}
206
+ config[:parent_class] = options.delete(:parent_class)
207
+ config[:parent_class] ||= (options.delete(:class_name) || symbol).to_s.classify.constantize rescue nil
208
+ config[:collection_name] = (options.delete(:collection_name) || symbol.to_s.pluralize).to_sym
209
+ config[:instance_name] = (options.delete(:instance_name) || symbol).to_sym
210
+ config[:param] = (options.delete(:param) || "#{symbol}_id").to_sym
211
+ config[:finder] = (options.delete(:finder) || :find).to_sym
212
+ config[:route_name] = (options.delete(:route_name) || symbol).to_s
213
+ config[:polymorphic] = polymorphic
214
+ end
215
+
216
+ # Regenerate url helpers unless block is given
217
+ if block_given?
218
+ class_eval(&block)
219
+ else
220
+ InheritedResources::UrlHelpers.create_resources_url_helpers!(self)
221
+ end
222
+ end
223
+ alias :nested_belongs_to :belongs_to
224
+
225
+ end
226
+ end
227
+
@@ -0,0 +1,89 @@
1
+ module InheritedResources #:nodoc:
2
+ module BelongsToHelpers #:nodoc:
3
+
4
+ # Private helpers, you probably don't have to worry with them.
5
+ private
6
+
7
+ # Overwrites the parent? method defined in base_helpers.rb.
8
+ # This one always returns true since it's added when associations
9
+ # are defined.
10
+ #
11
+ def parent?
12
+ true
13
+ end
14
+
15
+ # Evaluate the parent given. This is used to nest parents in the
16
+ # association chain.
17
+ #
18
+ def evaluate_parent(parent_config, chain = nil)
19
+ scoped_parent = if chain
20
+ chain.send(parent_config[:collection_name])
21
+ else
22
+ parent_config[:parent_class]
23
+ end
24
+
25
+ scoped_parent = scoped_parent.send(parent_config[:finder], params[parent_config[:param]])
26
+
27
+ instance_variable_set("@#{parent_config[:instance_name]}", scoped_parent)
28
+ end
29
+
30
+ # Overwrites the end_of_association_chain method.
31
+ #
32
+ # This methods gets your begin_of_association_chain, join it with your
33
+ # parents chain and returns the scoped association.
34
+ #
35
+ def end_of_association_chain
36
+ return resource_class unless parent?
37
+
38
+ chain = symbols_for_chain.inject(begin_of_association_chain) do |chain, symbol|
39
+ evaluate_parent(resources_configuration[symbol], chain)
40
+ end
41
+
42
+ chain = chain.send(method_for_association_chain) if method_for_association_chain
43
+
44
+ return chain
45
+ end
46
+
47
+ # If current controller is singleton, returns instance name to
48
+ # end_of_association_chain. This means that we will have the following
49
+ # chain:
50
+ #
51
+ # Project.find(params[:project_id]).manager
52
+ #
53
+ # Instead of:
54
+ #
55
+ # Project.find(params[:project_id]).managers
56
+ #
57
+ def method_for_association_chain
58
+ singleton ? nil : resource_collection_name
59
+ end
60
+
61
+ # Maps parents_symbols to build association chain.
62
+ #
63
+ # If the parents_symbols find :polymorphic, it goes through the
64
+ # params keys to see which polymorphic parent matches the given params.
65
+ #
66
+ def symbols_for_chain
67
+ parents_symbols.map do |symbol|
68
+ if symbol == :polymorphic
69
+ params_keys = params.keys
70
+
71
+ key = polymorphic_symbols.find do |poly|
72
+ params_keys.include? resources_configuration[poly][:param].to_s
73
+ end
74
+
75
+ raise ScriptError, "Could not find param for polymorphic association.
76
+ The request params keys are #{params.keys.inspect}
77
+ and the polymorphic associations are
78
+ #{polymorphic_symbols.inspect}." if key.nil?
79
+
80
+ instance_variable_set('@parent_type', key.to_sym)
81
+ else
82
+ symbol
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+ end