has_dynamic_columns 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69d28ea4aadd2689e5653aceb6092dd913dbb831
4
- data.tar.gz: 26a14d5b8a98d70565094e588f2a40ee3355de01
3
+ metadata.gz: 8e226dfbdf7a08adfe5c9fe89fd93704b94ce06c
4
+ data.tar.gz: f0a3a6de371ded59b17db4690d54093d2ab98d16
5
5
  SHA512:
6
- metadata.gz: cff1aba3d57c28d9ebe93d660555a6959e064546c5d16768bdc308836fc89facdbd46b578cd94adfccc0c18b8e7dd40019a8a9d694d2912d0d53c050bf6a5f66
7
- data.tar.gz: 9981d60459e0c61e76247ac59a11fe9a7558ec3429026de3904dd05a1e0c59cbc9957ede55a120d1896e50291ee8cba17f06dc9b1c0a8cd7f790ddf6b2e19b66
6
+ metadata.gz: 8dd05a3bd91dc8419c67506b463cb0c6741e1756901d9b4979202e4da4ea4c7a01b809de42d33a215f888ea490b458f5321ba2cad17291306d5e092db4bf0399
7
+ data.tar.gz: a543799ebbf31103a117652fe127a002e8fa6bbc00870e6c65c5eb54035f7c73af1eb2c8d81eed5eae3bddd3139cddb92c41da28fb749f39ee983511028da179
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  has_dynamic_columns
2
2
  ============
3
3
 
4
- Add dynamic columns to ActiveRecord models
4
+ This plugin gives ActiveRecord models the ability to dynamically define collectable data based on ***has_many*** and ***belongs_to*** relationships.
5
5
 
6
6
  Installation
7
7
  ============
8
8
 
9
9
  ```ruby
10
- gem 'has_dynamic_columns', :git => 'git://github.com/butchmarshall/has_dynamic_columns.git'
10
+ gem "has_dynamic_columns"
11
11
  ```
12
12
 
13
13
  The Active Record migration is required to create the has_dynamic_columns table. You can create that table by
@@ -19,6 +19,22 @@ running the following command:
19
19
  Usage
20
20
  ============
21
21
 
22
+ has_dynamic_columns
23
+
24
+ - as:
25
+ - the setter/getter method
26
+ - field_scope:
27
+ - **belongs_to** or **has_many** relationship
28
+
29
+ ## **belongs_to** relationship
30
+
31
+ Our example is a data model where an **account** ***has_many*** **customers** and each **customer** ***has_many*** **customer_addresses**
32
+
33
+ Each customers collectable info is uniquely defined by the associated account.
34
+
35
+ Each customer addresses collectable info is defined by the associated customers account.
36
+
37
+ **Models**
22
38
  ```ruby
23
39
  class Account < ActiveRecord::Base
24
40
  has_many :customers
@@ -27,8 +43,7 @@ end
27
43
 
28
44
  class Customer < ActiveRecord::Base
29
45
  belongs_to :account
30
- has_many :customer_addresses
31
- has_dynamic_columns field_scope: "account", as: "fields"
46
+ has_dynamic_columns field_scope: "account", as: "customer_fields"
32
47
  end
33
48
 
34
49
  class CustomerAddress < ActiveRecord::Base
@@ -37,3 +52,137 @@ class CustomerAddress < ActiveRecord::Base
37
52
  end
38
53
  ```
39
54
 
