acts_as_scd 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.
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