active_manageable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +52 -0
  3. data/.gitignore +20 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +42 -0
  6. data/.rubocop_rails.yml +201 -0
  7. data/.rubocop_rspec.yml +68 -0
  8. data/.standard.yml +5 -0
  9. data/Appraisals +27 -0
  10. data/CHANGELOG.md +5 -0
  11. data/CODE_OF_CONDUCT.md +84 -0
  12. data/Gemfile +6 -0
  13. data/Gemfile.lock +194 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +758 -0
  16. data/Rakefile +8 -0
  17. data/active_manageable.gemspec +75 -0
  18. data/bin/console +15 -0
  19. data/bin/setup +8 -0
  20. data/gemfiles/.bundle/config +2 -0
  21. data/gemfiles/rails_6_0.gemfile +8 -0
  22. data/gemfiles/rails_6_1.gemfile +8 -0
  23. data/gemfiles/rails_7_0.gemfile +8 -0
  24. data/lib/active_manageable/authorization/cancancan.rb +28 -0
  25. data/lib/active_manageable/authorization/pundit.rb +28 -0
  26. data/lib/active_manageable/base.rb +218 -0
  27. data/lib/active_manageable/configuration.rb +58 -0
  28. data/lib/active_manageable/methods/auxiliary/includes.rb +98 -0
  29. data/lib/active_manageable/methods/auxiliary/model_attributes.rb +59 -0
  30. data/lib/active_manageable/methods/auxiliary/order.rb +43 -0
  31. data/lib/active_manageable/methods/auxiliary/scopes.rb +59 -0
  32. data/lib/active_manageable/methods/auxiliary/select.rb +46 -0
  33. data/lib/active_manageable/methods/auxiliary/unique_search.rb +50 -0
  34. data/lib/active_manageable/methods/create.rb +20 -0
  35. data/lib/active_manageable/methods/destroy.rb +23 -0
  36. data/lib/active_manageable/methods/edit.rb +25 -0
  37. data/lib/active_manageable/methods/index.rb +49 -0
  38. data/lib/active_manageable/methods/new.rb +20 -0
  39. data/lib/active_manageable/methods/show.rb +25 -0
  40. data/lib/active_manageable/methods/update.rb +23 -0
  41. data/lib/active_manageable/pagination/kaminari.rb +39 -0
  42. data/lib/active_manageable/search/ransack.rb +38 -0
  43. data/lib/active_manageable/version.rb +5 -0
  44. data/lib/active_manageable.rb +43 -0
  45. metadata +373 -0