55
+ **Setup**
56
+ ```ruby
57
+ # ------------------------------------------------
58
+ # Create our first account
59
+ # ------------------------------------------------
60
+ account = Account.new(:name => "Account #1")
61
+
62
+ # Define a first_name field
63
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "first_name", :data_type => "string")
64
+ # Define a last_name field
65
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "last_name", :data_type => "string")
66
+ # Define a company field
67
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "company", :data_type => "string")
68
+
69
+ # save
70
+ account.save
71
+
72
+ # ------------------------------------------------
73
+ # Create our second account
74
+ # ------------------------------------------------
75
+ account = Account.new(:name => "Account #2")
76
+
77
+ # Define a first_name field
78
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "first_name", :data_type => "string")
79
+ # Define a last_name field
80
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "last_name", :data_type => "string")
81
+ # Define a country field
82
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "country", :data_type => "string")
83
+
84
+ # save
85
+ account.save
86
+ ```
87
+
88
+ **Data**
89
+ ```ruby
90
+ # Add a customer to our first account
91
+ account = Account.find(1)
92
+ customer = Customer.new(:account => account)
93
+ customer.customer_fields = {
94
+ "first_name" => "Butch",
95
+ "last_Name" => "Marshall",
96
+ "company" => "Aperture Science"
97
+ }
98
+ customer.save
99
+
100
+ # as_json
101
+ customer.as_json
102
+ # == { "id": 1, "account_id": 1, "customer_fields" => { "first_name" => "Butch", "last_Name" => "Marshall", "company" => "Aperture Science" } }
103
+
104
+ # Add a customer to our first account
105
+ account = Account.find(1)
106
+ customer = Customer.new(:account => account)
107
+ customer.customer_fields = {
108
+ "first_name" => "John",
109
+ "last_Name" => "Paterson",
110
+ "company" => "Aperture Science"
111
+ }
112
+ customer.save
113
+
114
+ # Add a customer to our second account
115
+ account = Account.find(2)
116
+ customer = Customer.new(:account => account)
117
+ customer.customer_fields = {
118
+ "first_name" => "Butch",
119
+ "last_Name" => "Marshall",
120
+ "country" => "Canada"
121
+ }
122
+ customer.save
123
+
124
+ # as_json
125
+ puts customer.as_json
126
+ # == { "id": 2, "account_id": 2, "customer_fields" => { "first_name" => "Butch", "last_Name" => "Marshall", "country" => "Canada" } }
127
+ ```
128
+
129
+ **Searching**
130
+ ```ruby
131
+
132
+ # ------------------------------------------------
133
+ # with_scope
134
+ # ------------------------------------------------
135
+
136
+ # ------------------------------------------------
137
+ # Find customers under the first account
138
+ # ------------------------------------------------
139
+ account = Account.find(1)
140
+
141
+ # 1 result
142
+ Customer.where.has_dynamic_scope({ :first_name => "Butch" }).with_scope(account)
143
+
144
+ # 1 result
145
+ Customer.where.has_dynamic_scope({ :first_name => "Butch", :company => "Aperture Science" }).with_scope(account)
146
+
147
+ # 0 results
148
+ Customer.where.has_dynamic_scope({ :first_name => "Butch", :company => "Blaaaaa" }).with_scope(account)
149
+
150
+ # 2 results
151
+ Customer.where.has_dynamic_scope({ :company => "Aperture Science" }).with_scope(account)
152
+
153
+ # ------------------------------------------------
154
+ # Find customers under the second account
155
+ # ------------------------------------------------
156
+ account = Account.find(2)
157
+ # 1 result
158
+ Customer.where.has_dynamic_scope({ :first_name => "Butch" }).with_scope(account)
159
+
160
+ # 1 result
161
+ Customer.where.has_dynamic_scope({ :first_name => "Butch", :country => "Canada" }).with_scope(account)
162
+
163
+ # 0 results
164
+ Customer.where.has_dynamic_scope({ :first_name => "Butch", :country => "Japan" }).with_scope(account)
165
+ ```
166
+
167
+ # ------------------------------------------------
168
+ # without_scope
169
+ # ------------------------------------------------
170
+
171
+ # 6 results
172
+ # finds everyone named butch, no matter what account they're apart of
173
+ Customer.where.has_dynamic_scope({ :first_name => "Butch" }).without_scope
174
+
175
+ # ------------------------------------------------
176
+ # with Arel
177
+ # WARNING: compound conditionals such as Customer.arel_table[:first_Name].matches("B%").and(Customer.arel_table[:first_Name].eq("Canada")) are NOT currently supported
178
+ # ------------------------------------------------
179
+
180
+ # 6 matches
181
+ Customer.where.has_dynamic_scope(Customer.arel_table[:first_Name].matches("B%")).without_scope
182
+
183
+ # 1 match
184
+ Customer.where.has_dynamic_scope(Customer.arel_table[:first_Name].eq("Canada")).with_scope(Account.find(1))
185
+
186
+ ## **has_many** relationship
187
+
188
+ TODO example.
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "active_record"
5
+ require "has_dynamic_columns"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "activerecord", [">= 3.0", "< 5.0"]
21
+ spec.add_dependency "activerecord", "~> 4.2"
22
22
 
23
23
  spec.add_development_dependency "bundler", "~> 1.7"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
@@ -1,5 +1,11 @@
1
1
  require "active_support"
2
+ require 'active_support/dependencies'
3
+ require "active_record"
2
4
 
5
+ require "has_dynamic_columns/active_record/query_methods"
6
+ require "has_dynamic_columns/active_record/relation"
7
+
8
+ require "has_dynamic_columns/model"
3
9
  require "has_dynamic_columns/version"
4
10
  require "has_dynamic_columns/dynamic_column"
5
11
  require "has_dynamic_columns/dynamic_column_option"
@@ -7,305 +13,6 @@ require "has_dynamic_columns/dynamic_column_validation"
7
13
  require "has_dynamic_columns/dynamic_column_datum"
8
14
 
9
15
  module HasDynamicColumns
