has_dynamic_columns 0.1.1 → 0.2.0

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