anchormodel 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +78 -2
  5. data/VERSION +1 -1
  6. data/anchormodel.gemspec +4 -4
  7. data/doc/Anchormodel/ActiveModelTypeValueSingle.html +113 -31
  8. data/doc/Anchormodel/Attribute.html +109 -9
  9. data/doc/Anchormodel/ModelMixin.html +75 -3
  10. data/doc/Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html +269 -0
  11. data/doc/Anchormodel/SimpleFormInputs/Helpers.html +124 -0
  12. data/doc/Anchormodel/SimpleFormInputs.html +124 -0
  13. data/doc/Anchormodel/Util.html +131 -15
  14. data/doc/Anchormodel/Version.html +3 -3
  15. data/doc/Anchormodel.html +97 -32
  16. data/doc/AnchormodelGenerator.html +3 -3
  17. data/doc/AnchormodelInput.html +140 -0
  18. data/doc/AnchormodelRadioButtonsInput.html +140 -0
  19. data/doc/_index.html +16 -4
  20. data/doc/class_list.html +1 -1
  21. data/doc/file.README.html +77 -5
  22. data/doc/frames.html +1 -1
  23. data/doc/index.html +77 -5
  24. data/doc/method_list.html +62 -6
  25. data/doc/top-level-namespace.html +4 -4
  26. data/lib/anchormodel/active_model_type_value_multi.rb +31 -0
  27. data/lib/anchormodel/active_model_type_value_single.rb +4 -2
  28. data/lib/anchormodel/attribute.rb +7 -1
  29. data/lib/anchormodel/model_mixin.rb +7 -0
  30. data/lib/anchormodel/simple_form_inputs/anchormodel_check_boxes_input.rb +23 -0
  31. data/lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb +8 -3
  32. data/lib/anchormodel/util.rb +57 -7
  33. data/lib/anchormodel.rb +7 -0
  34. data/test/active_record_model/user_test.rb +73 -9
  35. data/test/dummy/app/anchormodels/animal.rb +6 -0
  36. data/test/dummy/app/models/user.rb +1 -0
  37. data/test/dummy/db/migrate/20240425182000_add_animals_to_users.rb +5 -0
  38. data/test/dummy/db/schema.rb +2 -1
  39. metadata +11 -2
data/doc/method_list.html CHANGED
@@ -77,6 +77,14 @@
77
77
 
78
78
 
79
79
  <li class="odd ">
80
+ <div class="item">
81
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#attribute-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#attribute (method)">#attribute</a></span>
82
+ <small>Anchormodel::ActiveModelTypeValueSingle</small>
83
+ </div>
84
+ </li>
85
+
86
+
87
+ <li class="even ">
80
88
  <div class="item">
81
89
  <span class='object_link'><a href="Anchormodel/Attribute.html#attribute_name-instance_method" title="Anchormodel::Attribute#attribute_name (method)">#attribute_name</a></span>
82
90
  <small>Anchormodel::Attribute</small>
@@ -84,7 +92,7 @@
84
92
  </li>
85
93
 
86
94
 
87
- <li class="even ">
95
+ <li class="odd ">
88
96
  <div class="item">
89
97
  <span class='object_link'><a href="Anchormodel/ModelMixin.html#belongs_to_anchormodel-class_method" title="Anchormodel::ModelMixin.belongs_to_anchormodel (method)">belongs_to_anchormodel</a></span>
90
98
  <small>Anchormodel::ModelMixin</small>
@@ -92,7 +100,23 @@
92
100
  </li>
93
101
 
94
102
 
103
+ <li class="even ">
104
+ <div class="item">
105
+ <span class='object_link'><a href="Anchormodel/ModelMixin.html#belongs_to_anchormodels-class_method" title="Anchormodel::ModelMixin.belongs_to_anchormodels (method)">belongs_to_anchormodels</a></span>
106
+ <small>Anchormodel::ModelMixin</small>
107
+ </div>
108
+ </li>
109
+
110
+
95
111
  <li class="odd ">
