kadmin 0.3.1 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +44 -44
- data/Rakefile +146 -146
- data/app/assets/javascripts/modular/app.js +1388 -1388
- data/app/assets/javascripts/modular/application.js +3 -3
- data/app/assets/javascripts/modular/vendor.js +57671 -57671
- data/app/assets/stylesheets/kadmin/typeahead-select.scss +2 -0
- data/app/assets/stylesheets/modular/app-blue.css +2795 -2795
- data/app/assets/stylesheets/modular/app-custom.css +2795 -2795
- data/app/assets/stylesheets/modular/app-green.css +2795 -2795
- data/app/assets/stylesheets/modular/app-orange.css +2795 -2795
- data/app/assets/stylesheets/modular/app-purple.css +2795 -2795
- data/app/assets/stylesheets/modular/app-red.css +2795 -2795
- data/app/assets/stylesheets/modular/app-seagreen.css +2795 -2795
- data/app/assets/stylesheets/modular/app.css +2795 -2795
- data/app/assets/stylesheets/modular/custom.css +51 -19
- data/app/assets/stylesheets/modular/vendor.css +12999 -12999
- data/app/controllers/kadmin/application_controller.rb +64 -64
- data/app/controllers/kadmin/auth_controller.rb +98 -98
- data/app/controllers/kadmin/concerns/authorized_user.rb +67 -67
- data/app/controllers/kadmin/dash_controller.rb +19 -19
- data/app/decorators/kadmin/finder_decorator.rb +50 -50
- data/app/decorators/kadmin/pager_decorator.rb +33 -33
- data/app/helpers/kadmin/alert_helper.rb +59 -59
- data/app/helpers/kadmin/application_helper.rb +4 -4
- data/app/helpers/kadmin/bootstrap_helper.rb +23 -23
- data/app/helpers/kadmin/form_builder.rb +9 -9
- data/app/helpers/kadmin/forms/inverted_check_box.rb +10 -10
- data/app/helpers/kadmin/navigation_helper.rb +28 -28
- data/app/helpers/kadmin/pagination_helper.rb +95 -95
- data/app/views/kadmin/auth/login.html.erb +4 -4
- data/app/views/kadmin/components/_finder.html.erb +14 -18
- data/app/views/kadmin/components/finder/_empty.html.erb +3 -3
- data/app/views/kadmin/components/finder/_form.erb +10 -10
- data/app/views/kadmin/components/finder/_header.html.erb +14 -11
- data/app/views/kadmin/dash/index.html.erb +5 -5
- data/app/views/kadmin/error.html.erb +5 -5
- data/app/views/kadmin/helpers/_alerts.html.erb +4 -4
- data/app/views/kadmin/helpers/_form_errors.html.erb +10 -10
- data/app/views/layouts/modular/application.html.erb +134 -132
- data/config/initializers/action_view.rb +2 -2
- data/config/initializers/assets.rb +5 -5
- data/config/locales/de.yml +25 -25
- data/config/locales/en.yml +24 -24
- data/config/routes.rb +12 -12
- data/lib/kadmin.rb +22 -22
- data/lib/kadmin/auth.rb +31 -31
- data/lib/kadmin/auth/configuration.rb +66 -66
- data/lib/kadmin/auth/unauthorized_error.rb +14 -14
- data/lib/kadmin/auth/user.rb +15 -15
- data/lib/kadmin/auth/user_store.rb +21 -21
- data/lib/kadmin/configuration.rb +18 -18
- data/lib/kadmin/engine.rb +15 -15
- data/lib/kadmin/error.rb +7 -7
- data/lib/kadmin/errors/authorization.rb +15 -15
- data/lib/kadmin/finder.rb +66 -66
- data/lib/kadmin/form.rb +179 -179
- data/lib/kadmin/pager.rb +93 -93
- data/lib/kadmin/version.rb +3 -3
- data/test/dummy/README.rdoc +28 -28
- data/test/dummy/Rakefile +6 -6
- data/test/dummy/app/assets/javascripts/application.js +13 -13
- data/test/dummy/app/assets/stylesheets/application.css +15 -15
- data/test/dummy/app/controllers/admin/application_controller.rb +11 -11
- data/test/dummy/app/controllers/admin/people_controller.rb +89 -89
- data/test/dummy/app/controllers/admin_controller.rb +4 -4
- data/test/dummy/app/controllers/application_controller.rb +5 -5
- data/test/dummy/app/controllers/authorized_controller.rb +8 -8
- data/test/dummy/app/helpers/application_helper.rb +2 -2
- data/test/dummy/app/models/group.rb +8 -8
- data/test/dummy/app/models/group_person.rb +6 -6
- data/test/dummy/app/models/person.rb +20 -20
- data/test/dummy/app/views/admin/index.html.erb +1 -1
- data/test/dummy/app/views/admin/people/_form.html.erb +34 -34
- data/test/dummy/app/views/admin/people/_table.html.erb +33 -33
- data/test/dummy/app/views/admin/people/edit.html.erb +4 -4
- data/test/dummy/app/views/admin/people/index.html.erb +3 -3
- data/test/dummy/app/views/admin/people/new.html.erb +5 -5
- data/test/dummy/app/views/admin/people/show.html.erb +3 -3
- data/test/dummy/app/views/authorized/index.html.erb +1 -1
- data/test/dummy/app/views/layouts/application.html.erb +14 -14
- data/test/dummy/bin/bundle +3 -3
- data/test/dummy/bin/rails +4 -4
- data/test/dummy/bin/rake +4 -4
- data/test/dummy/bin/setup +29 -29
- data/test/dummy/config.ru +4 -4
- data/test/dummy/config/application.rb +39 -39
- data/test/dummy/config/boot.rb +5 -5
- data/test/dummy/config/database.yml +22 -22
- data/test/dummy/config/environment.rb +5 -5
- data/test/dummy/config/environments/development.rb +41 -41
- data/test/dummy/config/environments/production.rb +79 -79
- data/test/dummy/config/environments/test.rb +42 -42
- data/test/dummy/config/initializers/assets.rb +10 -10
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -7
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -3
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -4
- data/test/dummy/config/initializers/inflections.rb +16 -16
- data/test/dummy/config/initializers/kadmin.rb +24 -24
- data/test/dummy/config/initializers/mime_types.rb +4 -4
- data/test/dummy/config/initializers/session_store.rb +3 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -14
- data/test/dummy/config/locales/en.yml +17 -17
- data/test/dummy/config/routes.rb +13 -13
- data/test/dummy/config/secrets.yml +22 -22
- data/test/dummy/db/migrate/20161006114509_create_people.rb +11 -11
- data/test/dummy/db/migrate/20161006134459_create_groups.rb +11 -11
- data/test/dummy/db/migrate/20161006134746_create_group_people.rb +11 -11
- data/test/dummy/db/schema.rb +43 -43
- data/test/dummy/lib/forms/group_form.rb +16 -16
- data/test/dummy/lib/forms/person_form.rb +19 -19
- data/test/dummy/public/404.html +67 -67
- data/test/dummy/public/422.html +67 -67
- data/test/dummy/public/500.html +66 -66
- data/test/dummy/test/fixtures/children.yml +11 -11
- data/test/dummy/test/fixtures/group_people.yml +11 -11
- data/test/dummy/test/fixtures/groups.yml +11 -11
- data/test/dummy/test/fixtures/people.yml +11 -11
- data/test/dummy/test/models/group_person_test.rb +7 -7
- data/test/dummy/test/models/group_test.rb +7 -7
- data/test/kadmin/form_test.rb +6 -6
- data/test/test_helper.rb +32 -32
- metadata +54 -53
data/lib/kadmin/configuration.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
|
-
module Kadmin
|
2
|
-
class Configuration
|
3
|
-
# @return [Logger] An instance of a Ruby compatible logger
|
4
|
-
attr_accessor :logger
|
5
|
-
|
6
|
-
# @return [String] the path the engine is mounted at (used for authentication routes)
|
7
|
-
attr_accessor :mount_path
|
8
|
-
|
9
|
-
# @return [Array<Hash<Symbol, String>] list of admin links, format: { title: '', path: '' }
|
10
|
-
attr_accessor :navbar_links
|
11
|
-
|
12
|
-
def initialize
|
13
|
-
@mount_path = '/admin'
|
14
|
-
@logger = Rails.logger
|
15
|
-
@navbar_links = []
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
1
|
+
module Kadmin
|
2
|
+
class Configuration
|
3
|
+
# @return [Logger] An instance of a Ruby compatible logger
|
4
|
+
attr_accessor :logger
|
5
|
+
|
6
|
+
# @return [String] the path the engine is mounted at (used for authentication routes)
|
7
|
+
attr_accessor :mount_path
|
8
|
+
|
9
|
+
# @return [Array<Hash<Symbol, String>] list of admin links, format: { title: '', path: '' }
|
10
|
+
attr_accessor :navbar_links
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@mount_path = '/admin'
|
14
|
+
@logger = Rails.logger
|
15
|
+
@navbar_links = []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/kadmin/engine.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
|
-
# Rails dependencies
|
2
|
-
require 'bootstrap-sass'
|
3
|
-
require 'sass-rails'
|
4
|
-
require 'jquery-rails'
|
5
|
-
require 'select2-rails'
|
6
|
-
|
7
|
-
module Kadmin
|
8
|
-
class Engine < ::Rails::Engine
|
9
|
-
isolate_namespace Kadmin
|
10
|
-
|
11
|
-
initializer 'kadmin.install', after: :finisher_hook do
|
12
|
-
Kadmin.logger = Rails.logger
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
1
|
+
# Rails dependencies
|
2
|
+
require 'bootstrap-sass'
|
3
|
+
require 'sass-rails'
|
4
|
+
require 'jquery-rails'
|
5
|
+
require 'select2-rails'
|
6
|
+
|
7
|
+
module Kadmin
|
8
|
+
class Engine < ::Rails::Engine
|
9
|
+
isolate_namespace Kadmin
|
10
|
+
|
11
|
+
initializer 'kadmin.install', after: :finisher_hook do
|
12
|
+
Kadmin.logger = Rails.logger
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/kadmin/error.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
module Kadmin
|
2
|
-
# Base class for all gem errors
|
3
|
-
class Error < StandardError
|
4
|
-
end
|
5
|
-
end
|
6
|
-
|
7
|
-
require 'kadmin/errors/authorization'
|
1
|
+
module Kadmin
|
2
|
+
# Base class for all gem errors
|
3
|
+
class Error < StandardError
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'kadmin/errors/authorization'
|
@@ -1,15 +1,15 @@
|
|
1
|
-
module Kadmin
|
2
|
-
module Errors
|
3
|
-
class Authorization < Kadmin::Error
|
4
|
-
attr_reader :resource, :user, :reason
|
5
|
-
|
6
|
-
def initialize(resource, user, reason)
|
7
|
-
@resource = resource
|
8
|
-
@user = user
|
9
|
-
@reason = reason
|
10
|
-
|
11
|
-
super("#{@user} is unauthorized to access #{@resource} => #{@reason}")
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
1
|
+
module Kadmin
|
2
|
+
module Errors
|
3
|
+
class Authorization < Kadmin::Error
|
4
|
+
attr_reader :resource, :user, :reason
|
5
|
+
|
6
|
+
def initialize(resource, user, reason)
|
7
|
+
@resource = resource
|
8
|
+
@user = user
|
9
|
+
@reason = reason
|
10
|
+
|
11
|
+
super("#{@user} is unauthorized to access #{@resource} => #{@reason}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/kadmin/finder.rb
CHANGED
@@ -1,66 +1,66 @@
|
|
1
|
-
module Kadmin
|
2
|
-
class Finder
|
3
|
-
# @return [Kadmin::Pager] the pager to use (if any)
|
4
|
-
attr_reader :pager
|
5
|
-
|
6
|
-
# @return [Array<Kadmin::Finder::Filter>] array of filters applied to the finder
|
7
|
-
attr_reader :filters
|
8
|
-
|
9
|
-
# @return [ActiveRecord::Relation] the base relation to find items from
|
10
|
-
attr_reader :scope
|
11
|
-
|
12
|
-
# Simple filter structure
|
13
|
-
Filter = Struct.new(:column, :value)
|
14
|
-
|
15
|
-
# @param [ActiveRecord::Relation] scope base relation to page/filter on
|
16
|
-
def initialize(scope)
|
17
|
-
@scope = scope
|
18
|
-
@pager = nil
|
19
|
-
@filters = {}
|
20
|
-
@results = nil
|
21
|
-
end
|
22
|
-
|
23
|
-
# @param [String] name the filter name (should be unique)
|
24
|
-
# @param [String, Array<String>] column the column(s) name to filter on
|
25
|
-
# @param [String, Array<String>] value the value or values to look for (OR'd)
|
26
|
-
def filter(name:, column:, value:)
|
27
|
-
if column.present? && !@filters.key?(name)
|
28
|
-
@filters[name] = Kadmin::Finder::Filter.new(column, value)
|
29
|
-
if value.present?
|
30
|
-
@scope = @scope.where("#{@scope.table_name}.`#{column}` LIKE ?", value.tr('*', '%'))
|
31
|
-
@pager&.total = @scope.count
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
return self
|
36
|
-
end
|
37
|
-
|
38
|
-
# @param [Integer] offset optional; offset/index for the current page
|
39
|
-
# @param [Integer] size optional; size of a page
|
40
|
-
# @return [Kadmin::Finder] itself
|
41
|
-
def paginate(offset: nil, size: nil)
|
42
|
-
offset = offset.to_i
|
43
|
-
size = size.to_i
|
44
|
-
|
45
|
-
if size.positive? && offset >= 0
|
46
|
-
@pager = Kadmin::Pager.new(size: size, offset: offset)
|
47
|
-
end
|
48
|
-
|
49
|
-
return self
|
50
|
-
end
|
51
|
-
|
52
|
-
# @return [ActiveRecord::Relation] the filtered (and optionally paginated) results
|
53
|
-
def results
|
54
|
-
return @results ||= begin
|
55
|
-
results = @scope
|
56
|
-
results = @pager.page(results) unless @pager.nil?
|
57
|
-
results
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def find!
|
62
|
-
@results = nil
|
63
|
-
return results
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
1
|
+
module Kadmin
|
2
|
+
class Finder
|
3
|
+
# @return [Kadmin::Pager] the pager to use (if any)
|
4
|
+
attr_reader :pager
|
5
|
+
|
6
|
+
# @return [Array<Kadmin::Finder::Filter>] array of filters applied to the finder
|
7
|
+
attr_reader :filters
|
8
|
+
|
9
|
+
# @return [ActiveRecord::Relation] the base relation to find items from
|
10
|
+
attr_reader :scope
|
11
|
+
|
12
|
+
# Simple filter structure
|
13
|
+
Filter = Struct.new(:column, :value)
|
14
|
+
|
15
|
+
# @param [ActiveRecord::Relation] scope base relation to page/filter on
|
16
|
+
def initialize(scope)
|
17
|
+
@scope = scope
|
18
|
+
@pager = nil
|
19
|
+
@filters = {}
|
20
|
+
@results = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [String] name the filter name (should be unique)
|
24
|
+
# @param [String, Array<String>] column the column(s) name to filter on
|
25
|
+
# @param [String, Array<String>] value the value or values to look for (OR'd)
|
26
|
+
def filter(name:, column:, value:)
|
27
|
+
if column.present? && !@filters.key?(name)
|
28
|
+
@filters[name] = Kadmin::Finder::Filter.new(column, value)
|
29
|
+
if value.present?
|
30
|
+
@scope = @scope.where("#{@scope.table_name}.`#{column}` LIKE ?", value.tr('*', '%'))
|
31
|
+
@pager&.total = @scope.count
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
return self
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param [Integer] offset optional; offset/index for the current page
|
39
|
+
# @param [Integer] size optional; size of a page
|
40
|
+
# @return [Kadmin::Finder] itself
|
41
|
+
def paginate(offset: nil, size: nil)
|
42
|
+
offset = offset.to_i
|
43
|
+
size = size.to_i
|
44
|
+
|
45
|
+
if size.positive? && offset >= 0
|
46
|
+
@pager = Kadmin::Pager.new(size: size, offset: offset)
|
47
|
+
end
|
48
|
+
|
49
|
+
return self
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [ActiveRecord::Relation] the filtered (and optionally paginated) results
|
53
|
+
def results
|
54
|
+
return @results ||= begin
|
55
|
+
results = @scope
|
56
|
+
results = @pager.page(results) unless @pager.nil?
|
57
|
+
results
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def find!
|
62
|
+
@results = nil
|
63
|
+
return results
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/kadmin/form.rb
CHANGED
@@ -1,179 +1,179 @@
|
|
1
|
-
# TODO: Figure out a way to not have to require ActiveModel/ActiveRecord preemptively
|
2
|
-
# perhaps by making Kadmin::Form optionally requireable?
|
3
|
-
require 'active_model/naming'
|
4
|
-
require 'active_model/callbacks'
|
5
|
-
require 'active_model/translation'
|
6
|
-
require 'active_model/validator'
|
7
|
-
require 'active_model/validations'
|
8
|
-
require 'active_record/attribute_assignment'
|
9
|
-
|
10
|
-
module Kadmin
|
11
|
-
# Parsing is done by using attribute setters. If you have an attribute called
|
12
|
-
# name, then add a reader/writer for it, name and name=, and perform the
|
13
|
-
# parsing in name=. If there is no parsing to be done, you can simply delegate
|
14
|
-
# the method to the underlying model.
|
15
|
-
#
|
16
|
-
# If the attribute is a nested form, in the writer, simply instantiate that
|
17
|
-
# form, and pass the attributes on to it, then update the model's association
|
18
|
-
# (if any) to reflect the changes.
|
19
|
-
#
|
20
|
-
# Validation is performed like on a normal model or ActiveRecord object.
|
21
|
-
# If you have no extra validation to perform than that of the model, simply
|
22
|
-
# delegate the validate and valid? methods to the model.
|
23
|
-
#
|
24
|
-
# To use nested forms, you need to add a reader and a writer. For example,
|
25
|
-
# for a form called Person, with potentially X nested Person forms as children,
|
26
|
-
# you would have:
|
27
|
-
# @example
|
28
|
-
# class PersonForm < Form
|
29
|
-
# def children
|
30
|
-
# [@child1, @child2]
|
31
|
-
# end
|
32
|
-
#
|
33
|
-
# def children_attributes=(attributes)
|
34
|
-
# ...instantiate subforms and pass attributes...
|
35
|
-
# end
|
36
|
-
# end
|
37
|
-
class Form
|
38
|
-
# Provides common validators and methods to add custom ones
|
39
|
-
include ActiveModel::Validations
|
40
|
-
|
41
|
-
# Provides translation scope and helpers (useful for error messages)
|
42
|
-
# Also includes ActiveModel::Naming at the same time
|
43
|
-
extend ActiveModel::Translation
|
44
|
-
|
45
|
-
# @return [ActiveModel::Model] underlying model to populate
|
46
|
-
attr_reader :model
|
47
|
-
|
48
|
-
delegate :id, :persisted?, :to_key, :to_query, :to_param, :type_for_attribute, to: :model
|
49
|
-
|
50
|
-
def initialize(model)
|
51
|
-
@errors = ActiveModel::Errors.new(self)
|
52
|
-
@model = model
|
53
|
-
@form_input = {}
|
54
|
-
end
|
55
|
-
|
56
|
-
def to_model
|
57
|
-
return @model
|
58
|
-
end
|
59
|
-
|
60
|
-
# @!group Attributes assignment/manipulation
|
61
|
-
|
62
|
-
# Allows parsing of multi parameter attributes, such as those returned by
|
63
|
-
# the form helpers date_select, datetime_select, etc.
|
64
|
-
# Also allows nested attributes, but this is not currently in use.
|
65
|
-
include ActiveRecord::AttributeAssignment
|
66
|
-
|
67
|
-
# For now, we overload the method to accept all attributes.
|
68
|
-
# This is removed in Rails 5, so once we upgrade we can remove the overload.
|
69
|
-
def sanitize_for_mass_assignment(attributes)
|
70
|
-
return attributes
|
71
|
-
end
|
72
|
-
|
73
|
-
class << self
|
74
|
-
# Delegates the list of attributes to the model, both readers and writers.
|
75
|
-
# If the attribute value passed is a hash and not a symbol, assumes it is
|
76
|
-
# a hash of one key, whose value is an array contained :reader, :writer, or both.
|
77
|
-
# @example
|
78
|
-
# delegate_attributes :first_name, { last_name: [:reader] }
|
79
|
-
# @param [Array<Symbol, Hash<Symbol, Array<Symbol>>>] attributes list of attributes to delegate to the model
|
80
|
-
def delegate_attributes(*attributes)
|
81
|
-
delegates = attributes.each_with_object([]) do |attribute, acc|
|
82
|
-
case attribute
|
83
|
-
when Hash
|
84
|
-
key, value = attribute.first
|
85
|
-
acc << key if value.include?(:reader)
|
86
|
-
acc << "#{key}=" if value.include?(:writer)
|
87
|
-
when Symbol, String
|
88
|
-
acc.push(attribute, "#{attribute}=")
|
89
|
-
else
|
90
|
-
raise(ArgumentError, 'Attribute must be one of: Hash, Symbol, String')
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
delegate(*delegates, to: :model)
|
95
|
-
end
|
96
|
-
|
97
|
-
# Delegates a specified associations to other another form object
|
98
|
-
# @example
|
99
|
-
# delegate_associations :child, :parent, to: 'Forms::PersonForm'
|
100
|
-
cattr_accessor(:associations) { {} }
|
101
|
-
def delegate_association(association, to:)
|
102
|
-
self.associations[association] = to
|
103
|
-
|
104
|
-
# add a reader attribute
|
105
|
-
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
106
|
-
def #{association}
|
107
|
-
return self.associated_forms['#{association}']
|
108
|
-
end
|
109
|
-
METHOD
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def associated_forms
|
114
|
-
return @associated_forms ||= begin
|
115
|
-
self.class.associations.map do |name, form_class_name|
|
116
|
-
form_class = form_class_name.constantize
|
117
|
-
form_class.new(@model.public_send(name))
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# @!endgroup
|
123
|
-
|
124
|
-
# @!group Validation
|
125
|
-
|
126
|
-
validate :validate_model
|
127
|
-
def validate_model
|
128
|
-
unless @model.valid?
|
129
|
-
@model.errors.each do |attribute, error|
|
130
|
-
@errors.add(attribute, error)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
protected :validate_model
|
135
|
-
|
136
|
-
validate :validate_associated_forms
|
137
|
-
def validate_associated_forms
|
138
|
-
self.associated_forms.each do |_name, form|
|
139
|
-
next if form.valid?
|
140
|
-
form.errors.each do |_attribute, _error|
|
141
|
-
@errors.add(:base, :association_error, "associated #{form.model_name.human} form has some errors")
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
protected :validate_associated_forms
|
146
|
-
|
147
|
-
# @!endgroup
|
148
|
-
|
149
|
-
# @!group Persistence
|
150
|
-
|
151
|
-
def save
|
152
|
-
saved = false
|
153
|
-
@model.class.transaction do
|
154
|
-
saved = @model.save
|
155
|
-
self.associated_forms.each do |_name, form|
|
156
|
-
saved &&= form.save
|
157
|
-
end
|
158
|
-
|
159
|
-
raise ActiveRecord::Rollback unless saved
|
160
|
-
end
|
161
|
-
|
162
|
-
return saved
|
163
|
-
end
|
164
|
-
|
165
|
-
def save!
|
166
|
-
saved = false
|
167
|
-
@model.class.transaction do
|
168
|
-
saved = @model.save!
|
169
|
-
self.associated_forms.each do |_name, form|
|
170
|
-
saved &&= form.save! # no need to raise anything, save! will do so
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
return saved
|
175
|
-
end
|
176
|
-
|
177
|
-
# @!endgroup
|
178
|
-
end
|
179
|
-
end
|
1
|
+
# TODO: Figure out a way to not have to require ActiveModel/ActiveRecord preemptively
|
2
|
+
# perhaps by making Kadmin::Form optionally requireable?
|
3
|
+
require 'active_model/naming'
|
4
|
+
require 'active_model/callbacks'
|
5
|
+
require 'active_model/translation'
|
6
|
+
require 'active_model/validator'
|
7
|
+
require 'active_model/validations'
|
8
|
+
require 'active_record/attribute_assignment'
|
9
|
+
|
10
|
+
module Kadmin
|
11
|
+
# Parsing is done by using attribute setters. If you have an attribute called
|
12
|
+
# name, then add a reader/writer for it, name and name=, and perform the
|
13
|
+
# parsing in name=. If there is no parsing to be done, you can simply delegate
|
14
|
+
# the method to the underlying model.
|
15
|
+
#
|
16
|
+
# If the attribute is a nested form, in the writer, simply instantiate that
|
17
|
+
# form, and pass the attributes on to it, then update the model's association
|
18
|
+
# (if any) to reflect the changes.
|
19
|
+
#
|
20
|
+
# Validation is performed like on a normal model or ActiveRecord object.
|
21
|
+
# If you have no extra validation to perform than that of the model, simply
|
22
|
+
# delegate the validate and valid? methods to the model.
|
23
|
+
#
|
24
|
+
# To use nested forms, you need to add a reader and a writer. For example,
|
25
|
+
# for a form called Person, with potentially X nested Person forms as children,
|
26
|
+
# you would have:
|
27
|
+
# @example
|
28
|
+
# class PersonForm < Form
|
29
|
+
# def children
|
30
|
+
# [@child1, @child2]
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# def children_attributes=(attributes)
|
34
|
+
# ...instantiate subforms and pass attributes...
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
class Form
|
38
|
+
# Provides common validators and methods to add custom ones
|
39
|
+
include ActiveModel::Validations
|
40
|
+
|
41
|
+
# Provides translation scope and helpers (useful for error messages)
|
42
|
+
# Also includes ActiveModel::Naming at the same time
|
43
|
+
extend ActiveModel::Translation
|
44
|
+
|
45
|
+
# @return [ActiveModel::Model] underlying model to populate
|
46
|
+
attr_reader :model
|
47
|
+
|
48
|
+
delegate :id, :persisted?, :to_key, :to_query, :to_param, :type_for_attribute, to: :model
|
49
|
+
|
50
|
+
def initialize(model)
|
51
|
+
@errors = ActiveModel::Errors.new(self)
|
52
|
+
@model = model
|
53
|
+
@form_input = {}
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_model
|
57
|
+
return @model
|
58
|
+
end
|
59
|
+
|
60
|
+
# @!group Attributes assignment/manipulation
|
61
|
+
|
62
|
+
# Allows parsing of multi parameter attributes, such as those returned by
|
63
|
+
# the form helpers date_select, datetime_select, etc.
|
64
|
+
# Also allows nested attributes, but this is not currently in use.
|
65
|
+
include ActiveRecord::AttributeAssignment
|
66
|
+
|
67
|
+
# For now, we overload the method to accept all attributes.
|
68
|
+
# This is removed in Rails 5, so once we upgrade we can remove the overload.
|
69
|
+
def sanitize_for_mass_assignment(attributes)
|
70
|
+
return attributes
|
71
|
+
end
|
72
|
+
|
73
|
+
class << self
|
74
|
+
# Delegates the list of attributes to the model, both readers and writers.
|
75
|
+
# If the attribute value passed is a hash and not a symbol, assumes it is
|
76
|
+
# a hash of one key, whose value is an array contained :reader, :writer, or both.
|
77
|
+
# @example
|
78
|
+
# delegate_attributes :first_name, { last_name: [:reader] }
|
79
|
+
# @param [Array<Symbol, Hash<Symbol, Array<Symbol>>>] attributes list of attributes to delegate to the model
|
80
|
+
def delegate_attributes(*attributes)
|
81
|
+
delegates = attributes.each_with_object([]) do |attribute, acc|
|
82
|
+
case attribute
|
83
|
+
when Hash
|
84
|
+
key, value = attribute.first
|
85
|
+
acc << key if value.include?(:reader)
|
86
|
+
acc << "#{key}=" if value.include?(:writer)
|
87
|
+
when Symbol, String
|
88
|
+
acc.push(attribute, "#{attribute}=")
|
89
|
+
else
|
90
|
+
raise(ArgumentError, 'Attribute must be one of: Hash, Symbol, String')
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
delegate(*delegates, to: :model)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Delegates a specified associations to other another form object
|
98
|
+
# @example
|
99
|
+
# delegate_associations :child, :parent, to: 'Forms::PersonForm'
|
100
|
+
cattr_accessor(:associations) { {} }
|
101
|
+
def delegate_association(association, to:)
|
102
|
+
self.associations[association] = to
|
103
|
+
|
104
|
+
# add a reader attribute
|
105
|
+
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
106
|
+
def #{association}
|
107
|
+
return self.associated_forms['#{association}']
|
108
|
+
end
|
109
|
+
METHOD
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def associated_forms
|
114
|
+
return @associated_forms ||= begin
|
115
|
+
self.class.associations.map do |name, form_class_name|
|
116
|
+
form_class = form_class_name.constantize
|
117
|
+
form_class.new(@model.public_send(name))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @!endgroup
|
123
|
+
|
124
|
+
# @!group Validation
|
125
|
+
|
126
|
+
validate :validate_model
|
127
|
+
def validate_model
|
128
|
+
unless @model.valid?
|
129
|
+
@model.errors.each do |attribute, error|
|
130
|
+
@errors.add(attribute, error)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
protected :validate_model
|
135
|
+
|
136
|
+
validate :validate_associated_forms
|
137
|
+
def validate_associated_forms
|
138
|
+
self.associated_forms.each do |_name, form|
|
139
|
+
next if form.valid?
|
140
|
+
form.errors.each do |_attribute, _error|
|
141
|
+
@errors.add(:base, :association_error, "associated #{form.model_name.human} form has some errors")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
protected :validate_associated_forms
|
146
|
+
|
147
|
+
# @!endgroup
|
148
|
+
|
149
|
+
# @!group Persistence
|
150
|
+
|
151
|
+
def save
|
152
|
+
saved = false
|
153
|
+
@model.class.transaction do
|
154
|
+
saved = @model.save
|
155
|
+
self.associated_forms.each do |_name, form|
|
156
|
+
saved &&= form.save
|
157
|
+
end
|
158
|
+
|
159
|
+
raise ActiveRecord::Rollback unless saved
|
160
|
+
end
|
161
|
+
|
162
|
+
return saved
|
163
|
+
end
|
164
|
+
|
165
|
+
def save!
|
166
|
+
saved = false
|
167
|
+
@model.class.transaction do
|
168
|
+
saved = @model.save!
|
169
|
+
self.associated_forms.each do |_name, form|
|
170
|
+
saved &&= form.save! # no need to raise anything, save! will do so
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
return saved
|
175
|
+
end
|
176
|
+
|
177
|
+
# @!endgroup
|
178
|
+
end
|
179
|
+
end
|