anchormodel 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Gemfile.lock +6 -6
  4. data/README.md +78 -2
  5. data/VERSION +1 -1
  6. data/anchormodel.gemspec +4 -4
  7. data/doc/Anchormodel/ActiveModelTypeValueMulti.html +410 -0
  8. data/doc/Anchormodel/ActiveModelTypeValueSingle.html +113 -31
  9. data/doc/Anchormodel/Attribute.html +109 -9
  10. data/doc/Anchormodel/ModelMixin.html +3 -92
  11. data/doc/Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html +267 -0
  12. data/doc/Anchormodel/SimpleFormInputs/Helpers.html +124 -0
  13. data/doc/Anchormodel/SimpleFormInputs.html +124 -0
  14. data/doc/Anchormodel/Util.html +131 -15
  15. data/doc/Anchormodel/Version.html +3 -3
  16. data/doc/Anchormodel.html +97 -32
  17. data/doc/AnchormodelCheckBoxesInput.html +140 -0
  18. data/doc/AnchormodelGenerator.html +3 -3
  19. data/doc/AnchormodelInput.html +140 -0
  20. data/doc/AnchormodelRadioButtonsInput.html +140 -0
  21. data/doc/_index.html +16 -4
  22. data/doc/class_list.html +1 -1
  23. data/doc/file.README.html +77 -5
  24. data/doc/frames.html +1 -1
  25. data/doc/index.html +77 -5
  26. data/doc/method_list.html +49 -9
  27. data/doc/top-level-namespace.html +4 -4
  28. data/lib/anchormodel/active_model_type_value_multi.rb +31 -0
  29. data/lib/anchormodel/active_model_type_value_single.rb +4 -2
  30. data/lib/anchormodel/attribute.rb +7 -1
  31. data/lib/anchormodel/model_mixin.rb +7 -0
  32. data/lib/anchormodel/simple_form_inputs/anchormodel_check_boxes_input.rb +23 -0
  33. data/lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb +7 -3
  34. data/lib/anchormodel/util.rb +57 -7
  35. data/lib/anchormodel.rb +7 -0
  36. data/test/active_record_model/user_test.rb +73 -9
  37. data/test/dummy/app/anchormodels/animal.rb +6 -0
  38. data/test/dummy/app/models/user.rb +1 -0
  39. data/test/dummy/db/migrate/20240425182000_add_animals_to_users.rb +5 -0
  40. data/test/dummy/db/schema.rb +2 -1
  41. metadata +13 -2
data/doc/index.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  File: README
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
 
@@ -261,15 +261,87 @@
261
261
  <p>If you want to have multiple attributes in the same model pointing to the same Anchormodel, you need to disable <code>model_methods</code> for at least one of them (otherwise the model methods will clash in your model class):</p>
262
262
 
263
263
  <pre class="code ruby"><code class="ruby"><span class='comment'># app/models/user.rb