112
+ <div class="item">
113
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#cast (method)">#cast</a></span>
114
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
115
+ </div>
116
+ </li>
117
+
118
+
119
+ <li class="even ">
96
120
  <div class="item">
97
121
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#cast (method)">#cast</a></span>
98
122
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -100,7 +124,7 @@
100
124
  </li>
101
125
 
102
126
 
103
- <li class="even ">
127
+ <li class="odd ">
104
128
  <div class="item">
105
129
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#changed_in_place%3F-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#changed_in_place? (method)">#changed_in_place?</a></span>
106
130
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -108,7 +132,7 @@
108
132
  </li>
109
133
 
110
134
 
111
- <li class="odd ">
135
+ <li class="even ">
112
136
  <div class="item">
113
137
  <span class='object_link'><a href="Anchormodel.html#find-class_method" title="Anchormodel.find (method)">find</a></span>
114
138
  <small>Anchormodel</small>
@@ -116,6 +140,14 @@
116
140
  </li>
117
141
 
118
142
 
143
+ <li class="odd ">
144
+ <div class="item">
145
+ <span class='object_link'><a href="Anchormodel.html#form_collection-class_method" title="Anchormodel.form_collection (method)">form_collection</a></span>
146
+ <small>Anchormodel</small>
147
+ </div>
148
+ </li>
149
+
150
+
119
151
  <li class="even ">
120
152
  <div class="item">
121
153
  <span class='object_link'><a href="Anchormodel.html#index-instance_method" title="Anchormodel#index (method)">#index</a></span>
@@ -189,6 +221,14 @@
189
221
 
190
222
 
191
223
  <li class="odd ">
224
+ <div class="item">
225
+ <span class='object_link'><a href="Anchormodel/Attribute.html#multiple%3F-instance_method" title="Anchormodel::Attribute#multiple? (method)">#multiple?</a></span>
226
+ <small>Anchormodel::Attribute</small>
227
+ </div>
228
+ </li>
229
+
230
+
231
+ <li class="even ">
192
232
  <div class="item">
193
233
  <span class='object_link'><a href="Anchormodel/Attribute.html#optional-instance_method" title="Anchormodel::Attribute#optional (method)">#optional</a></span>
194
234
  <small>Anchormodel::Attribute</small>
@@ -196,6 +236,14 @@
196
236
  </li>
197
237
 
198
238
 
239
+ <li class="odd ">
240
+ <div class="item">
241
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#serializable? (method)">#serializable?</a></span>
242
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
243
+ </div>
244
+ </li>
245
+
246
+
199
247
  <li class="even ">
200
248
  <div class="item">
201
249
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serializable? (method)">#serializable?</a></span>
@@ -205,6 +253,14 @@
205
253
 
206
254
 
207
255
  <li class="odd ">
256
+ <div class="item">
257
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#serialize (method)">#serialize</a></span>
258
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
259
+ </div>
260
+ </li>
261
+
262
+
263
+ <li class="even ">
208
264
  <div class="item">
209
265
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serialize (method)">#serialize</a></span>
210
266
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -212,7 +268,7 @@
212
268
  </li>
213
269
 
214
270
 
215
- <li class="even ">
271
+ <li class="odd ">
216
272
  <div class="item">
217
273
  <span class='object_link'><a href="Anchormodel.html#setup!-class_method" title="Anchormodel.setup! (method)">setup!</a></span>
218
274
  <small>Anchormodel</small>
@@ -220,7 +276,7 @@
220
276
  </li>
221
277
 
222
278
 
223
- <li class="odd ">
279
+ <li class="even ">
224
280
  <div class="item">
225
281
  <span class='object_link'><a href="Anchormodel.html#to_s-instance_method" title="Anchormodel#to_s (method)">#to_s</a></span>
226
282
  <small>Anchormodel</small>
@@ -228,7 +284,7 @@
228
284
  </li>
229
285
 
230
286
 
231
- <li class="even ">
287
+ <li class="odd ">
232
288
  <div class="item">
233
289
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#type-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#type (method)">#type</a></span>
234
290
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  Top Level Namespace
8
8
 
