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