has_dynamic_columns 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5e48635b860c7cac4c800461dab2a70ee3abc71a
4
+ data.tar.gz: a8bcdec82cd09f2c0d6e78b3b79c5c75c2954c14
5
+ SHA512:
6
+ metadata.gz: a97dd9c49f2551362311091c7e66137e7baf7fd21d6f2c0fd24fa894c39ce34f32f0cffa98bc616ddb9305bd227b1b61661ac29d46e0da50544cbb3c6199490d
7
+ data.tar.gz: fda55ebb312d5a7ce6b65494c6fe70999245a48e639a26e046b14b899869eb749434f4932e07f1a1898fa1dc4829ae6c88bcbb56b3a7c81c4cf53df2b604e011
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ # .rspec
2
+ --format documentation --color --fail-fast
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rake'
4
+
5
+ group :test do
6
+ if ENV['RAILS_VERSION'] == 'edge'
7
+ gem 'activerecord', :github => 'rails/rails'
8
+ else
9
+ gem 'activerecord', (ENV['RAILS_VERSION'] || ['>= 3.0', '< 5.0'])
10
+ end
11
+
12
+ gem 'coveralls', :require => false
13
+ gem 'rspec', '>= 3'
14
+ gem 'rubocop', '>= 0.25'
15
+ end
16
+
17
+ # Specify your gem's dependencies in has_dynamic_columns.gemspec
18
+ gemspec
19
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Butch Marshall
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ has_dynamic_columns
2
+ ============
3
+
4
+ Add dynamic columns to ActiveRecord models
5
+
6
+ Installation
7
+ ============
8
+
9
+ ```ruby
10
+ gem 'has_dynamic_columns', :git => 'git://github.com/butchmarshall/has_dynamic_columns.git'
11
+ ```
12
+
13
+ The Active Record migration is required to create the has_dynamic_columns table. You can create that table by
14
+ running the following command:
15
+
16
+ rails generate has_dynamic_columns:active_record
17
+ rake db:migrate
18
+
19
+ Usage
20
+ ============
21
+
22
+ ```ruby
23
+ class Account < ActiveRecord::Base
24
+ has_many :customers
25
+ has_dynamic_columns
26
+ end
27
+
28
+ class Customer < ActiveRecord::Base
29
+ belongs_to :account
30
+ has_many :customer_addresses
31
+ has_dynamic_columns field_scope: "account", as: "fields"
32
+ end
33
+
34
+ class CustomerAddress < ActiveRecord::Base
35
+ belongs_to :customer
36
+ has_dynamic_columns field_scope: "customer.account"
37
+ end
38
+ ```
39
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "rspec/core/rake_task"
2
+ require "bundler/gem_tasks"
3
+
4
+ # Default directory to look in is `/specs`
5
+ # Run with `rake spec`
6
+ RSpec::Core::RakeTask.new(:spec) do |task|
7
+ task.rspec_opts = ['--color', '--format', 'nested']
8
+ end
9
+
10
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'has_dynamic_columns/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "has_dynamic_columns"
8
+ spec.version = HasDynamicColumns::VERSION
9
+ spec.authors = ["Butch Marshall"]
10
+ spec.email = ["butch.a.marshall@gmail.com"]
11
+ spec.summary = "Dynamic fields for activerecord models"
12
+ spec.description = "Adds ability to put dynamic fields into active record models"
13
+ spec.homepage = "https://github.com/butchmarshall/has_dynamic_columns"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activerecord", [">= 3.0", "< 5.0"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ end
@@ -0,0 +1,22 @@
1
+ require "generators/has_dynamic_columns/has_dynamic_columns_generator"
2
+ require "generators/has_dynamic_columns/next_migration_version"
3
+ require "rails/generators/migration"
4
+ require "rails/generators/active_record"
5
+
6
+ # Extend the HasDynamicColumnsGenerator so that it creates an AR migration
7
+ module HasDynamicColumns
8
+ class ActiveRecordGenerator < ::HasDynamicColumnsGenerator
9
+ include Rails::Generators::Migration
10
+ extend NextMigrationVersion
11
+
12
+ source_paths << File.join(File.dirname(__FILE__), "templates")
13
+
14
+ def create_migration_file
15
+ migration_template "migration.rb", "db/migrate/has_dynamic_columns.rb"
16
+ end
17
+
18
+ def self.next_migration_number(dirname)
19
+ ActiveRecord::Generators::Base.next_migration_number dirname
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ require 'rails/generators/base'
2
+ require 'has_dynamic_columns/compatibility'
3
+
4
+ class HasDynamicColumnsGenerator < Rails::Generators::Base
5
+ source_paths << File.join(File.dirname(__FILE__), 'templates')
6
+ end
@@ -0,0 +1,14 @@
1
+ module HasDynamicColumns
2
+ module NextMigrationVersion
3
+ # while methods have moved around this has been the implementation
4
+ # since ActiveRecord 3.0
5
+ def next_migration_number(dirname)
6
+ next_migration_number = current_migration_number(dirname) + 1
7
+ if ActiveRecord::Base.timestamped_migrations
8
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), format("%.14d", next_migration_number)].max
9
+ else
10
+ format("%.3d", next_migration_number)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ class CreateHasDynamicColumns < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :dynamic_columns do |t|
4
+ t.integer :field_scope_id
5
+ t.string :field_scope_type
6
+
7
+ t.string :dynamic_type
8
+ t.string :key
9
+ t.string :data_type
10
+
11
+ t.timestamps
12
+ end
13
+ add_index(:dynamic_columns, [:field_scope_id, :field_scope_type, :dynamic_type], name: 'index1')
14
+ create_table :dynamic_column_validations do |t|
15
+ t.integer :dynamic_column_id
16
+
17
+ t.string :error
18
+ t.string :regexp
19
+
20
+ t.timestamps
21
+ end
22
+ create_table :dynamic_column_options do |t|
23
+ t.integer :dynamic_column_id
24
+ t.string :key
25
+
26
+ t.timestamps
27
+ end
28
+ create_table :dynamic_column_data do |t|
29
+ t.string :owner_type
30
+ t.integer :owner_id
31
+ t.integer :dynamic_column_id
32
+ t.integer :dynamic_column_option_id
33
+ t.string :value
34
+
35
+ t.timestamps
36
+ end
37
+ add_index(:dynamic_column_data, [:owner_id, :owner_type, :dynamic_column_id], name: 'index2')
38
+ end
39
+
40
+ def self.down
41
+ drop_table :dynamic_columns
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ require 'active_support/version'
2
+
3
+ module HasDynamicColumns
4
+ module Compatibility
5
+ if ActiveSupport::VERSION::MAJOR >= 4
6
+ require 'active_support/proxy_object'
7
+
8
+ def self.executable_prefix
9
+ 'bin'
10
+ end
11
+
12
+ def self.proxy_object_class
13
+ ActiveSupport::ProxyObject
14
+ end
15
+ else
16
+ require 'active_support/basic_object'
17
+
18
+ def self.executable_prefix
19
+ 'script'
20
+ end
21
+
22
+ def self.proxy_object_class
23
+ ActiveSupport::BasicObject
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ module HasDynamicColumns
2
+ class DynamicColumn < ActiveRecord::Base
3
+ has_many :dynamic_column_options, :class_name => "HasDynamicColumns::DynamicColumnOption"
4
+ has_many :dynamic_column_validations, :class_name => "HasDynamicColumns::DynamicColumnValidation"
5
+ end
6
+ end
@@ -0,0 +1,60 @@
1
+ module HasDynamicColumns
2
+ class DynamicColumnDatum < ActiveRecord::Base
3
+ belongs_to :dynamic_column, :class_name => "HasDynamicColumns::DynamicColumn"
4
+ belongs_to :dynamic_column_option, :class_name => "HasDynamicColumns::DynamicColumnOption"
5
+ belongs_to :owner, :polymorphic => true
6
+
7
+ # Get value based on dynamic_column data_type
8
+ def value
9
+ if self.dynamic_column
10
+ case self.dynamic_column.data_type
11
+ when "list"
12
+ if self.dynamic_column_option
13
+ self.dynamic_column_option.key
14
+ end
15
+ when "datetime"
16
+ self[:value]
17
+ when "boolean"
18
+ (self[:value] == 1)
19
+ when "integer"
20
+ self[:value]
21
+ when "date"
22
+ self[:value]
23
+ when "string"
24
+ self[:value]
25
+ end
26
+ else
27
+ self[:value]
28
+ end
29
+ end
30
+
31
+ # Set value base don dynamic_column data_type
32
+ def value=v
33
+ if self.dynamic_column
34
+ case self.dynamic_column.data_type
35
+ when "list"
36
+ # Can only set the value to one of the option values
37
+ if option = self.dynamic_column.dynamic_column_options.select { |i| i.key == v }.first
38
+ self.dynamic_column_option = option
39
+ else
40
+ # Hacky, -1 indicates to the validator that an invalid option was set
41
+ self.dynamic_column_option = nil
42
+ self.dynamic_column_option_id = (v.to_s.length > 0)? -1 : nil
43
+ end
44
+ when "datetime"
45
+ self[:value] = v
46
+ when "boolean"
47
+ self[:value] = v
48
+ when "integer"
49
+ self[:value] = v
50
+ when "date"
51
+ self[:value] = v
52
+ when "string"
53
+ self[:value] = v
54
+ end
55
+ else
56
+ self[:value] = v
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ module HasDynamicColumns
2
+ class DynamicColumnOption < ActiveRecord::Base
3
+ belongs_to :dynamic_column, :class_name => "HasDynamicColumns::DynamicColumn"
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module HasDynamicColumns
2
+ class DynamicColumnValidation < ActiveRecord::Base
3
+ belongs_to :dynamic_column, :class_name => "HasDynamicColumns::DynamicColumn"
4
+
5
+ def is_valid?(str)
6
+ matches = Regexp.new(self["regexp"]).match(str.to_s)
7
+
8
+ return !matches.nil?
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module HasDynamicColumns
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,280 @@
1
+ require "active_support"
2
+
3
+ require "has_dynamic_columns/version"
4
+ require "has_dynamic_columns/dynamic_column"
5
+ require "has_dynamic_columns/dynamic_column_option"
6
+ require "has_dynamic_columns/dynamic_column_validation"
7
+ require "has_dynamic_columns/dynamic_column_datum"
8
+
9
+ 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
+ include ::HasDynamicColumns::Model::InstanceMethods
26
+
27
+ has_many :activerecord_#{configuration[:as]},
28
+ class_name: "HasDynamicColumns::DynamicColumn",
29
+ as: :field_scope
30
+ has_many :activerecord_#{configuration[:as]}_data,
31
+ class_name: "HasDynamicColumns::DynamicColumnDatum",
32
+ as: :owner
33
+
34
+ # only add to attr_accessible
35
+ # if the class has some mass_assignment_protection
36
+ if defined?(accessible_attributes) and !accessible_attributes.blank?
37
+ #attr_accessible :#{configuration[:column]}
38
+ end
39
+
40
+ validate :validate_dynamic_column_data
41
+
42
+ public
43
+ # Order by dynamic columns
44
+ def self.dynamic_order(field_scope, key, direction = :asc)
45
+ table = self.name.constantize.arel_table
46
+ column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_order_"+key.to_s)
47
+ column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_order_data_"+key.to_s)
48
+
49
+ field_scope_type = field_scope.class.name.constantize.to_s
50
+ dynamic_type = self.name.constantize.to_s
51
+ field_scope_id = field_scope.id
52
+
53
+ # Join on the column with the key
54
+ column_table_join_on = column_table
55
+ .create_on(
56
+ column_table[:field_scope_type].eq(field_scope_type).and(
57
+ column_table[:dynamic_type].eq(dynamic_type)
58
+ ).and(
59
+ column_table[:field_scope_id].eq(field_scope_id)
60
+ ).and(
61
+ column_table[:key].eq(key)
62
+ )
63
+ )
64
+
65
+ column_table_join = table.create_join(column_table, column_table_join_on)
66
+ query = joins(column_table_join)
67
+
68
+ # Join on all the data with the provided key
69
+ column_table_datum_join_on = column_datum_table
70
+ .create_on(
71
+ column_datum_table[:owner_id].eq(table[:id]).and(
72
+ column_datum_table[:owner_type].eq(dynamic_type)
73
+ ).and(
74
+ column_datum_table[:dynamic_column_id].eq(column_table[:id])
75
+ )
76
+ )
77
+
78
+ column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on)
79
+ query = query.joins(column_table_datum_join)
80
+
81
+ # Order
82
+ query = query.order(column_datum_table[:value].send(direction))
83
+
84
+ # Group required - we have many rows
85
+ query = query.group(table[:id])
86
+
87
+ query
88
+ end
89
+
90
+ # Find by dynamic columns
91
+ def self.dynamic_where(*args)
92
+ field_scope = args[0]
93
+ options = args.extract_options!
94
+
95
+ field_scope_type = field_scope.class.name.constantize.to_s
96
+ dynamic_type = self.name.constantize.to_s
97
+ field_scope_id = field_scope.id
98
+
99
+ table = self.name.constantize.arel_table
100
+ query = nil
101
+
102
+ # Need to join on each of the keys we are performing where on
103
+ options.each { |key, value|
104
+ column_table = HasDynamicColumns::DynamicColumn.arel_table.alias("dynamic_where_"+key.to_s)
105
+ column_datum_table = HasDynamicColumns::DynamicColumnDatum.arel_table.alias("dynamic_where_data_"+key.to_s)
106
+
107
+ # Join on the column with the key
108
+ column_table_join_on = column_table
109
+ .create_on(
110
+ column_table[:field_scope_type].eq(field_scope_type).and(
111
+ column_table[:dynamic_type].eq(dynamic_type)
112
+ ).and(
113
+ column_table[:field_scope_id].eq(field_scope_id)
114
+ ).and(
115
+ column_table[:key].eq(key)
116
+ )
117
+ )
118
+
119
+ column_table_join = table.create_join(column_table, column_table_join_on)
120
+ query = (query.nil?)? joins(column_table_join) : query.join(column_table_join)
121
+
122
+ # Join on all the data with the provided key
123
+ column_table_datum_join_on = column_datum_table
124
+ .create_on(
125
+ column_datum_table[:owner_id].eq(table[:id]).and(
126
+ column_datum_table[:owner_type].eq(dynamic_type)
127
+ ).and(
128
+ column_datum_table[:dynamic_column_id].eq(column_table[:id])
129
+ ).and(
130
+ column_datum_table[:value].matches("%"+value+"%")
131
+ )
132
+ )
133
+
134
+ column_table_datum_join = table.create_join(column_datum_table, column_table_datum_join_on)
135
+ query = query.joins(column_table_datum_join)
136
+ }
137
+ # Group required - we have many rows
138
+ query = (query.nil?)? group(table[:id]) : query.group(table[:id])
139
+
140
+ query
141
+ end
142
+
143
+ def as_json(*args)
144
+ json = super(*args)
145
+ options = args.extract_options!
146
+
147
+ if !options[:root].nil?
148
+ json[options[:root]][self.dynamic_columns_as] = self.send(self.dynamic_columns_as)
149
+ else
150
+ json[self.dynamic_columns_as] = self.send(self.dynamic_columns_as)
151
+ end
152
+
153
+ json
154
+ end
155
+
156
+ # Setter for dynamic field data
157
+ def #{configuration[:as]}=data
158
+ data.each_pair { |key, value|
159
+ # We dont play well with this key
160
+ if !self.storable_#{configuration[:as].to_s.singularize}_key?(key)
161
+ raise NoMethodError
162
+ end
163
+ dynamic_column = self.#{configuration[:as].to_s.singularize}_key_to_dynamic_column(key)
164
+
165
+ # We already have this key in database
166
+ if existing = self.activerecord_#{configuration[:as]}_data.select { |i| i.dynamic_column == dynamic_column }.first
167
+ existing.value = value
168
+ else
169
+ self.activerecord_#{configuration[:as]}_data.build(:dynamic_column => dynamic_column, :value => value)
170
+ end
171
+ }
172
+ end
173
+
174
+ def #{configuration[:as]}
175
+ h = {}
176
+ self.field_scope_#{configuration[:as]}.each { |i|
177
+ h[i.key] = nil
178
+ }
179
+ self.activerecord_#{configuration[:as]}_data.each { |i|
180
+ h[i.dynamic_column.key] = i.value unless !i.dynamic_column
181
+ }
182
+ h
183
+ end
184
+
185
+ def #{configuration[:as].to_s.singularize}_keys
186
+ self.field_scope_#{configuration[:as]}.collect { |i| i.key }
187
+ end
188
+
189
+ def field_scope_#{configuration[:as]}
190
+ self.field_scope.send("activerecord_"+self.field_scope.dynamic_columns_as).select { |i|
191
+ # Only get things with no dynamic type defined or dynamic types defined as this class
192
+ i.dynamic_type.to_s.empty? || i.dynamic_type.to_s == self.class.to_s
193
+ }
194
+ end
195
+
196
+ def dynamic_columns_as
197
+ "#{configuration[:as].to_s}"
198
+ end
199
+
200
+ protected
201
+ # Whether this is storable
202
+ def storable_#{configuration[:as].to_s.singularize}_key?(key)
203
+ self.#{configuration[:as].to_s.singularize}_keys.include?(key.to_s)
204
+ end
205
+
206
+ # Figures out which dynamic_column has which key
207
+ def #{configuration[:as].to_s.singularize}_key_to_dynamic_column(key)
208
+ found = nil
209
+ if record = self.send("field_scope_"+self.dynamic_columns_as).select { |i| i.key == key.to_s }.first
210
+ found = record
211
+ end
212
+ found
213
+ end
214
+
215
+ def field_scope
216
+ #{configuration[:field_scope]}
217
+ end
218
+ EOV
219
+ end
220
+ end
221
+
222
+ module InstanceMethods
223
+ # Validate all the dynamic_column_data at once
224
+ def validate_dynamic_column_data
225
+ field_scope = self.field_scope
226
+
227
+ if field_scope
228
+ # All the fields defined on the parent model
229
+ dynamic_columns = field_scope.send("activerecord_#{field_scope.dynamic_columns_as}")
230
+
231
+ self.send("activerecord_#{self.dynamic_columns_as}_data").each { |dynamic_column_datum|
232
+ # Collect all validation errors
233
+ validation_errors = []
234
+
235
+ if dynamic_column_datum.dynamic_column_option_id == -1
236
+ validation_errors << "invalid_option"
237
+ end
238
+
239
+ # Find the dynamic_column defined for this datum
240
+ dynamic_column = nil
241
+ dynamic_columns.each { |i|
242
+ if i == dynamic_column_datum.dynamic_column
243
+ dynamic_column = i
244
+ break
245
+ end
246
+ }
247
+ # We have a dynamic_column - validate
248
+ if dynamic_column
249
+ dynamic_column.dynamic_column_validations.each { |validation|
250
+ if !validation.is_valid?(dynamic_column_datum.value.to_s)
251
+ validation_errors << validation.error
252
+ end
253
+ }
254
+ else
255
+ # No field found - this is probably bad - should we throw an error?
256
+ validation_errors << "not_found"
257
+ end
258
+
259
+ # If any errors exist - add them
260
+ if validation_errors.length > 0
261
+ errors.add(:dynamic_columns, { "#{dynamic_column.key}" => validation_errors })
262
+ end
263
+ }
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ if defined?(Rails::Railtie)
271
+ class Railtie < Rails::Railtie
272
+ initializer 'has_dynamic_columns.insert_into_active_record' do
273
+ ActiveSupport.on_load :active_record do
274
+ ActiveRecord::Base.send(:include, HasDynamicColumns::Model)
275
+ end
276
+ end
277
+ end
278
+ else
279
+ ActiveRecord::Base.send(:include, HasDynamicColumns::Model) if defined?(ActiveRecord)
280
+ end
@@ -0,0 +1,178 @@
1
+ require 'spec_helper'
2
+
3
+ describe HasDynamicColumns do
4
+ let (:account) do
5
+ account = Account.new(:name => "Account #1")
6
+
7
+ # Setup dynamic fields for Customer under this account
8
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "first_name", :data_type => "string")
9
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "last_name", :data_type => "string")
10
+ account.activerecord_dynamic_columns.build(:dynamic_type => "Customer", :key => "email", :data_type => "string")
11
+
12
+ # Setup dynamic fields for CustomerAddress under this account
13
+ account.activerecord_dynamic_columns.build(:dynamic_type => "CustomerAddress", :key => "address_1", :data_type => "string")
14
+ account.activerecord_dynamic_columns.build(:dynamic_type => "CustomerAddress", :key => "address_2", :data_type => "string")
15
+
16
+ field = account.activerecord_dynamic_columns.build(:dynamic_type => "CustomerAddress", :key => "country", :data_type => "list")
17
+ field.dynamic_column_options.build(:key => "canada")
18
+ field.dynamic_column_options.build(:key => "usa")
19
+ field.dynamic_column_options.build(:key => "mexico")
20
+
21
+ field = account.activerecord_dynamic_columns.build(:dynamic_type => "CustomerAddress", :key => "city", :data_type => "list")
22
+ field.dynamic_column_options.build(:key => "toronto")
23
+ field.dynamic_column_options.build(:key => "alberta")
24
+ field.dynamic_column_options.build(:key => "vancouver")
25
+
26
+ field = account.activerecord_dynamic_columns.build(:dynamic_type => "CustomerAddress", :key => "province", :data_type => "list")
27
+ field.dynamic_column_options.build(:key => "ontario")
28
+ field.dynamic_column_options.build(:key => "quebec")
29
+
30
+ field = account.activerecord_dynamic_columns.build(:dynamic_type => "CustomerAddress", :key => "postal_code", :data_type => "string")
31
+ field.dynamic_column_validations.build(:regexp => "^[^$]+$", :error => "blank")
32
+ field.dynamic_column_validations.build(:regexp => "^[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJKLMNPRSTVWXYZ]( )?\\d[ABCEGHJKLMNPRSTVWXYZ]\\d$", :error => "invalid_format")
33
+
34
+ account
35
+ end
36
+
37
+ describe Customer do
38
+ subject(:customer) { Customer.new(:account => account) }
39
+ before do
40
+ customer.fields = {
41
+ "first_name" => "Butch",
42
+ "last_name" => "Marshall",
43
+ "email" => "butch.a.marshall@gmail.com",
44
+ }
45
+ end
46
+
47
+ context 'when it is valid' do
48
+ it 'should not find john' do
49
+ c = customer
50
+ c.save
51
+ a = c.account
52
+
53
+ expect(a.customers.dynamic_where(a, { first_name: "John" }).length).to eq(0)
54
+ end
55
+ it 'should find me' do
56
+ c = customer
57
+ c.save
58
+ a = c.account
59
+
60
+ expect(a.customers.dynamic_where(a, { first_name: "Butch" }).length).to eq(1)
61
+ end
62
+ it 'should return fields as json' do
63
+ json = customer.as_json(:root => "customer")
64
+
65
+ expect(json["customer"]["fields"]).to eq({
66
+ "first_name" => "Butch",
67
+ "last_name" => "Marshall",
68
+ "email" => "butch.a.marshall@gmail.com",
69
+ })
70
+ end
71
+ end
72
+
73
+ describe CustomerAddress do
74
+ subject(:customer_address) { CustomerAddress.new(:customer => customer) }
75
+
76
+ context 'when it has partial data' do
77
+ before do
78
+ customer_address.fields = {
79
+ "country" => "canada",
80
+ "province" => "ontario",
81
+ "city" => "toronto",
82
+ "postal_code" => "H0H0H0",
83
+ }
84
+ end
85
+
86
+ it 'should return nil for unset fields' do
87
+ json = customer_address.as_json(:root => "customer_address")
88
+
89
+ expect(json["customer_address"]["fields"]).to eq({
90
+ "address_1" => nil,
91
+ "address_2" => nil,
92
+ "country" => "canada",
93
+ "province" => "ontario",
94
+ "city" => "toronto",
95
+ "postal_code" => "H0H0H0",
96
+ })
97
+ end
98
+ end
99
+
100
+ context 'when it is valid' do
101
+ before do
102
+ customer_address.fields = {
103
+ "address_1" => "555 Bloor Street",
104
+ "country" => "canada",
105
+ "province" => "ontario",
106
+ "city" => "toronto",
107
+ "postal_code" => "H0H0H0",
108
+ }
109
+ end
110
+
111
+ it 'should return parent customer fields as json' do
112
+ json = customer_address.customer.as_json(:root => "customer")
113
+
114
+ expect(json["customer"]["fields"]).to eq({
115
+ "first_name" => "Butch",
116
+ "last_name" => "Marshall",
117
+ "email" => "butch.a.marshall@gmail.com",
118
+ })
119
+ end
120
+
121
+ it 'should return fields as json' do
122
+ json = customer_address.as_json(:root => "customer_address")
123
+ expect(json["customer_address"]["fields"]).to eq({
124
+ "address_1" => "555 Bloor Street",
125
+ "address_2" => nil,
126
+ "country" => "canada",
127
+ "province" => "ontario",
128
+ "city" => "toronto",
129
+ "postal_code" => "H0H0H0",
130
+ })
131
+ end
132
+
133
+ it 'should validate' do
134
+ expect(customer_address).to be_valid
135
+ end
136
+
137
+ it 'should save successfully' do
138
+ sub = customer_address
139
+ expect(sub.save).to eq(true)
140
+ end
141
+
142
+ it 'should should retrieve properly from the database' do
143
+ sub = customer_address
144
+ sub.save
145
+
146
+ customer = CustomerAddress.find(sub.id)
147
+ json = customer.as_json(:root => "customer_address")
148
+
149
+ expect(json["customer_address"]["fields"]).to eq({
150
+ "address_1" => "555 Bloor Street",
151
+ "address_2" => nil,
152
+ "country" => "canada",
153
+ "province" => "ontario",
154
+ "city" => "toronto",
155
+ "postal_code" => "H0H0H0",
156
+ })
157
+ end
158
+ end
159
+
160
+ context 'when it is invalid' do
161
+ before do
162
+ customer_address.fields = {
163
+ "address_1" => "555 Bloor Street",
164
+ "address_2" => nil,
165
+ "country" => "canadaaaaa",
166
+ "province" => "ontario",
167
+ "city" => "toronto",
168
+ "postal_code" => "H0H0H",
169
+ }
170
+ end
171
+
172
+ it 'should not validate' do
173
+ expect(customer_address).to_not be_valid
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,77 @@
1
+ require 'logger'
2
+ require 'rspec'
3
+
4
+ require 'active_support/dependencies'
5
+ require 'active_record'
6
+
7
+ require 'has_dynamic_columns'
8
+
9
+ if ENV['DEBUG_LOGS']
10
+
11
+ else
12
+
13
+ end
14
+ ENV['RAILS_ENV'] = 'test'
15
+
16
+ # Trigger AR to initialize
17
+ ActiveRecord::Base
18
+
19
+ module Rails
20
+ def self.root
21
+ '.'
22
+ end
23
+ end
24
+
25
+ # Add this directory so the ActiveSupport autoloading works
26
+ ActiveSupport::Dependencies.autoload_paths << File.dirname(__FILE__)
27
+
28
+ # Used to test interactions between DJ and an ORM
29
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
30
+ ActiveRecord::Migration.verbose = false
31
+
32
+ require "generators/has_dynamic_columns/templates/migration"
33
+ ActiveRecord::Schema.define do
34
+ CreateHasDynamicColumns.up
35
+
36
+ create_table :accounts, force: true do |t|
37
+ t.string :name
38
+ t.timestamps
39
+ end
40
+ create_table :customers, force: true do |t|
41
+ t.string :name
42
+ t.integer :account_id
43
+ t.timestamps
44
+ end
45
+ create_table :customer_addresses, force: true do |t|
46
+ t.string :name
47
+ t.integer :customer_id
48
+ t.timestamps
49
+ end
50
+ end
51
+
52
+ class Account < ActiveRecord::Base
53
+ has_many :customers
54
+ has_dynamic_columns
55
+ end
56
+
57
+ class Customer < ActiveRecord::Base
58
+ belongs_to :account
59
+ has_many :customer_addresses
60
+ has_dynamic_columns field_scope: "account", dynamic_type: "Customer", as: "fields"
61
+ end
62
+
63
+ class CustomerAddress < ActiveRecord::Base
64
+ belongs_to :customer
65
+ has_dynamic_columns field_scope: "customer.account", dynamic_type: "CustomerAddress", as: "fields"
66
+ end
67
+
68
+
69
+ RSpec.configure do |config|
70
+ config.after(:each) do
71
+
72
+ end
73
+
74
+ config.expect_with :rspec do |c|
75
+ c.syntax = :expect
76
+ end
77
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_dynamic_columns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Butch Marshall
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.7'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.7'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '10.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '10.0'
61
+ description: Adds ability to put dynamic fields into active record models
62
+ email:
63
+ - butch.a.marshall@gmail.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - ".gitignore"
69
+ - ".rspec"
70
+ - Gemfile
71
+ - LICENSE.txt
72
+ - README.md
73
+ - Rakefile
74
+ - has_dynamic_columns.gemspec
75
+ - lib/generators/has_dynamic_columns/active_record_generator.rb
76
+ - lib/generators/has_dynamic_columns/has_dynamic_columns_generator.rb
77
+ - lib/generators/has_dynamic_columns/next_migration_version.rb
78
+ - lib/generators/has_dynamic_columns/templates/migration.rb
79
+ - lib/has_dynamic_columns.rb
80
+ - lib/has_dynamic_columns/compatibility.rb
81
+ - lib/has_dynamic_columns/dynamic_column.rb
82
+ - lib/has_dynamic_columns/dynamic_column_datum.rb
83
+ - lib/has_dynamic_columns/dynamic_column_option.rb
84
+ - lib/has_dynamic_columns/dynamic_column_validation.rb
85
+ - lib/has_dynamic_columns/version.rb
86
+ - spec/has_dynamic_columns_spec.rb
87
+ - spec/spec_helper.rb
88
+ homepage: https://github.com/butchmarshall/has_dynamic_columns
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.4.8
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Dynamic fields for activerecord models
112
+ test_files:
113
+ - spec/has_dynamic_columns_spec.rb
114
+ - spec/spec_helper.rb