active_manageable 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8645a9930ecc8e01e70b9a98c188c832ffe543b0599d9e819f640cc64d6d88fd
4
- data.tar.gz: 8354ce84a33378e123b3a299c1e6db2218b1f9611898275340ea003a2cae608e
3
+ metadata.gz: 33e6916eea9e70cfa5c04813328b8e1cb21e93f3f2638705b2ec0b37748de51c
4
+ data.tar.gz: 37122950ad83b210bd043ce2fc556032451df60deef18dc4f58cead9917f1f1f
5
5
  SHA512:
6
- metadata.gz: 2334299accc2984ac5e590b3dd9d253b67188302ef4d86688a56cc8c6f29a5b893ac29076a00d871bf3dd87c0e3e1067c317d799ec4adfc356fe231bd86ebaad
7
- data.tar.gz: 3dc07fc9f551910ce7250f44901222ffffb04b0183b163fcc23067c574a50132703b43aab3df1fcc5347b077039a64f39f7e3b444a8064b6687db109eda6bb87
6
+ metadata.gz: d2afeaad9032f497b31208e0e4c8b21e209b40d81346dc1468a3eefdb251e1d6c5c01f6759d58e2cab99782116920845077fd62bcb90a54e20ac7c322c556647
7
+ data.tar.gz: 608affa3ab36c003c03c0af6f27d09b404cfd6e8b987bd6ec57b3605b7f93910b95409073afda386f1e4fb33998cb07bdcc1428ddcf23ae3bac5eb2e2b0eefb7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.2.0 - 2022-09-27
4
+
5
+ * Allow the configuration library options to accept a module
6
+ * Add `action_scope` method that can be overridden in order to retrieve and maintain records using a scope
7
+ * Rename `scoped_class` method to `authorization_scope`
8
+ * Move instance method definitions out of the ActiveSupport::Concern included block so that they can be overridden
9
+ * Split action methods into smaller overridable methods
10
+ * Add a yield to each of the action methods
11
+ * Perform yield and object create, update & destroy within a transaction
12
+ * Add public instance methods to return the default options
13
+ * When using the Kaminari library provide the ability to use the `without_count` mode to create a paginatable collection without counting the total number of records; either via the index method options or a class option or configuration option
14
+
3
15
  ## 0.1.2 - 2022-08-25
4
16
 
5
17
  * Upgrade gems including activerecord, actionpack, actionview, nokogiri & rack to resolve security advisory alerts
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_manageable (0.1.2)
4
+ active_manageable (0.2.0)
5
5
  activerecord (>= 6.0)
6
6
  activesupport (>= 6.0)
7
7
  flexitime (~> 1.0)
data/README.md CHANGED
@@ -49,7 +49,7 @@ def index
49
49
  end
50
50
  ```
51
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.
52
+ The manager classes provide standard implementations of the seven core CRUD methods. These can be extended or 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
53
 
54
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
55
 
@@ -110,6 +110,7 @@ gem install active_manageable
110
110
  - [Default Scopes](#default-scopes)
111
111
  - [Unique Search](#unique-search)
112
112
  - [Default Page Size](#default-page-size)
113
+ - [Paginate Without Count](#paginate-without-count)
113
114
  - [Current Method](#current-method)
114
115
  - [Index Method](#index-method)
115
116
  - [Authorization Scope](#index-authorization-scope)
@@ -131,6 +132,8 @@ gem install active_manageable
131
132
  - [Includes Option](#update-includes-option)
132
133
  - [Destroy Method](#destroy-method)
133
134
  - [Includes Option](#destroy-includes-option)
135
+ - [Extending the CRUD Methods](#extending-the-crud-methods)
136
+ - [Build and Retrieve Scoped Records](#build-and-retrieve-scoped-records)
134
137
  - [Attribute Value Parsing](#attribute-value-parsing)
135
138
  - [Date and DateTime Attribute Values](#date-and-datetime-attribute-values)
136
139
  - [Numeric Attribute Values](#numeric-attribute-values)
@@ -143,7 +146,7 @@ gem install active_manageable
143
146
 
144
147
  ## Configuration
145
148
 
146
- Create an initializer to configure the optional authorization, search and pagination libraries to use.
149
+ Create an initializer to configure the optional in-built authorization, search and pagination libraries to use.
147
150
 
148
151
  ```ruby
149
152
  ActiveManageable.config do |config|
@@ -153,6 +156,8 @@ ActiveManageable.config do |config|
153
156
  end
154
157
  ```
155
158
 
159
+ These library configuration options can also be set to a module in order to use a custom authorization, search or pagination implementation.
160
+
156
161
  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
162
 
158
163
  ```ruby
@@ -388,6 +393,31 @@ class AlbumManager < ActiveManageable::Base
388
393
  end
