garden_variety 2.0 → 4.0.0

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.
@@ -1,11 +1,8 @@
1
- require "pundit"
2
- require "garden_variety/actions"
3
-
4
1
  module GardenVariety
5
2
  module Controller
6
3
  extend ActiveSupport::Concern
7
4
 
8
- include Pundit
5
+ include Pundit::Authorization
9
6
 
10
7
  module ClassMethods
11
8
  # Macro to include garden variety implementations of specified
@@ -13,17 +10,15 @@ module GardenVariety
13
10
  # typical REST actions (index, show, new, create, edit, update,
14
11
  # destroy) are included.
15
12
  #
16
- # The optional +resources:+ parameter dictates which model class
17
- # and instance variables these actions use. The parameter's
18
- # default value derives from the controller name. The value must
19
- # be a resource name in plural form.
20
- #
21
- # The macro also defines the following accessor methods for use in
22
- # generic action and helper methods: +resources+, +resources=+,
23
- # +resource+, and +resource=+. These accessors get and set the
24
- # instance variables dictated by the +resources:+ parameter.
13
+ # @see GardenVariety::IndexAction
14
+ # @see GardenVariety::ShowAction
15
+ # @see GardenVariety::NewAction
16
+ # @see GardenVariety::CreateAction
17
+ # @see GardenVariety::EditAction
18
+ # @see GardenVariety::UpdateAction
19
+ # @see GardenVariety::DestroyAction
25
20
  #
26
- # @example default usage
21
+ # @example Default actions
27
22
  # # This...
28
23
  # class PostsController < ApplicationController
29
24
  # garden_variety
@@ -38,92 +33,30 @@ module GardenVariety
38
33
  # include GardenVariety::EditAction
39
34
  # include GardenVariety::UpdateAction
40
35
  # include GardenVariety::DestroyAction
41
- #
42
- # private
43
- #
44
- # def resource_class
45
- # Post
46
- # end
47
- #
48
- # def resources
49
- # @posts
50
- # end
51
- #
52
- # def resources=(models)
53
- # @posts = models
54
- # end
55
- #
56
- # def resource
57
- # @post
58
- # end
59
- #
60
- # def resource=(model)
61
- # @post = model
62
- # end
63
36
  # end
64
37
  #
65
- # @example custom usage
38
+ # @example Specific actions
66
39
  # # This...
67
- # class CountriesController < ApplicationController
68
- # garden_variety :index, resources: :locations
40
+ # class PostsController < ApplicationController
41
+ # garden_variety :index, :show
69
42
  # end
70
43
  #
71
44
  # # ...is equivalent to:
72
- # class CountriesController < ApplicationController
45
+ # class PostsController < ApplicationController
73
46
  # include GardenVariety::IndexAction
74
- #
75
- # private
76
- #
77
- # def resource_class
78
- # Location
79
- # end
80
- #
81
- # def resources
82
- # @locations
83
- # end
84
- #
85
- # def resources=(models)
86
- # @locations = models
87
- # end
88
- #
89
- # def resource
90
- # @location
91
- # end
92
- #
93
- # def resource=(model)
94
- # @location = model
95
- # end
47
+ # include GardenVariety::ShowAction
96
48
  # end
97
49
  #
98
50
  # @param actions [Array<:index, :show, :new, :create, :edit, :update, :destroy>]
99
- # @param resources [Symbol, String]
100
51
  # @return [void]
101
- def garden_variety(*actions, resources: controller_path)
102
- resources_attr = resources.to_s.underscore.tr("/", "_")
103
-
104
- class_eval <<-CODE
105
- private
106
-
107
- def resource_class # optimized override
108
- #{resources.to_s.classify}
52
+ # @raise [ArgumentError]
53
+ # if an invalid action is specified
54
+ def garden_variety(*actions)
55
+ actions.each do |action|
56
+ unless ::GardenVariety::ACTION_MODULES.key?(action)
57
+ raise ArgumentError, "Invalid action: #{action.inspect}"
109
58
  end
110
-
111
- def resources
112
- @#{resources_attr}
113
- end
114
-
115
- def resources=(models)
116
- @#{resources_attr} = models
117
- end
118
-
119
- def resource
120
- @#{resources_attr.singularize}
121
- end
122
-
123
- def resource=(model)
124
- @#{resources_attr.singularize} = model
125
- end
126
- CODE
59
+ end
127
60
 
