kadmin 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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