264
- </span> <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:role</span>
265
- <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:secondary_role</span><span class='comma'>,</span> <span class='const'>Role</span><span class='comma'>,</span> <span class='label'>model_methods:</span> <span class='kw'>false</span>
264
+ </span><span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:role</span>
265
+ <span class='id identifier rubyid_belongs_to_anchormodel'>belongs_to_anchormodel</span> <span class='symbol'>:secondary_role</span><span class='comma'>,</span> <span class='const'>Role</span><span class='comma'>,</span> <span class='label'>model_methods:</span> <span class='kw'>false</span>
266
+ </code></pre>
267
+
268
+ <h1 id="label-Attaching+multiple+Anchormodels+to+an+attribute+-28similar+to+a+has_many+collection-29">Attaching multiple Anchormodels to an attribute (similar to a has_many collection)</h1>
269
+
270
+ <p>Collections of Anchormodels are supported. Assuming that your <code>User</code> can have multiple <code>Role</code> anchormodels, your code can look as follows:</p>
271
+
272
+ <pre class="code ruby"><code class="ruby"><span class='comment'># app/models/user.rb
273
+ </span><span class='id identifier rubyid_belongs_to_anchormodels'>belongs_to_anchormodels</span> <span class='symbol'>:roles</span>
274
+ </code></pre>
275
+
276
+ <p>The method is deliberately called <code>belongs_to...</code> and not <code>has_many...</code> in order to indicate that the key is stored in a column of the model in which you are calling it. The rule of thumb for using a collection of Anchormodels is:</p>
277
+ <ul><li>
278
+ <p>Your column should be of type <code>string</code>, just like with the singular <code>belongs_to_anchormodel</code>.</p>
279
+ </li><li>
280
+ <p>Anchormodels will be stored in string form, separated by the <code>,</code> character</p>
281
+ </li><li>
282
+ <p>When reading the attribute, you will get a <code>Set</code>, thus duplicates are avoided.</p>
283
+ </li></ul>
284
+
285
+ <p>Example usage for a User model with multiple roles as shown above:</p>
286
+
287
+ <pre class="code ruby"><code class="ruby"><span class='id identifier rubyid_u'>u</span> <span class='op'>=</span> <span class='const'>User</span><span class='period'>.</span><span class='id identifier rubyid_first'>first</span>
288
+ <span class='id identifier rubyid_u'>u</span><span class='period'>.</span><span class='id identifier rubyid_roles'>roles</span> <span class='op'>=</span> <span class='qsymbols_beg'>%i[</span><span class='tstring_content'>moderator</span><span class='words_sep'> </span><span class='tstring_content'>admin</span><span class='tstring_end'>]</span></span> <span class='comment'># this will replace the user&#39;s roles by the two specified
289
+ </span><span class='id identifier rubyid_u'>u</span><span class='period'>.</span><span class='id identifier rubyid_roles'>roles</span> <span class='comment'># this will return a set of two Role Anchormodel instances, moderator and role
290
+ </span><span class='id identifier rubyid_u'>u</span><span class='period'>.</span><span class='id identifier rubyid_guest!'>guest!</span> <span class='comment'># this will add the role `guest` to the user&#39;s roles
291
+ </span><span class='id identifier rubyid_u'>u</span><span class='period'>.</span><span class='id identifier rubyid_guest?'>guest?</span> <span class='comment'># this will query whether the role `guest` is part of the user&#39;s roles
292
+ </span><span class='const'>User</span><span class='period'>.</span><span class='id identifier rubyid_moderator'>moderator</span> <span class='comment'># This will return all users that have the moderator role as part of their roles
293
+ </span></code></pre>
294
+
295
+ <p>For modifying a collection of Anchormodels, the following methods are implemented, the first three accepting a String, Symbol or Anchormodel:</p>
296
+
297
+ <pre class="code ruby"><code class="ruby"><span class='id identifier rubyid_u'>u</span><span class='period'>.</span><span class='id identifier rubyid_roles'>roles</span><span class='period'>.</span><span class='id identifier rubyid_add'>add</span><span class='lparen'>(</span><span class='symbol'>:moderator</span><span class='rparen'>)</span> <span class='comment'># same as u.moderator!
298
+ </span><span class='id identifier rubyid_u'>u</span><span class='period'>.</span><span class='id identifier rubyid_roles'>roles</span> <span class='op'>&lt;&lt;</span> <span class='symbol'>:moderator</span> <span class='comment'># alias of add
299
+ </span><span class='id identifier rubyid_u'>u</span><span class='period'>.</span><span class='id identifier rubyid_roles'>roles</span><span class='period'>.</span><span class='id identifier rubyid_delete'>delete</span><span class='lparen'>(</span><span class='symbol'>:moderator</span><span class='rparen'>)</span>
300
+ <span class='id identifier rubyid_u'>u</span><span class='period'>.</span><span class='id identifier rubyid_roles'>roles</span><span class='period'>.</span><span class='id identifier rubyid_clear'>clear</span>
301
+ </code></pre>
302
+
303
+ <p>Note that no other methods of Set are overwritten at this point - if you use any other methods mutating the underlying Set, your changes will not be applied.</p>
304
+
305
+ <h2 id="label-Basic+rails+form+for+a+collection+of+Anchormodels">Basic rails form for a collection of Anchormodels</h2>
306
+
307
+ <pre class="code ruby"><code class="ruby">&lt;%= form_with(model: user) do |form| %&gt;
308
+ &lt;%# ... %&gt;
309
+ &lt;%= form.collection_select :role, Role.all, :key, :label, multiple: true %&gt;
310
+ &lt;%# ... %&gt;
311
+ &lt;% end %&gt;
312
+ </code></pre>
313
+
314
+ <p>If you get an error due to unpermitted params, make sure, you are allowing array-style parameters: <code>params.require(:user).permit(roles: [])</code></p>
315
+
316
+ <h2 id="label-SimpleForm+for+a+collection+of+Anchormodels">SimpleForm for a collection of Anchormodels</h2>
317
+
318
+ <p>Anchormodel’s <a href="https://github.com/heartcombo/simple_form">simple_form</a> support also includes collections of Anchormodels.</p>
319
+
320
+ <p>Just like in the single Anchormodel implementation, a select input can be provided with:</p>
321
+
322
+ <pre class="code ruby"><code class="ruby">&lt;%= simple_form_for user do |f| %&gt;
323
+ &lt;%# ... %&gt;
324
+ &lt;%= f.input :role %&gt;
325
+ &lt;%# ... %&gt;
326
+ &lt;% end %&gt;
327
+ </code></pre>
328
+
329
+ <p>The input figures out automatically that it is operating on a collection, so the form code is the same as for a single Anchormodel.</p>
330
+
331
+ <p>However, radio buttons are unsuitable for collections, so use check boxes instead:</p>
332
+
333
+ <pre class="code ruby"><code class="ruby">&lt;%= simple_form_for user do |f| %&gt;
334
+ &lt;%# ... %&gt;
335
+ &lt;%= f.input :role, as: :anchormodel_check_boxes %&gt;
336
+ &lt;%# ... %&gt;
337
+ &lt;% end %&gt;
266
338
  </code></pre>
