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/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
|