389
394
  ```
390
395
 
396
+ ### Paginate Without Count
397
+
398
+ When using the [Kaminari](https://github.com/kaminari/kaminari) pagination library, the `paginate_without_count` method will result in the index method using the Kaminari `without_count` mode to create a paginatable collection without counting the total number of records.
399
+
400
+ ```ruby
401
+ class AlbumManager < ActiveManageable::Base
402
+ manageable :index
403
+ paginate_without_count
404
+ end
405
+ ```
406
+
407
+ It is also possible to control whether to use the `without_count` mode globally by setting the `paginate_without_count` configuration option.
408
+
409
+ ```ruby
410
+ ActiveManageable.config do |config|
411
+ config.paginate_without_count = true
412
+ end
413
+ ```
414
+
415
+ And it is also possible to control whether to use the `without_count` mode for an individual index method call by including the `:without_count` key within the `options` argument `:page` hash.
416
+
417
+ ```ruby
418
+ manager.index(options: {page: {number: 2, size: 10, without_count: true}})
419
+ ```
420
+
391
421
  ### Current Method
392
422
 
393
423
  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.
@@ -411,7 +441,7 @@ end
411
441
 
412
442
  ## Index Method
413
443
 
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.
444
+ 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; invokes a block (if provided); retrieves records using the various options described below; and returns the records which are also accessible via the `collection` attribute.
415
445
 
416
446
  ```ruby
417
447
  manager.index
@@ -490,7 +520,7 @@ If the class `has_unique_search` method has been used then this will be evaluate
490
520
 
491
521
  ## Show Method
492
522
 
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.
523
+ The `show` method has `id` and optional `options` keyword arguments. The `options` hash can contain `:includes` and `:select` keys. The method invokes a block (if provided); 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
524
 
495
525
  ```ruby
496
526
  manager.show(id: 1)
@@ -520,7 +550,7 @@ manager.show(id: 1, options: {select: [:id, :name, :artist_id, :released_at]})
520
550
 
521
551
  ## New Method
522
552
 
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.
553
+ 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; invokes a block (if provided); and returns the record which is also accessible via the `object` attribute.
524
554
 
525
555
  ```ruby
526
556
  manager.new
@@ -534,7 +564,7 @@ manager.new(attributes: {genre: "electronic", published_at: Date.current})
534
564
 
535
565
  ## Create Method
536
566
 
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.
567
+ 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; invokes a block (if provided) and attempts to save the record within a transaction; and returns the save result. The record is also accessible via the `object` attribute.
538
568
 
539
569
  ```ruby
540
570
  manager.create(attributes: {name: "Substance", genre: "electronic", published_at: Date.current})
@@ -544,7 +574,7 @@ The `attributes` argument values are combined with the class default values and
544
574
 
545
575
  ## Edit Method
546
576
 
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.
577
+ The `edit` method has `id` and optional `options` keyword arguments. The `options` hash can contain `:includes` and `:select` keys. The method invokes a block (if provided); 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
578
 
549
579
  ```ruby
550
580
  manager.edit(id: 1)