9
- &mdash; Documentation by YARD 0.9.28
9
+ &mdash; Documentation by YARD 0.9.34
10
10
 
11
11
  </title>
12
12
 
@@ -84,7 +84,7 @@
84
84
 
85
85
 
86
86
 
87
- <strong class="classes">Classes:</strong> <span class='object_link'><a href="Anchormodel.html" title="Anchormodel (class)">Anchormodel</a></span>, <span class='object_link'><a href="AnchormodelGenerator.html" title="AnchormodelGenerator (class)">AnchormodelGenerator</a></span>, <span class='object_link'><a href="AnchormodelInput.html" title="AnchormodelInput (class)">AnchormodelInput</a></span>, <span class='object_link'><a href="AnchormodelRadioButtonsInput.html" title="AnchormodelRadioButtonsInput (class)">AnchormodelRadioButtonsInput</a></span>
87
+ <strong class="classes">Classes:</strong> <span class='object_link'><a href="Anchormodel.html" title="Anchormodel (class)">Anchormodel</a></span>, <span class='object_link'><a href="AnchormodelCheckBoxesInput.html" title="AnchormodelCheckBoxesInput (class)">AnchormodelCheckBoxesInput</a></span>, <span class='object_link'><a href="AnchormodelGenerator.html" title="AnchormodelGenerator (class)">AnchormodelGenerator</a></span>, <span class='object_link'><a href="AnchormodelInput.html" title="AnchormodelInput (class)">AnchormodelInput</a></span>, <span class='object_link'><a href="AnchormodelRadioButtonsInput.html" title="AnchormodelRadioButtonsInput (class)">AnchormodelRadioButtonsInput</a></span>
88
88
 
89
89
 
90
90
  </p>
@@ -100,9 +100,9 @@
100
100
  </div>
101
101
 
102
102
  <div id="footer">
103
- Generated on Wed Apr 24 17:01:46 2024 by
103
+ Generated on Sat Apr 27 10:18:33 2024 by
104
104
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
- 0.9.28 (ruby-3.2.2).
105
+ 0.9.34 (ruby-3.2.2).
106
106
  </div>
107
107
 
108
108
  </div>
@@ -0,0 +1,31 @@
1
+ class Anchormodel::ActiveModelTypeValueMulti < Anchormodel::ActiveModelTypeValueSingle
2
+ # This converts DB or input to an Anchormodel instance
3
+ def cast(values)
4
+ return values.split(',').map { |value| super(value) }.compact.to_set
5
+ end
6
+
7
+ # This converts an Anchormodel instance to string for DB
8
+ def serialize(values)
9
+ return case values
10
+ when Enumerable
11
+ values.map { |value| super(value) }.compact.join(',')
12
+ when String
13
+ values
14
+ when nil
15
+ ''
16
+ else
17
+ fail "Attempt to set #{@attribute.attribute_name} to unsupported type #{values.class}"
18
+ end
19
+ end
20
+
21
+ def serializable?(values)
22
+ return case values
23
+ when Enumerable
24
+ values.map { |value| super(value) }.compact.join(',')
25
+ when String
26
+ values.split(',').map { |value| super(value) }.compact
27
+ else
28
+ false
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,7 @@
1
1
  # @see https://www.rubydoc.info/docs/rails/ActiveModel/Type/Value
2
2
  class Anchormodel::ActiveModelTypeValueSingle < ActiveModel::Type::Value
3
+ attr_reader :attribute
4
+
3
5
  def initialize(attribute)
4
6
  super()
5
7
  @attribute = attribute
@@ -9,14 +11,14 @@ class Anchormodel::ActiveModelTypeValueSingle < ActiveModel::Type::Value
9
11
  :anchormodel
10
12
  end
11
13
 
12
- # This converts an Anchormodel instance to string for DB
14
+ # This converts DB or input to an Anchormodel instance
13
15
  def cast(value)
14
16
  value = value.presence
15
17
  return value if value.is_a?(@attribute.anchormodel_class)
16
18
  return @attribute.anchormodel_class.find(value)
17
19
  end