128
61
  action_modules = actions.empty? ?
129
62
  ::GardenVariety::ACTION_MODULES.values :
@@ -131,84 +64,183 @@ module GardenVariety
131
64
 
132
65
  action_modules.each{|m| include m }
133
66
  end
134
- end
135
67
 
68
+ # Returns the controller model class. Defaults to a class
69
+ # corresponding to the singular-form of the controller name.
70
+ #
71
+ # @example
72
+ # class PostsController < ApplicationController
73
+ # end
74
+ #
75
+ # PostsController.model_class # == Post (class)
76
+ #
77
+ # @return [Class]
78
+ def model_class
79
+ @model_class ||= controller_path.classify.constantize
80
+ end
81
+
82
+ # Sets the controller model class.
83
+ #
84
+ # @example
85
+ # class PublishedPostsController < ApplicationController
86
+ # self.model_class = Post
87
+ # end
88
+ #
89
+ # @param klass [Class]
90
+ # @return [klass]
91
+ def model_class=(klass)
92
+ @model_name = nil
93
+ @model_class = klass
94
+ end
95
+
96
+ # @!visibility private
97
+ def model_name
98
+ @model_name ||= model_class.try(:model_name) || ActiveModel::Name.new(model_class)
99
+ end
100
+ end
136
101
 
137
102
  private
138
103
 
139
104
  # @!visibility public
140
- # Returns the class of the resource corresponding to the controller
141
- # name.
105
+ # Returns the value of the singular-form instance variable dictated
106
+ # by {ClassMethods#model_class ::model_class}.
142
107
  #
143
108
  # @example
144
- # PostsController.new.resource_class # == Post (class)
109
+ # class PostsController
110
+ # def show
111
+ # # This...
112
+ # self.model
113
+ # # ...is equivalent to:
114
+ # @post
115
+ # end
116
+ # end
117
+ #
118
+ # @return [Object]
119
+ def model
120
+ instance_variable_get(:"@#{self.class.model_name.singular}")
121
+ end
122
+
123
+ # @!visibility public
124
+ # Sets the value of the singular-form instance variable dictated
125
+ # by {ClassMethods#model_class ::model_class}.
126
+ #
127
+ # @example
128
+ # class PostsController
129
+ # def show
130
+ # # This...
131
+ # self.model = value
132
+ # # ...is equivalent to:
133
+ # @post = value
134
+ # end
135
+ # end
136
+ #
137
+ # @param value [Object]
138
+ # @return [value]
139
+ def model=(value)
140
+ instance_variable_set(:"@#{self.class.model_name.singular}", value)
141
+ end
142
+
143
+ # @!visibility public
144
+ # Returns the value of the plural-form instance variable dictated
145
+ # by {ClassMethods#model_class ::model_class}.
146
+ #
147
+ # @example
148
+ # class PostsController
149
+ # def index
150
+ # # This...
151
+ # self.collection
152
+ # # ...is equivalent to:
153
+ # @posts
154
+ # end
155
+ # end
156
+ #
157
+ # @return [Object]
158
+ def collection
159
+ instance_variable_get(:"@#{self.class.model_name.plural}")
160
+ end
161
+
162
+ # @!visibility public
163
+ # Sets the value of the plural-form instance variable dictated
164
+ # by {ClassMethods#model_class ::model_class}.
145
165
  #
146
- # @return [Class]
147
- def resource_class
148
- @resource_class ||= controller_path.classify.constantize
166
+ # @example
167
+ # class PostsController
168
+ # def index
169
+ # # This...
170
+ # self.collection = values
171
+ # # ...is equivalent to:
172
+ # @posts = values
173
+ # end
174
+ # end
175
+ #
176
+ # @param values [Object]
177
+ # @return [values]
178
+ def collection=(values)
179
+ instance_variable_set(:"@#{self.class.model_name.plural}", values)
149
180
  end
150
181
 
151
182
  # @!visibility public
152
- # Returns an ActiveRecord::Relation representing resource instances
153
- # corresponding to the controller. Designed for use in generic
154
- # +index+ action methods.
183
+ # Returns an ActiveRecord::Relation representing instances of
184
+ # {ClassMethods#model_class ::model_class}. Designed for use in
185
+ # generic +index+ action methods.
155
186
  #
