active_manageable 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +52 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +42 -0
- data/.rubocop_rails.yml +201 -0
- data/.rubocop_rspec.yml +68 -0
- data/.standard.yml +5 -0
- data/Appraisals +27 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +194 -0
- data/LICENSE.txt +21 -0
- data/README.md +758 -0
- data/Rakefile +8 -0
- data/active_manageable.gemspec +75 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_6_0.gemfile +8 -0
- data/gemfiles/rails_6_1.gemfile +8 -0
- data/gemfiles/rails_7_0.gemfile +8 -0
- data/lib/active_manageable/authorization/cancancan.rb +28 -0
- data/lib/active_manageable/authorization/pundit.rb +28 -0
- data/lib/active_manageable/base.rb +218 -0
- data/lib/active_manageable/configuration.rb +58 -0
- data/lib/active_manageable/methods/auxiliary/includes.rb +98 -0
- data/lib/active_manageable/methods/auxiliary/model_attributes.rb +59 -0
- data/lib/active_manageable/methods/auxiliary/order.rb +43 -0
- data/lib/active_manageable/methods/auxiliary/scopes.rb +59 -0
- data/lib/active_manageable/methods/auxiliary/select.rb +46 -0
- data/lib/active_manageable/methods/auxiliary/unique_search.rb +50 -0
- data/lib/active_manageable/methods/create.rb +20 -0
- data/lib/active_manageable/methods/destroy.rb +23 -0
- data/lib/active_manageable/methods/edit.rb +25 -0
- data/lib/active_manageable/methods/index.rb +49 -0
- data/lib/active_manageable/methods/new.rb +20 -0
- data/lib/active_manageable/methods/show.rb +25 -0
- data/lib/active_manageable/methods/update.rb +23 -0
- data/lib/active_manageable/pagination/kaminari.rb +39 -0
- data/lib/active_manageable/search/ransack.rb +38 -0
- data/lib/active_manageable/version.rb +5 -0
- data/lib/active_manageable.rb +43 -0
- 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).
|