18
20
 
19
- # This converts DB or input to an Anchormodel instance
21
+ # This converts an Anchormodel instance to string for DB
20
22
  def serialize(value)
21
23
  value = value.presence
22
24
  return case value
@@ -10,10 +10,16 @@ class Anchormodel::Attribute
10
10
  # @param attribute_name [String,Symbol] The name and database column of the attribute
11
11
  # @param anchormodel_class [Class] Class of the Anchormodel (omit if attribute `:foo_bar` holds an `FooBar`)
12
12
  # @param optional [Boolean] If true, a presence validation is added to the model.
13
- def initialize(model_class, attribute_name, anchormodel_class = nil, optional = false)
13
+ # @param multiple [Boolean] If true, this attribute holds multiple anchormodels.
14
+ def initialize(model_class, attribute_name, anchormodel_class = nil, optional = false, multiple = false)
14
15
  @model_class = model_class
15
16
  @attribute_name = attribute_name.to_sym
16
17
  @anchormodel_class = anchormodel_class
17
18
  @optional = optional
19
+ @multiple = multiple
20
+ end
21
+
22
+ def multiple?
23
+ @multiple
18
24
  end
19
25
  end
@@ -14,5 +14,12 @@ module Anchormodel::ModelMixin
14
14
  def belongs_to_anchormodel(*args, **kwargs)
15
15
  Anchormodel::Util.install_methods_in_model(self, *args, **kwargs)
16
16
  end
17
+
18
+ # Creates an attribute linking to an array of Anchormodels. The attribute should be
19
+ # present in the DB and the column should be named the singular of `attribute_name`.
20
+ # @see Anchormodel::Util#install_methods_in_model Parameters
21
+ def belongs_to_anchormodels(*args, **kwargs)
22
+ Anchormodel::Util.install_methods_in_model(self, *args, **kwargs, multiple: true)
23
+ end
17
24
  end
18
25
  end
@@ -0,0 +1,23 @@
1
+ unless defined? SimpleForm
2
+ begin
3
+ require 'simple_form'
4
+ rescue LoadError
5
+ nil
6
+ end
7
+
8
+ end
9
+ if defined? SimpleForm
10
+ class AnchormodelCheckBoxesInput < SimpleForm::Inputs::CollectionCheckBoxesInput
11
+ include Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon
12
+
13
+ private
14
+
15
+ def sf_selection_key
16
+ :checked
17
+ end
18
+
19
+ def before_render_input
20
+ @input_type = :check_boxes
21
+ end
22
+ end
23
+ end
@@ -14,20 +14,25 @@ did you `include Anchormodel::ModelMixin` in your `application_record.rb`? Affec
14
14
  Affected object: #{object.inspect}")
15
15
  end
16
16
  am_class = am_attr.anchormodel_class
17
+ options[:multiple] = true if am_attr.multiple? && options[:multiple] != false # allow overriding
17
18
 
18
19
  # Attempt to read selected key from html input options "value", as the caller might not know that this is a select.
19
20
  selected_key = input_options[:value]
20
21
  if selected_key.blank? && object
21
22
  # No selected key override present and a model is present, use the model to find out what to select.
22
23
  selected_am = object.send(@attribute_name)
23
- selected_key = selected_am&.key || am_class.all.first
24
+ if am_attr.multiple?
25
+ selected_key = selected_am&.map(&:key)&.map(&:to_s) || []
26
+ else
27
+ selected_key = (selected_am&.key || am_class.all.first).to_s
28
+ end
24
29
  end
25
30
 
26
31
  options.deep_merge!(
27
32
  label_method: :first,
28
33
  value_method: :second,
29
- sf_selection_key => selected_key.to_s,
30
- include_blank: am_attr.optional
34
+ sf_selection_key => selected_key,
35
+ include_blank: !am_attr.multiple? && am_attr.optional
31
36
  )
32
37
 
33
38
  @collection = collect(am_class.all)
@@ -5,21 +5,24 @@ module Anchormodel::Util
5
5
  # @param model_class [ActiveRecord::Base] Internal only. The model class that the attribute should be installed to.
