anchormodel 0.1.5 → 0.2.1

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