module HasHistory # TODO (uwe)(2007-10-31): Allow different strategies: timestamped_master, separate_history_table, separate_history_table_with_last # Adds a trigger to the model that saves a copy of the model in a history table. # # The default history model is called History, but can be overridden by the "class_name" option. # # All columns are copied unless excluded by the :except or :only options. # # The created_at field is never changed in the original model, and is therefore omitted in the history model. # # The updated_at column should preserve the original timestamps but changes meaning in the history table. It therefore is renamed to "valid_from". # # The original row is references by a foreign key column with default name "_id". # This can be overridden by the :foreign_key option. # # You can automagically generate the history model class at runtime by adding the :generate_model => true option. # In that case, the following option is also available: # # * :allow_updates # # See HasHistory#acts_as_history for a description of this option. # # examples: # # class Person < ActiveRecord::Base # has_history # end # # class Person < ActiveRecord::Base # has_history :class_name => 'PersonHistory' # end # # class Person < ActiveRecord::Base # has_history :except => :birthdate # end # # class Person < ActiveRecord::Base # has_history :except => [:birthdate, :social_security_no] # end # # class Person < ActiveRecord::Base # has_history :only => :weight # end # # class Person < ActiveRecord::Base # has_history :only => [:height, :weight] # end # # class Person < ActiveRecord::Base # has_history :foreign_key => :person_id # end # # class Person < ActiveRecord::Base # has_history :generate_model => true # end # # class Person < ActiveRecord::Base # has_history :generate_model => true, :allow_updates => true # end # # class Person < ActiveRecord::Base # has_history :generate_model => true, :allow_updates => true # end # def has_history options = {} options[:only] = [options[:only]] if options[:only].is_a? Symbol options[:except] = [options[:except]] if options[:except].is_a? Symbol history_model_class_name = (options[:class_name] && options[:class_name].to_s) || "#{name}History" if options[:generate_model] Object::class_eval <<-EOF class #{history_model_class_name} < ActiveRecord::Base acts_as_history :class_name => :#{table_name.singularize}, :allow_updates => #{options[:allow_updates].inspect} end EOF end history_model = history_model_class_name.constantize foreign_key = options[:foreign_key] || "#{table_name.singularize}_id".to_sym before_update do |record| old_values = find(record.id).attributes.symbolize_keys new_values = record.attributes.symbolize_keys values = {foreign_key => record.id}.update(old_values) old_values.delete(:updated_at) new_values.delete(:updated_at) unless new_values == old_values values[:valid_from] = values.delete(:updated_at) if values[:updated_at] values.delete(:id) values.delete_if {|k,v| not history_model.column_names.include? k.to_s} values.delete_if {|k,v| not options[:only].include? k} if options[:only] values.delete_if {|k,v| options[:except].include? k} if options[:except] history_model.create!(values) end end if options[:order] order = options[:order] elsif history_model.columns.find {|c| c.name == 'valid_from'} order = 'valid_from DESC' else order = 'id' end has_many history_model.table_name.pluralize.to_sym, :dependent => :destroy, :foreign_key => foreign_key, :order => order end # Adds a belongs_to relationship to a master model and blocks updates and destroy actions on this class. # # If the class name of the history model ends with "History", the default master model class name is automatically calculated (ie. PersonHistory => Person). # If the history class name does not end in "History", or you want to use another class name, you must override it with the :class_name option. # # Normally the history model is blocked for updates, but updates can be enabled by the :allow_updates option. # # The original row is references by a foreign key column with default name "_id". # This can be overridden by the "foreign_key" option. # # If you would like to inject the update trigger in the master model from the history model, use the :inject_trigger => true option. # If you do this, these additional options are available: # # * :only # * :except # # See HasHistory#has_history for a description of these options. # # examples: # # class PersonHistory < ActiveRecord::Base # acts_as_history # end # # class PersonRecords < ActiveRecord::Base # acts_as_history :class_name => 'Person' # end # # class Person < ActiveRecord::Base # acts_as_history :allow_updates => true # end # # class Person < ActiveRecord::Base # acts_as_history :inject_trigger => true, :except => :birthdate # end # # class Person < ActiveRecord::Base # acts_as_history :inject_trigger => true, :except => [:birthdate, :social_security_no] # end # # class Person < ActiveRecord::Base # acts_as_history :inject_trigger => true, :only => :weight # end # # class Person < ActiveRecord::Base # acts_as_history :inject_trigger => true, :only => [:height, :weight] # end # # class Person < ActiveRecord::Base # has_history :foreign_key => :person_id # end # # class Person < ActiveRecord::Base # has_history :generate_model => true # end # def acts_as_history options = {} model_name = options[:class_name] || (name =~ /(.*)History/ && $1.constantize.table_name.singularize.to_sym) raise "Master model class name '#{$1}' is missing and cannot be deduced." if model_name.nil? belongs_to model_name unless options[:allow_updates] validate_on_update do |record| record.errors.add "History:", "You may not change history!" end before_destroy do |record| record.errors.add :id, "You may not change history!" false end end end end