data/README.md ADDED
@@ -0,0 +1,758 @@
1
+ # ActiveManageable
2
+
3
+ ![Build Status](https://github.com/CircleSD/active_manageable/actions/workflows/ci.yml/badge.svg?branch=main)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ ActiveManageable provides a framework from which to create business logic "manager" classes in your Ruby on Rails application. Thus extending the MVC pattern to incorporate a business logic layer that sits between the controllers and models.
7
+
8
+ Moving your busines logic into a separate layer provides benefits including:
9
+
10
+ 1. skinny controllers & models
11
+ 2. reusable code that reduces duplication across application & API controllers and background jobs
12
+ 3. isolated unit tests for the business logic, allowing system & integration tests to remain true to their purpose of testing user interaction and the workflow of the application
13
+ 4. clear separation of concerns with controllers responsible for managing requests, views dealing with presentation and models handling attribute level validation and persistence
14
+ 5. clear & consistent interface
15
+
16
+ ActiveManageable business logic manager classes
17
+
18
+ 1. include methods for the seven standard CRUD actions: index, show, new, create, edit, update, and destroy
19
+ 2. can be configured to incorporate authentication, search and pagination logic
20
+ 3. enable specification of the associations to eager load, default attribute values, scopes & order when retrieving records and more
21
+ 4. perform advanced parsing of parameter values for date/datetime/numeric attributes
22
+
23
+ To show how ActiveManageable manager classes can be used to create DRY code in skinny controllers, we’ll refactor the following controller index method that retrieves records with an eager loaded association using Pundit, Ransack & Kaminari.
24
+
25
+ ```ruby
26
+ def index
27
+ search = policy_scope(User).ransack(params[:q])
28
+ search.sorts = "name asc" if q.sorts.empty?
29
+ authorize(User)
30
+ @users = search.result.includes(:address).page(params[:page])
31
+ end
32
+ ```
33
+
34
+ With ActiveManageable configured to use the Pundit, Ransack & Kaminari libraries, the following manager class includes the standard CRUD methods and sets the default order and association to eager load in the index method.
35
+
36
+ ```ruby
37
+ class UserManager < ActiveManageable::Base
38
+ manageable ActiveManageable::ALL_METHODS
39
+ default_order :name
40
+ default_includes :address, methods: :index
41
+ end
42
+ ```
43
+
44
+ Using the manager class, the controller index method can now be rewritten to only include a single call to the index method.
45
+
46
+ ```ruby
47
+ def index
48
+ @users = UserManager.new.index(options: {search: params[:q], page: {number: params[:page]}})
49
+ end
50
+ ```
51
+
52
+ The manager classes provide standard implementations of the seven core CRUD methods. These can be overwritten to perform custom business logic and the classes can also be extended to include the business logic for additional actions, both making use of the internal ActiveManageable methods and variables described in the [Adding Bespoke Methods](#adding-bespoke-methods) section.
53
+
54
+ With an Activity model in a CRM application to manage meetings & tasks, a complete action may be required. This could be implemented as follows:
55
+
56
+ ```ruby
57
+ class ActivityManager < ActiveManageable::Base
58
+ manageable ActiveManageable::ALL_METHODS
59
+
60
+ def complete(id:)
61
+ initialize_state
62
+ @target = model_class.find(id)
63
+ authorize(record: @target, action: :complete?)
64
+ @target.update(completed_by: current_user.id, completed_at: Time.zone.now)
65
+ end
66
+ end
67
+ ```
68
+
69
+ The controller method can then call the manager method, retrieve the activity that was completed and act on the result.
70
+
71
+ ```ruby
72
+ def complete
73
+ result = manager.complete(id: params[:id])
74
+ @activity = manager.object
75
+ # now redirect based on the result
76
+ end
77
+ ```
78
+
79
+ ## Installation
80
+
81
+ Add this line to your application's Gemfile:
82
+
83
+ ```ruby
84
+ gem 'active_manageable'
85
+ ```
86
+
87
+ And then execute:
88
+
89
+ ```ruby
90
+ bundle install
91
+ ```
92
+
93
+ Or install it yourself as:
94
+
95
+ ```ruby
96
+ gem install active_manageable
97
+ ```
98
+
99
+ ## Table of Contents
100
+
101
+ - [Configuration](#configuration)
102
+ - [Current User](#current-user)
103
+ - [Authorization](#authorization)
104
+ - [Class Definition](#class-definition)
105
+ - [Manageable Method](#manageable-method)
106
+ - [Default Includes](#default-includes)
107
+ - [Default Attribute Values](#default-attribute-values)
108
+ - [Default Select](#default-select)
109
+ - [Default Order](#default-order)
110
+ - [Default Scopes](#default-scopes)
111
+ - [Unique Search](#unique-search)
112
+ - [Default Page Size](#default-page-size)
113
+ - [Current Method](#current-method)
114
+ - [Index Method](#index-method)
115
+ - [Authorization Scope](#index-authorization-scope)
116
+ - [Search Option](#index-search-option)
117
+ - [Page Option](#index-page-option)
118
+ - [Order Option](#index-order-option)
119
+ - [Scopes Option](#index-scopes-option)
120
+ - [Includes Option](#index-includes-option)
121
+ - [Select Option](#index-select-option)
122
+ - [Distinct](#index-distinct)
123
+ - [Show Method](#show-method)
124
+ - [Includes Option](#show-includes-option)
125
+ - [Select Option](#show-select-option)
126
+ - [New Method](#new-method)
127
+ - [Create Method](#create-method)
128
+ - [Edit Method](#edit-method)
129
+ - [Includes Option](#edit-includes-option)
130
+ - [Update Method](#update-method)
131
+ - [Includes Option](#update-includes-option)
132
+ - [Destroy Method](#destroy-method)
133
+ - [Includes Option](#destroy-includes-option)
134
+ - [Attribute Value Parsing](#attribute-value-parsing)
135
+ - [Date and DateTime Attribute Values](#date-and-datetime-attribute-values)
136
+ - [Numeric Attribute Values](#numeric-attribute-values)
137
+ - [ActiveManageable Attributes](#activemanageable-attributes)
138
+ - [Adding Bespoke Methods](#adding-bespoke-methods)
139
+ - [Development](#development)
140
+ - [Contributing](#contributing)
141
+ - [License](#license)
142
+ - [Code of Conduct](#code-of-conduct)
143
+
144
+ ## Configuration
145
+
146
+ Create an initializer to configure the optional authorization, search and pagination libraries to use.
147
+
148
+ ```ruby
149
+ ActiveManageable.config do |config|
150
+ config.authorization_library = :pundit # or :cancancan
151
+ config.search_library = :ransack
152
+ config.pagination_library = :kaminari
153
+ end
154
+ ```
155
+
156
+ When eager loading associations the `includes` method is used by default but this can be changed via a configuration option that accepts `:includes`, `:preload` or `:eager_load`
157
+
158
+ ```ruby
159
+ ActiveManageable.config do |config|
160
+ config.default_loading_method = :preload
161
+ end
162
+ ```
163
+
164
+ ActiveManageable will attempt to determine the model class to use based on the class name and `subclass_suffix` configuration option. So if the class is named "AlbumManager" and an `Album` constant exists that will be used as the model class. If you want to use a suffix other than "Manager", the configuration option can be changed or alternatively each class can specify the model class to use when calling the `manageable` method.
165
+
166
+ ```ruby
167
+ ActiveManageable.config do |config|
168
+ config.subclass_suffix = "Concern"
169
+ end
170
+ ```
171
+
172
+ ```ruby
173
+ class BusinessLogic < ActiveManageable::Base
174
+ manageable ActiveManageable::ALL_METHODS, model_class: Album
175
+ end
176
+ ```
177
+
178
+ ## Current User
179
+
180
+ ActiveManageable uses its own `current_user` per-thread module attribute when performing authorization with one of the configuration libraries. This needs to be set before using its methods, for example in an `ApplicationController` filter.
181
+
182
+ ```ruby
183
+ around_action :setup_request
184
+
185
+ def setup_request
186
+ ActiveManageable.current_user = current_user
187
+ yield
188
+ ActiveManageable.current_user = nil
189
+ end
190
+ ```
191
+
192
+ The `current_user` can also be set or overridden for a block using the `with_current_user` method.
193
+
194
+ ```ruby
195
+ manager = AlbumManager.new
196
+ manager.with_current_user(user) do
197
+ manager.show(id: 1)
198
+ end
199
+ ```
200
+
201
+ And is accessible via an instance method.
202
+
203
+ ```ruby
204
+ manager = AlbumManager.new
205
+ manager.current_user
206
+ ```
207
+
208
+ ## Authorization
209
+
210
+ When using one of the configuration authorization libraries, each of the methods will perform authorization for the current user, method and either model class or record. If authorization fails an exception will be raised so you may choose to rescue the relevant exception.
211
+
212
+ Pundit - `Pundit::NotAuthorizedError`
213
+
214
+ CanCanCan - `CanCan::AccessDenied`
215
+
216
+ ## Class Definition
217
+
218
+ ### Manageable Method
219
+
220
+ Create a class that inherits from `ActiveManageable::Base` then use the `manageable` method to specify which methods should be included. Use the `ActiveManageable::ALL_METHODS` constant to include all methods (ie. :index, :show, :new, :create, :edit, :update and :destroy) or pass the required method name symbol(s).
221
+
222
+ ```ruby
223
+ class AlbumManager < ActiveManageable::Base
224
+ manageable ActiveManageable::ALL_METHODS
225
+ end
226
+ ```
227
+
228
+ ```ruby
229
+ class SongManager < ActiveManageable::Base
230
+ manageable :index, :show
231
+ end
232
+ ```
233
+
234
+ ### Default Includes
235
+
236
+ The `default_includes` method sets the default associations to eager load when fetching records in the index, show, edit, update and destroy methods. These defaults are only used if the `:options` argument for those methods does not contain a `:includes` key.
237
+
238
+ ```ruby
239
+ class AlbumManager < ActiveManageable::Base
240
+ manageable ActiveManageable::ALL_METHODS
241
+ default_includes :songs
242
+ end
243
+ ```
244
+
245
+ It accepts a single, array or hash of association names, optional `:methods` in which to eager load the associations and optional `:loading_method` if this needs to be different to the configuration `:default_loading_method`. It also accepts a lambda/proc to execute to return associations with optional `:methods`.
246
+
247
+ ```ruby
248
+ default_includes :songs, :artist, methods: [:index, :show]
249
+ default_includes songs: :artist, loading_method: :preload, methods: [:edit, :update]
250
+ default_includes -> { destroy_includes }, methods: :destroy
251
+
252
+ def destroy_includes
253
+ [:songs, {artist: :songs}]
254
+ end
255
+ ```
256
+
257
+ ### Default Attribute Values
258
+
259
+ The `default_attribute_values` the default attribute values to use when building a model object in the new and create methods. These defaults are combined with the attribute values from `:attributes` argument for those methods. When default and argument values contain the same attribute key, the value from the argument is used.
260
+
261
+ ```ruby
262
+ class AlbumManager < ActiveManageable::Base
263
+ manageable ActiveManageable::ALL_METHODS
264
+ default_attribute_values genre: "pop"
265
+ end
266
+ ```
267
+
268
+ It accepts either a hash of attribute values or a lambda/proc to execute to return a hash of attribute values and optional `:methods` in which in which to use the attribute values.
269
+
270
+ ```ruby
271
+ default_attribute_values genre: "pop", released_at: Date.current, methods: :new
272
+ default_attribute_values -> { create_attrs } , methods: :create
273
+
274
+ def create_attrs
275
+ {genre: "electronic", published_at: Date.current}
276
+ end
277
+ ```
278
+
279
+ ### Default Select
280
+
281
+ The `default_select` method sets the attributes to return in the SELECT statement used when fetching records in the index, show and edit methods. These defaults are only used if the `:options` argument for those methods does not contain a `:select` key.
282
+
283
+ ```ruby
284
+ class AlbumManager < ActiveManageable::Base
285
+ manageable ActiveManageable::ALL_METHODS
286
+ default_select :id, :name
287
+ end
288
+ ```
289
+
290
+ It accepts either an array of attribute names or a lambda/proc to execute to return an array of attribute names and optional `:methods` in which to use the attributes.
291
+
292
+ ```ruby
293
+ default_select :id, :name, :genre, methods: :show
294
+ default_select -> { select_attributes }, methods: [:index, :edit]
295
+
296
+ def select_attributes
297
+ [:id, :name, :genre, :released_at]
298
+ end
299
+ ```
300
+
301
+ ### Default Order
302
+
303
+ The `default_order` method sets the default order to use when fetching records in the index method. These defaults are only used if the `:options` argument for the method does not contain an `:order` key.
304
+
305
+ ```ruby
306
+ class AlbumManager < ActiveManageable::Base
307
+ manageable ActiveManageable::ALL_METHODS
308
+ default_order :name
309
+ end
310
+ ```
311
+
312
+ It accepts attributes in the same formats as the `ActiveRecord` order method or a lambda/proc to execute to return attributes in the recognised formats.
313
+
314
+ ```ruby
315
+ default_order "name DESC"
316
+ ```
317
+
318
+ ```ruby
319
+ default_order [:name, :id]
320
+ ```
321
+
322
+ ```ruby
323
+ default_order -> { order_attributes }
324
+
325
+ def order_attributes
326
+ ["name DESC", "id"]
327
+ end
328
+ ```
329
+
330
+ ### Default Scopes
331
+
332
+ The `default_scopes` method sets the default scope(s) to use when fetching records in the index method. These defaults are only used if the `:options` argument for the method does not contain a `:scopes` key.
333
+
334
+ ```ruby
335
+ class AlbumManager < ActiveManageable::Base
336
+ manageable ActiveManageable::ALL_METHODS
337
+ default_scopes :electronic
338
+ end
339
+ ```
340
+
341
+ It accepts a scope name, a hash containing scope name and argument, or an array of names/hashes. It also accepting a lambda/proc to execute to return a scope name, hash or array.
342
+
343
+ ```ruby
344
+ default_scopes {released_in_year: "1980"}
345
+ ```
346
+
347
+ ```ruby
348
+ default_scopes :rock, :electronic, {released_in_year: "1980"}
349
+ ```
350
+
351
+ ```ruby
352
+ default_scopes -> { index_scopes }
353
+
354
+ def index_scopes
355
+ [:rock, :electronic]
356
+ end
357
+ ```
358
+
359
+ ### Unique Search
360
+
361
+ The `has_unique_search` method specifies whether to use the distinct method when fetching records in the index method.
362
+
363
+ ```ruby
364
+ class AlbumManager < ActiveManageable::Base
365
+ manageable ActiveManageable::ALL_METHODS
366
+ has_unique_search
367
+ end
368
+ ```
369
+
370
+ It accepts no argument to always return unique records or a hash with :if or :unless keyword and a method name or lambda/proc to execute each time the index method is called.
371
+
372
+ ```ruby
373
+ has_unique_search if: :method_name
374
+ ```
375
+
376
+ ```ruby
377
+ has_unique_search unless: -> { lambda }
378
+ ```
379
+
380
+ ### Default Page Size
381
+
382
+ When using the [Kaminari](https://github.com/kaminari/kaminari) pagination library, the `default_page_size` method sets default page size to use when fetching records in the index method. The default is only used if the `:options` argument for the method does not contain a `:page` hash with a `:size` key.
383
+
384
+ ```ruby
385
+ class AlbumManager < ActiveManageable::Base
386
+ manageable ActiveManageable::ALL_METHODS
387
+ default_page_size 5
388
+ end
389
+ ```
390
+
391
+ ### Current Method
392
+
393
+ ActiveManageable includes a `current_method` attribute which returns the name of the method being executed as a symbol, which can potentially be used within methods in conjunction with a lambda for the default methods described above. Additionally, the method argument `options` and `attributes` are also accessible as attributes.
394
+
395
+ ```ruby
396
+ default_includes -> { method_includes }
397
+
398
+ def method_includes
399
+ case current_method
400
+ when :index
401
+ {songs: :artist}
402
+ when :show
403
+ [:label, :songs]
404
+ when :edit, :update
405
+ options.key?(:xyz) ? [:label, songs: :artists] : [:label, :songs]
406
+ else
407
+ :songs
408
+ end
409
+ end
410
+ ```
411
+
412
+ ## Index Method
413
+
414
+ The `index` method has an optional `options` keyword argument. The `options` hash can contain `:search`, `:order`, `:scopes`, `:page`, `:includes` and `:select` keys. The method performs authorization for the current user, method and model class using the configuration library; retrieves record using the various options described below; and returns the records which are also accessible via the `collection` attribute.
415
+
416
+ ```ruby
417
+ manager.index
418
+ ```
419
+
420
+ ### Index Authorization Scope
421
+
422
+ When using one of the configuration authorization libraries, the method retrieves records that the current user is authorized to access. For the Pundit authorization library, the method retrieves records filtered using the model's [policy scope](https://github.com/varvet/pundit#scopes). For the CanCanCan authorization library, the method retrieves records filtered using the [accessible_by scope](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/fetching_records.md) for the current user's ability.
423
+
424
+ ### Index Search Option
425
+
426
+ When using the [Ransack](https://github.com/activerecord-hackery/ransack) search library, the `options` argument `:search` key is used to set the Ransack filter and sorting. If either the `:search` key or its sorts `:s` key is not present, the method will order the records using the standard approach described below. The Ransack search object is accessible via the `ransack` attribute.
427
+
428
+ ```ruby
429
+ manager.index(options: {search: {artist_id_eq: 1, s: "name ASC"}})
430
+ ransack_search = manager.ransack
431
+ ```
432
+
433
+ ### Index Page Option
434
+
435
+ When using the [Kaminari](https://github.com/kaminari/kaminari) pagination library, the `options` argument `:page` hash is used to set the page number and size of records to retrieve. The page number is set using the `:number` key value and page size is set using the `:size` key value. If the `:size` key is not present, the class default is used and if a class default has not been set then the Kaminari application default is used.
436
+
437
+ ```ruby
438
+ manager.index(options: {page: {number: 2, size: 10}})
439
+ ```
440
+
441
+ ### Index Order Option
442
+
443
+ The `options` argument `:order` key provides the ability to specify the order in which to retrieve records and accepts attributes in the same formats as the `ActiveRecord` `order` method. When the `:order` key is not present, any class defaults are used.
444
+
445
+ ```ruby
446
+ manager.index(options: {order: "name DESC"})
447
+ ```
448
+
449
+ ### Index Scopes Option
450
+
451
+ The `options` argument `:scopes` key provides the ability to specify the scopes to use when retrieving records and accepts a scope name, a hash containing scope name and argument, or an array of names/hashes. When the `:scopes` key is not present, any class defaults are used.
452
+
453
+ ```ruby
454
+ manager.index(options: {scopes: :electronic})
455
+ ```
456
+
457
+ ```ruby
458
+ manager.index(options: {scopes: {released_in_year: "1980"}})
459
+ ```
460
+
461
+ ```ruby
462
+ manager.index(options: {scopes: [:rock, :electronic, {released_in_year: "1980"}]})
463
+ ```
464
+
465
+ ### Index Includes Option
466
+
467
+ The `options` argument `:includes` key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR `includes` method eg. a single association name, an array of names or a hash of names. When the `:includes` key is not present, any class defaults are used.
468
+
469
+ ```ruby
470
+ manager.index(options: {includes: [:artist, :songs]})
471
+ ```
472
+
473
+ The `:includes` key can also be used to vary the method used to eager load associations by providing `:associations` and `:loading_method` keys. When the `:loading_method` key is not present the method will use either the class default method (set using `default_includes`) or the configuration `default_loading_method`.
474
+
475
+ ```ruby
476
+ manager.index(options: {includes: {associations: :songs, loading_method: :preload}})
477
+ ```
478
+
479
+ ### Index Select Option
480
+
481
+ The `options` argument `:select` key provides the ability to limit the attributes returned in the SELECT statement. When the `:select` key is not present, any class defaults are used.
482
+
483
+ ```ruby
484
+ manager.index(options: {select: [:id, :name, :artist_id, :released_at]})
485
+ ```
486
+
487
+ ### Index Distinct
488
+
489
+ If the class `has_unique_search` method has been used then this will be evaluated to determine whether to use the distinct method when fetching the records.
490
+
491
+ ## Show Method
492
+
493
+ The `show` method has `id` and optional `options` keyword arguments. The `options` hash can contain `:includes` and `:select` keys. The method retrieves a record; performs authorization for the current user, method and record using the configuration library; and returns the record which is also accessible via the `object` attribute.
494
+
495
+ ```ruby
496
+ manager.show(id: 1)
497
+ ```
498
+
499
+ ### Show Includes Option
500
+
501
+ The `options` argument `:includes` key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR `includes` method eg. a single association name, an array of names or a hash of names. When the `:includes` key is not present, any class defaults are used.
502
+
503
+ ```ruby
504
+ manager.show(id: 1, options: {includes: [:artist, :songs]})
505
+ ```
506
+
507
+ The `:includes` key can also be used to vary the method used to eager load associations by providing `:associations` and `:loading_method` keys. When the `:loading_method` key is not present the method will use either the class default method (set using `default_includes`) or the configuration `default_loading_method`.
508
+
509
+ ```ruby
510
+ manager.show(id: 1, options: {includes: {associations: :songs, loading_method: :preload}})
511
+ ```
512
+
513
+ ### Show Select Option
514
+
515
+ The `options` argument `:select` key provides the ability to limit the attributes returned in the SELECT statement. When the `:select` key is not present, any class defaults are used.
516
+
517
+ ```ruby
518
+ manager.show(id: 1, options: {select: [:id, :name, :artist_id, :released_at]})
519
+ ```
520
+
521
+ ## New Method
522
+
523
+ The `new` method has an optional `attributes` keyword argument. The `attributes` argument is for an `ActionController::Parameters` or hash of attribute names and values to use when building the record. The method builds a record; performs authorization for the current user, method and record using the configuration library; and returns the record which is also accessible via the `object` attribute.
524
+
525
+ ```ruby
526
+ manager.new
527
+ ```
528
+
529
+ The `attributes` argument values are combined with the class default values and when the default and argument values contain the same attribute key, the value from the argument is used.
530
+
531
+ ```ruby
532
+ manager.new(attributes: {genre: "electronic", published_at: Date.current})
533
+ ```
534
+
535
+ ## Create Method
536
+
537
+ The `create` method has an `attributes` keyword argument. The `attributes` argument is for an `ActionController::Parameters` or hash of attribute names and values to use when building the record. The method builds a record; performs authorization for the current user, method and record using the configuration library; attempts to save the record and returns the save result. The record is also accessible via the `object` attribute.
538
+
539
+ ```ruby
540
+ manager.create(attributes: {name: "Substance", genre: "electronic", published_at: Date.current})
541
+ ```
542
+
543
+ The `attributes` argument values are combined with the class default values and when the default and argument values contain the same attribute key, the value from the argument is used.
544
+
545
+ ## Edit Method
546
+
547
+ The `edit` method has `id` and optional `options` keyword arguments. The `options` hash can contain `:includes` and `:select` keys. The method retrieves a record; performs authorization for the current user, method and record using the configuration library; and returns the record which is also accessible via the `object` attribute.
548
+
549
+ ```ruby
550
+ manager.edit(id: 1)
551
+ ```
552
+
553
+ ### Edit Includes Option
554
+
555
+ The `options` argument `:includes` key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR `includes` method eg. a single association name, an array of names or a hash of names. The `:select` key provides the ability to limit the attributes returned in the SELECT statement. When the `:includes` and `:select` keys are not present, any class defaults are used.
556
+
557
+ ```ruby
558
+ manager.edit(id: 1, options: {includes: [:artist, :songs], select: [:id, :name, :artist_id, :released_at]})
559
+ ```
560
+
561
+ The `:includes` key can also be used to vary the method used to eager load associations by providing `:associations` and `:loading_method` keys. When the `:loading_method` key is not present the method will use either the class default method (set using `default_includes`) or the configuration `default_loading_method`.
562
+
563
+ ```ruby
564
+ manager.edit(id: 1, options: {includes: {associations: :songs, loading_method: :preload}})
565
+ ```
566
+
567
+ ## Update Method
568
+
569
+ The `update` method has `id`, `attributes` and optional `options` keyword arguments. The `attributes` argument is for an `ActionController::Parameters` or hash of attribute names and values to use when updating the record. The `options` hash can contain an `:includes` key. The method retrieves a record; performs authorization for the current user, method and record using the configuration library; updates the attributes; attempts to save the record and returns the save result. The record is also accessible via the `object` attribute.
570
+
571
+ ```ruby
572
+ manager.update(id: 1, attributes: {genre: "electronic", published_at: Date.current})
573
+ ```
574
+
575
+ ### Update Includes Option
576
+
577
+ The `options` argument `:includes` key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR `includes` method eg. a single association name, an array of names or a hash of names. When the `:includes` key is not present, any class defaults are used.
578
+
579
+ ```ruby
580
+ manager.update(id: 1, attributes: {published_at: Date.current}, options: {includes: [:artist]})
581
+ ```
582
+
583
+ The `:includes` key can also be used to vary the method used to eager load associations by providing `:associations` and `:loading_method` keys. When the `:loading_method` key is not present the method will use either the class default method (set using `default_includes`) or the configuration `default_loading_method`.
584
+
585
+ ```ruby
586
+ manager.update(id: 1, attributes: {published_at: Date.current}, options: {includes: {associations: :songs, loading_method: :preload}})
587
+ ```
588
+
589
+ ## Destroy Method
590
+
591
+ The `destroy` method has `id` and optional `options` keyword arguments. The `options` hash can contain an `:includes` key. The method retrieves a record; performs authorization for the current user, method and record using the configuration library; attempts to destroy the record and returns the destroy result. The record is accessible via the `object` attribute.
592
+
593
+ ```ruby
594
+ manager.destroy(id: 1)
595
+ ```
596
+
597
+ ### Destroy Includes Option
598
+
599
+ The `options` argument `:includes` key provides the ability to specify associations to eager load and accepts associations names in the same formats as the AR `includes` method eg. a single association name, an array of names or a hash of names. When the `:includes` key is not present, any class defaults are used.
600
+
601
+ ```ruby
602
+ manager.destroy(id: 1, options: {includes: [:artist]})
603
+ ```
604
+
605
+ The `:includes` key can also be used to vary the method used to eager load associations by providing `:associations` and `:loading_method` keys. When the `:loading_method` key is not present the method will use either the class default method (set using `default_includes`) or the configuration `default_loading_method`.
606
+
607
+ ```ruby
608
+ manager.destroy(id: 1, options: {includes: {associations: :songs, loading_method: :preload}})
609
+ ```
610
+
611
+ ## Attribute Value Parsing
612
+
613
+ ### Date and DateTime Attribute Values
614
+
615
+ If you have users in the US where the date format is month/day/year you'll be aware that `ActiveRecord` does not support that string format. The issue is further complicated if you also have users in other countries that use the day/month/year format.
616
+
617
+ ```ruby
618
+ I18n.locale = :"en-US"
619
+ Album.new(published_at: "12/22/2022 14:21").published_at # => nil
620
+ ```
621
+
622
+ ActiveManageable caters for these different formats and provides greater flexibility to accept a wider variety of formats by parsing date and datetime values using the [Flexitime gem](https://github.com/CircleSD/flexitime) before setting a model object's attribute values. Flexitime uses the [rails-i18n gem](https://github.com/svenfuchs/rails-i18n) to determine whether the first date part is day or month and then returns an ActiveSupport [TimeZone](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html) object. ActiveManageable updates the `attributes` argument for the new, create and update methods to replace the value for any attributes with a data type of date or datetime and also updates the attributes values for any associations within the attributes hash.
623
+
624
+ ```ruby
625
+ I18n.locale = :"en-US"
626
+ ActiveManageable.current_user = User.first
627
+ manager = AlbumManager.new
628
+ manager.new(attributes: {published_at: "12/01/2022 14:21", songs_attributes: [{published_at: "12/01/2022 14:21"}]})
629
+ manager.object.published_at # => Thu, 01 Dec 2022 14:21:00.000000000 UTC +00:00
630
+ manager.object.songs.first.published_at # => Thu, 01 Dec 2022 14:21:00.000000000 UTC +00:00
631
+ manager.attributes # => {"published_at"=>Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00, ... }]}
632
+ ```
633
+
634
+ ```ruby
635
+ I18n.locale = :"en-GB"
636
+ ActiveManageable.current_user = User.first
637
+ manager = AlbumManager.new
638
+ manager.new(attributes: {published_at: "12/01/2022 14:21", songs_attributes: [{published_at: "12/01/2022 14:21"}]})
639
+ manager.object.published_at # => Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00
640
+ manager.object.songs.first.published_at # => Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00
641
+ manager.attributes # => {"published_at"=>Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00, ... }]}
642
+ ```
643
+
644
+ By default, the [Flexitime gem](https://github.com/CircleSD/flexitime) `parse` method returns time objects with a minute precision so to persist datetime values with seconds or milliseconds it is necessary to set the Flexitime configuration option accordingly.
645
+
646
+ ```ruby
647
+ ActiveManageable.current_user = User.first
648
+ manager = AlbumManager.new
649
+ manager.new(attributes: {published_at: "12/01/2022 14:21:45"})
650
+ manager.object.published_at # => Wed, 12 Jan 2022 14:21:00.000000000 UTC +00:00
651
+
652
+ Flexitime.precision = :sec
653
+ manager.new(attributes: {published_at: "12/01/2022 14:21:45"})
654
+ manager.object.published_at # => Wed, 12 Jan 2022 14:21:45.000000000 UTC +00:00
655
+ ```
656
+
657
+ ### Numeric Attribute Values
658
+
659
+ If you have users in the Netherlands or other countries that use a comma number separator then you ideally want to allow them to enter numeric values using that separator rather than a point separator. Unfortunately `ActiveRecord` does not support such a separator when setting attributes values.
660
+
661
+ ```ruby
662
+ I18n.locale = :nl
663
+ Album.new(length: "6,55").length.to_s # => "6.0"
664
+ ```
665
+
666
+ ActiveManageable caters for the comma number separator by replacing the comma with a point before setting a model object's attribute values. It uses the [rails-i18n gem](https://github.com/svenfuchs/rails-i18n) to determine if the locale number separator is a comma. It then updates the `attributes` argument for the new, create and update methods to replace the comma for any attributes with a data type of decimal or float and a value that contains only a single comma and no points. It also updates the attributes values for any associations within the attributes hash.
667
+
668
+ ```ruby
669
+ I18n.locale = :nl
670
+ ActiveManageable.current_user = User.first
671
+ manager = AlbumManager.new
672
+ manager.new(attributes: {length: "6,55", songs_attributes: [{length: "8,3"}]})
673
+ manager.object.length.to_s # => "6.55"
674
+ manager.object.songs.first.length.to_s # => "8.3"
675
+ manager.attributes # => {"length"=>"6.55", "songs_attributes"=>[{"length"=>"8.3"}]}
676
+ ```
677
+
678
+ ## ActiveManageable Attributes
679
+
680
+ ActiveManageable includes the following attributes:
681
+
682
+ `object` - the record from the show, new, create, edit, update and destroy methods
683
+
684
+ `collection` - the records retrieved by the index method
685
+
686
+ `current_method` - the name of the method being executed as a symbol eg. `:show`
687
+
688
+ `attributes` - an `ActiveSupport::HashWithIndifferentAccess` representation of the argument from the new, create and update methods (in the case of an `ActionController::Parameters` the attribute contains only the permitted keys)
689
+
690
+ `options` - an `ActiveSupport::HashWithIndifferentAccess` representation of the argument from the index, show, edit, update and destroy methods
691
+
692
+ `ransack` - the Ransack search object used when retrieving records in the index method (when using the Ransack search library)
693
+
694
+ ## Adding Bespoke Methods
695
+
696
+ The manager classes provide standard implementations of the seven core CRUD methods. These can be overwritten to perform custom business logic and the classes can also be extended to include the business logic for additional actions, both making use of the internal ActiveManageable methods and variables.
697
+
698
+ ```ruby
699
+ def complete(id:)
700
+ initialize_state
701
+ @target = model_class.find(id)
702
+ authorize(record: @target, action: :complete?)
703
+ @target.update(completed_by: current_user.id, completed_at: Time.zone.now)
704
+ end
705
+ ```
706
+
707
+ Each method should first call the `initialize_state` method which has optional `attributes` and `options` keyword arguments. This method sets the `@target` variable to nil, sets the `@current_method` variable to the name of the method being executed as a symbol (eg. `:complete`) and sets the `@attributes` and `@options` variables after performing [attribute values parsing](#attribute-value-parsing).
708
+
709
+ The `model_class` method returns the `ActiveRecord` class set either automatically or manually when calling `manageable`.
710
+
711
+ The `@target` instance variable makes the model object or `ActiveRecord::Relation` (in the case of the index method) accessible to the internal ActiveManageable methods. For external access, there are less ambiguous alias methods named `object` and `collection`.
712
+
713
+ The `authorize` method performs authorization for the current user, record and action using the configuration library. The `record` argument can be a model class or instance and the `action` argument is optional with the default being the method name.
714
+
715
+ ## Development
716
+
717
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
718
+
719
+ You can also experiment in the rails console using the dummy app. Within the spec/dummy directory:
720
+
721
+ 1. run `bin/rails db:setup` to create the database, load the schema, and initialize it with the seed data
722
+ 2. run `rails c`
723
+
724
+ Then in the console:
725
+
726
+ ```ruby
727
+ ActiveManageable.current_user = User.first
728
+ manager = AlbumManager.new
729
+ manager.index
730
+ ```
731
+
732
+ After making changes:
733
+
734
+ 1. run `rake spec` to run the tests and check the test coverage
735
+ 2. run `open coverage/index.html` to view the test coverage report
736
+ 3. run `bundle exec appraisal install` to install the appraisal dependencies
737
+ 4. run `bundle exec appraisal rspec` to run the tests against different versions of activerecord & activesupport
738
+ 5. run `bundle exec rubocop` to check the style of files
739
+
740
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
741
+
742
+ ## Contributing
743
+
744
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/CircleSD/active_manageable). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/CircleSD/active_manageable/blob/main/CODE_OF_CONDUCT.md).
745
+
746
+ 1. Fork it
747
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
748
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
749
+ 4. Push to the branch (`git push origin my-new-feature`)
750
+ 5. Create new Pull Request
751
+
752
+ ## License
753
+
754
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
755
+
756
+ ## Code of Conduct
757
+
758
+ Everyone interacting in the ActiveManageable project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/CircleSD/active_manageable/blob/main/CODE_OF_CONDUCT.md).