anchormodel 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/CHANGELOG.md +10 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +114 -3
  6. data/VERSION +1 -1
  7. data/anchormodel.gemspec +5 -5
  8. data/doc/Anchormodel/ActiveModelTypeValueSingle.html +697 -0
  9. data/doc/Anchormodel/Attribute.html +109 -9
  10. data/doc/Anchormodel/ModelMixin.html +75 -3
  11. data/doc/Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html +269 -0
  12. data/doc/Anchormodel/SimpleFormInputs/Helpers.html +124 -0
  13. data/doc/Anchormodel/SimpleFormInputs.html +124 -0
  14. data/doc/Anchormodel/Util.html +612 -0
  15. data/doc/Anchormodel/Version.html +3 -3
  16. data/doc/Anchormodel.html +99 -34
  17. data/doc/AnchormodelGenerator.html +201 -0
  18. data/doc/AnchormodelInput.html +140 -0
  19. data/doc/AnchormodelRadioButtonsInput.html +140 -0
  20. data/doc/_index.html +63 -4
  21. data/doc/class_list.html +1 -1
  22. data/doc/file.README.html +109 -6
  23. data/doc/frames.html +5 -10
  24. data/doc/index.html +109 -6
  25. data/doc/method_list.html +70 -6
  26. data/doc/top-level-namespace.html +4 -4
  27. data/lib/anchormodel/active_model_type_value_multi.rb +31 -0
  28. data/lib/anchormodel/active_model_type_value_single.rb +4 -2
  29. data/lib/anchormodel/attribute.rb +7 -1
  30. data/lib/anchormodel/model_mixin.rb +7 -0
  31. data/lib/anchormodel/simple_form_inputs/anchormodel_check_boxes_input.rb +23 -0
  32. data/lib/anchormodel/simple_form_inputs/anchormodel_input.rb +21 -0
  33. data/lib/anchormodel/simple_form_inputs/anchormodel_radio_buttons_input.rb +23 -0
  34. data/lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb +54 -0
  35. data/lib/anchormodel/util.rb +57 -7
  36. data/lib/anchormodel.rb +10 -0
  37. data/test/active_record_model/user_test.rb +73 -9
  38. data/test/dummy/app/anchormodels/animal.rb +6 -0
  39. data/test/dummy/app/models/user.rb +1 -0
  40. data/test/dummy/db/migrate/20240425182000_add_animals_to_users.rb +5 -0
  41. data/test/dummy/db/schema.rb +2 -1
  42. metadata +18 -4
  43. data/pkg/anchormodel-0.1.3.gem +0 -0
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>
@@ -149,6 +181,14 @@
149
181
 
150
182
 
151
183
  <li class="even ">
184
+ <div class="item">
185
+ <span class='object_link'><a href="Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html#input-instance_method" title="Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon#input (method)">#input</a></span>
186
+ <small>Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon</small>
187
+ </div>
188
+ </li>
189
+
190
+
191
+ <li class="odd ">
152
192
  <div class="item">
153
193
  <span class='object_link'><a href="Anchormodel.html#inspect-instance_method" title="Anchormodel#inspect (method)">#inspect</a></span>
154
194
  <small>Anchormodel</small>
@@ -156,7 +196,7 @@
156
196
  </li>
157
197
 
158
198
 
159
- <li class="odd ">
199
+ <li class="even ">
160
200
  <div class="item">
161
201
  <span class='object_link'><a href="Anchormodel/Util.html#install_methods_in_model-class_method" title="Anchormodel::Util.install_methods_in_model (method)">install_methods_in_model</a></span>
162
202
  <small>Anchormodel::Util</small>
@@ -164,7 +204,7 @@
164
204
  </li>
165
205
 
166
206
 
167
- <li class="even ">
207
+ <li class="odd ">
168
208
  <div class="item">
169
209
  <span class='object_link'><a href="Anchormodel.html#key-instance_method" title="Anchormodel#key (method)">#key</a></span>
