How to keep history in rails model - ruby-on-rails

There are three questions here, all of them set in bold.
I have a personnel database where historical data is also stored.
Each table has two columns start and stop which indicate the period during which this fact is valid.
When any field of a record changes, a new record is created with start=today, and stop=nil,
and the old record is given a stop=today-1 (or whatever the effective date of the change is instead of today).
When a record is to be deleted, instead of deleting, we simply set stop=today.
This way, it is easy to
give a history of any employee
give a snapshot of what things looked like on a particular date
Firstly this pattern seems common enough that probably I should not need to re-invent the wheel.
Yet in my search all I found were some gems called auditable or papertrail.
From a first glance, they did not seem to do what I want, but perhaps I am mistaken.
Do you know of a gem that does something similar?
If I don't find a solution, what is enough for my purposes is to have a few methods which works for all my models, like
def when(date)
self.where("start <= ? and ? <= stop", ondate, ondate)
end
I thought about inheriting
class MyRecord < ActiveRecord::Base
def when ...
end
end
and having all my models inherit from MyRecord, but this broke everything. Why?
I could just add my methods to ActiveRecord::Base, but that seems wrong.
What is the proper way to add a method to all models?
Answer
Parts 2 and 3 of this question have been answered before here. Since that contains Josh's answer (which I +1ed), as well as why inheriting from MyRecord doesn't work, I did not accept Josh's answer.
I still have not located a gem which maintains a good history, but two out of three ain't bad...

This sounds to me like an excellent opportunity to use an ActiveSupport::Concern. You could write something like:
History.rb
require 'active_support/concern'
module History
extend ActiveSupport::Concern
included do
def self.when(date)
self.where("start <= ? and ? <= stop", ondate, ondate)
end
end
end
Your model
class SomeClass < ActiveRecord::Base
include History
end
You could then call SomeClass.when(some_date_string) on any models that you need to include history on.

Related

Ruby On Rails: Disable `delete_all` when table name is present

delete_all is useful, but I never want to see it called on the same line with a table name. I'd like to disable things like TableName.destroy_all in both console and code.
One interesting issue happened earlier this month:
Application.destroy_all was called on a model instead of applications.destroy_all
(the model has_many applications)
For somebody new to ROR, it looks very similar, but the results were disastrous.
I'm open to some form of lint/code style tool, but that really wouldn't catch it in the console scenario. (Plus, I haven't been able to get rubo-cop to do something like this yet)
Basically, I'm asking for a way to make the console and codebase more secure so that newer developers can't inadvertantly delete everything in a table.
I'm not entirely clear on what you are trying to accomplish, but you could try overriding the method in your ApplicationModel with something like this (assuming Rails 5 or greater, or otherwise a root model in existence).
class ApplicationModel < ActiveRecord::Base
def self.destroy_all(*args)
raise('Cannot destroy all records of a model this way. Did you mean to delete a subset of records instead?')
end
end
Possibly make this method private if you'd like it even harder to run...
def self.destroy_all(*args)
raise('Cannot destroy all records of a model this way. Did you mean to delete a subset of records instead?')
end
private_class_method :destroy_all
You could get fancy and allow this to be bypassed with a special argument that you check for, but give this a try and see how it goes.

Rails timestamps for specific columns

I'm looking for a way to add a *_updated_at columns for several columns in a record.
It would work similarly to normal Rails timestamps, ie based on convention. If there's a DB column called author_updated_at, the class would automagically update it whenever author attribute changes.
Not too hard to write, but thought I'd ask here in case anyone has done it before or there's a gem around. I'd also be interested to know if there are any performance issues with this approach, though I think it should be negligible if using before_save. There's no extra reads or writes needed.
You might want to try paper_trail - it tracks changes to a model but in a slightly different fashion - instead of adding extra columns for each attribute to track it uses a table to store versions of the model. This gives you a full blown versioning system without doing much work.
You can easily revert to previous versions and track who created a revision etc.
But if all you need is a light-weight solution to store a timestamp for the changes to a limited set of attributes you can use model callbacks to set the *_updated_at columns.
module AttributeTimestamps
extend ActiveSupport::Concern
included do
after_validation(on: :update) do
# Use ActiveModel::Dirty to get the changed attributes
self.previous_changes.each_with_index do |attr|
setter = "#{attr}_updated_at="
self.call(setter, self.updated_at) if self.respond_to? setter
end
end
end
end
class MyModel < ActiveRecord::Base
include AttributeTimestamps
end
I solved with a different version of #max solution.
previous_changes wasn't working for me. I'm using Rails 4.2.
module AttributeTimestampable
extend ActiveSupport::Concern
included do
after_save do
self.changed.each do |attr|
timestamp = "#{attr}_updated_at"
update_column(timestamp, updated_at) if self.respond_to? timestamp
end
end
end
end

ActiveRecord: make all text fields have strip called on them before saving, unless specified otherwise

I've ran into various problems with various sites over the years with users putting spaces at the start/end of string and text fields. Sometimes these cause formatting/layout problems, sometimes they cause searching problems (ie search order looking wrong even though it isn't really), sometimes they actually crash the app.
I thought it would be useful, rather than putting in a bunch of before_save callbacks as i have done in the past, to add some functionality to ActiveRecord to automatically call .strip on any string/text fields before saving, unless i tell it not to, eg with do_not_strip :field_x, :field_y or something similar at the top of the class definition.
Before i go and figure out how to do this, has anyone seen a nicer solution? Just to be clear, i already know that i can do this:
before_save :strip_text_fields
def strip_text_fields
self.field_x.strip!
self.field_y.strip!
end
but i'm looking for a nicer way.
cheers, max
Here's a handy module that you could drop into lib and include in your models. It doesn't have the exceptions that you mentioned, but it looks for a strip! method which might be good enough. You could add the exceptions feature fairly easily, if needed.
# lib/attribute_stripping.rb
module AttributeStripping
def self.included(context)
context.send :before_validation, :strip_whitespace_from_attributes
end
def strip_whitespace_from_attributes
attributes.each_value { |v| v.strip! if v.respond_to? :strip! }
end
end
Use like this:
class MyModel < ActiveRecord::Base
include AttributeStripping
# ...
end
UPDATE (9/10/2013):
Revisiting this answer a couple of years later, I see how the winds have changed. There's a cleaner way to do it now. Create a module like this:
module AttributeStripper
def self.before_validation(model)
model.attributes.each_value { |v| v.strip! if v.respond_to? :strip! }
true
end
end
and set its method to be invoked at the right time in your model:
class MyModel < ActiveRecord::Base
before_validation AttributeStripper
# ...
end
This module is easier to test since it's not a mixin.
I have dealt with these sort of data integrity issues in various applications.
I used to manipulate the input like that.
But now, the best advice I have actually seen and followed is to store whatever the user types.
Then do post-processing on the backend to do the strip.
Create additional database fields (destripped) if you really want it in the database model table.
The main reason for this is one (primary) thing - when users want to revisit their data, i.e. edit, they're usually gonna expect to see what they typed in. A secondary reason is that you will avoid the possibility that your strip doesn't work right and either mangles the data or actually throw an error.
I've written a plugin for this purpose some time ago. I haven't tried it in a while and it doesn't have tests - so no guaranties that it still works. The upside would be a clean model:
class Story < ActiveRecord::Base
strip_strings :title, :abstract, :text
end