267
339
  </div></div>
268
340
 
269
341
  <div id="footer">
270
- Generated on Wed Apr 24 17:01:46 2024 by
342
+ Generated on Wed May 22 11:24:43 2024 by
271
343
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
272
- 0.9.28 (ruby-3.2.2).
344
+ 0.9.34 (ruby-3.2.2).
273
345
  </div>
274
346
 
275
347
  </div>
data/doc/method_list.html CHANGED
@@ -78,21 +78,29 @@
78
78
 
79
79
  <li class="odd ">
80
80
  <div class="item">
81
- <span class='object_link'><a href="Anchormodel/Attribute.html#attribute_name-instance_method" title="Anchormodel::Attribute#attribute_name (method)">#attribute_name</a></span>
82
- <small>Anchormodel::Attribute</small>
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
83
  </div>
84
84
  </li>
85
85
 
86
86
 
87
87
  <li class="even ">
88
88
  <div class="item">
89
- <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
- <small>Anchormodel::ModelMixin</small>
89
+ <span class='object_link'><a href="Anchormodel/Attribute.html#attribute_name-instance_method" title="Anchormodel::Attribute#attribute_name (method)">#attribute_name</a></span>
90
+ <small>Anchormodel::Attribute</small>
91
91
  </div>
92
92
  </li>
93
93
 
94
94
 
95
95
  <li class="odd ">
96
+ <div class="item">
97
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#cast (method)">#cast</a></span>
98
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
99
+ </div>
100
+ </li>
101
+
102
+
103
+ <li class="even ">
96
104
  <div class="item">
