has_dynamic_columns 0.0.1
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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/has_dynamic_columns.gemspec +25 -0
- data/lib/generators/has_dynamic_columns/active_record_generator.rb +22 -0
- data/lib/generators/has_dynamic_columns/has_dynamic_columns_generator.rb +6 -0
- data/lib/generators/has_dynamic_columns/next_migration_version.rb +14 -0
- data/lib/generators/has_dynamic_columns/templates/migration.rb +43 -0
- data/lib/has_dynamic_columns/compatibility.rb +27 -0
- data/lib/has_dynamic_columns/dynamic_column.rb +6 -0
- data/lib/has_dynamic_columns/dynamic_column_datum.rb +60 -0
- data/lib/has_dynamic_columns/dynamic_column_option.rb +5 -0
- data/lib/has_dynamic_columns/dynamic_column_validation.rb +11 -0
- data/lib/has_dynamic_columns/version.rb +3 -0
- data/lib/has_dynamic_columns.rb +280 -0
- data/spec/has_dynamic_columns_spec.rb +178 -0
- data/spec/spec_helper.rb +77 -0
- metadata +114 -0
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
data/.rspec
ADDED
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,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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|