@@ -566,7 +596,7 @@ manager.edit(id: 1, options: {includes: {associations: :songs, loading_method: :
566
596
 
567
597
  ## Update Method
568
598
 
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.
599
+ 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; assigns the attributes; invokes a block (if provided) and attempts to save the record within a transaction; and returns the save result. The record is also accessible via the `object` attribute.
570
600
 
571
601
  ```ruby
572
602
  manager.update(id: 1, attributes: {genre: "electronic", published_at: Date.current})
@@ -588,7 +618,7 @@ manager.update(id: 1, attributes: {published_at: Date.current}, options: {includ
588
618
 
589
619
  ## Destroy Method
590
620
 
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.
621
+ 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; invokes a block (if provided) and attempts to destroy the record within a transaction; and returns the destroy result. The record is accessible via the `object` attribute.
592
622
 
593
623
  ```ruby
594
624
  manager.destroy(id: 1)
@@ -608,6 +638,37 @@ The `:includes` key can also be used to vary the method used to eager load assoc
608
638
  manager.destroy(id: 1, options: {includes: {associations: :songs, loading_method: :preload}})
609
639
  ```
610
640
 
641
+ ## Extending the CRUD Methods
642
+
643
+ Each of the seven core CRUD methods include a `yield` so it is possible to extend the methods by passing a block.
644
+
645
+ ```ruby
646
+ def create(attributes:)
647
+ super do
648
+ @target.description = "change the object before it is created using a block"
649
+ end
650
+ end
651
+ ```
652
+
653
+ The logic within each of the methods has also been split into subsidiary methods so it is possible to extend or override these methods.
654
+
655
+ ```ruby
656
+ def create_object
657
+ @target.description = "change the object before it is created via an override"
658
+ super
659
+ end
660
+ ```
661
+
662
+ ## Build and Retrieve Scoped Records
663
+
664
+ Each of the seven core CRUD methods build and retrieve records using the `action_scope` method which by default returns the model class. In order to build or retrieve records using a scope it is possible to override this method.
665
+
666
+ ```ruby
667
+ def action_scope
668
+ current_user.albums
669
+ end
670
+ ```
671
+
611
672
  ## Attribute Value Parsing
612
673
 
613
674
  ### Date and DateTime Attribute Values
@@ -693,7 +754,7 @@ ActiveManageable includes the following attributes:
693
754
 
694
755
  ## Adding Bespoke Methods
695
756
 
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.
757
+ The manager classes provide standard implementations of the seven core CRUD methods. These can be extended or 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
758
 
698
759
  ```ruby
699
760
  def complete(id:)
@@ -737,7 +798,16 @@ After making changes:
737
798
  4. run `bundle exec appraisal rspec` to run the tests against different versions of activerecord & activesupport
738
799
  5. run `bundle exec rubocop` to check the style of files
739
800
 
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).
801
+ To install this gem onto your local machine, run `bundle exec rake install`.
802
+
803
+ To release a new version:
804
+
805
+ 1. update the version number in `version.rb`
806
+ 2. update the change log in `CHANGELOG.md`
807
+ 3. run `bundle` to update the `Gemfile.lock` version
808
+ 4. commit the changes
809
+ 5. run `bundle exec rake release` to create & push a git tag for the version and push the `.gem` file to [rubygems.org](https://rubygems.org).
810
+ 6. create a new release for the version on GitHub
741
811
 
742
812
  ## Contributing
743
813
 
@@ -7,21 +7,19 @@ module ActiveManageable
7
7
  module CanCanCan
8
8
  extend ActiveSupport::Concern
9
9
 
10
- included do
11
- private
10
+ private
12
11
 
13
- def authorize(record:, action: nil)
14
- action ||= @current_method
15
- current_ability.authorize!(action, record)
16
- end
12
+ def authorize(record:, action: nil)
13
+ action ||= @current_method
14
+ current_ability.authorize!(action, record)
15
+ end
17
16
 
18
- def scoped_class
19
- model_class.accessible_by(current_ability)
20
- end
17
+ def authorization_scope
18
+ model_class.accessible_by(current_ability)
19
+ end
21
20
 
22
- def current_ability
23
- ::Ability.new(current_user)
24
- end
21
+ def current_ability
22
+ ::Ability.new(current_user)
25
23
  end
26
24
  end
27
25
  end
@@ -7,21 +7,19 @@ module ActiveManageable
7
7
  module Pundit
8
8
  extend ActiveSupport::Concern
9
9
 
10
- included do
11
- private
10
+ private
12
11
 
13
- def authorize(record:, action: nil)
14
- action ||= authorize_action
15
- ::Pundit.authorize(current_user, record, action)
16
- end
12
+ def authorize(record:, action: nil)
13
+ action ||= authorize_action
14
+ ::Pundit.authorize(current_user, record, action)
15
+ end
17
16
 
18
- def scoped_class
19
- ::Pundit.policy_scope(current_user, model_class)
20
- end
17
+ def authorization_scope
18
+ ::Pundit.policy_scope(current_user, model_class)
19
+ end
21
20
 
22
- def authorize_action
23
- "#{@current_method}?".to_sym
24
- end
21
+ def authorize_action
22
+ "#{@current_method}?".to_sym
25
23
  end
26
24
  end
27
25
  end
@@ -4,12 +4,13 @@ module ActiveManageable
4
4
  class_attribute :defaults, instance_writer: false, instance_predicate: false
5
5
  class_attribute :module_initialize_state_methods, instance_writer: false, instance_predicate: false
6
6
 
7
- attr_reader :target, :current_method, :attributes, :options
7
+ attr_accessor :target
8
+ attr_reader :current_method, :attributes, :options
8
9
 
9
10
  # target provides a common variable to use within the CRUD, auxiliary & library methods
10
11
  # whereas the object & collection methods provide less ambiguous external access
11
- alias_method :object, :target
12
- alias_method :collection, :target
12
+ alias_attribute :object, :target
13
+ alias_attribute :collection, :target
13
14
 
14
15
  class << self
15
16
  # Ruby method called when a child class inherits from a parent class
@@ -58,6 +59,8 @@ module ActiveManageable
58
59
  include ActiveManageable::Authorization::Pundit
59
60
  when :cancancan
60
61
  include ActiveManageable::Authorization::CanCanCan
62
+ when Module
63
+ include ActiveManageable.configuration.authorization_library
61
64
  end
62
65
  end
63
66
 
@@ -65,6 +68,8 @@ module ActiveManageable
65
68
  case ActiveManageable.configuration.search_library
66
69
  when :ransack
67
70
  include ActiveManageable::Search::Ransack
71
+ when Module
72
+ include ActiveManageable.configuration.search_library
68
73
  end
69
74
  end
70
75
 
@@ -72,6 +77,8 @@ module ActiveManageable
72
77
  case ActiveManageable.configuration.pagination_library
73
78
  when :kaminari
74
79
  include ActiveManageable::Pagination::Kaminari
80
+ when Module
81
+ include ActiveManageable.configuration.pagination_library
75
82
  end
76
83
  end
77
84
 
@@ -109,6 +116,10 @@ module ActiveManageable
109
116
 
110
117
  private
111
118
 
119
+ def action_scope
120
+ model_class
121
+ end
122
+
112
123
  def initialize_state(attributes: {}, options: {})
113
124
  @target = nil
114
125
  @current_method = calling_method
@@ -6,31 +6,32 @@ module ActiveManageable
6
6
 
7
7
  class Configuration
8
8
  attr_reader :authorization_library, :search_library, :pagination_library, :default_loading_method
9
- attr_accessor :subclass_suffix
9
+ attr_accessor :subclass_suffix, :paginate_without_count
10
10
 
11
11
  def initialize
12
12
  @default_loading_method = :includes
13
13
  @subclass_suffix = "Manager"
14
+ @paginate_without_count = false
14
15
  end
15
16
 
16
17
  def authorization_library=(authorization_library)
17
18
  raise ArgumentError.new("Invalid authorization library") unless authorization_library_valid?(authorization_library)
18
- @authorization_library = authorization_library.to_sym
19
+ @authorization_library = authorization_library
19
20
  end
20
21
 
21
22
  def search_library=(search_library)
22
23
  raise ArgumentError.new("Invalid search library") unless search_library_valid?(search_library)
23
- @search_library = search_library.to_sym
24
+ @search_library = search_library
24
25
  end
25
26
 
26
27
  def pagination_library=(pagination_library)
27
28
  raise ArgumentError.new("Invalid pagination library") unless pagination_library_valid?(pagination_library)
28
- @pagination_library = pagination_library.to_sym
29
+ @pagination_library = pagination_library
29
30
  end
30
31
 
31
32
  def default_loading_method=(default_loading_method)
32
33
  raise ArgumentError.new("Invalid method for eager loading") unless default_loading_method_valid?(default_loading_method)
33
- @default_loading_method = default_loading_method.to_sym
34
+ @default_loading_method = default_loading_method
34
35
  end
35
36
 
36
37
  private
@@ -48,11 +49,19 @@ module ActiveManageable
48
49
  end
49
50
 
50
51
  def default_loading_method_valid?(default_loading_method)
51
- option_valid?(LOADING_METHODS, default_loading_method)
52
+ symbol_option_valid?(LOADING_METHODS, default_loading_method)
52
53
  end
53
54
 
54
55
  def option_valid?(options, option)
55
- option.present? && options.include?(option.to_s.to_sym)
56
+ symbol_option_valid?(options, option) || module_option_valid?(option)
57
+ end
58
+
59
+ def symbol_option_valid?(options, option)
60
+ option.is_a?(Symbol) && options.include?(option.to_s.to_sym)
61
+ end
62
+
63
+ def module_option_valid?(option)
64
+ option.is_a?(Module)
56
65
  end
57
66
  end
58
67
  end
@@ -33,7 +33,7 @@ module ActiveManageable
33
33
  # the associations and options we cannot use the array extract_options! method
34
34
  # as this removes the last element in the array if it's a hash.
35
35
  #
36
- # For example :-
36
+ # For example:-
37
37
  # default_includes songs: :artist, loading_method: :preload, methods: :index
38
38
  # results in an argument value of :-
39
39
  # [{:songs=>:artist, :loading_method=>:preload, :methods=>:index}]
@@ -58,38 +58,36 @@ module ActiveManageable
58
58
  end
59
59
  end
60
60
 
61
- included do
62
- private
61
+ def default_includes(method: @current_method)
62
+ includes = defaults[:includes] || {}
63
+ associations = includes.dig(method.try(:to_sym), :associations) || includes.dig(:all, :associations)
64
+ associations.is_a?(Proc) ? instance_exec(&associations) : associations
65
+ end
63
66
 
64
- # Accepts either an array/hash of associations
65
- # or a hash with associations and loading_method keys
66
- # so it's possible to specify loading_method on a per request basis.
67
- # Uses associations and loading_method from opts
68
- # or defaults for the method or defaults for all methods
69
- # or configuration default loading_method.
70
- def includes(opts)
71
- unless opts.is_a?(Hash) && opts.key?(:associations)
72
- opts = {associations: opts}
73
- end
74
- associations = opts[:associations] || get_default_includes_associations
75
- if associations.present?
76
- loading_method = opts[:loading_method] || get_default_includes_loading_method
77
- @target = @target.send(loading_method, associations)
78
- else
79
- @target
80
- end
81
- end
67
+ def default_loading_method(method: @current_method)
68
+ includes = defaults[:includes] || {}
69
+ loading_method = includes.dig(method.try(:to_sym), :loading_method) || includes.dig(:all, :loading_method)
70
+ loading_method || ActiveManageable.configuration.default_loading_method
71
+ end
82
72
 
83
- def get_default_includes_associations
84
- includes = defaults[:includes] || {}
85
- associations = includes.dig(@current_method, :associations) || includes.dig(:all, :associations)
86
- associations.is_a?(Proc) ? instance_exec(&associations) : associations
87
- end
73
+ private
88
74
 
89
- def get_default_includes_loading_method
90
- includes = defaults[:includes] || {}
91
- loading_method = includes.dig(@current_method, :loading_method) || includes.dig(:all, :loading_method)
92
- loading_method || ActiveManageable.configuration.default_loading_method
75
+ # Accepts either an array/hash of associations
76
+ # or a hash with associations and loading_method keys
77
+ # so it's possible to specify loading_method on a per request basis.
78
+ # Uses associations and loading_method from opts
79
+ # or defaults for the method or defaults for all methods
80
+ # or configuration default loading_method.
81
+ def includes(opts)
82
+ unless opts.is_a?(Hash) && opts.key?(:associations)
83
+ opts = {associations: opts}
84
+ end
85
+ associations = opts[:associations] || default_includes
86
+ if associations.present?
87
+ loading_method = opts[:loading_method] || default_loading_method
88
+ @target = @target.send(loading_method, associations)
89
+ else
90
+ @target
93
91
  end
94
92
  end
95
93
  end
@@ -33,25 +33,23 @@ module ActiveManageable
33
33
  end
34
34
  end
35
35
 
36
- included do
37
- private
36
+ # Returns the default attribute values for the method
37
+ # from the class attribute that can contain a hash of attribute values
38
+ # or a lambda/proc to execute to return attribute values
39
+ def default_attribute_values(method: @current_method)
40
+ default_attributes = defaults[:attributes] || {}
41
+ attributes = default_attributes[method.try(:to_sym)] || default_attributes[:all] || {}
42
+ attributes = (instance_exec(&attributes) || {}) if attributes.is_a?(Proc)
43
+ attributes.with_indifferent_access
44
+ end
38
45
 
39
- # Returns attribute values to use in the new and create methods
40
- # consisting of a merge of the method attributes argument
41
- # and class defaults with the method argument taking precedence
42
- def attribute_values
43
- @attributes.is_a?(Hash) ? @attributes.reverse_merge(get_default_attribute_values) : @attributes
44
- end
46
+ private
45
47
 
46
- # Get the default attribute values for the method
47
- # from the class attribute that can contain a hash of attribute values
48
- # or a lambda/proc to execute to return attribute values
49
- def get_default_attribute_values
50
- default_attributes = defaults[:attributes] || {}
51
- attributes = default_attributes[@current_method] || default_attributes[:all] || {}
52
- attributes = (instance_exec(&attributes) || {}) if attributes.is_a?(Proc)
53
- attributes.with_indifferent_access
54
- end
48
+ # Returns attribute values to use in the new and create methods
49
+ # consisting of a merge of the method attributes argument
50
+ # and class defaults with the method argument taking precedence
51
+ def attribute_values
52
+ @attributes.is_a?(Hash) ? @attributes.reverse_merge(default_attribute_values) : @attributes
55
53
  end
56
54
  end
57
55
  end
@@ -19,23 +19,21 @@ module ActiveManageable
19
19
  end
20
20
  end
21
21
 
22
- included do
23
- private
22
+ # Returns the default order attributes from the class attribute
23
+ # that can contain an array of attribute names or name & direction strings
24
+ # or a lambda/proc to execute to return an array of attribute names
25
+ def default_order
26
+ defaults[:order].is_a?(Proc) ? instance_exec(&defaults[:order]) : defaults[:order]
27
+ end
24
28
 
25
- def order(attributes)
26
- @target = @target.order(get_order_attributes(attributes))
27
- end
29
+ private
28
30
 
29
- def get_order_attributes(attributes)
30
- attributes || get_default_order_attributes
31
- end
31
+ def order(attributes)
32
+ @target = @target.order(order_attributes(attributes))
33
+ end
32
34
 
33
- # Get the default order attributes from the class attribute
34
- # that can contain an array of attribute names or name & direction strings
35
- # or a lambda/proc to execute to return an array of attribute names
36
- def get_default_order_attributes
37
- defaults[:order].is_a?(Proc) ? instance_exec(&defaults[:order]) : defaults[:order]
38
- end
35
+ def order_attributes(attributes)
36
+ attributes || default_order
39
37
  end
40
38
  end
41
39
  end
@@ -20,37 +20,44 @@ module ActiveManageable
20
20
  end
21
21
  end
22
22
 
23
- included do
24
- private
23
+ # Returns the default scopes in a hash of hashes with the key containing the scope name
24
+ # and value containing an array of scope arguments.
25
+ #
26
+ # For example:-
27
+ # {rock: [], electronic: [], released_in_year: ["1980"]}
28
+ def default_scopes
29
+ get_scopes
30
+ end
25
31
 
26
- def scopes(scopes)
27
- get_scopes(scopes).each { |name, args| @target = @target.send(name, *args) }
28
- @target
29
- end
32
+ private
30
33
 
31
- # Accepts a symbol or string scope name, hash containing scope name and argument,
32
- # lambda/proc to execute to return scope(s) or an array of those types
33
- # and when the argument is blank it uses the class default scopes.
34
- # Converts the scopes to a hash of hashes with the key containing the scope name
35
- # and value containing an array of scope arguments.
36
- def get_scopes(scopes = nil)
37
- scopes ||= defaults[:scopes]
34
+ def scopes(scopes)
35
+ get_scopes(scopes).each { |name, args| @target = @target.send(name, *args) }
36
+ @target
37
+ end
38
38
 
39
- Array.wrap(scopes).map do |scope|
40
- case scope
41
- when Symbol, String
42
- {scope => []}
43
- when Hash
44
- # ensure values are an array so they can be passed to the scope using splat operator
45
- scope.transform_values! { |v| Array.wrap(v) }
46
- when Proc
47
- # if the class default contains a lambda/proc that returns nil
48
- # don't call get_scopes as we don't want to end up in an infinite loop
49
- p_scopes = instance_exec(&scope)
50
- get_scopes(p_scopes) if p_scopes.present?
51
- end
52
- end.compact.reduce({}, :merge)
53
- end
39
+ # Accepts a symbol or string scope name, hash containing scope name and argument,
40
+ # lambda/proc to execute to return scope(s) or an array of those types
41
+ # and when the argument is blank it uses the class default scopes.
42
+ # Converts the scopes to a hash of hashes with the key containing the scope name
43
+ # and value containing an array of scope arguments.
44
+ def get_scopes(scopes = nil)
45
+ scopes ||= defaults[:scopes]
46
+
47
+ Array.wrap(scopes).map do |scope|
48
+ case scope
49
+ when Symbol, String
50
+ {scope => []}
51
+ when Hash
52
+ # ensure values are an array so they can be passed to the scope using splat operator
53
+ scope.transform_values! { |v| Array.wrap(v) }
54
+ when Proc
55
+ # if the class default contains a lambda/proc that returns nil
56
+ # don't call get_scopes as we don't want to end up in an infinite loop
57
+ p_scopes = instance_exec(&scope)
58
+ get_scopes(p_scopes) if p_scopes.present?
59
+ end
60
+ end.compact.reduce({}, :merge)
54
61
  end
55
62
  end
56
63
  end
@@ -24,21 +24,19 @@ module ActiveManageable
24
24
  end
25
25
  end
26
26
 
27
- included do
28
- private
27
+ # Returns the default select attributes for the method
28
+ # from the class attribute that can contain an array of attribute names
29
+ # or a lambdas/procs to execute to return an array of attribute names
30
+ def default_select(method: @current_method)
31
+ default_selects = defaults[:select] || {}
32
+ attributes = default_selects[method.try(:to_sym)] || default_selects[:all] || []
33
+ attributes.is_a?(Proc) ? instance_exec(&attributes) : attributes
34
+ end
29
35
 
30
- def select(attributes)
31
- @target = @target.select(attributes || get_default_select_attributes)
32
- end
36
+ private
33
37
 
34
- # Get the default select attributes for the method
35
- # from the class attribute that can contain an array of attribute names
36
- # or a lambdas/procs to execute to return an array of attribute names
37
- def get_default_select_attributes
38
- default_selects = defaults[:select] || {}
39
- attributes = default_selects[@current_method] || default_selects[:all] || []
40
- attributes.is_a?(Proc) ? instance_exec(&attributes) : attributes
41
- end
38
+ def select(attributes)
39
+ @target = @target.select(attributes || default_select)
42
40
  end
43
41
  end
44
42
  end
@@ -20,29 +20,29 @@ module ActiveManageable
20
20
 
21
21
  included do
22
22
  class_attribute :unique_search, instance_writer: false, instance_predicate: false
23
+ end
23
24
 
24
- private
25
-
26
- def unique_search?
27
- case unique_search
28
- when nil
29
- false
30
- when TrueClass, FalseClass
31
- unique_search
32
- when Hash
33
- evaluate_condition(*unique_search.first)
34
- end
25
+ def unique_search?
26
+ case unique_search
27
+ when nil
28
+ false
29
+ when TrueClass, FalseClass
30
+ unique_search
31
+ when Hash
32
+ evaluate_condition(*unique_search.first)
35
33
  end
34
+ end
35
+
36
+ private
36
37
 
37
- def evaluate_condition(condition, method)
38
- result = case method
39
- when Symbol
40
- method(method).call
41
- when Proc
42
- instance_exec(&method)
43
- end
44
- condition == :if ? result : !result
38
+ def evaluate_condition(condition, method)
39
+ result = case method
40
+ when Symbol
41
+ method(method).call
42
+ when Proc
43
+ instance_exec(&method)
45
44
  end
45
+ condition == :if ? result : !result
46
46
  end
47
47
  end
48
48
  end
@@ -5,16 +5,31 @@ module ActiveManageable
5
5
 
6
6
  included do
7
7
  include ActiveManageable::Methods::Auxiliary::ModelAttributes
8
+ end
8
9
 
9
- def create(attributes:)
10
- initialize_state(attributes: attributes)
10
+ def create(attributes:)
11
+ initialize_state(attributes: attributes)
11
12
 
12
- @target = model_class.new(attribute_values)
13
- authorize(record: @target)
13
+ @target = build_object_for_create
14
+ authorize(record: @target)
14
15
 
15
- @target.save
16
+ model_class.transaction do
17
+ yield if block_given?
18
+ create_object
19
+ rescue ActiveRecord::RecordInvalid
20
+ false
16
21
  end
17
22
  end
23
+
24
+ private
25
+
26
+ def build_object_for_create
27
+ action_scope.new(attribute_values)
28
+ end
29
+
30
+ def create_object
31
+ @target.save!
32
+ end
18
33
  end
19
34
  end
20
35
  end
@@ -5,19 +5,34 @@ module ActiveManageable
5
5
 
6
6
  included do
7
7
  include ActiveManageable::Methods::Auxiliary::Includes
8
+ end
8
9
 
9
- def destroy(id:, options: {})
10
- initialize_state(options: options)
10
+ def destroy(id:, options: {})
11
+ initialize_state(options: options)
11
12
 
12
- @target = model_class
13
- includes(@options[:includes])
13
+ @target = action_scope
14
+ includes(@options[:includes])
14
15
 
15
- @target = @target.find(id)
16
- authorize(record: @target)
16
+ @target = find_object_for_destroy(id: id)
17
+ authorize(record: @target)
17
18
 
18
- @target.destroy
19
+ model_class.transaction do
20
+ yield if block_given?
21
+ destroy_object
22
+ rescue ActiveRecord::RecordNotDestroyed
23
+ false
19
24
  end
20
25
  end
26
+
27
+ private
28
+
29
+ def find_object_for_destroy(id:)
30
+ @target.find(id)
31
+ end
32
+
33
+ def destroy_object
34
+ @target.destroy!
35
+ end
21
36
  end
22
37
  end
23
38
  end
@@ -6,19 +6,27 @@ module ActiveManageable
6
6
  included do
7
7
  include ActiveManageable::Methods::Auxiliary::Includes
8
8
  include ActiveManageable::Methods::Auxiliary::Select
9
+ end
10
+
11
+ def edit(id:, options: {})
12
+ initialize_state(options: options)
13
+
14
+ @target = action_scope
15
+ includes(@options[:includes])
16
+ select(@options[:select])
9
17
 
10
- def edit(id:, options: {})
11
- initialize_state(options: options)
18
+ yield if block_given?
12
19
 
13
- @target = model_class
14
- includes(@options[:includes])
15
- select(@options[:select])
20
+ @target = find_object_for_edit(id: id)
21
+ authorize(record: @target)
22
+
23
+ @target
24
+ end
16
25
 
17
- @target = @target.find(id)
18
- authorize(record: @target)
26
+ private
19
27
 
20
- @target
21
- end
28
+ def find_object_for_edit(id:)
29
+ @target.find(id)
22
30
  end
23
31
  end
24
32
  end
@@ -9,40 +9,42 @@ module ActiveManageable
9
9
  include ActiveManageable::Methods::Auxiliary::Includes
10
10
  include ActiveManageable::Methods::Auxiliary::Select
11
11
  include ActiveManageable::Methods::Auxiliary::UniqueSearch
12
+ end
13
+
14
+ def index(options: {})
15
+ initialize_state(options: options)
12
16
 
13
- def index(options: {})
14
- initialize_state(options: options)
17
+ @target = authorization_scope
18
+ authorize(record: model_class)
19
+ search(@options[:search])
20
+ order(@options[:order])
21
+ scopes(@options[:scopes])
22
+ page(@options[:page])
23
+ includes(@options[:includes])
24
+ select(@options[:select])
25
+ distinct(unique_search?)
15
26
 
16
- @target = scoped_class
17
- authorize(record: model_class)
18
- search(@options[:search])
19
- order(@options[:order])
20
- scopes(@options[:scopes])
21
- page(@options[:page])
22
- includes(@options[:includes])
23
- select(@options[:select])
24
- distinct(unique_search?)
27
+ yield if block_given?
25
28
 
26
- @target
27
- end
29
+ @target
30
+ end
28
31
 
29
- private
32
+ private
30
33
 
31
- def scoped_class
32
- model_class
33
- end
34
+ def authorization_scope
35
+ action_scope
36
+ end
34
37
 
35
- def search(opts)
36
- @target
37
- end
38
+ def search(opts)
39
+ @target
40
+ end
38
41
 
39
- def page(opts)
40
- @target
41
- end
42
+ def page(opts)
43
+ @target
44
+ end
42
45
 
43
- def distinct(value)
44
- @target = target.distinct(value)
45
- end
46
+ def distinct(value)
47
+ @target = target.distinct(value)
46
48
  end
47
49
  end
48
50
  end
@@ -5,15 +5,23 @@ module ActiveManageable
5
5
 
6
6
  included do
7
7
  include ActiveManageable::Methods::Auxiliary::ModelAttributes
8
+ end
9
+
10
+ def new(attributes: {})
11
+ initialize_state(attributes: attributes)
12
+
13
+ @target = build_object_for_new
14
+ authorize(record: @target)
8
15
 
9
- def new(attributes: {})
10
- initialize_state(attributes: attributes)
16
+ yield if block_given?
17
+
18
+ @target
19
+ end
11
20
 
12
- @target = model_class.new(attribute_values)
13
- authorize(record: @target)
21
+ private
14
22
 
15
- @target
16
- end
23
+ def build_object_for_new
24
+ action_scope.new(attribute_values)
17
25
  end
18
26
  end
19
27
  end
@@ -6,19 +6,27 @@ module ActiveManageable
6
6
  included do
7
7
  include ActiveManageable::Methods::Auxiliary::Includes
8
8
  include ActiveManageable::Methods::Auxiliary::Select
9
+ end
10
+
11
+ def show(id:, options: {})
12
+ initialize_state(options: options)
13
+
14
+ @target = action_scope
15
+ includes(@options[:includes])
16
+ select(@options[:select])
9
17
 
10
- def show(id:, options: {})
11
- initialize_state(options: options)
18
+ yield if block_given?
12
19
 
13
- @target = model_class
14
- includes(@options[:includes])
15
- select(@options[:select])
20
+ @target = find_object_for_show(id: id)
21
+ authorize(record: @target)
22
+
23
+ @target
24
+ end
16
25
 
17
- @target = @target.find(id)
18
- authorize(record: @target)
26
+ private
19
27
 
20
- @target
21
- end
28
+ def find_object_for_show(id:)
29
+ @target.find(id)
22
30
  end
23
31
  end
24
32
  end
@@ -5,19 +5,40 @@ module ActiveManageable
5
5
 
6
6
  included do
7
7
  include ActiveManageable::Methods::Auxiliary::Includes
8
+ end
9
+
10
+ def update(id:, attributes:, options: {})
11
+ initialize_state(attributes: attributes, options: options)
8
12
 
9
- def update(id:, attributes:, options: {})
10
- initialize_state(attributes: attributes, options: options)
13
+ @target = action_scope
14
+ includes(@options[:includes])
11
15
 
12
- @target = model_class
13
- includes(@options[:includes])
16
+ @target = find_object_for_update(id: id)
17
+ authorize(record: @target)
14
18
 
15
- @target = @target.find(id)
16
- authorize(record: @target)
19
+ assign_attributes_for_update
17
20
 
18
- @target.update(@attributes)
21
+ model_class.transaction do
22
+ yield if block_given?
23
+ update_object
24
+ rescue ActiveRecord::RecordInvalid
25
+ false
19
26
  end
20
27
  end
28
+
29
+ private
30
+
31
+ def find_object_for_update(id:)
32
+ @target.find(id)
33
+ end
34
+
35
+ def assign_attributes_for_update
36
+ @target.assign_attributes(@attributes)
37
+ end
38
+
39
+ def update_object
40
+ @target.save!
41
+ end
21
42
  end
22
43
  end
23
44
  end
@@ -17,22 +17,42 @@ module ActiveManageable
17
17
  def default_page_size(page_size)
18
18
  defaults[:page] = {size: page_size.to_i}
19
19
  end
20
+
21
+ # Class option used when determining whether to create a paginatable collection without counting the total number of records
22
+ # within the order of precedence based on the (1) method option, or (2) class option, or (3) configuration option
23
+ def paginate_without_count(without_count = true)
24
+ self.without_count = without_count
25
+ end
20
26
  end
21
27
 
22
28
  included do
23
- private
29
+ class_attribute :without_count, instance_writer: false, instance_predicate: false
30
+ end
24
31
 
25
- def page(opts)
26
- @target = @target.page(page_number(opts)).per(page_size(opts))
27
- end
32
+ private
28
33
 
29
- def page_number(opts)
30
- opts.try(:[], :number)
34
+ def page(opts)
35
+ @target = @target.page(page_number(opts)).per(page_size(opts)).tap do |target|
36
+ target.without_count if paginate_without_count?(opts)
31
37
  end
38
+ end
32
39
 
33
- def page_size(opts)
34
- opts.try(:[], :size) || defaults.dig(:page, :size)
35
- end
40
+ def page_number(opts)
41
+ opts.try(:[], :number)
42
+ end
43
+
44
+ def page_size(opts)
45
+ opts.try(:[], :size) || defaults.dig(:page, :size)
46
+ end
47
+
48
+ # Determine whether to create a paginatable collection without counting the total number of records
49
+ # in order of precedence based on the (1) method option, or (2) class option, or (3) configuration option
50
+ def paginate_without_count?(opts)
51
+ [
52
+ opts.try(:[], :without_count),
53
+ without_count,
54
+ ActiveManageable.configuration.paginate_without_count
55
+ ].compact.first
36
56
  end
37
57
  end
38
58
  end
@@ -11,26 +11,24 @@ module ActiveManageable
11
11
  attr_reader :ransack
12
12
 
13
13
  initialize_state_methods :initialize_ransack_state
14
+ end
14
15
 
15
- private
16
-
17
- def initialize_ransack_state
18
- @ransack = nil
19
- end
16
+ def initialize_ransack_state
17
+ @ransack = nil
18
+ end
20
19
 
21
- def search(opts)
22
- @ransack = @target.ransack(opts)
23
- @target = @ransack.result
24
- end
20
+ def search(opts)
21
+ @ransack = @target.ransack(opts)
22
+ @target = @ransack.result
23
+ end
25
24
 
26
- # Perform standard index module ordering when no ransack search params provided
27
- # or no ransack sorts params provided
28
- def order(attributes)
29
- if @ransack.blank? || @ransack.sorts.empty?
30
- @target = @target.order(get_order_attributes(attributes))
31
- else
32
- @target
33
- end
25
+ # Perform standard index module ordering when no ransack search params provided
26
+ # or no ransack sorts params provided
27
+ def order(attributes)
28
+ if @ransack.blank? || @ransack.sorts.empty?
29
+ @target = @target.order(order_attributes(attributes))
30
+ else
31
+ @target
34
32
  end
35
33
  end
36
34
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveManageable
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_manageable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Hilton
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2022-08-25 00:00:00.000000000 Z
12
+ date: 2022-09-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord