has_dynamic_columns 0.0.5 → 0.1.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/lib/has_dynamic_columns/version.rb +1 -1
- data/lib/has_dynamic_columns.rb +93 -64
- data/spec/has_dynamic_columns_spec.rb +148 -3
- data/spec/spec_helper.rb +39 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8adcb2fa0100a0caea34697193e9d0f008eb633a
|
4
|
+
data.tar.gz: 7dcd964a4ed65285fb819ad51f1f7656491b2610
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f933582fe5fce22e3cae11bb0af740581b08d221aa24b555fbcbb92323082b49bd5c19191204601d0a24a96a01382ee96f8a07a7387852bec36eddde7fe3904
|
7
|
+
data.tar.gz: 8b6f80d4116c9074149469cd4f6c33c047c1b96dabc517217578b2e0878eddbf2de0a770370398b868eaf81fc05491672fce6c131e3a043939f875d5e990f983
|
data/lib/has_dynamic_columns.rb
CHANGED
@@ -22,12 +22,18 @@ module HasDynamicColumns
|
|
22
22
|
configuration.update(options) if options.is_a?(Hash)
|
23
23
|
|
24
24
|
class_eval <<-EOV
|
25
|
+
alias_method :as_json_before_#{configuration[:as]}, :as_json
|
26
|
+
|
27
|
+
# Store all our configurations for usage later
|
28
|
+
@@has_dynamic_columns_configurations ||= []
|
29
|
+
@@has_dynamic_columns_configurations << #{configuration}
|
30
|
+
|
25
31
|
include ::HasDynamicColumns::Model::InstanceMethods
|
26
32
|
|
27
|
-
has_many :
|
33
|
+
has_many :activerecord_dynamic_columns,
|
28
34
|
class_name: "HasDynamicColumns::DynamicColumn",
|
29
35
|
as: :field_scope
|
30
|
-
has_many :
|
36
|
+
has_many :activerecord_dynamic_column_data,
|
31
37
|
class_name: "HasDynamicColumns::DynamicColumnDatum",
|
32
38
|
as: :owner,
|
33
39
|
autosave: true
|
@@ -38,7 +44,59 @@ module HasDynamicColumns
|
|
38
44
|
#attr_accessible :#{configuration[:column]}
|
39
45
|
end
|
40
46
|
|
41
|
-
validate
|
47
|
+
validate do |field_scope|
|
48
|
+
field_scope = self.get_#{configuration[:as]}_field_scope
|
49
|
+
|
50
|
+
if field_scope
|
51
|
+
# has_many association
|
52
|
+
if field_scope.respond_to?(:select) && field_scope.respond_to?(:collect)
|
53
|
+
|
54
|
+
# belongs_to association
|
55
|
+
else
|
56
|
+
# All the fields defined on the parent model
|
57
|
+
dynamic_columns = field_scope.send("activerecord_dynamic_columns")
|
58
|
+
|
59
|
+
self.send("activerecord_dynamic_column_data").each { |dynamic_column_datum|
|
60
|
+
# Collect all validation errors
|
61
|
+
validation_errors = []
|
62
|
+
|
63
|
+
if dynamic_column_datum.dynamic_column_option_id == -1
|
64
|
+
validation_errors << "invalid_option"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Find the dynamic_column defined for this datum
|
68
|
+
dynamic_column = nil
|
69
|
+
dynamic_columns.each { |i|
|
70
|
+
if i == dynamic_column_datum.dynamic_column
|
71
|
+
dynamic_column = i
|
72
|
+
break
|
73
|
+
end
|
74
|
+
}
|
75
|
+
# We have a dynamic_column - validate
|
76
|
+
if dynamic_column
|
77
|
+
dynamic_column.dynamic_column_validations.each { |validation|
|
78
|
+
if !validation.is_valid?(dynamic_column_datum.value.to_s)
|
79
|
+
validation_errors << validation.error
|
80
|
+
end
|
81
|
+
}
|
82
|
+
else
|
83
|
+
# No field found - this is probably bad - should we throw an error?
|
84
|
+
validation_errors << "not_found"
|
85
|
+
end
|
86
|
+
|
87
|
+
# If any errors exist - add them
|
88
|
+
if validation_errors.length > 0
|
89
|
+
if dynamic_column.nil?
|
90
|
+
puts validation_errors.inspect
|
91
|
+
#errors.add(:dynamic_columns, { "unknown" => validation_errors })
|
92
|
+
else
|
93
|
+
errors.add(:dynamic_columns, { dynamic_column.key.to_s => validation_errors })
|
94
|
+
end
|
95
|
+
end
|
96
|
+
}
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
42
100
|
|
43
101
|
public
|
44
102
|
# Order by dynamic columns
|
@@ -148,11 +206,13 @@ module HasDynamicColumns
|
|
148
206
|
json = super(*args)
|
149
207
|
options = args.extract_options!
|
150
208
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
209
|
+
@@has_dynamic_columns_configurations.each { |config|
|
210
|
+
if !options[:root].nil?
|
211
|
+
json[options[:root]][config[:as].to_s] = self.send(config[:as].to_s)
|
212
|
+
else
|
213
|
+
json[config[:as].to_s] = self.send(config[:as].to_s)
|
214
|
+
end
|
215
|
+
}
|
156
216
|
|
157
217
|
json
|
158
218
|
end
|
@@ -167,10 +227,10 @@ module HasDynamicColumns
|
|
167
227
|
dynamic_column = self.#{configuration[:as].to_s.singularize}_key_to_dynamic_column(key)
|
168
228
|
|
169
229
|
# We already have this key in database
|
170
|
-
if existing = self.
|
230
|
+
if existing = self.activerecord_dynamic_column_data.select { |i| i.dynamic_column == dynamic_column }.first
|
171
231
|
existing.value = value
|
172
232
|
else
|
173
|
-
self.
|
233
|
+
self.activerecord_dynamic_column_data.build(:dynamic_column => dynamic_column, :value => value)
|
174
234
|
end
|
175
235
|
}
|
176
236
|
end
|
@@ -180,9 +240,11 @@ module HasDynamicColumns
|
|
180
240
|
self.field_scope_#{configuration[:as]}.each { |i|
|
181
241
|
h[i.key] = nil
|
182
242
|
}
|
183
|
-
|
184
|
-
|
243
|
+
|
244
|
+
self.activerecord_dynamic_column_data.each { |i|
|
245
|
+
h[i.dynamic_column.key] = i.value unless !i.dynamic_column || !h.has_key?(i.dynamic_column.key)
|
185
246
|
}
|
247
|
+
|
186
248
|
h
|
187
249
|
end
|
188
250
|
|
@@ -191,17 +253,27 @@ module HasDynamicColumns
|
|
191
253
|
end
|
192
254
|
|
193
255
|
def field_scope_#{configuration[:as]}
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
256
|
+
# has_many relationship
|
257
|
+
if self.get_#{configuration[:as]}_field_scope.respond_to?(:select) && self.get_#{configuration[:as]}_field_scope.respond_to?(:collect)
|
258
|
+
self.get_#{configuration[:as]}_field_scope.collect { |i|
|
259
|
+
i.send("activerecord_dynamic_columns")
|
260
|
+
}.flatten.select { |i|
|
261
|
+
i.dynamic_type.to_s.empty? || i.dynamic_type.to_s == self.class.to_s
|
262
|
+
}
|
263
|
+
# belongs_to relationship
|
264
|
+
else
|
265
|
+
self.get_#{configuration[:as]}_field_scope.send("activerecord_dynamic_columns").select { |i|
|
266
|
+
# Only get things with no dynamic type defined or dynamic types defined as this class
|
267
|
+
i.dynamic_type.to_s.empty? || i.dynamic_type.to_s == self.class.to_s
|
268
|
+
}
|
269
|
+
end
|
198
270
|
end
|
199
271
|
|
200
|
-
|
201
|
-
|
272
|
+
protected
|
273
|
+
def get_#{configuration[:as]}_field_scope
|
274
|
+
#{configuration[:field_scope]}
|
202
275
|
end
|
203
276
|
|
204
|
-
protected
|
205
277
|
# Whether this is storable
|
206
278
|
def storable_#{configuration[:as].to_s.singularize}_key?(key)
|
207
279
|
self.#{configuration[:as].to_s.singularize}_keys.include?(key.to_s)
|
@@ -210,62 +282,19 @@ module HasDynamicColumns
|
|
210
282
|
# Figures out which dynamic_column has which key
|
211
283
|
def #{configuration[:as].to_s.singularize}_key_to_dynamic_column(key)
|
212
284
|
found = nil
|
213
|
-
if record = self.send(
|
285
|
+
if record = self.send('field_scope_#{configuration[:as]}').select { |i| i.key == key.to_s }.first
|
214
286
|
found = record
|
215
287
|
end
|
216
288
|
found
|
217
289
|
end
|
218
|
-
|
219
|
-
def field_scope
|
220
|
-
#{configuration[:field_scope]}
|
221
|
-
end
|
222
290
|
EOV
|
223
291
|
end
|
224
292
|
end
|
225
293
|
|
226
294
|
module InstanceMethods
|
227
295
|
# Validate all the dynamic_column_data at once
|
228
|
-
def validate_dynamic_column_data
|
229
|
-
field_scope = self.field_scope
|
230
|
-
|
231
|
-
if field_scope
|
232
|
-
# All the fields defined on the parent model
|
233
|
-
dynamic_columns = field_scope.send("activerecord_#{field_scope.dynamic_columns_as}")
|
234
|
-
|
235
|
-
self.send("activerecord_#{self.dynamic_columns_as}_data").each { |dynamic_column_datum|
|
236
|
-
# Collect all validation errors
|
237
|
-
validation_errors = []
|
296
|
+
def validate_dynamic_column_data(field_scope = nil)
|
238
297
|
|
239
|
-
if dynamic_column_datum.dynamic_column_option_id == -1
|
240
|
-
validation_errors << "invalid_option"
|
241
|
-
end
|
242
|
-
|
243
|
-
# Find the dynamic_column defined for this datum
|
244
|
-
dynamic_column = nil
|
245
|
-
dynamic_columns.each { |i|
|
246
|
-
if i == dynamic_column_datum.dynamic_column
|
247
|
-
dynamic_column = i
|
248
|
-
break
|
249
|
-
end
|
250
|
-
}
|
251
|
-
# We have a dynamic_column - validate
|
252
|
-
if dynamic_column
|
253
|
-
dynamic_column.dynamic_column_validations.each { |validation|
|
254
|
-
if !validation.is_valid?(dynamic_column_datum.value.to_s)
|
255
|
-
validation_errors << validation.error
|
256
|
-
end
|
257
|
-
}
|
258
|
-
else
|
259
|
-
# No field found - this is probably bad - should we throw an error?
|
260
|
-
validation_errors << "not_found"
|
261
|
-
end
|
262
|
-
|
263
|
-
# If any errors exist - add them
|
264
|
-
if validation_errors.length > 0
|
265
|
-
errors.add(:dynamic_columns, { "#{dynamic_column.key}" => validation_errors })
|
266
|
-
end
|
267
|
-
}
|
268
|
-
end
|
269
298
|
end
|
270
299
|
end
|
271
300
|
end
|
@@ -32,9 +32,154 @@ describe HasDynamicColumns do
|
|
32
32
|
field.dynamic_column_validations.build(:regexp => "^[^$]+$", :error => "blank")
|
33
33
|
field.dynamic_column_validations.build(:regexp => "^[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJKLMNPRSTVWXYZ]( )?\\d[ABCEGHJKLMNPRSTVWXYZ]\\d$", :error => "invalid_format")
|
34
34
|
|
35
|
+
# Product fields
|
36
|
+
account.activerecord_dynamic_columns.build(:dynamic_type => "Product", :key => "rarity", :data_type => "string")
|
37
|
+
|
35
38
|
account
|
36
39
|
end
|
37
40
|
|
41
|
+
describe Product do
|
42
|
+
subject(:product) {
|
43
|
+
Product.new(:name => "Product #1", :account => account)
|
44
|
+
}
|
45
|
+
before do
|
46
|
+
@category0 = Category.new(:name => "Category 0", :account => product.account)
|
47
|
+
@category0.save
|
48
|
+
|
49
|
+
@category1 = Category.new(:name => "Category 1", :account => product.account)
|
50
|
+
@category1.activerecord_dynamic_columns.build(:dynamic_type => "Product", :key => "vin_number", :data_type => "string")
|
51
|
+
@category1.save
|
52
|
+
|
53
|
+
@category2 = Category.new(:name => "Category 2", :account => product.account)
|
54
|
+
@category2.activerecord_dynamic_columns.build(:dynamic_type => "Product", :key => "serial_number", :data_type => "string")
|
55
|
+
@category2.save
|
56
|
+
|
57
|
+
@category3 = Category.new(:name => "Category 3", :account => product.account)
|
58
|
+
@category3.activerecord_dynamic_columns.build(:dynamic_type => "Product", :key => "funky_data", :data_type => "string")
|
59
|
+
@category3.activerecord_dynamic_columns.build(:dynamic_type => "Product", :key => "funkier_data", :data_type => "string")
|
60
|
+
@category3.save
|
61
|
+
|
62
|
+
@category4 = Category.new(:name => "Category 4", :account => product.account)
|
63
|
+
@category4.activerecord_dynamic_columns.build(:dynamic_type => "Product", :key => "funkiest_data", :data_type => "string")
|
64
|
+
@category4.activerecord_dynamic_columns.build(:dynamic_type => "Product", :key => "ok_data", :data_type => "string")
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'when it has a defined has_many relationship' do
|
68
|
+
|
69
|
+
context 'when it has_many categories' do
|
70
|
+
|
71
|
+
it 'should return empty category_fields when no categories associated' do
|
72
|
+
json = product.as_json
|
73
|
+
expect(json["category_fields"]).to eq({})
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should return empty category_fields when no category has no dynamic_columns' do
|
77
|
+
product.categories << @category0
|
78
|
+
json = product.as_json
|
79
|
+
expect(json["category_fields"]).to eq({})
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when not saved' do
|
83
|
+
it 'should return a categories fields' do
|
84
|
+
product.categories << @category1
|
85
|
+
product.category_fields = {
|
86
|
+
"vin_number" => "123"
|
87
|
+
}
|
88
|
+
|
89
|
+
json = product.as_json
|
90
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"123"})
|
91
|
+
expect(product.new_record?).to eq(true)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
context 'when saved' do
|
95
|
+
it 'should return a categories fields' do
|
96
|
+
product.categories << @category1
|
97
|
+
product.category_fields = {
|
98
|
+
"vin_number" => "345"
|
99
|
+
}
|
100
|
+
product.save
|
101
|
+
|
102
|
+
json = product.as_json
|
103
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"345"})
|
104
|
+
expect(product.new_record?).to eq(false)
|
105
|
+
|
106
|
+
product_id = product.id
|
107
|
+
product = Product.find(product_id)
|
108
|
+
json = product.as_json
|
109
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"345"})
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should kitchen sink' do
|
114
|
+
product.product_fields = {
|
115
|
+
"rarity" => "very rare"
|
116
|
+
}
|
117
|
+
|
118
|
+
# Add category 1 to the product - it should now have the fields of "vin number"
|
119
|
+
product.categories << @category1
|
120
|
+
product.categories << @category2
|
121
|
+
|
122
|
+
product.category_fields = {
|
123
|
+
"vin_number" => "first:this is the vin number",
|
124
|
+
"serial_number" => "first:serial number!"
|
125
|
+
}
|
126
|
+
json = product.as_json
|
127
|
+
expect(json["product_fields"]).to eq({"rarity"=>"very rare"})
|
128
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"first:this is the vin number", "serial_number"=>"first:serial number!"})
|
129
|
+
|
130
|
+
product.save
|
131
|
+
json = product.as_json
|
132
|
+
expect(json["product_fields"]).to eq({"rarity"=>"very rare"})
|
133
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"first:this is the vin number", "serial_number"=>"first:serial number!"})
|
134
|
+
|
135
|
+
product_id = product.id
|
136
|
+
product = Product.find(product_id)
|
137
|
+
json = product.as_json
|
138
|
+
expect(json["product_fields"]).to eq({"rarity"=>"very rare"})
|
139
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"first:this is the vin number", "serial_number"=>"first:serial number!"})
|
140
|
+
|
141
|
+
product.category_fields = {
|
142
|
+
"serial_number" => "second:serial number!"
|
143
|
+
}
|
144
|
+
json = product.as_json
|
145
|
+
expect(json["product_fields"]).to eq({"rarity"=>"very rare"})
|
146
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"first:this is the vin number", "serial_number"=>"second:serial number!"})
|
147
|
+
|
148
|
+
product.save
|
149
|
+
json = product.as_json
|
150
|
+
expect(json["product_fields"]).to eq({"rarity"=>"very rare"})
|
151
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"first:this is the vin number", "serial_number"=>"second:serial number!"})
|
152
|
+
|
153
|
+
product = Product.find(product_id)
|
154
|
+
json = product.as_json
|
155
|
+
expect(json["product_fields"]).to eq({"rarity"=>"very rare"})
|
156
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"first:this is the vin number", "serial_number"=>"second:serial number!"})
|
157
|
+
|
158
|
+
expect(@category4.new_record?).to eq(true)
|
159
|
+
|
160
|
+
product.categories << @category3
|
161
|
+
product.categories << @category4
|
162
|
+
|
163
|
+
expect(@category4.new_record?).to eq(false)
|
164
|
+
|
165
|
+
product.category_fields = {
|
166
|
+
"funkier_data" => "this is funkier data",
|
167
|
+
"ok_data" => "this is ok data"
|
168
|
+
}
|
169
|
+
json = product.as_json
|
170
|
+
expect(json["product_fields"]).to eq({"rarity"=>"very rare"})
|
171
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"first:this is the vin number", "serial_number"=>"second:serial number!", "funky_data"=>nil, "funkier_data"=>"this is funkier data", "funkiest_data"=>nil, "ok_data"=>"this is ok data"})
|
172
|
+
|
173
|
+
product.save
|
174
|
+
product = Product.find(product_id)
|
175
|
+
json = product.as_json
|
176
|
+
expect(json["product_fields"]).to eq({"rarity"=>"very rare"})
|
177
|
+
expect(json["category_fields"]).to eq({"vin_number"=>"first:this is the vin number", "serial_number"=>"second:serial number!", "funky_data"=>nil, "funkier_data"=>"this is funkier data", "funkiest_data"=>nil, "ok_data"=>"this is ok data"})
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
38
183
|
describe Customer do
|
39
184
|
subject(:customer) { Customer.new(:account => account) }
|
40
185
|
before do
|
@@ -56,7 +201,7 @@ describe HasDynamicColumns do
|
|
56
201
|
end
|
57
202
|
it 'should find me' do
|
58
203
|
c = customer
|
59
|
-
c.save
|
204
|
+
expect(c.save).to eq(true)
|
60
205
|
a = c.account
|
61
206
|
|
62
207
|
expect(a.customers.dynamic_where(a, { first_name: "Butch" }).length).to eq(1)
|
@@ -183,10 +328,10 @@ describe HasDynamicColumns do
|
|
183
328
|
it 'should should retrieve properly from the database' do
|
184
329
|
sub = customer_address
|
185
330
|
sub.save
|
186
|
-
|
331
|
+
|
187
332
|
customer = CustomerAddress.find(sub.id)
|
188
333
|
json = customer.as_json(:root => "customer_address")
|
189
|
-
|
334
|
+
|
190
335
|
expect(json["customer_address"]["fields"]).to eq({
|
191
336
|
"address_1" => "555 Bloor Street",
|
192
337
|
"address_2" => nil,
|
data/spec/spec_helper.rb
CHANGED
@@ -47,6 +47,22 @@ ActiveRecord::Schema.define do
|
|
47
47
|
t.integer :customer_id
|
48
48
|
t.timestamps
|
49
49
|
end
|
50
|
+
create_table :products, force: true do |t|
|
51
|
+
t.string :name
|
52
|
+
t.integer :account_id
|
53
|
+
t.timestamps
|
54
|
+
end
|
55
|
+
create_table :categories, force: true do |t|
|
56
|
+
t.string :name
|
57
|
+
t.integer :account_id
|
58
|
+
t.timestamps
|
59
|
+
end
|
60
|
+
create_table :category_owners, force: true do |t|
|
61
|
+
t.integer :category_id
|
62
|
+
t.integer :owner_id
|
63
|
+
t.string :owner_type
|
64
|
+
t.timestamps
|
65
|
+
end
|
50
66
|
end
|
51
67
|
|
52
68
|
class Account < ActiveRecord::Base
|
@@ -65,6 +81,29 @@ class CustomerAddress < ActiveRecord::Base
|
|
65
81
|
has_dynamic_columns field_scope: "customer.account", dynamic_type: "CustomerAddress", as: "fields"
|
66
82
|
end
|
67
83
|
|
84
|
+
class Product < ActiveRecord::Base
|
85
|
+
belongs_to :account
|
86
|
+
has_many :category_owners, :as => :owner
|
87
|
+
has_many :categories, :through => :category_owners
|
88
|
+
|
89
|
+
# Fields defined via the account
|
90
|
+
has_dynamic_columns field_scope: "account", dynamic_type: "Product", as: "product_fields"
|
91
|
+
|
92
|
+
# Fields defined via any associated categories
|
93
|
+
has_dynamic_columns field_scope: "categories", dynamic_type: "Product", as: "category_fields"
|
94
|
+
end
|
95
|
+
|
96
|
+
class Category < ActiveRecord::Base
|
97
|
+
belongs_to :account
|
98
|
+
has_many :category_owners
|
99
|
+
|
100
|
+
has_dynamic_columns field_scope: "account"
|
101
|
+
end
|
102
|
+
|
103
|
+
class CategoryOwner < ActiveRecord::Base
|
104
|
+
belongs_to :category
|
105
|
+
belongs_to :owner, :polymorphic => true
|
106
|
+
end
|
68
107
|
|
69
108
|
RSpec.configure do |config|
|
70
109
|
config.after(:each) do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: has_dynamic_columns
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Butch Marshall
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-08-
|
11
|
+
date: 2015-08-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|