156
187
  # @example
157
188
  # class PostsController < ApplicationController
158
189
  # def index
159
- # @posts = list_resources.where(status: "published")
190
+ # @posts = find_collection.where(status: "published")
160
191
  # end
161
192
  # end
162
193
  #
163
194
  # @return [ActiveRecord::Relation]
164
- def list_resources
165
- resource_class.all
195
+ def find_collection
196
+ self.class.model_class.all
166
197
  end
167
198
 
168
199
  # @!visibility public
169
- # Returns a model instance corresponding to the controller and the
170
- # id parameter of the current request (i.e. +params[:id]+).
171
- # Designed for use in generic +show+, +edit+, +update+, and
172
- # +destroy+ action methods.
200
+ # Returns an instance of {ClassMethods#model_class ::model_class}
201
+ # matching the +:id+ parameter of the current request (i.e.
202
+ # +params[:id]+). Designed for use in generic +show+, +edit+,
203
+ # +update+, and +destroy+ action methods.
173
204
  #
174
205
  # @example
175
206
  # class PostsController < ApplicationController
176
207
  # def show
177
- # @post = find_resource
208
+ # @post = find_model
178
209
  # end
179
210
  # end
180
211
  #
181
212
  # @return [ActiveRecord::Base]
182
- def find_resource
183
- resource_class.find(params[:id])
213
+ # @raise [ActiveRecord::RecordNotFound]
214
+ # if a model instance with matching +:id+ cannot be found
215
+ def find_model
216
+ self.class.model_class.find(params[:id])
184
217
  end
185
218
 
186
219
  # @!visibility public
187
- # Returns a new model instance corresponding to the controller.
220
+ # Returns a new instance of {ClassMethods#model_class ::model_class}.
188
221
  # Designed for use in generic +new+ and +create+ action methods.
189
222
  #
190
223
  # @example
191
224
  # class PostsController < ApplicationController
192
225
  # def new
193
- # @post = new_resource
226
+ # @post = new_model
194
227
  # end
195
228
  # end
196
229
  #
197
230
  # @return [ActiveRecord::Base]
198
- def new_resource
199
- resource_class.new
231
+ def new_model
232
+ self.class.model_class.new
200
233
  end
201
234
 
202
235
  # @!visibility public
203
- # Authorizes the given model for the current action via the model
204
- # Pundit policy, and populates the model attributes with the current
205
- # request params permitted by the model policy. Returns the given
206
- # model modified but not persisted.
236
+ # Populates the given +model+'s attributes with the current request
237
+ # params permitted by the corresponding Pundit policy. Returns the
238
+ # given +model+ modified but not persisted.
207
239
  #
208
240
  # @example
209
241
  # class PostsController < ApplicationController
210
242
  # def create
211
- # @post = vest(Post.new)
243
+ # @post = assign_attributes(authorize(Post.new))
212
244
  # if @post.save
213
245
  # redirect_to @post
214
246
  # else
@@ -219,31 +251,29 @@ module GardenVariety
219
251
  #
220
252
  # @param model [ActiveRecord::Base]
221
253
  # @return [ActiveRecord::Base]
222
- def vest(model)
223
- authorize(model)
254
+ def assign_attributes(model)
224
255
  model.assign_attributes(permitted_attributes(model))
225
256
  model
226
257
  end
227
258
 
228
259
  # @!visibility public
229
- # Returns Hash of values for interpolation in flash messages via
230
- # I18n. By default, returns +resource_name+ and
231
- # +resource_capitalized+ values appropriate to the controller.
260
+ # Returns a Hash of values for interpolation in flash messages via
261
+ # I18n. By default, returns a +:model_name+ key / value pair with
262
+ # the humanized name of {ClassMethods#model_class ::model_class}.
232
263
  # Override this method to provide your own values. Be aware that
233
- # certain option names, such as +default+ and +scope+, are reserved
234
- # by the I18n gem, and can not be used for interpolation. See the
235
- # {https://www.rubydoc.info/gems/i18n I18n documentation} for more
236
- # information.
264
+ # certain option names, such as +:default+ and +:scope+, are
265
+ # reserved by the I18n gem, and can not be used for interpolation.
266
+ # See the {https://www.rubydoc.info/gems/i18n I18n documentation}
267
+ # for more information.
237
268
  #