10
- module Model
11
- def self.included(base)
12
- base.send :extend, ClassMethods
13
- end
14
-
15
- module ClassMethods
16
- def has_dynamic_columns(*args)
17
- options = args.extract_options!
18
- configuration = {
19
- :as => "dynamic_columns",
20
- :field_scope => nil,
21
- }
22
- configuration.update(options) if options.is_a?(Hash)
23
-
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
-
31
- include ::HasDynamicColumns::Model::InstanceMethods
32
-
33
- has_many :activerecord_dynamic_columns,
34
- class_name: "HasDynamicColumns::DynamicColumn",
35
- as: :field_scope
36
- has_many :activerecord_dynamic_column_data,
37
- class_name: "HasDynamicColumns::DynamicColumnDatum",
38
- as: :owner,
39
- autosave: true
40
-
41
- # only add to attr_accessible
42
- # if the class has some mass_assignment_protection
43
- if defined?(accessible_attributes) and !accessible_attributes.blank?
44
- #attr_accessible :#{configuration[:column]}
45
- end
46
-
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
- # TODO - fix from the has_many - need to fix validations
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
100
-
101
- public
102
- # Order by dynamic columns
103
- def self.dynamic_order(field_scope, key, direction = :asc)
104
- table = self.name.constantize.arel_table
105
- column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_order_"+key.to_s)
106
- column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_order_data_"+key.to_s)
107
-
108
- field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
109
- field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
110
- dynamic_type = self.name.constantize.to_s
111
-
112
- # Join on the column with the key
113
- on_query = column_table[:key].eq(key)
114
- if !field_scope_type.nil?
115
- on_query = on_query.and(
116
- column_table[:field_scope_type].eq(field_scope_type)
117
- )
118
- end
119
- if !field_scope_id.nil?
120
- on_query = on_query.and(
121
- column_table[:field_scope_id].eq(field_scope_id)
122
- )
123
- end
124
-
125
- column_table_join_on = column_table
126
- .create_on(
127
- on_query
128
- )
129
-
130
- column_table_join = table.create_join(column_table, column_table_join_on)
131
- query = joins(column_table_join)
132
-
133
- # Join on all the data with the provided key
134
- column_table_datum_join_on = column_datum_table
135
- .create_on(
136
- column_datum_table[:owner_id].eq(table[:id]).and(
137
- column_datum_table[:owner_type].eq(dynamic_type)
138
- ).and(
139
- column_datum_table[:dynamic_column_id].eq(column_table[:id])
140
- )
141
- )
142
-
143
- column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on)
144
- query = query.joins(column_table_datum_join)
145
-
146
- # Order
147
- query = query.order(column_datum_table[:value].send(direction))
148
-
149
- # Group required - we have many rows
150
- query = query.group(table[:id])
151
-
152
- query
153
- end
154
-
155
- # Find by dynamic columns
156
- def self.dynamic_where(*args)
157
- field_scope = args[0].is_a?(Hash) ? nil : args[0]
158
- options = args.extract_options!
159
-
160
- field_scope_id = (!field_scope.nil?) ? field_scope.id : nil
161
- field_scope_type = (!field_scope.nil?) ? field_scope.class.name.constantize.to_s : nil
162
- dynamic_type = self.name.constantize.to_s
163
-
164
- table = self.name.constantize.arel_table
165
- query = nil
166
-
167
- # Need to join on each of the keys we are performing where on
168
- options.each { |key, value|
169
- # Don't bother with empty values
170
- next if value.to_s.empty?
171
-
172
- column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_"+key.to_s)
173
- column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_"+key.to_s)
174
-
175
- # Join on the column with the key
176
- on_query = column_table[:key].eq(key)
177
- if !field_scope_type.nil?
178
- on_query = on_query.and(
179
- column_table[:field_scope_type].eq(field_scope_type)
180
- )
181
- end
182
- if !field_scope_id.nil?
183
- on_query = on_query.and(
184
- column_table[:field_scope_id].eq(field_scope_id)
185
- )
186
- end
187
-
188
- column_table_join_on = column_table
189
- .create_on(
190
- on_query
191
- )
192
-
193
- column_table_join = table.create_join(column_table, column_table_join_on)
194
- query = (query.nil?)? joins(column_table_join) : query.joins(column_table_join)
195
-
196
- # Join on all the data with the provided key
197
- column_table_datum_join_on = column_datum_table
198
- .create_on(
199
- column_datum_table[:owner_id].eq(table[:id]).and(
200
- column_datum_table[:owner_type].eq(dynamic_type)
201
- ).and(
202
- column_datum_table[:dynamic_column_id].eq(column_table[:id])
203
- ).and(
204
- column_datum_table[:value].matches("%"+value+"%")
205
- )
206
- )
207
-
208
- column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on)
209
- query = query.joins(column_table_datum_join)
210
- }
211
- # Group required - we have many rows
212
- query = (query.nil?)? group(table[:id]) : query.group(table[:id])
213
-
214
- query
215
- end
216
-
217
- def as_json(*args)
218
- json = super(*args)
219
- options = args.extract_options!
220
-
221
- @@has_dynamic_columns_configurations.each { |config|
222
- if !options[:root].nil?
223
- json[options[:root]][config[:as].to_s] = self.send(config[:as].to_s)
224
- else
225
- json[config[:as].to_s] = self.send(config[:as].to_s)
226
- end
227
- }
228
-
229
- json
230
- end
231
-
232
- # Setter for dynamic field data
233
- def #{configuration[:as]}=data
234
- data.each_pair { |key, value|
235
- # We dont play well with this key
236
- if !self.storable_#{configuration[:as].to_s.singularize}_key?(key)
237
- raise NoMethodError
238
- end
239
- dynamic_column = self.#{configuration[:as].to_s.singularize}_key_to_dynamic_column(key)
240
-
241
- # We already have this key in database
242
- if existing = self.activerecord_dynamic_column_data.select { |i| i.dynamic_column == dynamic_column }.first
243
- existing.value = value
244
- else
245
- self.activerecord_dynamic_column_data.build(:dynamic_column => dynamic_column, :value => value)
246
- end
247
- }
248
- end
249
-
250
- def #{configuration[:as]}
251
- h = {}
252
- self.field_scope_#{configuration[:as]}.each { |i|
253
- h[i.key] = nil
254
- }
255
-
256
- self.activerecord_dynamic_column_data.each { |i|
257
- h[i.dynamic_column.key] = i.value unless !i.dynamic_column || !h.has_key?(i.dynamic_column.key)
258
- }
259
-
260
- h
261
- end
262
-
263
- def #{configuration[:as].to_s.singularize}_keys
264
- self.field_scope_#{configuration[:as]}.collect { |i| i.key }
265
- end
266
-
267
- def field_scope_#{configuration[:as]}
268
- # has_many relationship
269
- if self.get_#{configuration[:as]}_field_scope.respond_to?(:select) && self.get_#{configuration[:as]}_field_scope.respond_to?(:collect)
270
- self.get_#{configuration[:as]}_field_scope.collect { |i|
271
- i.send("activerecord_dynamic_columns")
272
- }.flatten.select { |i|
273
- i.dynamic_type.to_s.empty? || i.dynamic_type.to_s == self.class.to_s
274
- }
275
- # belongs_to relationship
276
- else
277
- self.get_#{configuration[:as]}_field_scope.send("activerecord_dynamic_columns").select { |i|
278
- # Only get things with no dynamic type defined or dynamic types defined as this class
279
- i.dynamic_type.to_s.empty? || i.dynamic_type.to_s == self.class.to_s
280
- }
281
- end
282
- end
283
-
284
- protected
285
- def get_#{configuration[:as]}_field_scope
286
- #{configuration[:field_scope]}
287
- end
288
-
289
- # Whether this is storable
290
- def storable_#{configuration[:as].to_s.singularize}_key?(key)
291
- self.#{configuration[:as].to_s.singularize}_keys.include?(key.to_s)
292
- end
293
-
294
- # Figures out which dynamic_column has which key
295
- def #{configuration[:as].to_s.singularize}_key_to_dynamic_column(key)
296
- found = nil
297
- if record = self.send('field_scope_#{configuration[:as]}').select { |i| i.key == key.to_s }.first
298
- found = record
299
- end
300
- found
301
- end
302
- EOV
303
- end
304
- end
305
-
306
- module InstanceMethods
307
- end
308
- end
309
16
  end
310
17
 
311
18
  if defined?(Rails::Railtie)
@@ -313,9 +20,13 @@ if defined?(Rails::Railtie)
313
20
  initializer 'has_dynamic_columns.insert_into_active_record' do
314
21
  ActiveSupport.on_load :active_record do
315
22
  ActiveRecord::Base.send(:include, HasDynamicColumns::Model)
23
+ ActiveRecord::Relation.send(:include, HasDynamicColumns::ActiveRecord::Relation)
24
+ ActiveRecord::QueryMethods.send(:include, HasDynamicColumns::ActiveRecord::QueryMethods)
316
25
  end
317
26
  end
318
27
  end
319
- else
320
- ActiveRecord::Base.send(:include, HasDynamicColumns::Model) if defined?(ActiveRecord)
28
+ elsif defined?(ActiveRecord)
29
+ ActiveRecord::Base.send(:include, HasDynamicColumns::Model)
30
+ ActiveRecord::Relation.send(:include, HasDynamicColumns::ActiveRecord::Relation)
31
+ ActiveRecord::QueryMethods.send(:include, HasDynamicColumns::ActiveRecord::QueryMethods)
321
32
  end