6
6
  # @param attribute_name [String,Symbol] The name and database column of the attribute
7
7
  # @param anchormodel_class [Class] Class of the Anchormodel (omit if attribute `:foo_bar` holds a `FooBar`)
8
- # @param optional [Boolean] If false, a presence validation is added to the model.
8
+ # @param optional [Boolean] If false, a presence validation is added to the model. Forced to true if multiple is true.
9
+ # @param multiple [Boolean] Internal only. Distinguishes between `belongs_to_anchormodel` and `belongs_to_anchormodels`.
9
10
  # @param model_readers [Boolean] If true, the model is given an ActiveRecord::Enum style method `my_model.my_key?` reader for each key in the anchormodel
10
11
  # @param model_writers [Boolean] If true, the model is given an ActiveRecord::Enum style method `my_model.my_key!` writer for each key in the anchormodel
11
12
  # @param model_scopes [Boolean] If true, the model is given an ActiveRecord::Enum style scope `MyModel.mykey` for each key in the anchormodel
12
13
  # @param model_methods [Boolean, NilClass] If non-nil, this mass-assigns and overrides `model_readers`, `model_writers` and `model_scopes`
13
14
  def self.install_methods_in_model(model_class, attribute_name, anchormodel_class = nil,
14
15
  optional: false,
16
+ multiple: false,
15
17
  model_readers: true,
16
18
  model_writers: true,
17
19
  model_scopes: true,
18
20
  model_methods: nil)
19
21
 
22
+ optional = true if multiple
20
23
  anchormodel_class ||= attribute_name.to_s.classify.constantize
21
24
  attribute_name = attribute_name.to_sym
22
- attribute = Anchormodel::Attribute.new(self, attribute_name, anchormodel_class, optional)
25
+ attribute = Anchormodel::Attribute.new(model_class, attribute_name, anchormodel_class, optional, multiple)
23
26
 
24
27
  # Mass configurations if model_methods was specfied
25
28
  unless model_methods.nil?
@@ -38,11 +41,43 @@ module Anchormodel::Util
38
41
 
39
42
  # Make casting work
40
43
  # Define serializer/deserializer
41
- active_model_type_value = Anchormodel::ActiveModelTypeValueSingle.new(attribute)
44
+ active_model_type_value = (multiple ? Anchormodel::ActiveModelTypeValueMulti : Anchormodel::ActiveModelTypeValueSingle).new(attribute)
42
45
 
43
46
  # Overwrite reader to force building anchors at every retrieval
44
47
  model_class.define_method(attribute_name.to_s) do
45
- active_model_type_value.deserialize(read_attribute_before_type_cast(attribute_name))
48
+ result = active_model_type_value.deserialize(read_attribute_before_type_cast(attribute_name))
49
+
50
+ # If this attribute holds multiple anchormodels (`belongs_to_anchormodels`), patch the Array before returning in order to implement collection modifiers:
51
+ if multiple
52
+ model = self # fetching the model in order to pass it to the implementation of the following via reflection to make it available for storage
53
+ single_tv = Anchormodel::ActiveModelTypeValueSingle.new(attribute) # Used for casting inputs to properly compare them to the set.
54
+
55
+ # Adding
56
+ result.define_singleton_method('add') do |value_to_add|
57
+ super(single_tv.cast(value_to_add))
58
+ model.write_attribute(attribute_name, active_model_type_value.serialize(self))
59
+ next self
60
+ end
61
+ result.define_singleton_method('<<') { |value_to_add| add(single_tv.cast(value_to_add)) }
62
+
63
+ # Deleting
64
+ result.define_singleton_method('delete') do |value_to_delete|
65
+ super(single_tv.cast(value_to_delete))
66
+ model.write_attribute(attribute_name, active_model_type_value.serialize(self))
67
+ next self
68
+ end
69
+
70
+ # Clearing
71
+ result.define_singleton_method('clear') do
72
+ super()
73
+ model.write_attribute(attribute_name, active_model_type_value.serialize(self))
74
+ next self
75
+ end
76
+
77
+ # In the future, further methods could be supported. e.g. delete_if, subtract etc.
78
+ end
79
+
80
+ return result
46
81
  end