238
- # @return [Hash]
269
+ # @return [Hash{Symbol => #to_s}]
239
270
  def flash_options
240
- { resource_name: resource_class.model_name.human.downcase,
241
- resource_capitalized: resource_class.model_name.human }
271
+ { model_name: self.class.model_name.human }
242
272
  end
243
273
 
244
274
  # @!visibility public
245
275
  # Returns a flash message appropriate to the controller, the current
246
- # action, and a given status. The flash message is looked up via
276
+ # action, and a given +status+. The flash message is looked up via
247
277
  # I18n using a prioritized list of possible keys. The key priority
248
278
  # is as follows:
249
279
  #
@@ -265,12 +295,16 @@ module GardenVariety
265
295
  # # en:
266
296
  # # success: "Success!"
267
297
  # # create:
268
- # # success: "%{resource_capitalized} created."
298
+ # # success: "%{model_name} created."
269
299
  # # delete:
270
- # # success: "%{resource_capitalized} deleted."
300
+ # # success: "%{model_name} deleted."
271
301
  # # posts:
272
302
  # # create:
273
303
  # # success: "Congratulations on your new post!"
304
+ # # messages:
305
+ # # drafts:
306
+ # # update:
307
+ # # success: "Draft saved."
274
308
  #
275
309
  # # via PostsController#create
276
310
  # flash_message(:success) # == "Congratulations on your new post!"
@@ -281,21 +315,6 @@ module GardenVariety
281
315
  # # via PostsController#delete
282
316
  # flash_message(:success) # == "Post deleted."
283
317
  #
284
- # @example Namespaced controller
285
- # ### config/locales/garden_variety.en.yml
286
- # # en:
287
- # # create:
288
- # # success: "Created new %{resource_name}."
289
- # # update:
290
- # # success: "Updated %{resource_name}."
291
- # # messages:
292
- # # drafts:
293
- # # update:
294
- # # success: "Draft saved."
295
- #
296
- # # via Messages::DraftsController#create
297
- # flash_message(:success) # == "Created new draft."
298
- #
299
318
  # # via Messages::DraftsController#update
300
319
  # flash_message(:success) # == "Draft saved."
301
320
  #
@@ -304,12 +323,12 @@ module GardenVariety
304
323
  def flash_message(status)
305
324
  controller_key = controller_path.tr("/", I18n.default_separator)
306
325
  keys = [
307
- :"#{controller_key}.#{action_name}.#{status}",
308
- :"#{controller_key}.#{action_name}.#{status}_html",
309
- :"#{action_name}.#{status}",
310
- :"#{action_name}.#{status}_html",
311
- :"#{status}",
312
- :"#{status}_html",
326
+ :"flash.#{controller_key}.#{action_name}.#{status}",
327
+ :"flash.#{controller_key}.#{action_name}.#{status}_html",
328
+ :"flash.#{action_name}.#{status}",
329
+ :"flash.#{action_name}.#{status}_html",
330
+ :"flash.#{status}",
331
+ :"flash.#{status}_html",
313
332
  ]
314
333
  helpers.translate(keys.shift, default: keys, **flash_options)
315
334
  end
@@ -1,10 +1,6 @@
1
- require "rails/railtie"
2
- require "garden_variety/controller"
3
- require "garden_variety/current_user_stub"
4
-
5
1
  module GardenVariety
6
2
  # @!visibility private
7
- class Railtie < Rails::Railtie
3
+ class Railtie < ::Rails::Railtie
8
4
  # Render 404 on Pundit::NotAuthorizedError in production. (Helpful
9
5
  # error pages will still be shown in development.) Code 404 is used
10
6
  # because it is more discreet than 403, because it is explicitly
@@ -12,17 +8,10 @@ module GardenVariety
12
8
  # and because Rails includes a default 404 page, but not a 403 page.
13
9
  config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] ||= :not_found
14
10
 
15
- initializer "garden_variety.stub_current_user" do |app|
16
- ActiveSupport.on_load :action_controller do
17
- unless ActionController::Base.instance_methods.include?(:current_user)
18
- ActionController::Base.send :include, GardenVariety::CurrentUserStub
19
- end
20
- end
21
- end
22
-
23
- initializer "garden_variety.extend_action_controller" do |app|
11
+ initializer "garden_variety" do |app|
24
12
  ActiveSupport.on_load :action_controller do
