karsthammer-inherited_resources 1.1.2

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 (36) hide show
  1. data/CHANGELOG +119 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +516 -0
  4. data/Rakefile +43 -0
  5. data/lib/generators/rails/USAGE +10 -0
  6. data/lib/generators/rails/inherited_resources_controller_generator.rb +11 -0
  7. data/lib/generators/rails/templates/controller.rb +5 -0
  8. data/lib/inherited_resources.rb +37 -0
  9. data/lib/inherited_resources/actions.rb +67 -0
  10. data/lib/inherited_resources/base.rb +44 -0
  11. data/lib/inherited_resources/base_helpers.rb +270 -0
  12. data/lib/inherited_resources/belongs_to_helpers.rb +97 -0
  13. data/lib/inherited_resources/blank_slate.rb +12 -0
  14. data/lib/inherited_resources/class_methods.rb +267 -0
  15. data/lib/inherited_resources/dsl.rb +26 -0
  16. data/lib/inherited_resources/polymorphic_helpers.rb +155 -0
  17. data/lib/inherited_resources/responder.rb +6 -0
  18. data/lib/inherited_resources/singleton_helpers.rb +95 -0
  19. data/lib/inherited_resources/url_helpers.rb +188 -0
  20. data/lib/inherited_resources/version.rb +3 -0
  21. data/test/aliases_test.rb +144 -0
  22. data/test/association_chain_test.rb +125 -0
  23. data/test/base_test.rb +278 -0
  24. data/test/belongs_to_test.rb +105 -0
  25. data/test/class_methods_test.rb +132 -0
  26. data/test/customized_base_test.rb +168 -0
  27. data/test/customized_belongs_to_test.rb +76 -0
  28. data/test/defaults_test.rb +70 -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/singleton_test.rb +83 -0
  34. data/test/test_helper.rb +40 -0
  35. data/test/url_helpers_test.rb +665 -0
  36. metadata +142 -0
