deja_vue 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/Manifest ADDED
@@ -0,0 +1,9 @@
1
+ Manifest
2
+ README.rdoc
3
+ Rakefile
4
+ deja_vue.gemspec
5
+ lib/deja_vue.rb
6
+ lib/deja_vue/has_deja_vue.rb
7
+ lib/deja_vue/history.rb
8
+ lib/deja_vue/version.rb
9
+ rails/init.rb
data/README.rdoc ADDED
@@ -0,0 +1,75 @@
1
+ = DejaVue
2
+
3
+ Yet another gem to store version of your models. But it uses mongodb as
4
+ backend. Based on PaperTrail (http://github.com/airblade/paper_trail/).
5
+
6
+ == Why choose DejaVue
7
+
8
+ The main goal of DejaVue is to keep track of associated models. It's
9
+ useful for a model whose relations change among time and to see how
10
+ it really looked like at a certain point.
11
+
12
+ There are 3 kinds of versioning: create, update, destroy
13
+ Differently from PaperTrail, each version will record the current model info.
14
+ So PapelTrail _create_ versions have reify == nil
15
+ DejaVue have _create_ version == model_version_when_creating
16
+
17
+ == Installing
18
+
19
+ $ bundle install
20
+ $ gem install gemcutter
21
+ $ gem build deja_vue.gemspec
22
+ $ gem install ./deja_vue-*.gem
23
+
24
+ == Beggining with DejaVue: Add to your model
25
+
26
+ Add a single line and your model will start beeing versionated:
27
+
28
+ def SomeModel < ActiveRecord::Base
29
+ has_deja_vue
30
+ end
31
+
32
+ Or, for example, use the ignore option:
33
+
34
+ def SomeModel < ActiveRecord::Base
35
+ has_deja_vue :ignore => :location
36
+ end
37
+
38
+
39
+ == More Options
40
+
41
+ You might pass some options, including:
42
+ :ignore | An Array of fields that will be ignored. If
43
+ | only those fields have changed, then no version
44
+ | will be created.
45
+ | has_deja_vue :ignore => :last_exported_on
46
+ :associations | Store associated models to be record with the model
47
+ | so it can be fully restored. Right now it works only
48
+ | with has_one / belongs_to relationships.
49
+ | Ex.:
50
+ | class Account < ActiveRecord::Base
51
+ | has_one :account_preference
52
+ |
53
+ | has_deja_vue :associations => [:account_preference]
54
+ | end
55
+ |
56
+ :extra_info_fields | Almost the same as the above option. But it handles
57
+ | more simple info (like strings, integers, floats).
58
+ | Useful to store a tag_list field or a counter cache
59
+ | Ex:
60
+ |
61
+ | class BlogPost < ActiveRecord::Base
62
+ | acts_as_taggable_on :tags
63
+ |
64
+ | has_deja_vue :extra_info_fields => [:tag_list]
65
+ | end
66
+ |
67
+ :who_did_it | a default value for record who_did_it when you
68
+ | are not in a request-response cycle (ex. in a job).
69
+ | Ex.:
70
+ | has_deja_vue :who_did_it => 'admin'
71
+ | Alternatively you are able to set the user right
72
+ | before save the model that will be versionated, using:
73
+ | DejaVue.who_did_it = 'otavio'
74
+ | SomeModel.save
75
+
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ begin
2
+ # Rspec 1.3.0
3
+ require 'spec/rake/spectask'
4
+ desc 'Default: run specs'
5
+ task :default => :spec
6
+ Spec::Rake::SpecTask.new do |t|
7
+ t.spec_files = FileList["spec/**/*_spec.rb"]
8
+ end
9
+
10
+ Spec::Rake::SpecTask.new('rcov') do |t|
11
+ t.spec_files = FileList["spec/**/*_spec.rb"]
12
+ t.rcov = true
13
+ t.rcov_opts = ['--exclude', 'spec']
14
+ end
15
+
16
+ rescue LoadError
17
+ puts "Rspec not available. Install it with: gem install rspec"
18
+ end
19
+
data/deja_vue.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $:.unshift lib unless $:.include?(lib)
4
+
5
+ require 'deja_vue/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = %q{deja_vue}
9
+ s.version = DejaVue::VERSION
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
12
+ s.authors = ["Riopro Informatica Ltda"]
13
+ s.date = %q{2010-08-11}
14
+ s.description = %q{DejaVue -> Ruby versioning Gem using MongoDB as backend}
15
+ s.email = %q{riopro@riopro.com.br}
16
+ s.extra_rdoc_files = ["README.rdoc", "lib/deja_vue.rb"]
17
+ s.files = ["Manifest", "README.rdoc", "Rakefile", "deja_vue.gemspec", "lib/deja_vue.rb", "lib/deja_vue/has_deja_vue.rb", "lib/deja_vue/history.rb", "rails/init.rb"]
18
+ s.homepage = %q{http://github.com/riopro/deja_vue}
19
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "deja_vue", "--main", "README.rdoc"]
20
+ s.require_paths = ["lib"]
21
+ s.rubyforge_project = %q{deja_vue}
22
+ s.rubygems_version = %q{1.3.7}
23
+ s.summary = %q{Keep track of your models changing history, using MongoDB as backend}
24
+
25
+ if s.respond_to? :specification_version then
26
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
27
+ s.specification_version = 3
28
+ end
29
+ end
data/lib/deja_vue.rb ADDED
@@ -0,0 +1,98 @@
1
+ require 'deja_vue/has_deja_vue'
2
+ require 'deja_vue/history'
3
+
4
+ # Based on PaperTrail (http://github.com/airblade/paper_trail/).
5
+ # Using:
6
+ # Add an initializer at config/initializers and require 'deja_vue'
7
+ # Add to your model:
8
+ # has_deja_vue
9
+ # You might pass some options, including:
10
+ # :ignore | An Array of fields that will be ignored. If
11
+ # | only those fields have changed, then no version
12
+ # | will be created.
13
+ # | has_deja_vue :ignore => :last_exported_on
14
+ # :associations | Store associated models to be record with the model
15
+ # | so it can be fully restored. Right now it works only
16
+ # | with has_one / belongs_to relationships.
17
+ # | Ex.:
18
+ # | class Account < ActiveRecord::Base
19
+ # | has_one :account_preference
20
+ # |
21
+ # | has_deja_vue :associations => [:account_preference]
22
+ # | end
23
+ # |
24
+ # :extra_info_fields | Almost the same as the above option. But it handles
25
+ # | more simple info (like strings, integers, floats).
26
+ # | Useful to store a tag_list field or a counter cache
27
+ # | Ex:
28
+ # |
29
+ # | class BlogPost < ActiveRecord::Base
30
+ # | acts_as_taggable_on :tags
31
+ # |
32
+ # | has_deja_vue :extra_info_fields => [:tag_list]
33
+ # | end
34
+ # |
35
+ # :who_did_it | a default value for record who_did_it when you
36
+ # | are not in a request-response cycle (ex. in a job).
37
+ # | Ex.:
38
+ # | has_deja_vue :who_did_it => 'admin'
39
+ # | Alternatively you are able to set the user right
40
+ # | before save the model that will be versionated, using:
41
+ # | DejaVue.who_did_it = 'otavio'
42
+ # | SomeModel.save
43
+ #
44
+ # There are 3 kinds of versioning: create, update, destroy
45
+ # Differently from PaperTrail, each version will record the current model info.
46
+ # So PapelTrail _create_ versions have reify == nil
47
+ # DejaVue have _create_ version == model_version_when_creating
48
+ module DejaVue
49
+
50
+ def self.included(base)
51
+ base.before_filter :set_deja_vue_user
52
+ end
53
+
54
+ def self.who_did_it
55
+ Thread.current[:who_did_it]
56
+ end
57
+
58
+ def self.who_did_it=(value)
59
+ Thread.current[:who_did_it] = value
60
+ end
61
+
62
+ # Used to set an user to be versionated, execute the block and
63
+ # then rollback to the thread's regular user.
64
+ # Example:
65
+ #
66
+ # @blog_post = BlogPost.find 10
67
+ # @blog_post.title = "new title"
68
+ # DejaVue.setting_user_as('someone') do
69
+ # @blog_post.save
70
+ # end
71
+ def self.setting_user_as(value, &block)
72
+ actual_user = Thread.current[:who_did_it]
73
+ Thread.current[:who_did_it] = value
74
+ yield if block_given?
75
+ Thread.current[:who_did_it] = actual_user
76
+ end
77
+
78
+ protected
79
+
80
+ # Returns the user who is responsible for any changes that occur.
81
+ # By default this calls `current_user` and returns the result.
82
+ #
83
+ # Override this method in your controller to call a different
84
+ # method, e.g. `current_person`, or anything you like.
85
+ def user_for_deja_vue
86
+ return current_user if current_user && current_user.is_a?(String)
87
+ current_user[:id] if current_user && current_user[:id]
88
+ end
89
+
90
+ private
91
+
92
+ # Verifies if current controller responds to current_user method
93
+ def set_deja_vue_user
94
+ Thread.current[:who_did_it] = user_for_deja_vue
95
+ end
96
+ end
97
+
98
+ ActionController::Base.send :include, DejaVue
@@ -0,0 +1,85 @@
1
+ module DejaVue
2
+
3
+ def self.included(base)
4
+ base.send :extend, ClassMethods
5
+ end
6
+
7
+
8
+ module ClassMethods
9
+ # Options:
10
+ # :ignore an array of attributes for which a new +Version+ will not be created if only they change.
11
+ def has_deja_vue(options = {})
12
+ send :include, InstanceMethods
13
+
14
+ cattr_accessor :deja_vue_options
15
+ options[:ignore] = options[:ignore].map &:to_s if options[:ignore]
16
+ self.deja_vue_options = options
17
+
18
+ cattr_accessor :deja_vue_active
19
+ self.deja_vue_active = true
20
+
21
+ before_save :check_for_version_changes
22
+ after_update :record_update_version
23
+ after_create :record_create_version
24
+ after_destroy :record_destroy_version
25
+
26
+ end
27
+
28
+ # FIXME: not implemented yet
29
+ def deja_vue_off
30
+ self.deja_vue_active = false
31
+ end
32
+
33
+ # FIXME: not implemented yet
34
+ def deja_vue_on
35
+ self.deja_vue_active = true
36
+ end
37
+ end
38
+
39
+ module InstanceMethods
40
+
41
+ # return array with changed fields after object has been saved
42
+ # (created or updated).
43
+ # Whe need this to validate if the changes must be recorded after save
44
+ def version_changes
45
+ @version_changes
46
+ end
47
+
48
+ # Search for all change history from the current object
49
+ def histories(extra_options={})
50
+ default_options = {:versionable_type => self.class.to_s, :versionable_id => self.id.to_s}
51
+ History.where(extra_options.merge(default_options)).sort(:created_at.desc).all
52
+ end
53
+
54
+ # Search for a change history from the current object by id
55
+ def history(param_id)
56
+ History.where(:versionable_type => self.class.to_s, :versionable_id => self.id.to_s, :id => param_id).first
57
+ end
58
+
59
+ private
60
+
61
+ def check_for_version_changes
62
+ @version_changes = self.changed
63
+ end
64
+
65
+ def versionate_as(kind_of_version)
66
+ History.versionate(self, kind_of_version, deja_vue_options.merge(:who_did_it => DejaVue.who_did_it)) if DejaVue.who_did_it
67
+ History.versionate(self, kind_of_version, deja_vue_options) unless DejaVue.who_did_it
68
+ end
69
+
70
+ def record_update_version
71
+ versionate_as("update")
72
+ end
73
+
74
+ def record_create_version
75
+ versionate_as("create")
76
+ end
77
+
78
+ def record_destroy_version
79
+ versionate_as("destroy")
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ ActiveRecord::Base.send :include, DejaVue
@@ -0,0 +1,184 @@
1
+ class History
2
+ include MongoMapper::Document
3
+
4
+ key :versionable_type, String, :required => true, :index => true
5
+ key :versionable_id, String, :required => true, :index => true
6
+ key :kind_of_version, String, :required => true
7
+ key :version_attributes, Hash, :required => true
8
+ key :version_associations,Hash, :index => true
9
+ key :extra_info, Hash # para armazenar informações relacionadas, como tag_list
10
+ key :changed_fields, Array
11
+ key :who_did_it, String
12
+ timestamps!
13
+ ensure_index :created_at
14
+
15
+
16
+ #
17
+ # Validations
18
+ #
19
+ validates_true_for :version_attributes, :logic => lambda { !version_attributes.empty? }
20
+
21
+ #
22
+ # Constants
23
+ #
24
+
25
+ DEFAULT_IGNORED_FIELDS = ["updated_at", "created_at"]
26
+
27
+ KIND_OF_VERSIONS = %w( create update destroy )
28
+
29
+ #
30
+ # Class Methods
31
+ #
32
+
33
+ def self.versionate(object, kind_of_version, options={})
34
+ return false if object.blank?
35
+ History.new.create_version(object, kind_of_version, options)
36
+ end
37
+
38
+ # marreta para teste rodar, por causa do i18n do remarkable, FIXME
39
+ def self.human_name(foo)
40
+ "History"
41
+ end
42
+
43
+ #
44
+ # Instance Methods
45
+ #
46
+
47
+ def create_version(object, kind_of_version, options={})
48
+ self.ignored_fields= options[:ignore] if options[:ignore]
49
+ self.who_did_it = check_and_retrieve_user(options[:who_did_it]) if options[:who_did_it]
50
+ self.kind_of_version = kind_of_version
51
+ self.versionable_type = object.class.name
52
+ self.versionable_id = object.id.to_s
53
+
54
+ unless self.kind_of_version == "destroy"
55
+ return false unless self.has_changed?(object)
56
+ end
57
+ self.version_attributes = attributes_filter(object.attributes)
58
+ store_version_associations object, options
59
+ store_extra_info(object, options)
60
+ self.save
61
+ end
62
+
63
+ # Rebuild the versionated object.
64
+ # Do not rebuild if it's not saved
65
+ def version
66
+ return nil if self.new_record?
67
+ @version ||= recreate_version_object
68
+ end
69
+
70
+ # fields that whose change won't cause a history trace
71
+ def ignored_fields
72
+ @ignored_fields || DEFAULT_IGNORED_FIELDS
73
+ end
74
+
75
+ def ignored_fields=(fields_array)
76
+ fields = nil
77
+ fields = fields_array if fields_array.is_a?(Array)
78
+ fields = [fields_array.to_s] if ( fields_array.is_a?(String) || fields_array.is_a?(Symbol) )
79
+ return false unless fields.is_a?(Array)
80
+ @ignored_fields = ( DEFAULT_IGNORED_FIELDS + fields ).uniq
81
+ end
82
+
83
+ # return true id object has changed
84
+ def has_changed?(object)
85
+ return false unless object.try(:version_changes)
86
+ !store_changed_fields(object).empty?
87
+ end
88
+
89
+ def next_version
90
+ History.where(:created_at.gte => self.created_at, # could have the same creation date, as long it's not the same object
91
+ :id.ne => self.id, # ne => not equal to
92
+ :versionable_type => self.versionable_type,
93
+ :versionable_id => self.versionable_id).sort(:created_at.asc).first
94
+ end
95
+
96
+ def previous_version
97
+ History.where(:created_at.lte => self.created_at, # could have the same creation date, as long it's not the same object
98
+ :id.ne => self.id, # ne => not equal to
99
+ :versionable_type => self.versionable_type,
100
+ :versionable_id => self.versionable_id).sort(:created_at.desc).first
101
+ end
102
+
103
+ protected
104
+
105
+ # Retrieve the versionated object
106
+ def recreate_version_object
107
+ model = recreate_model(self.versionable_type, self.version_attributes)
108
+ model.id = self.versionable_id
109
+
110
+ self.version_associations.each do |associated_object, associated_object_attributes|
111
+ model.try "#{associated_object.to_s}=", recreate_model(associated_object, associated_object_attributes)
112
+ end
113
+ update_fields model, self.extra_info
114
+ model
115
+ end
116
+
117
+ # recreates object from a class name and its attributes
118
+ def recreate_model(class_name, attributes_hash={})
119
+ associated_klass = class_name.to_s.camelize.constantize.new
120
+ update_fields associated_klass, attributes_hash
121
+ associated_klass
122
+ end
123
+
124
+ # updates values for an instantiated class
125
+ def update_fields(class_object, attributes_hash={})
126
+ attributes_hash.each do |key, value|
127
+ class_object.send "#{key.to_s}=", value
128
+ end
129
+ end
130
+
131
+ # If user is a Class, try to retrieve user
132
+ # from an Authlogic User Session.
133
+ # If it is an Authlogic object, return the user id.
134
+ #
135
+ # Else it returns the current object.
136
+ def check_and_retrieve_user(user)
137
+ if user.is_a? Class
138
+ begin
139
+ user.find.user.id
140
+ rescue
141
+ nil
142
+ end
143
+ else
144
+ user
145
+ end
146
+ end
147
+
148
+ def store_version_associations(object, options)
149
+ self.version_associations = {}
150
+ options[:associations].each do |association|
151
+ self.version_associations.store(association, attributes_filter( object.try(association).attributes) ) if object.try(association)
152
+ end if options[:associations]
153
+ true
154
+ end
155
+
156
+ def store_changed_fields(object)
157
+ self.changed_fields = object.version_changes - self.ignored_fields if self.ignored_fields.is_a?(Array)
158
+ self.changed_fields
159
+ end
160
+
161
+ def store_extra_info(object, options)
162
+ self.extra_info = {}
163
+ options[:extra_info_fields].each do |extra_info|
164
+ self.extra_info.store( extra_info, object.try(extra_info) ) if object.try(extra_info)
165
+ end if options[:extra_info_fields]
166
+ self.extra_info = attributes_filter(self.extra_info)
167
+ true
168
+ end
169
+
170
+ def attributes_filter(key_value={})
171
+ key_value.each do |k,v|
172
+ key_value[k]=v.to_s if key_value[k].is_a?(Date)
173
+ end
174
+ key_value.each do |k,v|
175
+ key_value[k]=v.utc.to_s if key_value[k].is_a?(Time)
176
+ end
177
+ key_value.each do |k,v|
178
+ key_value[k]=v.to_f if key_value[k].is_a?(BigDecimal)
179
+ end
180
+
181
+ key_value
182
+ end
183
+
184
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ config.after_initialize do
2
+ require 'deja_vue'
3
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: deja_vue
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 2
10
+ version: 0.1.2
11
+ platform: ruby
12
+ authors:
13
+ - Riopro Informatica Ltda
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-08-11 00:00:00 -03:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: DejaVue -> Ruby versioning Gem using MongoDB as backend
23
+ email: riopro@riopro.com.br
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README.rdoc
30
+ - lib/deja_vue.rb
31
+ files:
32
+ - Manifest
33
+ - README.rdoc
34
+ - Rakefile
35
+ - deja_vue.gemspec
36
+ - lib/deja_vue.rb
37
+ - lib/deja_vue/has_deja_vue.rb
38
+ - lib/deja_vue/history.rb
39
+ - rails/init.rb
40
+ has_rdoc: true
41
+ homepage: http://github.com/riopro/deja_vue
42
+ licenses: []
43
+
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --line-numbers
47
+ - --inline-source
48
+ - --title
49
+ - deja_vue
50
+ - --main
51
+ - README.rdoc
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 3
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 11
69
+ segments:
70
+ - 1
71
+ - 2
72
+ version: "1.2"
73
+ requirements: []
74
+
75
+ rubyforge_project: deja_vue
76
+ rubygems_version: 1.3.7
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Keep track of your models changing history, using MongoDB as backend
80
+ test_files: []
81
+