47
82
 
48
83
  # Override writer to fail early when an invalid target value is specified
@@ -61,7 +96,11 @@ module Anchormodel::Util
61
96
  fail("Anchormodel reader #{entry.key}? already defined for #{self}, add `model_readers: false` to `belongs_to_anchormodel :#{attribute_name}`.")
62
97
  end
63
98
  model_class.define_method(:"#{entry.key}?") do
64
- public_send(attribute_name.to_s) == entry
99
+ if multiple
100
+ public_send(attribute_name.to_s).include?(entry)
101
+ else
102
+ public_send(attribute_name.to_s) == entry
103
+ end
65
104
  end
66
105
  end
67
106
  end
@@ -74,7 +113,11 @@ module Anchormodel::Util
74
113
  fail("Anchormodel writer #{entry.key}! already defined for #{self}, add `model_writers: false` to `belongs_to_anchormodel :#{attribute_name}`.")
75
114
  end
76
115
  model_class.define_method(:"#{entry.key}!") do
77
- public_send(:"#{attribute_name}=", entry)
116
+ if multiple
117
+ public_send(attribute_name.to_s) << entry
118
+ else
119
+ public_send(:"#{attribute_name}=", entry)
120
+ end
78
121
  end
79
122
  end
80
123
  end
@@ -86,7 +129,14 @@ module Anchormodel::Util
86
129
  if model_class.respond_to?(entry.key)
87
130
  fail("Anchormodel scope #{entry.key} already defined for #{self}, add `model_scopes: false` to `belongs_to_anchormodel :#{attribute_name}`.")
88
131
  end
89
- model_class.scope(entry.key, -> { where(attribute_name => entry.key) })
132
+ if multiple
133
+ model_class.scope(entry.key, lambda {
134
+ where("#{attribute_name} LIKE ? OR #{attribute_name} LIKE ? OR #{attribute_name} LIKE ? OR #{attribute_name} LIKE ?",
135
+ "%#{entry.key},%", "%#{entry.key}", "#{entry.key},%", entry.key.to_s)
136
+ })
137
+ else
138
+ model_class.scope(entry.key, -> { where(attribute_name => entry.key) })
139
+ end
90
140
  end
91
141
  end
92
142
  end
data/lib/anchormodel.rb CHANGED
@@ -25,6 +25,11 @@ class Anchormodel
25
25
  entries_list
26
26
  end
27
27
 
28
+ # Returns an array of tuples [label, key] suitable for passing as a collection to some form input helpers
29
+ def self.form_collection
30
+ entries_list.map { |el| [el.label, el.key.to_s] }
31
+ end
32
+
28
33
  # Retrieves a particular value given the key. Fails if not found.
29
34
  # @param key [String,Symbol] The key of the value that should be retrieved.
30
35
  def self.find(key)
@@ -79,9 +84,11 @@ end
79
84
 
80
85
  require 'anchormodel/util'
81
86
  require 'anchormodel/active_model_type_value_single'
87
+ require 'anchormodel/active_model_type_value_multi'
82
88
  require 'anchormodel/attribute'
83
89
  require 'anchormodel/model_mixin'
84
90
  require 'anchormodel/version'
85
91
  require 'anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common'
86
92
  require 'anchormodel/simple_form_inputs/anchormodel_input'
87
93
  require 'anchormodel/simple_form_inputs/anchormodel_radio_buttons_input'
94
+ require 'anchormodel/simple_form_inputs/anchormodel_check_boxes_input'
@@ -21,10 +21,6 @@ class UserTest < Minitest::Test
21
21
  )
22
22
  end
23
23
 
24
- def test_missing_key
25
- assert_raises { Role.find(:does_not_exist) }
26
- end
27
-
28
24
  def test_basic_setters_and_getters
29
25
  u = User.create!(role: 'guest', locale: 'de') # String assignment
30
26
  assert_equal Role.find(:guest), u.role
