acts_as_scd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +43 -0
  4. data/Rakefile +32 -0
  5. data/lib/acts_as_scd/base_class_methods.rb +101 -0
  6. data/lib/acts_as_scd/block_updater.rb +142 -0
  7. data/lib/acts_as_scd/class_methods.rb +240 -0
  8. data/lib/acts_as_scd/initialize.rb +114 -0
  9. data/lib/acts_as_scd/instance_methods.rb +105 -0
  10. data/lib/acts_as_scd/period.rb +135 -0
  11. data/lib/acts_as_scd/version.rb +3 -0
  12. data/lib/acts_as_scd.rb +31 -0
  13. data/lib/tasks/acts_as_scd_tasks.rake +4 -0
  14. data/test/acts_as_scd_test.rb +680 -0
  15. data/test/dummy/README.rdoc +28 -0
  16. data/test/dummy/Rakefile +6 -0
  17. data/test/dummy/app/assets/javascripts/application.js +13 -0
  18. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  19. data/test/dummy/app/controllers/application_controller.rb +5 -0
  20. data/test/dummy/app/helpers/application_helper.rb +2 -0
  21. data/test/dummy/app/models/association.rb +9 -0
  22. data/test/dummy/app/models/city.rb +19 -0
  23. data/test/dummy/app/models/commercial_delegate.rb +9 -0
  24. data/test/dummy/app/models/country.rb +29 -0
  25. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  26. data/test/dummy/bin/bundle +3 -0
  27. data/test/dummy/bin/rails +4 -0
  28. data/test/dummy/bin/rake +4 -0
  29. data/test/dummy/config/application.rb +23 -0
  30. data/test/dummy/config/boot.rb +5 -0
  31. data/test/dummy/config/database.yml +25 -0
  32. data/test/dummy/config/environment.rb +5 -0
  33. data/test/dummy/config/environments/development.rb +37 -0
  34. data/test/dummy/config/environments/production.rb +83 -0
  35. data/test/dummy/config/environments/test.rb +41 -0
  36. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  37. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  38. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  39. data/test/dummy/config/initializers/inflections.rb +16 -0
  40. data/test/dummy/config/initializers/mime_types.rb +4 -0
  41. data/test/dummy/config/initializers/session_store.rb +3 -0
  42. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  43. data/test/dummy/config/locales/en.yml +23 -0
  44. data/test/dummy/config/locales/scd.en.yml +7 -0
  45. data/test/dummy/config/routes.rb +56 -0
  46. data/test/dummy/config/secrets.yml +22 -0
  47. data/test/dummy/config.ru +4 -0
  48. data/test/dummy/db/test.sqlite3 +0 -0
  49. data/test/dummy/log/test.log +24444 -0
  50. data/test/dummy/public/404.html +67 -0
  51. data/test/dummy/public/422.html +67 -0
  52. data/test/dummy/public/500.html +66 -0
  53. data/test/dummy/public/favicon.ico +0 -0
  54. data/test/fixtures/cities.yml +95 -0
  55. data/test/fixtures/countries.yml +83 -0
  56. data/test/test_helper.rb +15 -0
  57. 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
@@ -0,0 +1,3 @@
1
+ module ActsAsScd
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :acts_as_scd do
3
+ # # Task goes here
4
+ # end