anchormodel 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/README.md +114 -3
- data/VERSION +1 -1
- data/anchormodel.gemspec +5 -5
- data/doc/Anchormodel/ActiveModelTypeValueSingle.html +697 -0
- data/doc/Anchormodel/Attribute.html +109 -9
- data/doc/Anchormodel/ModelMixin.html +75 -3
- data/doc/Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html +269 -0
- data/doc/Anchormodel/SimpleFormInputs/Helpers.html +124 -0
- data/doc/Anchormodel/SimpleFormInputs.html +124 -0
- data/doc/Anchormodel/Util.html +612 -0
- data/doc/Anchormodel/Version.html +3 -3
- data/doc/Anchormodel.html +99 -34
- data/doc/AnchormodelGenerator.html +201 -0
- data/doc/AnchormodelInput.html +140 -0
- data/doc/AnchormodelRadioButtonsInput.html +140 -0
- data/doc/_index.html +63 -4
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +109 -6
- data/doc/frames.html +5 -10
- data/doc/index.html +109 -6
- data/doc/method_list.html +70 -6
- data/doc/top-level-namespace.html +4 -4
- data/lib/anchormodel/active_model_type_value_multi.rb +31 -0
- data/lib/anchormodel/active_model_type_value_single.rb +4 -2
- data/lib/anchormodel/attribute.rb +7 -1
- data/lib/anchormodel/model_mixin.rb +7 -0
- data/lib/anchormodel/simple_form_inputs/anchormodel_check_boxes_input.rb +23 -0
- data/lib/anchormodel/simple_form_inputs/anchormodel_input.rb +21 -0
- data/lib/anchormodel/simple_form_inputs/anchormodel_radio_buttons_input.rb +23 -0
- data/lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb +54 -0
- data/lib/anchormodel/util.rb +57 -7
- data/lib/anchormodel.rb +10 -0
- data/test/active_record_model/user_test.rb +73 -9
- data/test/dummy/app/anchormodels/animal.rb +6 -0
- data/test/dummy/app/models/user.rb +1 -0
- data/test/dummy/db/migrate/20240425182000_add_animals_to_users.rb +5 -0
- data/test/dummy/db/schema.rb +2 -1
- metadata +18 -4
- 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="
|
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="
|
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="
|
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="
|
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="
|
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="
|
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
|
-
— Documentation by YARD 0.9.
|
9
|
+
— 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
|
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.
|
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
|
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
|
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
|
-
|
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
|
data/lib/anchormodel/util.rb
CHANGED
@@ -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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|