@@ -57,11 +53,6 @@ class UserTest < Minitest::Test
57
53
  assert Role.find(:moderator) < Role.find(:admin)
58
54
  end
59
55
 
60
- def test_presence_validation
61
- valentine = User.new
62
- assert_raises(ActiveRecord::RecordInvalid) { valentine.save! }
63
- end
64
-
65
56
  def test_alternative_column_name
66
57
  ben = User.create!(
67
58
  role: Role.find(:moderator),
@@ -119,6 +110,19 @@ class UserTest < Minitest::Test
119
110
  assert_nil u.secondary_role
120
111
  end
121
112
 
113
+ ###---
114
+ # Testing failures
115
+ ###---
116
+
117
+ def test_presence_validation
118
+ valentine = User.new
119
+ assert_raises(ActiveRecord::RecordInvalid) { valentine.save! }
120
+ end
121
+
122
+ def test_missing_key
123
+ assert_raises { Role.find(:does_not_exist) }
124
+ end
125
+
122
126
  # Attempting to create a model with an invalid constant name should fail
123
127
  def test_invalid_key_update
124
128
  assert_raises(RuntimeError) { User.create!(role: :admin, locale: :de, preferred_locale: :invalid) }
@@ -137,4 +141,64 @@ class UserTest < Minitest::Test
137
141
  ActiveRecord::Base.connection.execute(sql)
138
142
  assert_raises(RuntimeError) { User.first.role }
139
143
  end
144
+
145
+ ###---
146
+ # Testing multiple anchormodel associations
147
+ ###---
148
+
149
+ def test_multi_basics
150
+ u = User.create!(role: 'guest', locale: 'de')
151
+ assert_equal(Set.new, u.animals)
152
+ # Adding
153
+ u.animals << 'cat'
154
+ assert_equal(Set.new([Animal.find(:cat)]), u.animals)
155
+ u.animals.add :dog
156
+ assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), u.animals)
157
+ u.animals.add Animal.find(:horse)
158
+ assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog), Animal.find(:horse)]), u.animals)
159
+ # Removing
160
+ u.animals.delete 'cat'
161
+ assert_equal(Set.new([Animal.find(:dog), Animal.find(:horse)]), u.animals)
162
+ u.animals.delete :dog
163
+ assert_equal(Set.new([Animal.find(:horse)]), u.animals)
164
+ u.animals.delete Animal.find(:horse)
165
+ assert_equal(false, u.animals.any?)
166
+ # Setting
167
+ u.animals = %i[cat dog]
168
+ assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), u.animals)
169
+ # Clearing
170
+ u.animals.clear
171
+ assert_equal(false, u.animals.any?)
172
+ end
173
+
174
+ def test_multi_save_load
175
+ u = User.create!(role: 'guest', locale: 'de')
176
+ u.animals = %i[cat dog]
177
+ u.save!
178
+ freshly_loaded_u = User.first
179
+ assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), freshly_loaded_u.animals)
180
+ end
181
+
182
+ def test_multi_model_readers_and_writers
183
+ u = User.create!(role: 'guest', locale: 'de')
184
+ u.cat!
185
+ u.cat!
186
+ assert_equal(Set.new([Animal.find(:cat)]), u.animals) # tolerate no duplicate cat
187
+ assert_equal(true, u.cat?)
188
+ u.dog!
189
+ assert_equal(true, u.cat?)
190
+ assert_equal(true, u.dog?)
191
+ assert_equal(false, u.horse?)
192
+ end
193
+
194
+ def test_multi_model_scopes
195
+ u = User.create!(role: 'guest', locale: 'fr', animals: %w[dog cat])
196
+ v = User.create!(role: 'guest', locale: 'it', animals: %w[dog horse])
197
+ assert_equal(0, User.rat.count)
198
+ assert_equal(1, User.cat.count)
199
+ assert_equal(1, User.horse.count)
200
+ assert_equal(2, User.dog.count)
201
+ assert_equal(u.id, User.cat.first.id)
202
+ assert_equal(v.id, User.horse.first.id)
203
+ end
140
204
  end
