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