active_manageable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# load a file relative to the current location
|
4
|
+
require_relative "lib/active_manageable/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "active_manageable"
|
8
|
+
spec.version = ActiveManageable::VERSION
|
9
|
+
spec.authors = ["Chris Hilton", "Chris Branson"]
|
10
|
+
spec.email = ["449774+chrismhilton@users.noreply.github.com", "138595+chrisbranson@users.noreply.github.com"]
|
11
|
+
|
12
|
+
spec.summary = "Business logic framework for Ruby on Rails"
|
13
|
+
spec.description = "Framework for business logic classes in a Ruby on Rails application"
|
14
|
+
spec.homepage = "https://github.com/CircleSD/active_manageable"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Minimum version of Ruby compatible with Rails 7.0
|
18
|
+
spec.required_ruby_version = ">= 2.7.0"
|
19
|
+
|
20
|
+
# Metadata used on gem’s profile page on rubygems.org
|
21
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
22
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
23
|
+
spec.metadata["changelog_uri"] = "https://github.com/CircleSD/active_manageable/blob/main/CHANGELOG.md"
|
24
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/CircleSD/active_manageable/issues"
|
25
|
+
spec.metadata["documentation_uri"] = spec.homepage
|
26
|
+
|
27
|
+
# Specify which files should be added to the gem when it is released.
|
28
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
29
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
30
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Binary folder where the gem’s executables are located
|
34
|
+
spec.bindir = "exe"
|
35
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
36
|
+
|
37
|
+
# Add lib directory to $LOAD_PATH to make code available via the require statement
|
38
|
+
spec.require_paths = ["lib"]
|
39
|
+
|
40
|
+
# Register runtime and development dependencies
|
41
|
+
# including gems that are essential to test and build this gem
|
42
|
+
|
43
|
+
# rails dependencies
|
44
|
+
spec.add_dependency "activerecord", ">= 6.0"
|
45
|
+
spec.add_dependency "activesupport", ">= 6.0"
|
46
|
+
|
47
|
+
# gem dependencies
|
48
|
+
spec.add_dependency "rails-i18n"
|
49
|
+
spec.add_dependency "flexitime", "~> 1.0"
|
50
|
+
|
51
|
+
# test dependencies
|
52
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
53
|
+
spec.add_development_dependency "appraisal"
|
54
|
+
spec.add_development_dependency "sqlite3", "~> 1.4.0"
|
55
|
+
spec.add_development_dependency "rspec-rails"
|
56
|
+
spec.add_development_dependency "rails-controller-testing"
|
57
|
+
spec.add_development_dependency "factory_bot_rails"
|
58
|
+
spec.add_development_dependency "shoulda-matchers"
|
59
|
+
spec.add_development_dependency "simplecov"
|
60
|
+
|
61
|
+
# test modules
|
62
|
+
spec.add_development_dependency "pundit"
|
63
|
+
spec.add_development_dependency "cancancan"
|
64
|
+
spec.add_development_dependency "ransack"
|
65
|
+
spec.add_development_dependency "kaminari"
|
66
|
+
|
67
|
+
# linter dependencies
|
68
|
+
spec.add_development_dependency "rubocop", "1.23.0"
|
69
|
+
spec.add_development_dependency "standard"
|
70
|
+
spec.add_development_dependency "rubocop-rails"
|
71
|
+
spec.add_development_dependency "rubocop-rspec"
|
72
|
+
|
73
|
+
# For more information and examples about making a new gem, checkout our
|
74
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
75
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "active_manageable"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#
|
2
|
+
# Authorization methods for CanCanCan
|
3
|
+
# https://github.com/CanCanCommunity/cancancan
|
4
|
+
#
|
5
|
+
module ActiveManageable
|
6
|
+
module Authorization
|
7
|
+
module CanCanCan
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
private
|
12
|
+
|
13
|
+
def authorize(record:, action: nil)
|
14
|
+
action ||= @current_method
|
15
|
+
current_ability.authorize!(action, record)
|
16
|
+
end
|
17
|
+
|
18
|
+
def scoped_class
|
19
|
+
model_class.accessible_by(current_ability)
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_ability
|
23
|
+
::Ability.new(current_user)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#
|
2
|
+
# Authorization methods for Pundit
|
3
|
+
# https://github.com/varvet/pundit
|
4
|
+
#
|
5
|
+
module ActiveManageable
|
6
|
+
module Authorization
|
7
|
+
module Pundit
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
private
|
12
|
+
|
13
|
+
def authorize(record:, action: nil)
|
14
|
+
action ||= authorize_action
|
15
|
+
::Pundit.authorize(current_user, record, action)
|
16
|
+
end
|
17
|
+
|
18
|
+
def scoped_class
|
19
|
+
::Pundit.policy_scope(current_user, model_class)
|
20
|
+
end
|
21
|
+
|
22
|
+
def authorize_action
|
23
|
+
"#{@current_method}?".to_sym
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
module ActiveManageable
|
2
|
+
class Base
|
3
|
+
class_attribute :model_class, instance_writer: false, instance_predicate: false
|
4
|
+
class_attribute :defaults, instance_writer: false, instance_predicate: false
|
5
|
+
class_attribute :module_initialize_state_methods, instance_writer: false, instance_predicate: false
|
6
|
+
|
7
|
+
attr_reader :target, :current_method, :attributes, :options
|
8
|
+
|
9
|
+
# target provides a common variable to use within the CRUD, auxiliary & library methods
|
10
|
+
# whereas the object & collection methods provide less ambiguous external access
|
11
|
+
alias_method :object, :target
|
12
|
+
alias_method :collection, :target
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# Ruby method called when a child class inherits from a parent class
|
16
|
+
def inherited(subclass)
|
17
|
+
super
|
18
|
+
# necessary to set default value here rather than in class_attribute declaration
|
19
|
+
# otherwise all subclasses share the same hash/array instance
|
20
|
+
subclass.defaults = {}
|
21
|
+
subclass.module_initialize_state_methods = []
|
22
|
+
end
|
23
|
+
|
24
|
+
delegate :current_user, to: ActiveManageable
|
25
|
+
|
26
|
+
# Include the required action methods in your class
|
27
|
+
# either all methods using the ActiveManageable::ALL_METHODS constant
|
28
|
+
# or selective methods from :index, :show, :new, :create, :edit, :update, :destroy
|
29
|
+
# and optionally set the :model_class
|
30
|
+
#
|
31
|
+
# For example:-
|
32
|
+
# manageable ActiveManageable::ALL_METHODS
|
33
|
+
# manageable ActiveManageable::ALL_METHODS, model_class: Album
|
34
|
+
# manageable :index, :show
|
35
|
+
def manageable(*methods)
|
36
|
+
options = methods.extract_options!.dup
|
37
|
+
|
38
|
+
methods = ActiveManageable::Methods.constants if methods[0] == ActiveManageable::ALL_METHODS
|
39
|
+
|
40
|
+
methods.each do |method|
|
41
|
+
include ActiveManageable::Methods.const_get(method.to_s.classify)
|
42
|
+
end
|
43
|
+
|
44
|
+
include_authorization
|
45
|
+
include_search
|
46
|
+
include_pagination
|
47
|
+
|
48
|
+
options.each { |key, value| send(:"#{key}=", value) }
|
49
|
+
|
50
|
+
set_model_class
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def include_authorization
|
56
|
+
case ActiveManageable.configuration.authorization_library
|
57
|
+
when :pundit
|
58
|
+
include ActiveManageable::Authorization::Pundit
|
59
|
+
when :cancancan
|
60
|
+
include ActiveManageable::Authorization::CanCanCan
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def include_search
|
65
|
+
case ActiveManageable.configuration.search_library
|
66
|
+
when :ransack
|
67
|
+
include ActiveManageable::Search::Ransack
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def include_pagination
|
72
|
+
case ActiveManageable.configuration.pagination_library
|
73
|
+
when :kaminari
|
74
|
+
include ActiveManageable::Pagination::Kaminari
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def set_model_class
|
79
|
+
self.model_class ||= begin
|
80
|
+
if name.end_with?(ActiveManageable.configuration.subclass_suffix)
|
81
|
+
name.chomp(ActiveManageable.configuration.subclass_suffix).classify.constantize
|
82
|
+
end
|
83
|
+
rescue NameError
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def initialize_state_methods(*methods)
|
88
|
+
module_initialize_state_methods.concat(methods)
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_method_defaults(key:, value:, methods:)
|
92
|
+
methods = Array(methods).map(&:to_sym)
|
93
|
+
methods << :all if methods.empty?
|
94
|
+
defaults[key] ||= {}
|
95
|
+
methods.each { |method| defaults[key][method] = value }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def current_user
|
100
|
+
@current_user || ActiveManageable.current_user
|
101
|
+
end
|
102
|
+
|
103
|
+
def with_current_user(user)
|
104
|
+
@current_user = user
|
105
|
+
yield
|
106
|
+
ensure
|
107
|
+
@current_user = nil
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def initialize_state(attributes: {}, options: {})
|
113
|
+
@target = nil
|
114
|
+
@current_method = calling_method
|
115
|
+
@attributes = normalize_attributes(state_argument_to_hwia(attributes))
|
116
|
+
@options = state_argument_to_hwia(options)
|
117
|
+
module_initialize_state_methods.each { |method| send(method) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def authorize(record:, action: nil)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns the name of the calling method
|
124
|
+
# using caller_locations that returns the current execution stack
|
125
|
+
# and catering for inheritance and two occurrences of initialize_state in the execution stack
|
126
|
+
# when the ransack module is included as it has its own definition of the method that calls super
|
127
|
+
# https://www.lucascaton.com.br/2016/11/04/ruby-how-to-get-the-name-of-the-calling-method
|
128
|
+
def calling_method
|
129
|
+
my_caller = caller_locations(1..1).first.label
|
130
|
+
caller_locations[1..].find { |location| location.label != my_caller }.label.to_sym
|
131
|
+
end
|
132
|
+
|
133
|
+
# Converts a state argument to a ActiveSupport::HashWithIndifferentAccess.
|
134
|
+
# The purpose of the method is to return a duplicate object for arguments like the attributes
|
135
|
+
# so that any changes that are made internally do not affect the source object.
|
136
|
+
#
|
137
|
+
# For a Hash returns an ActiveSupport::HashWithIndifferentAccess.
|
138
|
+
# For a ActiveSupport::HashWithIndifferentAccess returns a duplicate ActiveSupport::HashWithIndifferentAccess.
|
139
|
+
# For an ActionController::Parameters returns a safe ActiveSupport::HashWithIndifferentAccess
|
140
|
+
# representation of the parameters with all unpermitted keys removed.
|
141
|
+
#
|
142
|
+
# NB: For an ActionController::Parameters we experimented with using the deep_dup method
|
143
|
+
# to return a duplicate ActionController::Parameters but this caused issues
|
144
|
+
# for the attributes argument when the params had been permiited and then nested params are replaced with a hash.
|
145
|
+
# That would be fine if the attributes were then used for mass assignment, however,
|
146
|
+
# first we call normalize_attribute_values which uses the each method
|
147
|
+
# which converts all hashes into ActionController::Parameters with the permitted attribute set to false
|
148
|
+
# so when performing mass assignment that resulted in a ActiveModel::ForbiddenAttributesError.
|
149
|
+
def state_argument_to_hwia(arg)
|
150
|
+
case arg
|
151
|
+
when Hash
|
152
|
+
arg.with_indifferent_access
|
153
|
+
when action_controller_params?
|
154
|
+
arg.to_h
|
155
|
+
else
|
156
|
+
arg
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns true if the object is an ActionController::Parameters
|
161
|
+
# using class.name so the gem does not need a dependency on ActionPack simply for a case statement
|
162
|
+
def action_controller_params?
|
163
|
+
->(object) { object.class.name == "ActionController::Parameters" }
|
164
|
+
end
|
165
|
+
|
166
|
+
def normalize_attributes(attributes)
|
167
|
+
normalize_attribute_values(model_class, attributes)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Parse date & datetime attribute values and normalize decimal separator for numeric attributes
|
171
|
+
def normalize_attribute_values(model_class, attributes)
|
172
|
+
case attributes
|
173
|
+
when Hash
|
174
|
+
attributes.each do |key, value|
|
175
|
+
if key.end_with?("_attributes")
|
176
|
+
association_class = model_association_class(model_class, key.delete_suffix("_attributes"))
|
177
|
+
normalize_attribute_values(association_class, value) if association_class.present?
|
178
|
+
else
|
179
|
+
case model_attribute_type(model_class, key)
|
180
|
+
when :date
|
181
|
+
attributes[key] = Flexitime.parse(value).try(:to_date) || value
|
182
|
+
when :datetime
|
183
|
+
attributes[key] = Flexitime.parse(value) || value
|
184
|
+
when :decimal, :float
|
185
|
+
attributes[key] = normalize_decimal_separator(value)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
when Array
|
190
|
+
attributes.each { |attrs| normalize_attribute_values(model_class, attrs) }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def model_association_class(model_class, association_name)
|
195
|
+
model_class.reflect_on_association(association_name).try(:class_name).try(:constantize)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns the type of the attribute with the given name
|
199
|
+
# and could be overridden by a child class in order to
|
200
|
+
# return the type of both column and non-column based attributes
|
201
|
+
# when a non-column based attribute value needed to be normalized
|
202
|
+
def model_attribute_type(model_class, attribute_name)
|
203
|
+
model_class.type_for_attribute(attribute_name).type
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns a value with a comma separator replaced with a point separator
|
207
|
+
def normalize_decimal_separator(value)
|
208
|
+
normalize_decimal_separator?(value) ? value.tr(",", ".") : value
|
209
|
+
end
|
210
|
+
|
211
|
+
# Normalize decimal separator when the locale number separator is a comma
|
212
|
+
# and the value does not include a point and includes only one comma
|
213
|
+
def normalize_decimal_separator?(value)
|
214
|
+
I18n.t("number.format.separator") == "," &&
|
215
|
+
value.to_s.count(".") == 0 && value.to_s.count(",") == 1
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ActiveManageable
|
2
|
+
AUTHORIZATION_LIBRARIES = %i[pundit cancancan].freeze
|
3
|
+
SEARCH_LIBRARIES = %i[ransack].freeze
|
4
|
+
PAGINATION_LIBRARIES = %i[kaminari].freeze
|
5
|
+
LOADING_METHODS = %i[includes preload eager_load].freeze
|
6
|
+
|
7
|
+
class Configuration
|
8
|
+
attr_reader :authorization_library, :search_library, :pagination_library, :default_loading_method
|
9
|
+
attr_accessor :subclass_suffix
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@default_loading_method = :includes
|
13
|
+
@subclass_suffix = "Manager"
|
14
|
+
end
|
15
|
+
|
16
|
+
def authorization_library=(authorization_library)
|
17
|
+
raise ArgumentError.new("Invalid authorization library") unless authorization_library_valid?(authorization_library)
|
18
|
+
@authorization_library = authorization_library.to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def search_library=(search_library)
|
22
|
+
raise ArgumentError.new("Invalid search library") unless search_library_valid?(search_library)
|
23
|
+
@search_library = search_library.to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
def pagination_library=(pagination_library)
|
27
|
+
raise ArgumentError.new("Invalid pagination library") unless pagination_library_valid?(pagination_library)
|
28
|
+
@pagination_library = pagination_library.to_sym
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_loading_method=(default_loading_method)
|
32
|
+
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
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def authorization_library_valid?(authorization_library)
|
39
|
+
option_valid?(AUTHORIZATION_LIBRARIES, authorization_library)
|
40
|
+
end
|
41
|
+
|
42
|
+
def search_library_valid?(search_library)
|
43
|
+
option_valid?(SEARCH_LIBRARIES, search_library)
|
44
|
+
end
|
45
|
+
|
46
|
+
def pagination_library_valid?(pagination_library)
|
47
|
+
option_valid?(PAGINATION_LIBRARIES, pagination_library)
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_loading_method_valid?(default_loading_method)
|
51
|
+
option_valid?(LOADING_METHODS, default_loading_method)
|
52
|
+
end
|
53
|
+
|
54
|
+
def option_valid?(options, option)
|
55
|
+
option.present? && options.include?(option.to_s.to_sym)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module ActiveManageable
|
2
|
+
module Methods
|
3
|
+
module Auxiliary
|
4
|
+
module Includes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
# Sets the default associations to eager load when fetching records
|
9
|
+
# in the index, show, edit, update and destroy methods
|
10
|
+
# if the methods :options argument does not contain a :includes key;
|
11
|
+
# accepting a single, array or hash of association names;
|
12
|
+
# optional :methods in which to eager load the associations;
|
13
|
+
# and optional :loading_method if this needs to be different to the configuration :default_loading_method
|
14
|
+
# also accepting a lambda/proc to execute to return association names and optional :methods
|
15
|
+
#
|
16
|
+
# For example:-
|
17
|
+
# default_includes :songs
|
18
|
+
# default_includes :songs, :artist, methods: :show
|
19
|
+
# default_includes songs: :artist, loading_method: :eager_load, methods: [:index, :edit]
|
20
|
+
# default_includes -> { includes_associations }
|
21
|
+
# default_includes -> { includes_associations }, methods: [:index, :edit]
|
22
|
+
def default_includes(*associations)
|
23
|
+
options = extract_includes_options!(associations)
|
24
|
+
loading_method = options[:loading_method] || ActiveManageable.configuration.default_loading_method
|
25
|
+
assoc = associations.first.is_a?(Proc) ? associations.first : associations
|
26
|
+
value = {associations: assoc, loading_method: loading_method}
|
27
|
+
add_method_defaults(key: :includes, value: value, methods: options[:methods])
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# As the associations argument may contain a single hash containing both
|
33
|
+
# the associations and options we cannot use the array extract_options! method
|
34
|
+
# as this removes the last element in the array if it's a hash.
|
35
|
+
#
|
36
|
+
# For example :-
|
37
|
+
# default_includes songs: :artist, loading_method: :preload, methods: :index
|
38
|
+
# results in an argument value of :-
|
39
|
+
# [{:songs=>:artist, :loading_method=>:preload, :methods=>:index}]
|
40
|
+
#
|
41
|
+
# So instead this method, like extract_options!, ascertains
|
42
|
+
# if the last element in the array is a hash but then it extracts
|
43
|
+
# the specific options and only removes the last element if it's empty.
|
44
|
+
# Therefore catering for the variety of association formats
|
45
|
+
# and potential presence of the options eg.
|
46
|
+
#
|
47
|
+
# [:songs, :artist, {:loading_method=>:preload, :methods=>"index"}]
|
48
|
+
# [:artist, {:songs=>:artist}, {:loading_method=>:preload, :methods=>:index}]
|
49
|
+
# [{:songs=>:artist, :loading_method=>:preload, :methods=>:index}]
|
50
|
+
def extract_includes_options!(associations)
|
51
|
+
if associations.last.is_a?(Hash)
|
52
|
+
options = associations.last.extract!(:loading_method, :methods)
|
53
|
+
associations.pop if associations.last.empty?
|
54
|
+
options
|
55
|
+
else
|
56
|
+
{}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
included do
|
62
|
+
private
|
63
|
+
|
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
|
82
|
+
|
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
|
88
|
+
|
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
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ActiveManageable
|
2
|
+
module Methods
|
3
|
+
module Auxiliary
|
4
|
+
module ModelAttributes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
# Sets the default attribute values to use when building a model object
|
9
|
+
# in the new and create methods and these defaults are combined with
|
10
|
+
# the attribute values from the methods :attributes argument;
|
11
|
+
# accepting either a hash of attribute values or a lambda/proc
|
12
|
+
# to execute to return a hash of attribute values;
|
13
|
+
# and optional :methods in which to use the attribute values.
|
14
|
+
#
|
15
|
+
# For example:-
|
16
|
+
# default_attribute_values genre: "pop"
|
17
|
+
# default_attribute_values genre: "pop", released_at: Date.current, methods: :new
|
18
|
+
# default_attribute_values -> { default_attrs }
|
19
|
+
# default_attribute_values -> { default_attrs }, methods: [:new, :create]
|
20
|
+
def default_attribute_values(*attributes)
|
21
|
+
case attributes.first
|
22
|
+
when Hash
|
23
|
+
# when the argument contains a hash - extract the methods from the hash
|
24
|
+
# attributes value [{:name=>"Dark Side of the Moon", :genre=>"rock", :methods=>:create}]
|
25
|
+
methods = attributes.first.delete(:methods)
|
26
|
+
when Proc
|
27
|
+
# when the argument contains a lambda/proc - extract the methods from the array
|
28
|
+
# attributes value [#<Proc:0x0000000110174c90 ... (lambda)>, {:methods=>:create}]
|
29
|
+
methods = attributes.extract_options![:methods]
|
30
|
+
end
|
31
|
+
attrs = attributes.first
|
32
|
+
add_method_defaults(key: :attributes, value: attrs, methods: methods)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
included do
|
37
|
+
private
|
38
|
+
|
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
|
45
|
+
|
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
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveManageable
|
2
|
+
module Methods
|
3
|
+
module Auxiliary
|
4
|
+
module Order
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
# Sets the default order to use when fetching records in the index method
|
9
|
+
# if the index :options argument does not contain an :order key;
|
10
|
+
# accepting attributes in the same formats as the ActiveRecord order method
|
11
|
+
# or a lambda/proc to execute to return attributes in the recognised formats.
|
12
|
+
#
|
13
|
+
# For example:-
|
14
|
+
# default_order :name
|
15
|
+
# default_order "name DESC"
|
16
|
+
# default_order -> { order_attributes }
|
17
|
+
def default_order(*attributes)
|
18
|
+
defaults[:order] = attributes.first.is_a?(Proc) ? attributes.first : attributes
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
included do
|
23
|
+
private
|
24
|
+
|
25
|
+
def order(attributes)
|
26
|
+
@target = @target.order(get_order_attributes(attributes))
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_order_attributes(attributes)
|
30
|
+
attributes || get_default_order_attributes
|
31
|
+
end
|
32
|
+
|
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
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|