@@ -0,0 +1,6 @@
1
+ class Animal < Anchormodel
2
+ new :cat
3
+ new :dog
4
+ new :horse
5
+ new :rat
6
+ end
@@ -3,4 +3,5 @@ class User < ApplicationRecord
3
3
  belongs_to_anchormodel :secondary_role, Role, optional: true, model_readers: false, model_writers: false, model_scopes: false
4
4
  belongs_to_anchormodel :locale, model_methods: false
5
5
  belongs_to_anchormodel :preferred_locale, Locale
6
+ belongs_to_anchormodels :animals
6
7
  end
@@ -0,0 +1,5 @@
1
+ class AddAnimalsToUsers < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :users, :animals, :string, default: '', null: false
4
+ end
5
+ end
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema[7.0].define(version: 20_230_124_084_241) do
13
+ ActiveRecord::Schema[7.0].define(version: 20_240_425_182_000) do
14
14
  create_table 'users', force: :cascade do |t|
15
15
  t.string 'role'
16
16
  t.string 'locale'
@@ -18,5 +18,6 @@ ActiveRecord::Schema[7.0].define(version: 20_230_124_084_241) do
18
18
  t.datetime 'updated_at', null: false
19
19
  t.string 'secondary_role'
20
20
  t.string 'preferred_locale', default: 'en', null: false
21
+ t.string 'animals', default: '', null: false
21
22
  end
22
23
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anchormodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Kalbermatter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-24 00:00:00.000000000 Z
11
+ date: 2024-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -173,9 +173,14 @@ files:
173
173
  - doc/Anchormodel/ActiveModelTypeValueSingle.html
174
174
  - doc/Anchormodel/Attribute.html
175
175
  - doc/Anchormodel/ModelMixin.html
176
+ - doc/Anchormodel/SimpleFormInputs.html
177
+ - doc/Anchormodel/SimpleFormInputs/Helpers.html
178
+ - doc/Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html
176
179
  - doc/Anchormodel/Util.html
177
180
  - doc/Anchormodel/Version.html
178
181
  - doc/AnchormodelGenerator.html
182
+ - doc/AnchormodelInput.html
183
+ - doc/AnchormodelRadioButtonsInput.html
179
184
  - doc/_index.html
180
185
  - doc/class_list.html
181
186
  - doc/css/common.css
@@ -191,9 +196,11 @@ files:
191
196
  - doc/method_list.html
192
197
  - doc/top-level-namespace.html
193
198
  - lib/anchormodel.rb
199
+ - lib/anchormodel/active_model_type_value_multi.rb
194
200
  - lib/anchormodel/active_model_type_value_single.rb
195
201
  - lib/anchormodel/attribute.rb
196
202
  - lib/anchormodel/model_mixin.rb
203
+ - lib/anchormodel/simple_form_inputs/anchormodel_check_boxes_input.rb
197
204
  - lib/anchormodel/simple_form_inputs/anchormodel_input.rb
198
205
  - lib/anchormodel/simple_form_inputs/anchormodel_radio_buttons_input.rb
199
206
  - lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb
@@ -206,6 +213,7 @@ files:
206
213
  - test/active_record_model/user_test.rb
207
214
  - test/dummy/.gitignore
208
215
  - test/dummy/Rakefile
216
+ - test/dummy/app/anchormodels/animal.rb
209
217
  - test/dummy/app/anchormodels/locale.rb
210
218
  - test/dummy/app/anchormodels/role.rb
211
219
  - test/dummy/app/helpers/application_helper.rb
@@ -230,6 +238,7 @@ files:
230
238
  - test/dummy/config/puma.rb
231
239
  - test/dummy/config/routes.rb
232
240
  - test/dummy/db/migrate/20230107173151_create_users.rb
241
+ - test/dummy/db/migrate/20240425182000_add_animals_to_users.rb
233
242
  - test/dummy/db/schema.rb
234
243
  - test/dummy/db/seeds.rb
235
244
  - test/dummy/lib/tasks/.keep