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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -44
  3. data/Rakefile +146 -146
  4. data/app/assets/javascripts/modular/app.js +1388 -1388
  5. data/app/assets/javascripts/modular/application.js +3 -3
  6. data/app/assets/javascripts/modular/vendor.js +57671 -57671
  7. data/app/assets/stylesheets/kadmin/typeahead-select.scss +2 -0
  8. data/app/assets/stylesheets/modular/app-blue.css +2795 -2795
  9. data/app/assets/stylesheets/modular/app-custom.css +2795 -2795
  10. data/app/assets/stylesheets/modular/app-green.css +2795 -2795
  11. data/app/assets/stylesheets/modular/app-orange.css +2795 -2795
  12. data/app/assets/stylesheets/modular/app-purple.css +2795 -2795
  13. data/app/assets/stylesheets/modular/app-red.css +2795 -2795
  14. data/app/assets/stylesheets/modular/app-seagreen.css +2795 -2795
  15. data/app/assets/stylesheets/modular/app.css +2795 -2795
  16. data/app/assets/stylesheets/modular/custom.css +51 -19
  17. data/app/assets/stylesheets/modular/vendor.css +12999 -12999
  18. data/app/controllers/kadmin/application_controller.rb +64 -64
  19. data/app/controllers/kadmin/auth_controller.rb +98 -98
  20. data/app/controllers/kadmin/concerns/authorized_user.rb +67 -67
  21. data/app/controllers/kadmin/dash_controller.rb +19 -19
  22. data/app/decorators/kadmin/finder_decorator.rb +50 -50
  23. data/app/decorators/kadmin/pager_decorator.rb +33 -33
  24. data/app/helpers/kadmin/alert_helper.rb +59 -59
  25. data/app/helpers/kadmin/application_helper.rb +4 -4
  26. data/app/helpers/kadmin/bootstrap_helper.rb +23 -23
  27. data/app/helpers/kadmin/form_builder.rb +9 -9
  28. data/app/helpers/kadmin/forms/inverted_check_box.rb +10 -10
  29. data/app/helpers/kadmin/navigation_helper.rb +28 -28
  30. data/app/helpers/kadmin/pagination_helper.rb +95 -95
  31. data/app/views/kadmin/auth/login.html.erb +4 -4
  32. data/app/views/kadmin/components/_finder.html.erb +14 -18
  33. data/app/views/kadmin/components/finder/_empty.html.erb +3 -3
  34. data/app/views/kadmin/components/finder/_form.erb +10 -10
  35. data/app/views/kadmin/components/finder/_header.html.erb +14 -11
  36. data/app/views/kadmin/dash/index.html.erb +5 -5
  37. data/app/views/kadmin/error.html.erb +5 -5
  38. data/app/views/kadmin/helpers/_alerts.html.erb +4 -4
  39. data/app/views/kadmin/helpers/_form_errors.html.erb +10 -10
  40. data/app/views/layouts/modular/application.html.erb +134 -132
  41. data/config/initializers/action_view.rb +2 -2
  42. data/config/initializers/assets.rb +5 -5
  43. data/config/locales/de.yml +25 -25
  44. data/config/locales/en.yml +24 -24
  45. data/config/routes.rb +12 -12
  46. data/lib/kadmin.rb +22 -22
  47. data/lib/kadmin/auth.rb +31 -31
  48. data/lib/kadmin/auth/configuration.rb +66 -66
  49. data/lib/kadmin/auth/unauthorized_error.rb +14 -14
  50. data/lib/kadmin/auth/user.rb +15 -15
  51. data/lib/kadmin/auth/user_store.rb +21 -21
  52. data/lib/kadmin/configuration.rb +18 -18
  53. data/lib/kadmin/engine.rb +15 -15
  54. data/lib/kadmin/error.rb +7 -7
  55. data/lib/kadmin/errors/authorization.rb +15 -15
  56. data/lib/kadmin/finder.rb +66 -66
  57. data/lib/kadmin/form.rb +179 -179
  58. data/lib/kadmin/pager.rb +93 -93
  59. data/lib/kadmin/version.rb +3 -3
  60. data/test/dummy/README.rdoc +28 -28
  61. data/test/dummy/Rakefile +6 -6
  62. data/test/dummy/app/assets/javascripts/application.js +13 -13
  63. data/test/dummy/app/assets/stylesheets/application.css +15 -15
  64. data/test/dummy/app/controllers/admin/application_controller.rb +11 -11
  65. data/test/dummy/app/controllers/admin/people_controller.rb +89 -89
  66. data/test/dummy/app/controllers/admin_controller.rb +4 -4
  67. data/test/dummy/app/controllers/application_controller.rb +5 -5
  68. data/test/dummy/app/controllers/authorized_controller.rb +8 -8
  69. data/test/dummy/app/helpers/application_helper.rb +2 -2
  70. data/test/dummy/app/models/group.rb +8 -8
  71. data/test/dummy/app/models/group_person.rb +6 -6
  72. data/test/dummy/app/models/person.rb +20 -20
  73. data/test/dummy/app/views/admin/index.html.erb +1 -1
  74. data/test/dummy/app/views/admin/people/_form.html.erb +34 -34
  75. data/test/dummy/app/views/admin/people/_table.html.erb +33 -33
  76. data/test/dummy/app/views/admin/people/edit.html.erb +4 -4
  77. data/test/dummy/app/views/admin/people/index.html.erb +3 -3
  78. data/test/dummy/app/views/admin/people/new.html.erb +5 -5
  79. data/test/dummy/app/views/admin/people/show.html.erb +3 -3
  80. data/test/dummy/app/views/authorized/index.html.erb +1 -1
  81. data/test/dummy/app/views/layouts/application.html.erb +14 -14
  82. data/test/dummy/bin/bundle +3 -3
  83. data/test/dummy/bin/rails +4 -4
  84. data/test/dummy/bin/rake +4 -4
  85. data/test/dummy/bin/setup +29 -29
  86. data/test/dummy/config.ru +4 -4
  87. data/test/dummy/config/application.rb +39 -39
  88. data/test/dummy/config/boot.rb +5 -5
  89. data/test/dummy/config/database.yml +22 -22
  90. data/test/dummy/config/environment.rb +5 -5
  91. data/test/dummy/config/environments/development.rb +41 -41
  92. data/test/dummy/config/environments/production.rb +79 -79
  93. data/test/dummy/config/environments/test.rb +42 -42
  94. data/test/dummy/config/initializers/assets.rb +10 -10
  95. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -7
  96. data/test/dummy/config/initializers/cookies_serializer.rb +3 -3
  97. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -4
  98. data/test/dummy/config/initializers/inflections.rb +16 -16
  99. data/test/dummy/config/initializers/kadmin.rb +24 -24
  100. data/test/dummy/config/initializers/mime_types.rb +4 -4
  101. data/test/dummy/config/initializers/session_store.rb +3 -3
  102. data/test/dummy/config/initializers/wrap_parameters.rb +14 -14
  103. data/test/dummy/config/locales/en.yml +17 -17
  104. data/test/dummy/config/routes.rb +13 -13
  105. data/test/dummy/config/secrets.yml +22 -22
  106. data/test/dummy/db/migrate/20161006114509_create_people.rb +11 -11
  107. data/test/dummy/db/migrate/20161006134459_create_groups.rb +11 -11
  108. data/test/dummy/db/migrate/20161006134746_create_group_people.rb +11 -11
  109. data/test/dummy/db/schema.rb +43 -43
  110. data/test/dummy/lib/forms/group_form.rb +16 -16
  111. data/test/dummy/lib/forms/person_form.rb +19 -19
  112. data/test/dummy/public/404.html +67 -67
  113. data/test/dummy/public/422.html +67 -67
  114. data/test/dummy/public/500.html +66 -66
  115. data/test/dummy/test/fixtures/children.yml +11 -11
  116. data/test/dummy/test/fixtures/group_people.yml +11 -11
  117. data/test/dummy/test/fixtures/groups.yml +11 -11
  118. data/test/dummy/test/fixtures/people.yml +11 -11
  119. data/test/dummy/test/models/group_person_test.rb +7 -7
  120. data/test/dummy/test/models/group_test.rb +7 -7
  121. data/test/kadmin/form_test.rb +6 -6
  122. data/test/test_helper.rb +32 -32
  123. metadata +54 -53
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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