iknow_view_models 2.8.4
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 +7 -0
- data/.circleci/config.yml +115 -0
- data/.gitignore +36 -0
- data/.travis.yml +31 -0
- data/Appraisals +9 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +19 -0
- data/Rakefile +21 -0
- data/appveyor.yml +22 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0_beta.gemfile +15 -0
- data/iknow_view_models.gemspec +49 -0
- data/lib/iknow_view_models.rb +12 -0
- data/lib/iknow_view_models/railtie.rb +8 -0
- data/lib/iknow_view_models/version.rb +5 -0
- data/lib/view_model.rb +333 -0
- data/lib/view_model/access_control.rb +154 -0
- data/lib/view_model/access_control/composed.rb +216 -0
- data/lib/view_model/access_control/open.rb +13 -0
- data/lib/view_model/access_control/read_only.rb +13 -0
- data/lib/view_model/access_control/tree.rb +264 -0
- data/lib/view_model/access_control_error.rb +10 -0
- data/lib/view_model/active_record.rb +383 -0
- data/lib/view_model/active_record/association_data.rb +178 -0
- data/lib/view_model/active_record/association_manipulation.rb +389 -0
- data/lib/view_model/active_record/cache.rb +265 -0
- data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
- data/lib/view_model/active_record/cloner.rb +113 -0
- data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
- data/lib/view_model/active_record/controller.rb +77 -0
- data/lib/view_model/active_record/controller_base.rb +185 -0
- data/lib/view_model/active_record/nested_controller_base.rb +93 -0
- data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
- data/lib/view_model/active_record/update_context.rb +252 -0
- data/lib/view_model/active_record/update_data.rb +749 -0
- data/lib/view_model/active_record/update_operation.rb +810 -0
- data/lib/view_model/active_record/visitor.rb +77 -0
- data/lib/view_model/after_transaction_runner.rb +29 -0
- data/lib/view_model/callbacks.rb +219 -0
- data/lib/view_model/changes.rb +62 -0
- data/lib/view_model/config.rb +29 -0
- data/lib/view_model/controller.rb +142 -0
- data/lib/view_model/deserialization_error.rb +437 -0
- data/lib/view_model/deserialize_context.rb +16 -0
- data/lib/view_model/error.rb +191 -0
- data/lib/view_model/error_view.rb +35 -0
- data/lib/view_model/record.rb +367 -0
- data/lib/view_model/record/attribute_data.rb +48 -0
- data/lib/view_model/reference.rb +31 -0
- data/lib/view_model/references.rb +48 -0
- data/lib/view_model/registry.rb +73 -0
- data/lib/view_model/schemas.rb +45 -0
- data/lib/view_model/serialization_error.rb +10 -0
- data/lib/view_model/serialize_context.rb +118 -0
- data/lib/view_model/test_helpers.rb +103 -0
- data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
- data/lib/view_model/traversal_context.rb +126 -0
- data/lib/view_model/utils.rb +24 -0
- data/lib/view_model/utils/collections.rb +49 -0
- data/test/helpers/arvm_test_models.rb +59 -0
- data/test/helpers/arvm_test_utilities.rb +187 -0
- data/test/helpers/callback_tracer.rb +27 -0
- data/test/helpers/controller_test_helpers.rb +270 -0
- data/test/helpers/match_enumerator.rb +58 -0
- data/test/helpers/query_logging.rb +71 -0
- data/test/helpers/test_access_control.rb +56 -0
- data/test/helpers/viewmodel_spec_helpers.rb +326 -0
- data/test/unit/view_model/access_control_test.rb +769 -0
- data/test/unit/view_model/active_record/alias_test.rb +35 -0
- data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
- data/test/unit/view_model/active_record/cache_test.rb +351 -0
- data/test/unit/view_model/active_record/cloner_test.rb +313 -0
- data/test/unit/view_model/active_record/controller_test.rb +561 -0
- data/test/unit/view_model/active_record/counter_test.rb +80 -0
- data/test/unit/view_model/active_record/customization_test.rb +388 -0
- data/test/unit/view_model/active_record/has_many_test.rb +957 -0
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
- data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
- data/test/unit/view_model/active_record/has_one_test.rb +334 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
- data/test/unit/view_model/active_record/poly_test.rb +320 -0
- data/test/unit/view_model/active_record/shared_test.rb +285 -0
- data/test/unit/view_model/active_record/version_test.rb +121 -0
- data/test/unit/view_model/active_record_test.rb +542 -0
- data/test/unit/view_model/callbacks_test.rb +582 -0
- data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
- data/test/unit/view_model/record_test.rb +524 -0
- data/test/unit/view_model/traversal_context_test.rb +371 -0
- data/test/unit/view_model_test.rb +62 -0
- metadata +490 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
require 'view_model'
|
|
2
|
+
require 'view_model/test_helpers'
|
|
3
|
+
|
|
4
|
+
require 'minitest/unit'
|
|
5
|
+
require 'minitest/hooks'
|
|
6
|
+
|
|
7
|
+
module ViewModelSpecHelpers
|
|
8
|
+
module Base
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
include Minitest::Hooks # not a concern, can I do this?
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
around do |&block|
|
|
14
|
+
@builders = []
|
|
15
|
+
super(&block)
|
|
16
|
+
@builders.each do |b|
|
|
17
|
+
b.teardown
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def namespace
|
|
23
|
+
Object
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def viewmodel_base
|
|
27
|
+
ViewModelBase
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def model_base
|
|
31
|
+
ApplicationRecord
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def model_class
|
|
35
|
+
viewmodel_class.model_class
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def child_model_class
|
|
39
|
+
child_viewmodel_class.model_class
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def view_name
|
|
43
|
+
viewmodel_class.view_name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def child_view_name
|
|
47
|
+
child_viewmodel_class.view_name
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def viewmodel_class
|
|
51
|
+
@viewmodel_class ||= define_viewmodel_class(
|
|
52
|
+
:Model,
|
|
53
|
+
spec: model_attributes,
|
|
54
|
+
namespace: namespace,
|
|
55
|
+
viewmodel_base: viewmodel_base,
|
|
56
|
+
model_base: model_base).tap { |klass| yield(klass) if block_given? }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def child_viewmodel_class
|
|
60
|
+
@child_viewmodel_class ||= define_viewmodel_class(
|
|
61
|
+
:Child,
|
|
62
|
+
spec: child_attributes,
|
|
63
|
+
namespace: namespace,
|
|
64
|
+
viewmodel_base: viewmodel_base,
|
|
65
|
+
model_base: model_base).tap { |klass| yield(klass) if block_given? }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def create_viewmodel!
|
|
69
|
+
viewmodel_class.new(create_model!)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_model!
|
|
73
|
+
new_model.tap { |m| m.save! }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def model_attributes
|
|
77
|
+
ViewModel::TestHelpers::ARVMBuilder::Spec.new(
|
|
78
|
+
schema: ->(t) { t.string :name },
|
|
79
|
+
model: ->(m) {},
|
|
80
|
+
viewmodel: ->(v) { attribute :name }
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def child_attributes
|
|
85
|
+
ViewModel::TestHelpers::ARVMBuilder::Spec.new(
|
|
86
|
+
schema: ->(t) { t.string :name },
|
|
87
|
+
model: ->(m) {},
|
|
88
|
+
viewmodel: ->(v) { attribute :name }
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def define_viewmodel_class(name, **args, &block)
|
|
93
|
+
builder = ViewModel::TestHelpers::ARVMBuilder.new(name, **args, &block)
|
|
94
|
+
@builders << builder
|
|
95
|
+
builder.viewmodel
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def subject_association
|
|
99
|
+
raise RuntimeError.new('Test model does not have a child association')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def subject_association_name
|
|
103
|
+
subject_association.association_name
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
module Single
|
|
108
|
+
extend ActiveSupport::Concern
|
|
109
|
+
include ViewModelSpecHelpers::Base
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
module ParentAndBelongsToChild
|
|
113
|
+
extend ActiveSupport::Concern
|
|
114
|
+
include ViewModelSpecHelpers::Base
|
|
115
|
+
|
|
116
|
+
def model_attributes
|
|
117
|
+
super.merge(schema: ->(t) { t.references :child, foreign_key: true },
|
|
118
|
+
model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
|
|
119
|
+
viewmodel: ->(v) { association :child })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def child_attributes
|
|
123
|
+
super.merge(model: ->(m) { has_one :model, inverse_of: :child })
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# parent depends on child, ensure it's touched first
|
|
127
|
+
def viewmodel_class
|
|
128
|
+
child_viewmodel_class
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def subject_association
|
|
133
|
+
viewmodel_class._association_data('child')
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
module List
|
|
138
|
+
extend ActiveSupport::Concern
|
|
139
|
+
include ViewModelSpecHelpers::Base
|
|
140
|
+
|
|
141
|
+
def model_attributes
|
|
142
|
+
super.merge(schema: ->(t) { t.integer :next_id },
|
|
143
|
+
model: ->(m) {
|
|
144
|
+
belongs_to :next, class_name: self.name, inverse_of: :previous, dependent: :destroy
|
|
145
|
+
has_one :previous, class_name: self.name, foreign_key: :next_id, inverse_of: :next
|
|
146
|
+
},
|
|
147
|
+
viewmodel: ->(v) { association :next })
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def subject_association
|
|
151
|
+
viewmodel_class._association_data('next')
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
module ParentAndHasOneChild
|
|
156
|
+
extend ActiveSupport::Concern
|
|
157
|
+
include ViewModelSpecHelpers::Base
|
|
158
|
+
|
|
159
|
+
def model_attributes
|
|
160
|
+
super.merge(
|
|
161
|
+
model: ->(m) { has_one :child, inverse_of: :model, dependent: :destroy },
|
|
162
|
+
viewmodel: ->(v) { association :child }
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def child_attributes
|
|
167
|
+
super.merge(
|
|
168
|
+
schema: ->(t) { t.references :model, foreign_key: true },
|
|
169
|
+
model: ->(m) { belongs_to :model, inverse_of: :child }
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# child depends on parent, ensure it's touched first
|
|
174
|
+
def child_viewmodel_class
|
|
175
|
+
viewmodel_class
|
|
176
|
+
super
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def subject_association
|
|
180
|
+
viewmodel_class._association_data('child')
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
module ParentAndHasManyChildren
|
|
185
|
+
extend ActiveSupport::Concern
|
|
186
|
+
include ViewModelSpecHelpers::Base
|
|
187
|
+
|
|
188
|
+
def model_attributes
|
|
189
|
+
super.merge(
|
|
190
|
+
model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
|
|
191
|
+
viewmodel: ->(v) { association :children }
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def child_attributes
|
|
196
|
+
super.merge(
|
|
197
|
+
schema: ->(t) { t.references :model, foreign_key: true },
|
|
198
|
+
model: ->(m) { belongs_to :model, inverse_of: :children }
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# child depends on parent, ensure it's touched first
|
|
203
|
+
def child_viewmodel_class
|
|
204
|
+
viewmodel_class
|
|
205
|
+
super
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def subject_association
|
|
209
|
+
viewmodel_class._association_data('children')
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
module ParentAndOrderedChildren
|
|
214
|
+
extend ActiveSupport::Concern
|
|
215
|
+
include ViewModelSpecHelpers::Base
|
|
216
|
+
|
|
217
|
+
def model_attributes
|
|
218
|
+
super.merge(
|
|
219
|
+
model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
|
|
220
|
+
viewmodel: ->(v) { association :children },
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def child_attributes
|
|
225
|
+
super.merge(
|
|
226
|
+
schema: ->(t) { t.references :model, foreign_key: true; t.float :position, null: false },
|
|
227
|
+
model: ->(m) { belongs_to :model, inverse_of: :children },
|
|
228
|
+
viewmodel: ->(v) { acts_as_list :position },
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def child_viewmodel_class
|
|
233
|
+
# child depends on parent, ensure it's touched first
|
|
234
|
+
viewmodel_class
|
|
235
|
+
|
|
236
|
+
# Add a deferrable unique position constraiont
|
|
237
|
+
super do |klass|
|
|
238
|
+
model = klass.model_class
|
|
239
|
+
table = model.table_name
|
|
240
|
+
model.connection.execute <<-SQL
|
|
241
|
+
ALTER TABLE #{table} ADD CONSTRAINT #{table}_unique_on_model_and_position UNIQUE(model_id, position) DEFERRABLE INITIALLY DEFERRED
|
|
242
|
+
SQL
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def subject_association
|
|
247
|
+
viewmodel_class._association_data('children')
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
module ParentAndSharedChild
|
|
252
|
+
extend ActiveSupport::Concern
|
|
253
|
+
include ViewModelSpecHelpers::Base
|
|
254
|
+
|
|
255
|
+
def model_attributes
|
|
256
|
+
super.merge(
|
|
257
|
+
schema: ->(t) { t.references :child, foreign_key: true },
|
|
258
|
+
model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
|
|
259
|
+
viewmodel: ->(v) { association :child, shared: true }
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def child_attributes
|
|
264
|
+
super.merge(
|
|
265
|
+
model: ->(m) { has_one :model, inverse_of: :child }
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# parent depends on child, ensure it's touched first
|
|
270
|
+
def viewmodel_class
|
|
271
|
+
child_viewmodel_class
|
|
272
|
+
super
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def subject_association
|
|
276
|
+
viewmodel_class._association_data('child')
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
module ParentAndHasManyThroughChildren
|
|
281
|
+
extend ActiveSupport::Concern
|
|
282
|
+
include ViewModelSpecHelpers::Base
|
|
283
|
+
|
|
284
|
+
def model_attributes
|
|
285
|
+
super.merge(
|
|
286
|
+
model: ->(m) { has_many :model_children, inverse_of: :model, dependent: :destroy },
|
|
287
|
+
viewmodel: ->(v) { association :children, shared: true, through: :model_children, through_order_attr: :position }
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def child_attributes
|
|
292
|
+
super.merge(
|
|
293
|
+
model: ->(m) { has_many :model_children, inverse_of: :child, dependent: :destroy }
|
|
294
|
+
)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def join_model_class
|
|
298
|
+
# depends on parent and child
|
|
299
|
+
viewmodel_class
|
|
300
|
+
child_viewmodel_class
|
|
301
|
+
|
|
302
|
+
@join_model_class ||=
|
|
303
|
+
begin
|
|
304
|
+
define_viewmodel_class(:ModelChild) do
|
|
305
|
+
define_schema do |t|
|
|
306
|
+
t.references :model, foreign_key: true
|
|
307
|
+
t.references :child, foreign_key: true
|
|
308
|
+
t.float :position
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
define_model do
|
|
312
|
+
belongs_to :model
|
|
313
|
+
belongs_to :child
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
no_viewmodel
|
|
317
|
+
end
|
|
318
|
+
ModelChild
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def subject_association
|
|
323
|
+
viewmodel_class._association_data('children')
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../helpers/arvm_test_utilities.rb"
|
|
4
|
+
require_relative "../../helpers/arvm_test_models.rb"
|
|
5
|
+
require_relative '../../helpers/viewmodel_spec_helpers.rb'
|
|
6
|
+
|
|
7
|
+
require "minitest/autorun"
|
|
8
|
+
require 'minitest/unit'
|
|
9
|
+
|
|
10
|
+
require 'rspec/expectations/minitest_integration'
|
|
11
|
+
|
|
12
|
+
require "view_model/active_record"
|
|
13
|
+
|
|
14
|
+
class ViewModel::AccessControlTest < ActiveSupport::TestCase
|
|
15
|
+
include ARVMTestUtilities
|
|
16
|
+
|
|
17
|
+
class ComposedTest < ActiveSupport::TestCase
|
|
18
|
+
include ARVMTestUtilities
|
|
19
|
+
|
|
20
|
+
def before_all
|
|
21
|
+
super
|
|
22
|
+
|
|
23
|
+
build_viewmodel(:List) do
|
|
24
|
+
define_schema do |t|
|
|
25
|
+
t.string :car
|
|
26
|
+
t.integer :cdr_id
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
define_model do
|
|
30
|
+
belongs_to :cdr, class_name: :List, dependent: :destroy
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_viewmodel do
|
|
34
|
+
attribute :car
|
|
35
|
+
association :cdr
|
|
36
|
+
|
|
37
|
+
def self.new_serialize_context(**args)
|
|
38
|
+
super(access_control: TestAccessControl.new, **args)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.new_deserialize_context(**args)
|
|
42
|
+
super(access_control: TestAccessControl.new, **args)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def setup
|
|
49
|
+
ComposedTest.const_set(:TestAccessControl, Class.new(ViewModel::AccessControl::Composed))
|
|
50
|
+
enable_logging!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def teardown
|
|
54
|
+
ComposedTest.send(:remove_const, :TestAccessControl)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_visible_if
|
|
58
|
+
TestAccessControl.visible_if!("car is visible1") do
|
|
59
|
+
view.car == "visible1"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
TestAccessControl.visible_if!("car is visible2") do
|
|
63
|
+
view.car == "visible2"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
assert_serializes(ListView, List.create!(car: "visible1"))
|
|
67
|
+
assert_serializes(ListView, List.create!(car: "visible2"))
|
|
68
|
+
ex = refute_serializes(ListView, List.create!(car: "bad"), /none of the possible/)
|
|
69
|
+
assert_equal(2, ex.reasons.count)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_visible_unless
|
|
73
|
+
TestAccessControl.visible_if!("always") { true }
|
|
74
|
+
|
|
75
|
+
TestAccessControl.visible_unless!("car is invisible") do
|
|
76
|
+
view.car == "invisible"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
assert_serializes(ListView, List.create!(car: "ok"))
|
|
80
|
+
refute_serializes(ListView, List.create!(car: "invisible"), /not permitted.*car is invisible/)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_editable_if
|
|
84
|
+
TestAccessControl.visible_if!("always") { true }
|
|
85
|
+
|
|
86
|
+
TestAccessControl.editable_if!("car is editable1") do
|
|
87
|
+
view.car == "editable1"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
TestAccessControl.editable_if!("car is editable2") do
|
|
91
|
+
view.car == "editable2"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
assert_deserializes(ListView, List.create!(car: "editable1")) { |v, _| v["car"] = "unchecked" }
|
|
95
|
+
assert_deserializes(ListView, List.create!(car: "editable2")) { |v, _| v["car"] = "unchecked" }
|
|
96
|
+
assert_deserializes(ListView, List.create!(car: "forbidden")) { |v, _| v["car"] = "forbidden" } # no change so permitted
|
|
97
|
+
refute_deserializes(ListView, List.create!(car: "forbidden"), /none of the possible/) { |v, _| v["car"] = "unchecked" }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_editable_unless
|
|
101
|
+
TestAccessControl.visible_if!("always") { true }
|
|
102
|
+
TestAccessControl.editable_if!("always") { true }
|
|
103
|
+
|
|
104
|
+
TestAccessControl.editable_unless!("car is uneditable") do
|
|
105
|
+
view.car == "uneditable"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
assert_deserializes(ListView, List.create!(car: "ok")) { |v, _| v["car"] = "unchecked" }
|
|
109
|
+
assert_deserializes(ListView, List.create!(car: "uneditable")) { |v, _| v["car"] = "uneditable" } # no change so permitted
|
|
110
|
+
refute_deserializes(ListView, List.create!(car: "uneditable"), /car is uneditable/) { |v, _| v["car"] = "unchecked" }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_edit_valid_if
|
|
114
|
+
TestAccessControl.visible_if!("always") { true }
|
|
115
|
+
|
|
116
|
+
TestAccessControl.edit_valid_if!("car is validedit") do
|
|
117
|
+
view.car == "validedit"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
assert_deserializes(ListView, List.create!(car: "unchecked")) { |v, _| v["car"] = "validedit" }
|
|
121
|
+
assert_deserializes(ListView, List.create!(car: "unmodified")) { |v, _| v["car"] = "unmodified" } # no change so permitted
|
|
122
|
+
refute_deserializes(ListView, List.create!(car: "unchecked"), /none of the possible/) { |v, _| v["car"] = "bad" }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_edit_valid_unless
|
|
126
|
+
TestAccessControl.visible_if!("always") { true }
|
|
127
|
+
TestAccessControl.edit_valid_if!("always") { true }
|
|
128
|
+
TestAccessControl.edit_valid_unless!("car is invalidedit") do
|
|
129
|
+
view.car == "invalidedit"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
assert_deserializes(ListView, List.create!(car: "unchecked")) { |v, _| v["car"] = "ok" }
|
|
133
|
+
assert_deserializes(ListView, List.create!(car: "invalidedit")) { |v, _| v["car"] = "invalidedit" }
|
|
134
|
+
refute_deserializes(ListView, List.create!(car: "unchecked"), /car is invalidedit/) { |v, _| v["car"] = "invalidedit" }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_editable_and_edit_valid
|
|
138
|
+
TestAccessControl.visible_if!("always") { true }
|
|
139
|
+
|
|
140
|
+
TestAccessControl.editable_if!("original car permits") do
|
|
141
|
+
view.car == "permitoriginal"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
TestAccessControl.edit_valid_if!("resulting car permits") do
|
|
145
|
+
view.car == "permitresult"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# at least one valid
|
|
149
|
+
assert_deserializes(ListView, List.create!(car: "permitoriginal")) { |v, _| v["car"] = "permitresult" }
|
|
150
|
+
assert_deserializes(ListView, List.create!(car: "badoriginal")) { |v, _| v["car"] = "permitresult" }
|
|
151
|
+
assert_deserializes(ListView, List.create!(car: "permitoriginal")) { |v, _| v["car"] = "badresult" }
|
|
152
|
+
|
|
153
|
+
# no valid
|
|
154
|
+
ex = refute_deserializes(ListView, List.create!(car: "badoriginal"), /none of the possible/) { |v, _| v["car"] = "badresult" }
|
|
155
|
+
|
|
156
|
+
assert_equal(2, ex.reasons.count)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_inheritance
|
|
160
|
+
child_access_control = Class.new(ViewModel::AccessControl::Composed)
|
|
161
|
+
child_access_control.include_from(TestAccessControl)
|
|
162
|
+
|
|
163
|
+
TestAccessControl.visible_if!("car is ancestor") { view.car == "ancestor" }
|
|
164
|
+
child_access_control.visible_if!("car is descendent") { view.car == "descendent" }
|
|
165
|
+
|
|
166
|
+
s_ctx = ListView.new_serialize_context(access_control: child_access_control.new)
|
|
167
|
+
|
|
168
|
+
assert_serializes(ListView, List.create!(car: "ancestor"), serialize_context: s_ctx)
|
|
169
|
+
assert_serializes(ListView, List.create!(car: "descendent"), serialize_context: s_ctx)
|
|
170
|
+
ex = refute_serializes(ListView, List.create!(car: "foreigner"), serialize_context: s_ctx)
|
|
171
|
+
assert_equal(2, ex.reasons.count)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class TreeTest < ActiveSupport::TestCase
|
|
176
|
+
include ARVMTestUtilities
|
|
177
|
+
|
|
178
|
+
def before_all
|
|
179
|
+
super
|
|
180
|
+
|
|
181
|
+
# Tree1 is a root, which owns Tree2.
|
|
182
|
+
build_viewmodel(:Tree1) do
|
|
183
|
+
define_schema do |t|
|
|
184
|
+
t.string :val
|
|
185
|
+
t.integer :tree2_id
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
define_model do
|
|
189
|
+
belongs_to :tree2, class_name: :Tree2, dependent: :destroy
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
define_viewmodel do
|
|
193
|
+
attribute :val
|
|
194
|
+
association :tree2
|
|
195
|
+
|
|
196
|
+
def self.new_serialize_context(**args)
|
|
197
|
+
super(access_control: TestAccessControl.new, **args)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.new_deserialize_context(**args)
|
|
201
|
+
super(access_control: TestAccessControl.new, **args)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
build_viewmodel(:Tree2) do
|
|
207
|
+
define_schema do |t|
|
|
208
|
+
t.string :val
|
|
209
|
+
t.integer :tree1_id
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
define_model do
|
|
213
|
+
belongs_to :tree1, class_name: :Tree1, dependent: :destroy
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
define_viewmodel do
|
|
217
|
+
attribute :val
|
|
218
|
+
association :tree1, shared: true, optional: false
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def setup
|
|
224
|
+
TreeTest.const_set(:TestAccessControl, Class.new(ViewModel::AccessControl::Tree))
|
|
225
|
+
enable_logging!
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def teardown
|
|
229
|
+
TreeTest.send(:remove_const, :TestAccessControl)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def make_tree(*vals)
|
|
233
|
+
tree = vals.each_slice(2).reverse_each.inject(nil) do |rest, (t1, t2)|
|
|
234
|
+
Tree1.new(val: t1, tree2: Tree2.new(val: t2, tree1: rest))
|
|
235
|
+
end
|
|
236
|
+
tree.save!
|
|
237
|
+
tree
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def dig_tree(root, refs, attr, *rest)
|
|
241
|
+
raise "Root missing attribute '#{attr}'" unless root.has_key?(attr)
|
|
242
|
+
|
|
243
|
+
child = root[attr]
|
|
244
|
+
|
|
245
|
+
if (child_ref = child["_ref"])
|
|
246
|
+
child = refs[child_ref]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
if rest.empty?
|
|
250
|
+
child
|
|
251
|
+
else
|
|
252
|
+
dig_tree(child, refs, *rest)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def test_visibility_from_root
|
|
257
|
+
TestAccessControl.view "Tree1" do
|
|
258
|
+
visible_if!("true") { true }
|
|
259
|
+
|
|
260
|
+
root_children_visible_if!("root children visible") do
|
|
261
|
+
view.val == "rule:visible_children"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
refute_serializes(Tree1View, make_tree("arbitrary parent", "invisible child"))
|
|
266
|
+
assert_serializes(Tree1View, make_tree("rule:visible_children", "visible child"))
|
|
267
|
+
|
|
268
|
+
# nested root
|
|
269
|
+
refute_serializes(Tree1View, make_tree("rule:visible_children", "visible child", "arbitrary parent", "invisible child"))
|
|
270
|
+
assert_serializes(Tree1View, make_tree("rule:visible_children", "visible child", "rule:visible_children", "visible child"))
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def test_visibility_veto_from_root
|
|
274
|
+
TestAccessControl.view "Tree1" do
|
|
275
|
+
root_children_visible_unless!("root children invisible") do
|
|
276
|
+
view.val == "rule:invisible_children"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
TestAccessControl.always do
|
|
281
|
+
visible_if!("true") { true }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
assert_serializes(Tree1View, make_tree("arbitrary parent", "visible child"))
|
|
285
|
+
refute_serializes(Tree1View, make_tree("rule:invisible_children", "invisible child"))
|
|
286
|
+
|
|
287
|
+
# nested root
|
|
288
|
+
assert_serializes(Tree1View, make_tree("arbitrary parent", "visible child", "arbitrary nested parent", "visible child"))
|
|
289
|
+
refute_serializes(Tree1View, make_tree("arbitrary parent", "visible child", "rule:invisible_children", "invisible child"))
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def test_editability_from_root
|
|
293
|
+
TestAccessControl.always do
|
|
294
|
+
visible_if!("always") { true }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
TestAccessControl.view "Tree1" do
|
|
298
|
+
editable_if!("true") { true }
|
|
299
|
+
|
|
300
|
+
root_children_editable_if!("root children editable") do
|
|
301
|
+
view.val == "rule:editable_children"
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
refute_deserializes(Tree1View, make_tree("arbitrary parent", "uneditable child")) { |v, r|
|
|
306
|
+
dig_tree(v, r, "tree2")["val"] = "change"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
assert_deserializes(Tree1View, make_tree("rule:editable_children", "editable child")) { |v, r|
|
|
310
|
+
dig_tree(v, r, "tree2")["val"] = "change"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# nested root
|
|
314
|
+
refute_deserializes(Tree1View, make_tree("rule:editable_children", "editable child", "arbitrary parent", "uneditable child")) { |v, r|
|
|
315
|
+
dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
assert_deserializes(Tree1View, make_tree("arbitrary parent", "uneditable child", "rule:editable_children", "editable child")) { |v, r|
|
|
319
|
+
dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
|
|
320
|
+
}
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def test_editability_veto_from_root
|
|
324
|
+
TestAccessControl.always do
|
|
325
|
+
visible_if!("always") { true }
|
|
326
|
+
editable_if!("always") { true }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
TestAccessControl.view "Tree1" do
|
|
330
|
+
root_children_editable_unless!("root children uneditable") do
|
|
331
|
+
view.val == "rule:uneditable_children"
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
refute_deserializes(Tree1View, make_tree("rule:uneditable_children", "uneditable child")) { |v, r|
|
|
336
|
+
dig_tree(v, r, "tree2")["val"] = "change"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
assert_deserializes(Tree1View, make_tree("arbitrary parent", "editable child")) { |v, r|
|
|
340
|
+
dig_tree(v, r, "tree2")["val"] = "change"
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# nested root
|
|
344
|
+
refute_deserializes(Tree1View, make_tree("arbitrary parent", "editable child", "rule:uneditable_children", "uneditable child")) { |v, r|
|
|
345
|
+
dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
assert_deserializes(Tree1View, make_tree("rule:uneditable_children", "uneditable child", "arbitrary parent", "editable child")) { |v, r|
|
|
349
|
+
dig_tree(v, r, "tree2", "tree1", "tree2")["val"] = "change"
|
|
350
|
+
}
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def test_type_independence
|
|
354
|
+
TestAccessControl.view "Tree1" do
|
|
355
|
+
visible_if!("tree1 visible") do
|
|
356
|
+
view.val == "tree1visible"
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
TestAccessControl.view "Tree2" do
|
|
361
|
+
visible_if!("tree2 visible") do
|
|
362
|
+
view.val == "tree2visible"
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
refute_serializes(Tree1View, make_tree("tree1invisible", "tree2visible"))
|
|
367
|
+
assert_serializes(Tree1View, make_tree("tree1visible", "tree2visible"))
|
|
368
|
+
refute_serializes(Tree1View, make_tree("tree1visible", "tree2invisible"))
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def test_visibility_always_composition
|
|
372
|
+
TestAccessControl.view "Tree1" do
|
|
373
|
+
visible_if!("tree1 visible") do
|
|
374
|
+
view.val == "tree1visible"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
TestAccessControl.always do
|
|
379
|
+
visible_if!("tree2 visible") do
|
|
380
|
+
view.val == "alwaysvisible"
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
refute_serializes(Tree1View, Tree1.create(val: "bad"))
|
|
385
|
+
assert_serializes(Tree1View, Tree1.create(val: "tree1visible"))
|
|
386
|
+
assert_serializes(Tree1View, Tree1.create(val: "alwaysvisible"))
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def test_editability_always_composition
|
|
390
|
+
TestAccessControl.view "Tree1" do
|
|
391
|
+
editable_if!("editable1") { view.val == "editable1" }
|
|
392
|
+
edit_valid_if!("editvalid1") { view.val == "editvalid1" }
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
TestAccessControl.always do
|
|
396
|
+
editable_if!("editable2") { view.val == "editable2" }
|
|
397
|
+
edit_valid_if!("editvalid2") { view.val == "editvalid2" }
|
|
398
|
+
|
|
399
|
+
visible_if!("always") { true }
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
refute_deserializes(Tree1View, Tree1.create!(val: "bad")) { |v, _| v["val"] = "alsobad" }
|
|
403
|
+
|
|
404
|
+
assert_deserializes(Tree1View, Tree1.create!(val: "editable1")) { |v, _| v["val"] = "unchecked" }
|
|
405
|
+
assert_deserializes(Tree1View, Tree1.create!(val: "editable2")) { |v, _| v["val"] = "unchecked" }
|
|
406
|
+
|
|
407
|
+
assert_deserializes(Tree1View, Tree1.create!(val: "unchecked")) { |v, _| v["val"] = "editvalid1" }
|
|
408
|
+
assert_deserializes(Tree1View, Tree1.create!(val: "unchecked")) { |v, _| v["val"] = "editvalid2" }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def test_ancestry
|
|
412
|
+
TestAccessControl.view "Tree1" do
|
|
413
|
+
visible_if!("parent tree1") { view.val == "parenttree1" }
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
TestAccessControl.always do
|
|
417
|
+
visible_if!("parent always") { view.val == "parentalways" }
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Child must be set up after parent is fully defined
|
|
421
|
+
child_access_control = Class.new(ViewModel::AccessControl::Tree)
|
|
422
|
+
child_access_control.include_from(TestAccessControl)
|
|
423
|
+
|
|
424
|
+
child_access_control.view "Tree1" do
|
|
425
|
+
visible_if!("child tree1") { view.val == "childtree1" }
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
child_access_control.always do
|
|
429
|
+
visible_if!("child always") { view.val == "childalways" }
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
s_ctx = Tree1View.new_serialize_context(access_control: child_access_control.new)
|
|
433
|
+
|
|
434
|
+
refute_serializes(Tree1View, Tree1.create!(val: "bad"), serialize_context: s_ctx)
|
|
435
|
+
|
|
436
|
+
assert_serializes(Tree1View, Tree1.create!(val: "parenttree1"), serialize_context: s_ctx)
|
|
437
|
+
assert_serializes(Tree1View, Tree1.create!(val: "parentalways"), serialize_context: s_ctx)
|
|
438
|
+
assert_serializes(Tree1View, Tree1.create!(val: "childtree1"), serialize_context: s_ctx)
|
|
439
|
+
assert_serializes(Tree1View, Tree1.create!(val: "childalways"), serialize_context: s_ctx)
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Integration-test access control, callbacks and viewmodel change tracking: do
|
|
444
|
+
# the edit checks get called as expected with the correct changes?
|
|
445
|
+
class ChangeTrackingTest < ActiveSupport::TestCase
|
|
446
|
+
include ARVMTestUtilities
|
|
447
|
+
include ViewModelSpecHelpers::List
|
|
448
|
+
extend Minitest::Spec::DSL
|
|
449
|
+
|
|
450
|
+
def assert_changes_match(changes, new, deleted, children, attributes, associations)
|
|
451
|
+
assert_equal(
|
|
452
|
+
changes,
|
|
453
|
+
ViewModel::Changes.new(
|
|
454
|
+
new: new,
|
|
455
|
+
deleted: deleted,
|
|
456
|
+
changed_children: children,
|
|
457
|
+
changed_attributes: attributes,
|
|
458
|
+
changed_associations: associations))
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
describe 'with parent and points-to child test models' do
|
|
462
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
|
463
|
+
|
|
464
|
+
def new_model
|
|
465
|
+
model_class.new(name: 'a')
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def new_model_with_child
|
|
469
|
+
model_class.new(name: 'a', child: child_model_class.new(name: 'b'))
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
it 'records a created model' do
|
|
473
|
+
view = {
|
|
474
|
+
'_type' => view_name,
|
|
475
|
+
'name' => 'a',
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
ctx = viewmodel_class.new_deserialize_context
|
|
479
|
+
vm = viewmodel_class.deserialize_from_view(view, deserialize_context: ctx)
|
|
480
|
+
|
|
481
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
482
|
+
assert_changes_match(vm_changes, true, false, false, ['name'], [])
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it 'records a destroyed model' do
|
|
486
|
+
vm = create_viewmodel!
|
|
487
|
+
|
|
488
|
+
ctx = viewmodel_class.new_deserialize_context
|
|
489
|
+
vm.destroy!(deserialize_context: ctx)
|
|
490
|
+
|
|
491
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
492
|
+
assert_changes_match(vm_changes, false, true, false, [], [])
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
it 'records a change to an attribute' do
|
|
496
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
|
497
|
+
view['name'] = nil
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
501
|
+
assert_changes_match(vm_changes, false, false, false, ['name'], [])
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
it 'records a new child' do
|
|
505
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
|
506
|
+
view['child'] = { '_type' => child_view_name, 'name' => 'b' }
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
510
|
+
assert_changes_match(vm_changes, false, false, true, [], ['child'])
|
|
511
|
+
|
|
512
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
|
513
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
it 'records a replaced child' do
|
|
517
|
+
m = new_model_with_child.tap(&:save!)
|
|
518
|
+
old_child = m.child
|
|
519
|
+
|
|
520
|
+
vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
|
|
521
|
+
view['child'] = { '_type' => child_view_name, 'name' => 'c' }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
525
|
+
assert_changes_match(vm_changes, false, false, true, [], ['child'])
|
|
526
|
+
|
|
527
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
|
528
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
|
529
|
+
|
|
530
|
+
oc_changes = ctx.valid_edit_changes(
|
|
531
|
+
ViewModel::Reference.new(child_viewmodel_class, old_child.id))
|
|
532
|
+
assert_changes_match(oc_changes, false, true, false, [], [])
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
it 'records an edited child' do
|
|
536
|
+
m = new_model_with_child.tap(&:save!)
|
|
537
|
+
|
|
538
|
+
vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
|
|
539
|
+
view['child']['name'] = 'c'
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# The parent node itself wasn't changed, so must not have been
|
|
543
|
+
# valid_edit checked
|
|
544
|
+
refute(ctx.was_edited?(vm.to_reference))
|
|
545
|
+
assert_changes_match(vm.previous_changes, false, false, true, [], [])
|
|
546
|
+
|
|
547
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
|
548
|
+
assert_changes_match(c_changes, false, false, false, ['name'], [])
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
it 'records a deleted child' do
|
|
552
|
+
m = new_model_with_child.tap(&:save!)
|
|
553
|
+
old_child = m.child
|
|
554
|
+
|
|
555
|
+
vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
|
|
556
|
+
view['child'] = nil
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
560
|
+
assert_changes_match(vm_changes, false, false, true, [], ['child'])
|
|
561
|
+
|
|
562
|
+
oc_changes = ctx.valid_edit_changes(
|
|
563
|
+
ViewModel::Reference.new(child_viewmodel_class, old_child.id))
|
|
564
|
+
assert_changes_match(oc_changes, false, true, false, [], [])
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
describe 'with parent and pointed-to child test models' do
|
|
569
|
+
include ViewModelSpecHelpers::ParentAndOrderedChildren
|
|
570
|
+
|
|
571
|
+
def new_model
|
|
572
|
+
model_class.new(
|
|
573
|
+
name: 'a',
|
|
574
|
+
children: [child_model_class.new(name: 'x', position: 1),
|
|
575
|
+
child_model_class.new(name: 'y', position: 2)])
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
it 'records new children' do
|
|
579
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
|
580
|
+
view['children'].concat(
|
|
581
|
+
[
|
|
582
|
+
{ '_type' => child_view_name, 'name' => 'b' },
|
|
583
|
+
{ '_type' => child_view_name, 'name' => 'c' },
|
|
584
|
+
])
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
588
|
+
assert_changes_match(vm_changes, false, false, true, [], ['children'])
|
|
589
|
+
|
|
590
|
+
new_children, existing_children = vm.children.partition do |c|
|
|
591
|
+
c.name < 'm'
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
new_children.each do |c|
|
|
595
|
+
c_changes = ctx.valid_edit_changes(c.to_reference)
|
|
596
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
existing_children.each do |c|
|
|
600
|
+
refute(ctx.was_edited?(c.to_reference))
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
it 'records replaced children' do
|
|
605
|
+
m = create_model!
|
|
606
|
+
replaced_child = m.children.last
|
|
607
|
+
|
|
608
|
+
vm, ctx = alter_by_view!(viewmodel_class, m) do |view, _refs|
|
|
609
|
+
view['children'].pop
|
|
610
|
+
view['children'] << { '_type' => child_view_name, 'name' => 'b' }
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
refute(vm.children.include?(replaced_child))
|
|
614
|
+
|
|
615
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
616
|
+
assert_changes_match(vm_changes, false, false, true, [], ['children'])
|
|
617
|
+
|
|
618
|
+
new_child = vm.children.detect { |c| c.name == 'b' }
|
|
619
|
+
c_changes = ctx.valid_edit_changes(new_child.to_reference)
|
|
620
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
|
621
|
+
|
|
622
|
+
oc_changes = ctx.valid_edit_changes(
|
|
623
|
+
ViewModel::Reference.new(child_viewmodel_class, replaced_child.id))
|
|
624
|
+
assert_changes_match(oc_changes, false, true, false, [], [])
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
it 'records reordered children' do
|
|
628
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
|
629
|
+
view['children'].reverse!
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
633
|
+
assert_changes_match(vm_changes, false, false, false, [], ['children'])
|
|
634
|
+
|
|
635
|
+
vm.children.each do |c|
|
|
636
|
+
refute(ctx.was_edited?(c.to_reference))
|
|
637
|
+
end
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
describe 'with parent and shared child test models' do
|
|
642
|
+
include ViewModelSpecHelpers::ParentAndSharedChild
|
|
643
|
+
|
|
644
|
+
def new_model
|
|
645
|
+
model_class.new(name: 'a', child: child_model_class.new(name: 'z'))
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
it 'records an change to child without a tree change' do
|
|
649
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, refs|
|
|
650
|
+
view['child'] = { '_ref' => 'cref' }
|
|
651
|
+
refs.clear['cref'] = { '_type' => child_view_name, 'name' => 'b' }
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
655
|
+
assert_changes_match(vm_changes, false, false, false, [], ['child'])
|
|
656
|
+
|
|
657
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
|
658
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
it 'records an edited child without a tree change' do
|
|
662
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |_view, refs|
|
|
663
|
+
refs.values.first.merge!('name' => 'b')
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
refute(ctx.was_edited?(vm.to_reference))
|
|
667
|
+
assert_changes_match(vm.previous_changes, false, false, false, [], [])
|
|
668
|
+
|
|
669
|
+
c_changes = ctx.valid_edit_changes(vm.child.to_reference)
|
|
670
|
+
assert_changes_match(c_changes, false, false, false, ['name'], [])
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
it 'records a deleted child' do
|
|
674
|
+
vm = create_viewmodel!
|
|
675
|
+
old_child = vm.child
|
|
676
|
+
|
|
677
|
+
vm, ctx = alter_by_view!(viewmodel_class, vm.model) do |view, refs|
|
|
678
|
+
view['child'] = nil
|
|
679
|
+
refs.clear
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
683
|
+
assert_changes_match(vm_changes, false, false, false, [], ['child'])
|
|
684
|
+
|
|
685
|
+
refute(ctx.was_edited?(old_child.to_reference))
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
describe 'with has_many_through children test models' do
|
|
690
|
+
include ViewModelSpecHelpers::ParentAndHasManyThroughChildren
|
|
691
|
+
|
|
692
|
+
def new_model
|
|
693
|
+
model_class.new(
|
|
694
|
+
name: 'a',
|
|
695
|
+
model_children: [
|
|
696
|
+
join_model_class.new(position: 1, child: child_model_class.new(name: 'x')),
|
|
697
|
+
join_model_class.new(position: 2, child: child_model_class.new(name: 'y')),
|
|
698
|
+
])
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
it 'records new children' do
|
|
702
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, refs|
|
|
703
|
+
view['children'].concat([{ '_ref' => 'new1' }, { '_ref' => 'new2' }])
|
|
704
|
+
refs['new1'] = { '_type' => child_view_name, 'name' => 'b' }
|
|
705
|
+
refs['new2'] = { '_type' => child_view_name, 'name' => 'c' }
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
709
|
+
assert_changes_match(vm_changes, false, false, false, [], ['children'])
|
|
710
|
+
|
|
711
|
+
new_children, existing_children = vm.children.partition do |c|
|
|
712
|
+
c.name < 'm'
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
new_children.each do |c|
|
|
716
|
+
c_changes = ctx.valid_edit_changes(c.to_reference)
|
|
717
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
existing_children.each do |c|
|
|
721
|
+
refute(ctx.was_edited?(c.to_reference))
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
it 'records replaced children' do
|
|
726
|
+
vm = create_viewmodel!
|
|
727
|
+
old_child = vm.children.first
|
|
728
|
+
|
|
729
|
+
vm, ctx = alter_by_view!(viewmodel_class, vm.model) do |view, refs|
|
|
730
|
+
refs.delete(view['children'].pop['_ref'])
|
|
731
|
+
|
|
732
|
+
view['children'] << { '_ref' => 'new1' }
|
|
733
|
+
refs['new1'] = { '_type' => child_view_name, 'name' => 'b' }
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
737
|
+
assert_changes_match(vm_changes, false, false, false, [], ['children'])
|
|
738
|
+
|
|
739
|
+
new_children, existing_children = vm.children.partition do |c|
|
|
740
|
+
c.name < 'm'
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
new_children.each do |c|
|
|
744
|
+
c_changes = ctx.valid_edit_changes(c.to_reference)
|
|
745
|
+
assert_changes_match(c_changes, true, false, false, ['name'], [])
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
existing_children.each do |c|
|
|
749
|
+
refute(ctx.was_edited?(c.to_reference))
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
refute(ctx.was_edited?(old_child.to_reference))
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
it 'records reordered children' do
|
|
756
|
+
vm, ctx = alter_by_view!(viewmodel_class, create_model!) do |view, _refs|
|
|
757
|
+
view['children'].reverse!
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
vm_changes = ctx.valid_edit_changes(vm.to_reference)
|
|
761
|
+
assert_changes_match(vm_changes, false, false, false, [], ['children'])
|
|
762
|
+
|
|
763
|
+
vm.children.each do |c|
|
|
764
|
+
refute(ctx.was_edited?(c.to_reference))
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
end
|