acts_as_scd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +43 -0
- data/Rakefile +32 -0
- data/lib/acts_as_scd/base_class_methods.rb +101 -0
- data/lib/acts_as_scd/block_updater.rb +142 -0
- data/lib/acts_as_scd/class_methods.rb +240 -0
- data/lib/acts_as_scd/initialize.rb +114 -0
- data/lib/acts_as_scd/instance_methods.rb +105 -0
- data/lib/acts_as_scd/period.rb +135 -0
- data/lib/acts_as_scd/version.rb +3 -0
- data/lib/acts_as_scd.rb +31 -0
- data/lib/tasks/acts_as_scd_tasks.rake +4 -0
- data/test/acts_as_scd_test.rb +680 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/association.rb +9 -0
- data/test/dummy/app/models/city.rb +19 -0
- data/test/dummy/app/models/commercial_delegate.rb +9 -0
- data/test/dummy/app/models/country.rb +29 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +83 -0
- data/test/dummy/config/environments/test.rb +41 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/locales/scd.en.yml +7 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +24444 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/fixtures/cities.yml +95 -0
- data/test/fixtures/countries.yml +83 -0
- data/test/test_helper.rb +15 -0
- metadata +185 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
module ActsAsScd
|
2
|
+
|
3
|
+
# Internal value to represent the start of time
|
4
|
+
START_OF_TIME = 0
|
5
|
+
# Internal value to represent the end of time
|
6
|
+
END_OF_TIME = 99999999
|
7
|
+
|
8
|
+
# TODO: paremeterize the column names
|
9
|
+
|
10
|
+
# Column that represents the identity of an entity
|
11
|
+
IDENTITY_COLUMN = :identity
|
12
|
+
# Column that represents start of an iteration's life
|
13
|
+
START_COLUMN = :effective_from
|
14
|
+
# Column that represents end of an iteration's life
|
15
|
+
END_COLUMN = :effective_to
|
16
|
+
|
17
|
+
def self.initialize_scd(model)
|
18
|
+
model.extend ClassMethods
|
19
|
+
|
20
|
+
# Current iterations
|
21
|
+
model.scope :current, ->{model.where("#{model.effective_to_column_sql} = :date", :date=>END_OF_TIME)}
|
22
|
+
model.scope :initial, ->{model.where("#{model.effective_from_column_sql} = :date", :date=>START_OF_TIME)}
|
23
|
+
# Iterations effective at given date
|
24
|
+
# Note that since Array has an 'at' method, this cannot be applied directly to
|
25
|
+
# associations (the Array method would be used after generating an Array from the query).
|
26
|
+
# It is necessary to use .scoped.at(...) for associations.
|
27
|
+
model.scope :at, ->(date=nil){
|
28
|
+
# TODO: consider renaming this to current_at or active_at to avoid having to use
|
29
|
+
# scoped with associations
|
30
|
+
if date.present?
|
31
|
+
model.where(%{#{model.effective_from_column_sql}<=:date AND #{model.effective_to_column_sql}>:date}, :date=>model.effective_date(date))
|
32
|
+
else
|
33
|
+
model.current
|
34
|
+
end
|
35
|
+
}
|
36
|
+
# Iterations superseded/terminated
|
37
|
+
model.scope :ended, ->{model.where("#{model.effective_to_column_sql} < :date", :date=>END_OF_TIME)}
|
38
|
+
model.scope :earliest, ->(identity=nil){
|
39
|
+
if identity
|
40
|
+
identity_column = model.identity_column_sql('earliest_tmp')
|
41
|
+
if Array==identity
|
42
|
+
identity_list = identity.map{|i| model.connection.quote(i)}*','
|
43
|
+
where_condition = "WHERE #{identity_column} IN (#{identity_list})"
|
44
|
+
else
|
45
|
+
where_condition = "WHERE #{identity_column}=#{model.connection.quote(identity)}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
model.where(
|
49
|
+
%{(#{model.identity_column_sql}, #{model.effective_from_column_sql}) IN
|
50
|
+
(SELECT #{model.identity_column_sql('earliest_tmp')},
|
51
|
+
MIN(#{model.effective_from_column_sql('earliest_tmp')}) AS earliest_from
|
52
|
+
FROM #{model.table_name} AS "earliest_tmp"
|
53
|
+
#{where_condition}
|
54
|
+
GROUP BY #{model.identity_column_sql('earliest_tmp')})
|
55
|
+
}
|
56
|
+
)
|
57
|
+
}
|
58
|
+
# Latest iteration (terminated or current) of each identity
|
59
|
+
model.scope :latest, ->(identity=nil){
|
60
|
+
if identity
|
61
|
+
identity_column = model.identity_column_sql('latest_tmp')
|
62
|
+
if Array===identity
|
63
|
+
identity_list = identity.map{|i| model.connection.quote(i)}*','
|
64
|
+
where_condition = "WHERE #{identity_column} IN (#{identity_list})"
|
65
|
+
else
|
66
|
+
where_condition = "WHERE #{identity_column}=#{model.connection.quote(identity)}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
model.where(
|
70
|
+
%{(#{model.identity_column_sql}, #{model.effective_to_column_sql}) IN
|
71
|
+
(SELECT #{model.identity_column_sql('latest_tmp')},
|
72
|
+
MAX(#{model.effective_to_column_sql('latest_tmp')}) AS latest_to
|
73
|
+
FROM #{model.table_name} AS "latest_tmp"
|
74
|
+
#{where_condition}
|
75
|
+
GROUP BY #{model.identity_column_sql('latest_tmp')})
|
76
|
+
}
|
77
|
+
)
|
78
|
+
}
|
79
|
+
# Last superseded/terminated iterations
|
80
|
+
# model.scope :last_ended, ->{model.where(%{#{model.effective_to_column_sql} = (SELECT max(#{model.effective_to_column_sql('max_to_tmp')}) FROM "#{model.table_name}" AS "max_to_tmp" WHERE #{model.effective_to_column_sql('max_to_tmp')}<#{END_OF_TIME})})}
|
81
|
+
# last iterations of terminated identities
|
82
|
+
# model.scope :terminated, ->{model.where(%{#{model.effective_to_column_sql}<#{END_OF_TIME} AND #{model.effective_to_column_sql}=(SELECT max(#{model.effective_to_column_sql('max_to_tmp')}) FROM "#{model.table_name}" AS "max_to_tmp")})}
|
83
|
+
model.scope :terminated, ->(identity=nil){
|
84
|
+
where_condition = identity && " WHERE #{model.identity_column_sql('max_to_tmp')}=#{model.connection.quote(identity)} "
|
85
|
+
model.where(
|
86
|
+
%{#{model.effective_to_column_sql}<#{END_OF_TIME}
|
87
|
+
AND (#{model.identity_column_sql}, #{model.effective_to_column_sql}) IN
|
88
|
+
(SELECT #{model.identity_column_sql('max_to_tmp')},
|
89
|
+
max(#{model.effective_to_column_sql('max_to_tmp')})
|
90
|
+
FROM "#{model.table_name}" AS "max_to_tmp" #{where_condition})
|
91
|
+
}
|
92
|
+
)
|
93
|
+
}
|
94
|
+
# iterations superseded
|
95
|
+
model.scope :superseded, ->(identity=nil){
|
96
|
+
where_condition = identity && " AND #{model.identity_column_sql('max_to_tmp')}=#{model.connection.quote(identity)} "
|
97
|
+
model.where(
|
98
|
+
%{(#{model.identity_column_sql}, #{model.effective_to_column_sql}) IN
|
99
|
+
(SELECT #{model.identity_column_sql('max_to_tmp')},
|
100
|
+
max(#{model.effective_to_column_sql('max_to_tmp')})
|
101
|
+
FROM "#{model.table_name}" AS "max_to_tmp"
|
102
|
+
WHERE #{model.effective_to_column_sql('max_to_tmp')}<#{END_OF_TIME})
|
103
|
+
#{where_condition}
|
104
|
+
AND EXISTS (SELECT * FROM "#{model.table_name}" AS "ex_from_tmp"
|
105
|
+
WHERE #{model.effective_from_column_sql('ex_from_tmp')}==#{model.effective_to_column_sql})
|
106
|
+
}
|
107
|
+
)
|
108
|
+
}
|
109
|
+
model.before_validation :compute_identity
|
110
|
+
model.validates_uniqueness_of IDENTITY_COLUMN, :scope=>[START_COLUMN, END_COLUMN], :message=>"El periodo de vigencia no es válido"
|
111
|
+
model.before_destroy :remove_this_iteration
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module ActsAsScd
|
2
|
+
|
3
|
+
# TODO: replace identity by send(IDENTITY_COLUMN)...
|
4
|
+
|
5
|
+
def current
|
6
|
+
self.class.find_by_identity(identity)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initial
|
10
|
+
self.class.initial.where(IDENTITY_COLUMN=>identity).first
|
11
|
+
end
|
12
|
+
|
13
|
+
def at(date=nil)
|
14
|
+
if date.present?
|
15
|
+
self.class.find_by_identity(identity, date)
|
16
|
+
else
|
17
|
+
current
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def successor
|
22
|
+
return nil if effective_to==END_OF_TIME
|
23
|
+
self.class.where(identity:identity, effective_from:effective_to).first
|
24
|
+
end
|
25
|
+
|
26
|
+
def antecessor
|
27
|
+
return nil if effective_from==START_OF_TIME
|
28
|
+
self.class.where(identity:identity, effective_to:effective_from).first
|
29
|
+
end
|
30
|
+
|
31
|
+
def successors
|
32
|
+
return self.class.where('1=0') if effective_to==END_OF_TIME
|
33
|
+
self.class.where(identity:identity).where('effective_from>=:date', date: effective_to).reorder('effective_from')
|
34
|
+
end
|
35
|
+
|
36
|
+
def antecessors
|
37
|
+
return self.class.where('1=0') if effective_from==START_OF_TIME
|
38
|
+
self.class.where(identity:identity).where('effective_to<=:date', date: effective_from).reorder('effective_to')
|
39
|
+
end
|
40
|
+
|
41
|
+
def history
|
42
|
+
self.class.all_of(identity)
|
43
|
+
end
|
44
|
+
|
45
|
+
def latest
|
46
|
+
self.class.where(identity:identity).reorder('effective_to desc').limit(1).first
|
47
|
+
end
|
48
|
+
|
49
|
+
def earliest
|
50
|
+
self.class.where(identity:identity).reorder('effective_from asc').limit(1).first
|
51
|
+
end
|
52
|
+
|
53
|
+
def terminate_identity(finish=Date.today)
|
54
|
+
finish = self.class.effective_date(finish)
|
55
|
+
update_attributes END_COLUMN=>finish
|
56
|
+
end
|
57
|
+
|
58
|
+
def ended?
|
59
|
+
effective_to < END_OF_TIME
|
60
|
+
end
|
61
|
+
|
62
|
+
def ended_at?(date)
|
63
|
+
effective_to <= self.class.effective_date(date)
|
64
|
+
end
|
65
|
+
|
66
|
+
def effective_period
|
67
|
+
Period[effective_from, effective_to]
|
68
|
+
end
|
69
|
+
|
70
|
+
def effective_from_date
|
71
|
+
Period::DateValue[effective_from].to_date
|
72
|
+
end
|
73
|
+
|
74
|
+
def effective_to_date
|
75
|
+
Period::DateValue[effective_to].to_date
|
76
|
+
end
|
77
|
+
|
78
|
+
def initial?
|
79
|
+
effective_period.initial?
|
80
|
+
end
|
81
|
+
|
82
|
+
def current?
|
83
|
+
effective_period.current?
|
84
|
+
end
|
85
|
+
|
86
|
+
def past_limited?
|
87
|
+
effective_period.past_limited?
|
88
|
+
end
|
89
|
+
|
90
|
+
def future_limited?
|
91
|
+
effective_period.future_limited?
|
92
|
+
end
|
93
|
+
|
94
|
+
def limited?
|
95
|
+
effective_period.limited?
|
96
|
+
end
|
97
|
+
|
98
|
+
def remove_this_iteration
|
99
|
+
s = successor
|
100
|
+
s.update_attributes effective_from: self.effective_from if s
|
101
|
+
a = antecessor
|
102
|
+
a.update_attributes effective_to: self.effective_to if a
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module ActsAsScd
|
2
|
+
|
3
|
+
class Period
|
4
|
+
|
5
|
+
class DateValue
|
6
|
+
def initialize(d)
|
7
|
+
|
8
|
+
d = d.strftime('%Y%m%d') if d.respond_to?(:strftime)
|
9
|
+
if String===d && d =~ /\A(\d\d\d\d)-(\d\d)-(\d\d)\Z/
|
10
|
+
d = $1.to_i*10000 + $2.to_i*100 + $3.to_i
|
11
|
+
end
|
12
|
+
@value = d && d.to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :value
|
16
|
+
|
17
|
+
def to_date
|
18
|
+
begin
|
19
|
+
Date.new *parse
|
20
|
+
rescue
|
21
|
+
raise parse.inspect
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse
|
26
|
+
y = @value/10000
|
27
|
+
v = @value%10000
|
28
|
+
m = v/100
|
29
|
+
d = v%100
|
30
|
+
[y,m,d]
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
if @value==START_OF_TIME
|
35
|
+
''
|
36
|
+
elsif @value==END_OF_TIME
|
37
|
+
''
|
38
|
+
else
|
39
|
+
y,m,d = parse
|
40
|
+
I18n.l Date.new(y, m, d)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
include ModalSupport::BracketConstructor
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.date(date)
|
48
|
+
DateValue[date].value
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.date_to_s(date)
|
52
|
+
DateValue[date].to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :start, :end
|
56
|
+
def from
|
57
|
+
@start
|
58
|
+
end
|
59
|
+
def to
|
60
|
+
@end
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize(from, to)
|
64
|
+
@start = Period.date(from || START_OF_TIME)
|
65
|
+
@end = Period.date(to || END_OF_TIME)
|
66
|
+
end
|
67
|
+
|
68
|
+
include ModalSupport::StateEquivalent
|
69
|
+
|
70
|
+
def includes?(date)
|
71
|
+
date = Period.date(date)
|
72
|
+
@start <= date && date < @end
|
73
|
+
end
|
74
|
+
|
75
|
+
include ModalSupport::BracketConstructor
|
76
|
+
|
77
|
+
def to_s(options={})
|
78
|
+
if @start<=START_OF_TIME
|
79
|
+
if @end>=END_OF_TIME
|
80
|
+
options[:always] || I18n.t(:"scd.period.always") || '-'
|
81
|
+
else
|
82
|
+
"#{options[:until] || I18n.t(:"scd.period.until") || 'to'} #{Period.date_to_s(@end)}"
|
83
|
+
end
|
84
|
+
else
|
85
|
+
if @end>=END_OF_TIME
|
86
|
+
"#{options[:since] || I18n.t(:"scd.period.from") || 'since'} #{Period.date_to_s(@start)}"
|
87
|
+
else
|
88
|
+
[Period.date_to_s(@start), options[:between] || I18n.t(:"scd.period.between") || '-', Period.date_to_s(@end)].compact*' '
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def valid?
|
94
|
+
@start < @end
|
95
|
+
end
|
96
|
+
|
97
|
+
def empty?
|
98
|
+
@start >= @end
|
99
|
+
end
|
100
|
+
|
101
|
+
def past_limited?
|
102
|
+
@start > START_OF_TIME
|
103
|
+
end
|
104
|
+
|
105
|
+
def future_limited?
|
106
|
+
@end < END_OF_TIME
|
107
|
+
end
|
108
|
+
|
109
|
+
def limited?
|
110
|
+
past_limited? || future_limited?
|
111
|
+
end
|
112
|
+
|
113
|
+
def initial?
|
114
|
+
@start == START_OF_TIME
|
115
|
+
end
|
116
|
+
|
117
|
+
def current?
|
118
|
+
@end == END_OF_TIME
|
119
|
+
end
|
120
|
+
|
121
|
+
def reference_date
|
122
|
+
if @start <= START_OF_TIME
|
123
|
+
if @end >= END_OF_TIME
|
124
|
+
DateValue[Date.today].value
|
125
|
+
else
|
126
|
+
DateValue[DateValue[@end].to_date - 1].value
|
127
|
+
end
|
128
|
+
else
|
129
|
+
@start
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
data/lib/acts_as_scd.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'modalsupport'
|
2
|
+
require 'acts_as_scd/initialize'
|
3
|
+
require 'acts_as_scd/period'
|
4
|
+
require 'acts_as_scd/instance_methods'
|
5
|
+
require 'acts_as_scd/class_methods'
|
6
|
+
require 'acts_as_scd/base_class_methods'
|
7
|
+
require 'acts_as_scd/block_updater'
|
8
|
+
|
9
|
+
module ActsAsScd
|
10
|
+
|
11
|
+
begin
|
12
|
+
require 'rails'
|
13
|
+
|
14
|
+
class Railtie < Rails::Railtie
|
15
|
+
initializer 'acts_as_scd.insert_into_active_record' do
|
16
|
+
ActiveSupport.on_load :active_record do
|
17
|
+
# ActiveRecord::Base.send(:include, ActsAsScd)
|
18
|
+
ActiveRecord::Base.extend ActsAsScd::BaseClassMethods
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
rescue LoadError
|
23
|
+
# ActiveRecord::Base.send(:include, ActAsScd) if defined?(ActiveRecord)
|
24
|
+
ActiveRecord::Base.extend ActsAsScd::BaseClassMethods if defined?(ActiveRecord)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.included(model)
|
28
|
+
initialize_scd model
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|