rails/ActiveRecord: How can I specify a different class for a particular attribute?

In rails 3, I have a database model that has a birthday field, of type date. For various reasons (e.g. a date without a known year), I'd like to have this be of a class other than Date, and/or to mix something in to the Date class for only objects created from this attribute. What's the best way to go about this?
For example, suppose I want to do something like:
class BirthDate < Date
def to_s
case year
when 1 # if I store a date with a null year, it sets it to 1
strftime("%m/%d")
else
super
end
end
end
That class does get me the behavior I want (for the moment; it's entirely possible I'll modify it in future), and I can get my model to give me access to this using something like:
class Person < ActiveRecord::Base
def bday
BirthDate.new(birthday.year, birthday.month, birthday.day)
end
end
And then just change my views to use bday instead of birthday. This seems kind of wasteful, though (having to create a new object from one already created -- never mind that Date doesn't seem to have a "copy constructor" type thing? (Or am I just missing it somewhere?)
Anyway, I would think that perhaps ActiveRecord might provide something that might look like:
class Person < ActiveRecord::Base
class_for :birthday => BirthDate
end
Is there anything like that? I've done a bunch of google searches, and looked through a number of docs, and haven't found it. But since I don't know what it might be called,
(Perhaps it's time to start digging through the source -- I've heard that's a good way to learn more about a platform. Then again, some say otherwise ;))
You can override the getter for the attribute manually:
def birthday
BirthDate.parse(birthday_before_type_cast)
end

Rails Multiple Table Inheritance question

I am starting to implement an MTI solution and have a basic question. I have 3 physical models - SMSNotifications, EmailNotifications, TwitterNotifications and they are subclasses of notification. At times in my code, I want to say Notifications.find(:all)so that I can get a set of results sorted by their creation time. Then I want to do things based on their subclass. What is the way to write Notifications.find(:all) and have Rails look through the subclass tables and combine the results? Right now Rails still thinks I have a physical Notifications table which goes against my MTI design.
I am also considering the possibility that I should be using STI instead. I would probably have 10 empty columns per row but if getting all notifications requires a query for each type of notification, then I feel like this could be a big issue.
Thanks!
Yes, you will need separate queries for each type. If this is a dealbreaker, then you should go with either STI or the mixed model approach advocated in your previous question.
If you use https://github.com/hzamani/acts_as_relation:
rails g model notification notification_type:string notification_id:integer <other columns>
rails g model email_notification <columns>
class EmailNotification < ActiveRecord::Base
acts_as :notification
end
....
then the code you want might be something like this:
Notification.all.each do |n|
case n.notification_type
when "EmailNotification"
...
when "SMSNotification"
...
end
end
For this purpose, you can make the subclasses have a polymorphic association with some specific model (X). You can edit the callback after_initialize to associate one entry of this specific model X with each of your subclasses on creation. In this way, you can perform find on the target (X) of the mentioned polymorphic association.
I recently forked a promising project to implement multiple table inheritance and class inheritance in Rails. I have spent a few days subjecting it to rapid development, fixes, commenting and documentation and have re-released it as CITIER Class Inheritance and Table Inheritance Embeddings for Rails.
I think it should allow you to do what you needed by asking for all notifications, it will still return the correct models. If you were to do something like
Notification.all().each do |n|
if n.class == 'EmailNotification'
#Do something
end
end
Or even define a function in the root Notification class and overload it in subclasses to return something different.
Consider giving it a look: http://peterhamilton.github.com/citier
I am finding it so useful! I would (by the way) welcome any help for the community in issues and testing, code cleanup etc! I know this is something many people would appreciate.
Please make sure you update regularly however because like I said, it has been improving/evolving by the day.

Resources