97
105
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#cast (method)">#cast</a></span>
98
106
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -100,7 +108,7 @@
100
108
  </li>
101
109
 
102
110
 
103
- <li class="even ">
111
+ <li class="odd ">
104
112
  <div class="item">
105
113
  <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
114
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -108,7 +116,7 @@
108
116
  </li>
109
117
 
110
118
 
111
- <li class="odd ">
119
+ <li class="even ">
112
120
  <div class="item">
113
121
  <span class='object_link'><a href="Anchormodel.html#find-class_method" title="Anchormodel.find (method)">find</a></span>
114
122
  <small>Anchormodel</small>
@@ -116,6 +124,14 @@
116
124
  </li>
117
125
 
118
126
 
127
+ <li class="odd ">
128
+ <div class="item">
129
+ <span class='object_link'><a href="Anchormodel.html#form_collection-class_method" title="Anchormodel.form_collection (method)">form_collection</a></span>
130
+ <small>Anchormodel</small>
131
+ </div>
132
+ </li>
133
+
134
+
119
135
  <li class="even ">
120
136
  <div class="item">
121
137
  <span class='object_link'><a href="Anchormodel.html#index-instance_method" title="Anchormodel#index (method)">#index</a></span>
@@ -189,6 +205,14 @@
189
205
 
190
206
 
191
207
  <li class="odd ">
208
+ <div class="item">
209
+ <span class='object_link'><a href="Anchormodel/Attribute.html#multiple%3F-instance_method" title="Anchormodel::Attribute#multiple? (method)">#multiple?</a></span>
210
+ <small>Anchormodel::Attribute</small>
211
+ </div>
212
+ </li>
213
+
214
+
215
+ <li class="even ">
192
216
  <div class="item">
193
217
  <span class='object_link'><a href="Anchormodel/Attribute.html#optional-instance_method" title="Anchormodel::Attribute#optional (method)">#optional</a></span>
194
218
  <small>Anchormodel::Attribute</small>
@@ -196,6 +220,14 @@
196
220
  </li>
197
221
 
198
222
 
223
+ <li class="odd ">
224
+ <div class="item">
225
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#serializable? (method)">#serializable?</a></span>
226
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
227
+ </div>
228
+ </li>
229
+
230
+
199
231
  <li class="even ">
200
232
  <div class="item">
201
233
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serializable? (method)">#serializable?</a></span>
@@ -205,6 +237,14 @@
205
237
 
206
238
 
207
239
  <li class="odd ">
240
+ <div class="item">
241
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#serialize (method)">#serialize</a></span>
242
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
243
+ </div>
244
+ </li>
245
+
246
+
247
+ <li class="even ">
208
248
  <div class="item">
209
249
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serialize (method)">#serialize</a></span>
210
250
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -212,7 +252,7 @@
212
252
  </li>
213
253
 
214
254
 
215
- <li class="even ">
255
+ <li class="odd ">
216
256
  <div class="item">
217
257
  <span class='object_link'><a href="Anchormodel.html#setup!-class_method" title="Anchormodel.setup! (method)">setup!</a></span>
218
258
  <small>Anchormodel</small>
@@ -220,7 +260,7 @@
220
260
  </li>
221
261
 
222
262
 
223
- <li class="odd ">
263
+ <li class="even ">
224
264
  <div class="item">
225
265
  <span class='object_link'><a href="Anchormodel.html#to_s-instance_method" title="Anchormodel#to_s (method)">#to_s</a></span>
226
266
  <small>Anchormodel</small>
@@ -228,7 +268,7 @@
228
268
  </li>
229
269
 
230
270
 
231
- <li class="even ">
271
+ <li class="odd ">
232
272
  <div class="item">
233
273
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#type-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#type (method)">#type</a></span>
234
274
  <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 Wed May 22 11:24:43 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,24 @@ 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
31
35
  )
32
36
 
33
37
  @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'