kadmin 0.3.1 → 0.3.2
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 +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
|