@@ -0,0 +1,6 @@
1
+ module InheritedResources
2
+ class Responder < ActionController::Responder
3
+ include Responders::FlashResponder
4
+ include Responders::HttpCacheResponder
5
+ end
6
+ end
@@ -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,188 @@
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
+ unless parents_symbols.empty?
80
+ generate_url_and_path_helpers nil, :parent, resource_segments, resource_ivars
81
+ generate_url_and_path_helpers :edit, :parent, resource_segments, resource_ivars
82
+ end
83
+
84
+ # This is the default route configuration, later we have to deal with
85
+ # exception from polymorphic and singleton cases.
86
+ #
87
+ collection_segments << resource_config[:route_collection_name]
88
+ resource_segments << resource_config[:route_instance_name]
89
+ resource_ivars << :"@#{resource_config[:instance_name]}"
90
+
91
+ # In singleton cases, we do not send the current element instance variable
92
+ # because the id is not in the URL. For example, we should call:
93
+ #
94
+ # project_manager_url(@project)
95
+ #
96
+ # Instead of:
97
+ #
98
+ # project_manager_url(@project, @manager)
99
+ #
100
+ # Another exception in singleton cases is that collection url does not
101
+ # exist. In such cases, we create the parent collection url. So in the
102
+ # manager case above, the collection url will be:
103
+ #
104
+ # project_url(@project)
105
+ #
106
+ # If the singleton does not have a parent, it will default to root_url.
107
+ #
108
+ # Finally, polymorphic cases we have to give hints to the polymorphic url
109
+ # builder. This works by attaching new ivars as symbols or records.
110
+ #
111
+ if singleton
112
+ collection_segments.pop
113
+ resource_ivars.pop
114
+
115
+ if polymorphic
116
+ resource_ivars << resource_config[:instance_name].inspect
117
+ new_ivars = resource_ivars
118
+ end
119
+ elsif polymorphic
120
+ collection_ivars << '(@_resource_class_new ||= resource_class.new)'
121
+ end
122
+
123
+ # If route is uncountable then add "_index" suffix to collection index route name
124
+ #
125
+ if !singleton && resource_config[:route_collection_name] == resource_config[:route_instance_name]
126
+ collection_segments << :index
127
+ end
128
+
129
+ generate_url_and_path_helpers nil, :collection, collection_segments, collection_ivars
130
+ generate_url_and_path_helpers :new, :resource, resource_segments, new_ivars || collection_ivars
131
+ generate_url_and_path_helpers nil, :resource, resource_segments, resource_ivars
132
+ generate_url_and_path_helpers :edit, :resource, resource_segments, resource_ivars
133
+ end
134
+
135
+ def generate_url_and_path_helpers(prefix, name, resource_segments, resource_ivars) #:nodoc:
136
+ ivars = resource_ivars.dup
137
+
138
+ singleton = self.resources_configuration[:self][:singleton]
139
+ polymorphic = self.parents_symbols.include?(:polymorphic)
140
+
141
+ # If it's not a singleton, ivars are not empty, not a collection or
142
+ # not a "new" named route, we can pass a resource as argument.
143
+ #
144
+ unless (singleton && name != :parent) || ivars.empty? || name == :collection || prefix == :new
145
+ ivars.push "(given_args.first || #{ivars.pop})"
146
+ end
147
+
148
+ # In collection in polymorphic cases, allow an argument to be given as a
149
+ # replacemente for the parent.
150
+ #
151
+ if name == :collection && polymorphic
152
+ index = ivars.index(:parent)
153
+ ivars.insert index, "(given_args.first || parent)"
154
+ ivars.delete(:parent)
155
+ end
156
+
157
+ # When polymorphic is true, the segments must be replace by :polymorphic
158
+ # and ivars should be gathered into an array, which is compacted when
159
+ # optional.
160
+ #
161
+ if polymorphic
162
+ segments = :polymorphic
163
+ ivars = "[#{ivars.join(', ')}]"
164
+ ivars << '.compact' if self.resources_configuration[:polymorphic][:optional]
165
+ else
166
+ segments = resource_segments.empty? ? 'root' : resource_segments.join('_')
167
+ ivars = ivars.join(', ')
168
+ end
169
+
170
+ prefix = prefix ? "#{prefix}_" : ''
171
+ ivars << (ivars.empty? ? 'given_options' : ', given_options')
172
+
173
+ class_eval <<-URL_HELPERS, __FILE__, __LINE__
174
+ protected
175
+ def #{prefix}#{name}_path(*given_args)
176
+ given_options = given_args.extract_options!
177
+ #{prefix}#{segments}_path(#{ivars})
178
+ end
179
+
180
+ def #{prefix}#{name}_url(*given_args)
181
+ given_options = given_args.extract_options!
182
+ #{prefix}#{segments}_url(#{ivars})
183
+ end
184
+ URL_HELPERS
185
+ end
186
+
187
+ end
188
+ end
@@ -0,0 +1,3 @@
1
+ module InheritedResources
2
+ VERSION = '1.1.2'.freeze
3
+ end
@@ -0,0 +1,144 @@
1
+ require File.expand_path('test_helper', File.dirname(__FILE__))
2
+
3
+ class Student
4
+ extend ActiveModel::Naming
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_requested_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, :errors => { :fail => true }))
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(expectations={})
136
+ @mock_student ||= begin
137
+ student = mock(expectations.except(:errors))
138
+ student.stubs(:class).returns(Student)
139
+ student.stubs(:errors).returns(expectations.fetch(:errors, {}))
140
+ student
141
+ end
142
+ end
143
+ end
144
+
@@ -0,0 +1,125 @@
1
+ require File.expand_path('test_helper', File.dirname(__FILE__))
2
+
3
+ class Pet
4
+ extend ActiveModel::Naming
5
+ end
6
+
7
+ class Puppet
8
+ extend ActiveModel::Naming
9
+ end
10
+
11
+ class PetsController < InheritedResources::Base
12
+ attr_accessor :current_user
13
+
14
+ def edit
15
+ @pet = 'new pet'
16
+ edit!
17
+ end
18
+
19
+ protected
20
+ def collection
21
+ @pets ||= end_of_association_chain.all
22
+ end
23
+
24
+ def begin_of_association_chain
25
+ @current_user
26
+ end
27
+ end
28
+
29
+ class BeginOfAssociationChainTest < ActionController::TestCase
30
+ tests PetsController
31
+
32
+ def setup
33
+ @controller.current_user = mock()
34
+ end
35
+
36
+ def test_begin_of_association_chain_is_called_on_index
37
+ @controller.current_user.expects(:pets).returns(Pet)
38
+ Pet.expects(:all).returns(mock_pet)
39
+ get :index
40
+ assert_response :success
41
+ assert_equal 'Index HTML', @response.body.strip
42
+ end
43
+
44
+ def test_begin_of_association_chain_is_called_on_new
45
+ @controller.current_user.expects(:pets).returns(Pet)
46
+ Pet.expects(:build).returns(mock_pet)
47
+ get :new
48
+ assert_response :success
49
+ assert_equal 'New HTML', @response.body.strip
50
+ end
51
+
52
+ def test_begin_of_association_chain_is_called_on_show
53
+ @controller.current_user.expects(:pets).returns(Pet)
54
+ Pet.expects(:find).with('47').returns(mock_pet)
55
+ get :show, :id => '47'
56
+ assert_response :success
57
+ assert_equal 'Show HTML', @response.body.strip
58
+ end
59
+
60
+ def test_instance_variable_should_not_be_set_if_already_defined
61
+ @controller.current_user.expects(:pets).never
62
+ Pet.expects(:find).never
63
+ get :edit
64
+ assert_response :success
65
+ assert_equal 'new pet', assigns(:pet)
66
+ end
67
+
68
+ def test_model_is_not_initialized_with_nil
69
+ @controller.current_user.expects(:pets).returns(Pet)
70
+ Pet.expects(:build).with({}).returns(mock_pet)
71
+ get :new
72
+ assert_equal mock_pet, assigns(:pet)
73
+ end
74
+
75
+ def test_begin_of_association_chain_is_included_in_chain
76
+ @controller.current_user.expects(:pets).returns(Pet)
77
+ Pet.expects(:build).with({}).returns(mock_pet)
78
+ get :new
79
+ assert_equal [@controller.current_user], @controller.send(:association_chain)
80
+ end
81
+
82
+ protected
83
+ def mock_pet(stubs={})
84
+ @mock_pet ||= mock(stubs)
85
+ end
86
+
87
+ end
88
+
89
+ class PuppetsController < InheritedResources::Base
90
+ optional_belongs_to :pet
91
+ end
92
+
93
+ class AssociationChainTest < ActionController::TestCase
94
+ tests PuppetsController
95
+
96
+ def setup
97
+ @controller.stubs(:resource_url).returns('/')
98
+ @controller.stubs(:collection_url).returns('/')
99
+ end
100
+
101
+ def test_parent_is_added_to_association_chain
102
+ Pet.expects(:find).with('37').returns(mock_pet)
103
+ mock_pet.expects(:puppets).returns(Puppet)
104
+ Puppet.expects(:find).with('42').returns(mock_puppet)
105
+ mock_puppet.expects(:destroy)
106
+ delete :destroy, :id => '42', :pet_id => '37'
107
+ assert_equal [mock_pet], @controller.send(:association_chain)
108
+ end
109
+
110
+ def test_parent_is_added_to_association_chain_if_not_available
111
+ Puppet.expects(:find).with('42').returns(mock_puppet)
112
+ mock_puppet.expects(:destroy)
113
+ delete :destroy, :id => '42'
114
+ assert_equal [], @controller.send(:association_chain)
115
+ end
116
+
117
+ protected
118
+ def mock_pet(stubs={})
119
+ @mock_pet ||= mock(stubs)
120
+ end
121
+
122
+ def mock_puppet(stubs={})
123
+ @mock_puppet ||= mock(stubs)
124
+ end
125
+ end