inherited_resources 0.9.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 (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
+