170
210
  <small>Anchormodel</small>
@@ -172,7 +212,7 @@
172
212
  </li>
173
213
 
174
214
 
175
- <li class="odd ">
215
+ <li class="even ">
176
216
  <div class="item">
177
217
  <span class='object_link'><a href="Anchormodel.html#label-instance_method" title="Anchormodel#label (method)">#label</a></span>
178
218
  <small>Anchormodel</small>
@@ -180,6 +220,14 @@
180
220
  </li>
181
221
 
182
222
 
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
+
183
231
  <li class="even ">
184
232
  <div class="item">
185
233
  <span class='object_link'><a href="Anchormodel/Attribute.html#optional-instance_method" title="Anchormodel::Attribute#optional (method)">#optional</a></span>
@@ -189,6 +237,14 @@
189
237
 
190
238
 
191
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
+
247
+ <li class="even ">
192
248
  <div class="item">
193
249
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serializable? (method)">#serializable?</a></span>
194
250
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -196,6 +252,14 @@
196
252
  </li>
197
253
 
198
254
 
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
+
199
263
  <li class="even ">
200
264
  <div class="item">
201
265
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serialize (method)">#serialize</a></span>
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  Top Level Namespace
8
8
 
9
- &mdash; Documentation by YARD 0.9.36
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>
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 Tue Apr 23 17:20:43 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.36 (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
@@ -0,0 +1,21 @@
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 AnchormodelInput < SimpleForm::Inputs::CollectionSelectInput
11
+ include Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon
12
+
13
+ private
14
+
15
+ def sf_selection_key
16
+ :selected
17
+ end
18
+
19
+ def before_render_input; end
20
+ end
21
+ 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 AnchormodelRadioButtonsInput < SimpleForm::Inputs::CollectionRadioButtonsInput
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 = :radio_buttons
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ class Anchormodel
2
+ module SimpleFormInputs
3
+ module Helpers
4
+ module AnchormodelInputsCommon
5
+ def input(wrapper_options = nil)
6
+ unless object.respond_to?(:anchormodel_attributes)
7
+ fail("The form field object does not appear to respond to `anchormodel_attributes`, \
8
+ did you `include Anchormodel::ModelMixin` in your `application_record.rb`? Affected object: #{object.inspect}")
9
+ end
10
+
11
+ am_attr = object.anchormodel_attributes[@attribute_name]
12
+ unless am_attr
13
+ fail("#{@attribute_name.inspect} does not look like an Anchormodel attribute, is `belongs_to_anchormodel` called in your model? \
14
+ Affected object: #{object.inspect}")
15
+ end
16
+ am_class = am_attr.anchormodel_class
17
+ options[:multiple] = true if am_attr.multiple? && options[:multiple] != false # allow overriding
18
+
19
+ # Attempt to read selected key from html input options "value", as the caller might not know that this is a select.
20
+ selected_key = input_options[:value]
21
+ if selected_key.blank? && object
22
+ # No selected key override present and a model is present, use the model to find out what to select.
23
+ selected_am = object.send(@attribute_name)
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
29
+ end
30
+
31
+ options.deep_merge!(
32
+ label_method: :first,
33
+ value_method: :second,
34
+ sf_selection_key => selected_key,
35
+ include_blank: !am_attr.multiple? && am_attr.optional
36
+ )
37
+
38
+ @collection = collect(am_class.all)
39
+
40
+ before_render_input
41
+
42
+ super
43
+ end
44
+
45
+ private
46
+
47
+ # Takes an array of objects implementing the methods `label` and `key` and returns an array suitable for simple_form select fields.
48
+ def collect(flat_array)
49
+ return flat_array.map { |entry| [entry.label, entry.key] }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -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,6 +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'
91
+ require 'anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common'
92
+ require 'anchormodel/simple_form_inputs/anchormodel_input'
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