25
- ActionController::Base.send :include, GardenVariety::Controller
13
+ include GardenVariety::CurrentUserStub unless instance_methods.include?(:current_user)
14
+ include GardenVariety::Controller
26
15
  end
27
16
  end
28
17
  end
@@ -0,0 +1,28 @@
1
+ module GardenVariety
2
+ # @!visibility private
3
+ module TalentScout
4
+
5
+ module ModelSearchClassOverride
6
+ def model_search_class
7
+ @model_search_class ||= "#{model_class}Search".constantize
8
+ end
9
+ end
10
+
11
+ ::TalentScout::Controller::ClassMethods.prepend(ModelSearchClassOverride)
12
+
13
+ module FindCollectionOverride
14
+ private
15
+ def find_collection
16
+ if self.class.model_search_class?
17
+ @search = model_search
18
+ @search.results.all
19
+ else
20
+ super
21
+ end
22
+ end
23
+ end
24
+
25
+ ::GardenVariety::Controller.prepend(FindCollectionOverride)
26
+
27
+ end
28
+ end
@@ -1,3 +1,3 @@
1
1
  module GardenVariety
2
- VERSION = "2.0"
2
+ VERSION = "4.0.0"
3
3
  end
@@ -1,2 +1,15 @@
1
+ require "pundit"
1
2
  require "garden_variety/version"
3
+ require "garden_variety/actions"
4
+ require "garden_variety/controller"
5
+ require "garden_variety/current_user_stub"
6
+
7
+ begin
8
+ require "talent_scout"
9
+ rescue LoadError
10
+ # do nothing
11
+ else
12
+ require "garden_variety/talent_scout"
13
+ end
14
+
2
15
  require "garden_variety/railtie"
@@ -1,9 +1,11 @@
1
- require "rails/generators/base"
1
+ require "generators/garden/optimized_generate_action"
2
2
 
3
3
  # @!visibility private
4
4
  module Garden
5
5
  module Generators
6
6
  class InstallGenerator < Rails::Generators::Base
7
+ include OptimizedGenerateAction
8
+
7
9
  source_root File.join(__dir__, "templates")
8
10
 
9
11
  def copy_locales
@@ -0,0 +1,11 @@
1
+ en:
2
+ flash:
3
+ create:
4
+ success: '%{model_name} was successfully created.'
5
+ error: '%{model_name} could not be created.'
6
+ update:
7
+ success: '%{model_name} was successfully updated.'
8
+ error: '%{model_name} could not be updated.'
9
+ destroy:
10
+ success: '%{model_name} was successfully destroyed.'
11
+ error: '%{model_name} could not be destroyed.'
@@ -0,0 +1,14 @@
1
+ # @!visibility private
2
+ module Garden
3
+ module OptimizedGenerateAction
4
+ def generate(what, *args)
5
+ log :generate, what
6
+
7
+ in_root do
8
+ silence_warnings do
9
+ ::Rails::Command.invoke("generate", [what, *args])
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,9 +1,11 @@
1
- require "rails/generators/base"
1
+ require "generators/garden/optimized_generate_action"
2
2
 
3
3
  # @!visibility private
4
4
  module Garden
5
5
  module Generators
6
6
  class ScaffoldGenerator < Rails::Generators::Base
7
+ include OptimizedGenerateAction
8
+
7
9
  source_root File.join(__dir__, "templates")
8
10
 
9
11
  argument :resource, type: :string
@@ -13,6 +15,9 @@ module Garden
13
15
  # inherited from Rails::Generators::Base
14
16
  class_option :template_engine
15
17
 
18
+ class_option :talent_scout, type: :boolean, default: true,
19
+ desc: "Invoke talent_scout:search generator"
20
+
16
21
  # override +initialize+ because it is the only way to reliably
17
22
  # capture the raw input arguments in order to pass them on to
18
23
  # `rails generate resource` (Thor neglects to provide an accessor,
@@ -40,6 +45,12 @@ module Garden
40
45
  def generate_pundit_policy
41
46
  generate("pundit:policy", resource)
42
47
  end
48
+
49
+ def generate_talent_scout_search
50
+ if defined?(::TalentScout) && options.talent_scout?
51
+ generate("talent_scout:search", resource)
52
+ end
53
+ end
43
54
  end
44
55
  end
45
56
  end