inherited_resources 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/CHANGELOG +103 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +524 -0
  4. data/Rakefile +40 -0
  5. data/lib/inherited_resources.rb +23 -0
  6. data/lib/inherited_resources/actions.rb +79 -0
  7. data/lib/inherited_resources/base.rb +42 -0
  8. data/lib/inherited_resources/base_helpers.rb +363 -0
  9. data/lib/inherited_resources/belongs_to_helpers.rb +89 -0
  10. data/lib/inherited_resources/class_methods.rb +338 -0
  11. data/lib/inherited_resources/dsl.rb +26 -0
  12. data/lib/inherited_resources/dumb_responder.rb +20 -0
  13. data/lib/inherited_resources/has_scope_helpers.rb +83 -0
  14. data/lib/inherited_resources/legacy/respond_to.rb +156 -0
  15. data/lib/inherited_resources/legacy/responder.rb +200 -0
  16. data/lib/inherited_resources/polymorphic_helpers.rb +155 -0
  17. data/lib/inherited_resources/singleton_helpers.rb +95 -0
  18. data/lib/inherited_resources/url_helpers.rb +179 -0
  19. data/test/aliases_test.rb +139 -0
  20. data/test/association_chain_test.rb +125 -0
  21. data/test/base_test.rb +225 -0
  22. data/test/belongs_to_test.rb +87 -0
  23. data/test/class_methods_test.rb +138 -0
  24. data/test/customized_base_test.rb +162 -0
  25. data/test/customized_belongs_to_test.rb +76 -0
  26. data/test/defaults_test.rb +70 -0
  27. data/test/flash_test.rb +88 -0
  28. data/test/has_scope_test.rb +139 -0
  29. data/test/nested_belongs_to_test.rb +108 -0
  30. data/test/optional_belongs_to_test.rb +164 -0
  31. data/test/polymorphic_test.rb +186 -0
  32. data/test/redirect_to_test.rb +51 -0
  33. data/test/respond_to_test.rb +155 -0
  34. data/test/singleton_test.rb +83 -0
  35. data/test/test_helper.rb +38 -0
  36. data/test/url_helpers_test.rb +537 -0
  37. metadata +89 -0
@@ -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,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
@@ -0,0 +1,179 @@
1
+ module InheritedResources
2
+ # = URLHelpers
3
+ #
4
+ # When you use InheritedResources it creates some UrlHelpers for you.
5
+ # And they handle everything for you.
6
+ #
7
+ # # /posts/1/comments
8
+ # resource_url # => /posts/1/comments/#{@comment.to_param}
9
+ # resource_url(comment) # => /posts/1/comments/#{comment.to_param}
10
+ # new_resource_url # => /posts/1/comments/new
11
+ # edit_resource_url # => /posts/1/comments/#{@comment.to_param}/edit
12
+ # collection_url # => /posts/1/comments
13
+ # parent_url # => /posts/1
14
+ #
15
+ # # /projects/1/tasks
16
+ # resource_url # => /projects/1/tasks/#{@task.to_param}
17
+ # resource_url(task) # => /projects/1/tasks/#{task.to_param}
18
+ # new_resource_url # => /projects/1/tasks/new
19
+ # edit_resource_url # => /projects/1/tasks/#{@task.to_param}/edit
20
+ # collection_url # => /projects/1/tasks
21
+ # parent_url # => /projects/1
22
+ #
23
+ # # /users
24
+ # resource_url # => /users/#{@user.to_param}
25
+ # resource_url(user) # => /users/#{user.to_param}
26
+ # new_resource_url # => /users/new
27
+ # edit_resource_url # => /users/#{@user.to_param}/edit
28
+ # collection_url # => /users
29
+ # parent_url # => /
30
+ #
31
+ # The nice thing is that those urls are not guessed during runtime. They are
32
+ # all created when you inherit.
33
+ #
34
+ module UrlHelpers
35
+
36
+ # This method hard code url helpers in the class.
37
+ #
38
+ # We are doing this because is cheaper than guessing them when our action
39
+ # is being processed (and even more cheaper when we are using nested
40
+ # resources).
41
+ #
42
+ # When we are using polymorphic associations, those helpers rely on
43
+ # polymorphic_url Rails helper.
44
+ #
45
+ def create_resources_url_helpers!
46
+ resource_segments, resource_ivars = [], []
47
+ resource_config = self.resources_configuration[:self]
48
+
49
+ singleton = self.resources_configuration[:self][:singleton]
50
+ polymorphic = self.parents_symbols.include?(:polymorphic)
51
+
52
+ # Add route_prefix if any.
53
+ unless resource_config[:route_prefix].blank?
54
+ if polymorphic
55
+ resource_ivars << resource_config[:route_prefix].to_s.inspect
56
+ else
57
+ resource_segments << resource_config[:route_prefix]
58
+ end
59
+ end
60
+
61
+ # Deal with belongs_to associations and polymorphic associations.
62
+ # Remember that we don't have to build the segments in polymorphic cases,
63
+ # because the url will be polymorphic_url.
64
+ #
65
+ self.parents_symbols.each do |symbol|
66
+ if symbol == :polymorphic
67
+ resource_ivars << :parent
68
+ else
69
+ config = self.resources_configuration[symbol]
70
+ resource_segments << config[:route_name]
71
+ resource_ivars << :"@#{config[:instance_name]}"
72
+ end
73
+ end
74
+
75
+ collection_ivars = resource_ivars.dup
76
+ collection_segments = resource_segments.dup
77
+
78
+ # Generate parent url before we add resource instances.
79
+ generate_url_and_path_helpers nil, :parent, resource_segments, resource_ivars
80
+
81
+ # This is the default route configuration, later we have to deal with
82
+ # exception from polymorphic and singleton cases.
83
+ #
84
+ collection_segments << resource_config[:route_collection_name]
85
+ resource_segments << resource_config[:route_instance_name]
86
+ resource_ivars << :"@#{resource_config[:instance_name]}"
87
+
88
+ # In singleton cases, we do not send the current element instance variable
89
+ # because the id is not in the URL. For example, we should call:
90
+ #
91
+ # project_manager_url(@project)
92
+ #
93
+ # Instead of:
94
+ #
95
+ # project_manager_url(@project, @manager)
96
+ #
97
+ # Another exception in singleton cases is that collection url does not
98
+ # exist. In such cases, we create the parent collection url. So in the
99
+ # manager case above, the collection url will be:
100
+ #
101
+ # project_url(@project)
102
+ #
103
+ # If the singleton does not have a parent, it will default to root_url.
104
+ #
105
+ # Finally, polymorphic cases we have to give hints to the polymorphic url
106
+ # builder. This works by attaching new ivars as symbols or records.
107
+ #
108
+ if singleton
109
+ collection_segments.pop
110
+ resource_ivars.pop
111
+
112
+ if polymorphic
113
+ resource_ivars << resource_config[:instance_name].inspect
114
+ new_ivars = resource_ivars
115
+ end
116
+ elsif polymorphic
117
+ collection_ivars << '(@_resource_class_new ||= resource_class.new)'
118
+ end
119
+
120
+ generate_url_and_path_helpers nil, :collection, collection_segments, collection_ivars
121
+ generate_url_and_path_helpers :new, :resource, resource_segments, new_ivars || collection_ivars
122
+ generate_url_and_path_helpers nil, :resource, resource_segments, resource_ivars
123
+ generate_url_and_path_helpers :edit, :resource, resource_segments, resource_ivars
124
+ end
125
+
126
+ def generate_url_and_path_helpers(prefix, name, resource_segments, resource_ivars) #:nodoc:
127
+ ivars = resource_ivars.dup
128
+
129
+ singleton = self.resources_configuration[:self][:singleton]
130
+ polymorphic = self.parents_symbols.include?(:polymorphic)
131
+
132
+ # If it's not a singleton, ivars are not empty, not a collection or
133
+ # not a "new" named route, we can pass a resource as argument.
134
+ #
135
+ unless (singleton && name != :parent) || ivars.empty? || name == :collection || prefix == :new
136
+ ivars.push "(given_args.first || #{ivars.pop})"
137
+ end
138
+
139
+ # In collection in polymorphic cases, allow an argument to be given as a
140
+ # replacemente for the parent.
141
+ #
142
+ if name == :collection && polymorphic
143
+ index = ivars.index(:parent)
144
+ ivars.insert index, "(given_args.first || parent)"
145
+ ivars.delete(:parent)
146
+ end
147
+
148
+ # When polymorphic is true, the segments must be replace by :polymorphic
149
+ # and ivars should be gathered into an array, which is compacted when
150
+ # optional.
151
+ #
152
+ if polymorphic
153
+ segments = :polymorphic
154
+ ivars = "[#{ivars.join(', ')}]"
155
+ ivars << '.compact' if self.resources_configuration[:polymorphic][:optional]
156
+ else
157
+ segments = resource_segments.empty? ? 'root' : resource_segments.join('_')
158
+ ivars = ivars.join(', ')
159
+ end
160
+
161
+ prefix = prefix ? "#{prefix}_" : ''
162
+ ivars << (ivars.empty? ? 'given_options' : ', given_options')
163
+
164
+ class_eval <<-URL_HELPERS, __FILE__, __LINE__
165
+ protected
166
+ def #{prefix}#{name}_path(*given_args)
167
+ given_options = given_args.extract_options!
168
+ #{prefix}#{segments}_path(#{ivars})
169
+ end
170
+
171
+ def #{prefix}#{name}_url(*given_args)
172
+ given_options = given_args.extract_options!
173
+ #{prefix}#{segments}_url(#{ivars})
174
+ end
175
+ URL_HELPERS
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,139 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class Student;
4
+ def self.human_name; 'Student'; end
5
+ end
6
+
7
+ class ApplicationController < ActionController::Base
8
+ include InheritedResources::DSL
9
+ end
10
+
11
+ class StudentsController < ApplicationController
12
+ inherit_resources
13
+ respond_to :html, :xml
14
+
15
+ def edit
16
+ edit! do |format|
17
+ format.xml { render :text => 'Render XML' }
18
+ end
19
+ end
20
+
21
+ def new
22
+ @something = 'magical'
23
+ new!
24
+ end
25
+
26
+ create!(:location => "http://test.host/") do |success, failure|
27
+ success.html { render :text => "I won't redirect!" }
28
+ failure.xml { render :text => "I shouldn't be rendered" }
29
+ end
30
+
31
+ update! do |success, failure|
32
+ success.html { redirect_to(resource_url) }
33
+ failure.html { render :text => "I won't render!" }
34
+ end
35
+
36
+ destroy! do |format|
37
+ format.html { render :text => "Destroyed!" }
38
+ end
39
+ end
40
+
41
+ class AliasesTest < ActionController::TestCase
42
+ tests StudentsController
43
+
44
+ def test_assignments_before_calling_alias
45
+ Student.stubs(:new).returns(mock_student)
46
+ get :new
47
+ assert_response :success
48
+ assert_equal 'magical', assigns(:something)
49
+ end
50
+
51
+ def test_controller_should_render_new
52
+ Student.stubs(:new).returns(mock_student)
53
+ get :new
54
+ assert_response :success
55
+ assert_equal 'New HTML', @response.body.strip
56
+ end
57
+
58
+ def test_expose_the_resquested_user_on_edit
59
+ Student.expects(:find).with('42').returns(mock_student)
60
+ get :edit, :id => '42'
61
+ assert_equal mock_student, assigns(:student)
62
+ assert_response :success
63
+ end
64
+
65
+ def test_controller_should_render_edit
66
+ Student.stubs(:find).returns(mock_student)
67
+ get :edit
68
+ assert_response :success
69
+ assert_equal 'Edit HTML', @response.body.strip
70
+ end
71
+
72
+ def test_render_xml_when_it_is_given_as_a_block
73
+ @request.accept = 'application/xml'
74
+ Student.stubs(:find).returns(mock_student)
75
+ get :edit
76
+ assert_response :success
77
+ assert_equal 'Render XML', @response.body
78
+ end
79
+
80
+ def test_is_not_redirected_on_create_with_success_if_success_block_is_given
81
+ Student.stubs(:new).returns(mock_student(:save => true))
82
+ @controller.stubs(:resource_url).returns('http://test.host/')
83
+ post :create
84
+ assert_response :success
85
+ assert_equal "I won't redirect!", @response.body
86
+ end
87
+
88
+ def test_dumb_responder_quietly_receives_everything_on_failure
89
+ @request.accept = 'text/html'
90
+ Student.stubs(:new).returns(mock_student(:save => false, :errors => {:some => :error}))
91
+ @controller.stubs(:resource_url).returns('http://test.host/')
92
+ post :create
93
+ assert_response :success
94
+ assert_equal "New HTML", @response.body.strip
95
+ end
96
+
97
+ def test_html_is_the_default_when_only_xml_is_overwriten
98
+ @request.accept = '*/*'
99
+ Student.stubs(:new).returns(mock_student(:save => false, :errors => {:some => :error}))
100
+ @controller.stubs(:resource_url).returns('http://test.host/')
101
+ post :create
102
+ assert_response :success
103
+ assert_equal "New HTML", @response.body.strip
104
+ end
105
+
106
+ def test_wont_render_edit_template_on_update_with_failure_if_failure_block_is_given
107
+ Student.stubs(:find).returns(mock_student(:update_attributes => false))
108
+ put :update
109
+ assert_response :success
110
+ assert_equal "I won't render!", @response.body
111
+ end
112
+
113
+ def test_dumb_responder_quietly_receives_everything_on_success
114
+ Student.stubs(:find).returns(mock_student(:update_attributes => true))
115
+ @controller.stubs(:resource_url).returns('http://test.host/')
116
+ put :update, :id => '42', :student => {:these => 'params'}
117
+ assert_equal mock_student, assigns(:student)
118
+ end
119
+
120
+ def test_block_is_called_when_student_is_destroyed
121
+ Student.stubs(:find).returns(mock_student(:destroy => true))
122
+ delete :destroy
123
+ assert_response :success
124
+ assert_equal "Destroyed!", @response.body
125
+ end
126
+
127
+ def test_options_are_used_in_respond_with
128
+ @request.accept = "application/xml"
129
+ Student.stubs(:new).returns(mock_student(:save => true, :to_xml => "XML"))
130
+ post :create
131
+ assert_equal "http://test.host/", @response.location
132
+ end
133
+
134
+ protected
135
+ def mock_student(stubs={})
136
+ @mock_student ||= mock(stubs)
137
+ end
138
+ end
139
+