Rails show views for PaperTrail versions - ruby-on-rails

I have quite an odd usecase for PaperTrail. In my scenario a version is based on a physical event happening. Due to this, versions of my model are only created when I say.
Because of the above. I wish those versions to be capable of the following things:
When I run something like Model.all, this should ignore versions as expected but in some cases I wish versions to be returned as normal so I need to be able to do something like Model.withVersions to be returned all the current records and all associated versions. Is this possible?
Also, I need to be able to build a page for versions. Ie: Rails Show Action and View.
How would I accomplish this? I need to be able to craft a link to one of the pages that would output something similar to /model/23 which would take you to a page about a particular version of that item.
I know this is sort of an odd issue but wondering if anyone would have any input on this strange usecase and how I can sometimes bring versions to the foreground to act like existing things and othertimes to stay in the background as normal.

Basically what we're doing here is using the fact that PaperTrail adds a new method to your model called versions which returns an array of PaperTrail::Version objects, one for each version of the model. On any of those version objects you can call .reify to get the model at that version.
Now, I'm not sure how you'd want those versions to be returned, so let's say we create an array of versions for each model and store them in a hash keyed by the primary keys of each model:
hash = {}
Model.all.each { |m| hash[m.id] = m.versions.map(&:reify) }
# so to see the array of all versions of model with id=1
hash[1]
Re your show page, you will need to pass the version as a parameter, eg. /model/23?version=5 and then in your show action you'll do something like:
#model = Model.find(params[:id])
if params[:version]
#model = #model.versions[params[:version]-1].reify
end

Related

Make a copy / clone of an active object in ruby, and make changes to it, without messing with the database

I am new to ruby. I've coded in many languages, and normally get things quickly if there is a good reference and things are explained logically. I am going out of my mind. I've looked at every possible question related to this on stackoverflow, as well as on other websites. Everybody says use .clone or .dub or freeze and even something like Marshal.load(Marshal.dump(arr)) but none of those work.
I just want a copy of the original object, that I can modify at runtime, without it making any changes to the database.
The rails project loads a bunch of products into an object, which is then injected into a dropdown. Let's say it's called #products. The client wants me to remove certain entries from being displayed in the dropdown, but they must NOT be removed from the database.
In php for instance, you would just load the db object into a variable, and delete what you do not want by id for instance, and then loop through the resulting object / array and that creates the drop down. This makes NO alteration to the database.
I realized very quickly, Ruby does not work like that, and it deletes things direct from the database, even if I use .clone or .dup.
Let's say I use tempproducts = #products, and I do something like tempproducts.delete(11) , I don't want the system to go and delete #products(11) as well.
This is an elementary function of programming, why can't I figure out to do something as simple as this?
Thanks kindly to anybody that can help me out with this, or even post a link to the correct answer!
Checkout Array#select method to filter collection of records
e.g Let's say your Product model has one column shipping_category and you want to show only product with local and zonal shipping categories then it will look like
#product.select { |p| p.shipping_category == 'local' || shipping_category == 'zonal' }
Ideally you should use https://guides.rubyonrails.org/active_record_querying.html to filter data based on some condition at DB level use where method of Active Record.

Rails - ActiveModel::Serializer virtual attribute error

I'm using the active_model_serializers gem for a RoR API. Versions:
Rails: 4.2.8
Ruby: 2.2.5
active_model_serializers: 0.10.0
I'm using a virtual attribute in a serializer. I get it by using a sub query when I retrieve the objects from the database.
You can find the code here: Github Gist
This is the error I'm getting:
undefined method 'number_of_reservations' for DiscountSchedule...
This field isn't defined in the table nor in the model (attr_accessor)
I'm not sure why it doesn't work, I have a very similar serializer and it's working OK.
Any help will be appreciated.
EDIT:
I have another serializer where the virtual/calculated field is working OK. My guess on this is that since AR is making a bunch of LEFT OUTER JOINS and the SELECT list of the query is very big at some point something is getting broke.
The link won't work for me as I don't have access at my work place, however, from the error I can recommend you to check if you have defined the attributes like this in your serializer attributes :number_of_reservations and have an action in the serializer that says
def number_of_reservations
// Your logic goes here.
end
I suspect this question has to be about ActiveRecord, rather than AMS. You're trying to use select and alias to collect some computed (aggregate) attribute along with objects themselves. This, unfortunately, won't work in ActiveRecord, at least not in versions below 4.2.X. And this is why you're observing this behavior, there is no number_of_reservations in your models.
To see what's going on, try to inspect #objects here: https://gist.github.com/LuisDeHaro/ebf92781b449aa1ee7b85f8f552dd672#file-some_controller-rb-L17
Indeed: the issue was by the big amount of LEFT JOINS that the includes(:table_name) is generating. The serializer then does not know what to do.
I found a monkey-patch gem that works for AR (Rails 4 & 5) that fix this.
https://github.com/alekseyl/rails_select_on_includes
So, the virtual field number_of_reservations is picked up by the serializer like a charm.
And, you might be wondering: why do you want to retrieve a field that is not in the table definition in the database. A: well, in some scenarios you will need a calculated field for EVERY row you are retrieving. A SQL sub query is one of the most efficient ways to do so.
It's working now for me.

Rails - how to save existing record and apply update to a copy?

I have a webpage that tracks budgets containing a LOT of variables, stored in 40+ columns. Over time, adjustments are made to these budgets, but I need to be able to track changes over time and year to year. I tried adding a private method to my model that should create a duplicate of the existing record triggered by a :before_update callback. However, it's not working. The update changes the existing record, and the original is not preserved at all.
Model:
class Budget < ActiveRecord::Base
before_update :copy_budget
private
def copy_budget
#budget = Budget.find(params[:id])
#budget.dup
#budget.save
end
end
I'm still learning rails, (this is in Rails 4) and I think this would have been the best way to do this. If not, is there a better way to set the form to ALWAYS post a new record instead of routing to update if a record already exists?
Currently the form_for line looks like this:
<%= form_for(#budget) do |f| %>
Everything works as it should, with the exception of the duplication not happening. What am I missing? Is it possible the .dup function is also duplicating the :id? This is assigned by auto-increment in the MySQL db I an using, so if .dup is copying EVERYTHING, is there a way to copy all of the data except the :id into a new record?
Thanks in advance for any suggestions.
the dup method returns the new object without an id, it doesn't update it in place. Since your copy_budget method is already an instance method on Budget, you also would not need to (and you wouldn't even be able to, since params aren't accessible in models) look up the budget by id and instead could just use the current instance (self). So the following changed would fix the copy_budget method for you, but you are still copying an already modified object, just before it gets saved to the database
def copy_budget
copy_of_budget = self.dup
copy_of_budget.save
end
it would work the way you're expecting it to work. However, you aren't linking the copy in anyway to the current version of the Budget (no way to tell Budget id = 1 is an older version of Budget id = 2). I'd recommend taking a look at a gem such as PaperTrail (I'm sure there are lots of others if that one doesn't suit your needs) which has already thought through a lot of the problems and features with keeping a history of record changed.

Using PaperTrail, can I opt out of `object_changes` for a particular model or attribute?

This is somewhat related to #837 in that I have a large data column on my models, however I think I may be better served by the opposite of what's proposed in that issue - that is, to maintain the object column but not the object_changes column.
We had been running with no versions.object_changes column. Now that I've added this column, I realized I am writing a lot of data I don't care about for the data column in object_changes - since a tiny change to data causes it to be written out to versions effectively 3x (once in object and twice in object_changes for the before and after).
I don't think skip or ignore is what I want, because I would indeed like the changes to data to produce a new version.
Should I go down the custom version model route? Or what do you recommend?
Some options, in descending order of recommendation (most highly recommended first):
version_limit (Supported) - Save disk space instead by limiting the number of versions you create for a given record, using version_limit. (https://github.com/airblade/paper_trail#2e-limiting-the-number-of-versions-created)
Custom table (Supported) - Custom version model, custom table without object_changes column. Precludes the experimental associations feature (track_associations must be false [the default])
Patch recordable_object_changes, method 1 (Not supported) - Custom version model, but still using the versions table. Override #paper_trail to return a custom child class of RecordTrail which overrides RecordTrail#recordable_object_changes. Overriding these methods breaks your warranty.
Patch recordable_object_changes, method 2 (Not supported) - Override RecordTrail#recordable_object_changes, adding a class-check conditional. Call super for all but the model you want to hack. Overriding this method breaks your warranty.
Custom serializer (Supported, but not for this) - Custom serializer with class-check conditional, and some way of telling whether you're serializing object_changes and not object. Probably a bad idea, seems really hacky.
Finally, I'd be happy to review a PR that adds a new feature, the ability to configure, on a per-model basis, which data should be written to the object_changes column. If you're serious about working on that, and seeing it through to the finish, please open a new issue so we can discuss it further. There are a few different designs that could work.
Update, 2019: We now have object_changes_adapter It's only for expert users, and probably not my top recommendation.

Rails model versioning - one model with independently versioned columns

I have a model for a multilingual website with wiki functionality which contains various fields I wish to have versioned, however I need to revert changes made to some columns but not others.
For example, I store the English and Spanish versions of a written guide for each model instance.
Imagine:
User A edits the Spanish guide and adds profanity
User B later edits the English guide and makes useful changes.
Typically versioning means that reverting the changes made by user A also will revert later changes by User B, despite the fact that in my case these are two separate concerns.
So I need to essentially have scoped version histories. For example using papertrail:
#guide.rb
has_paper_trail :only => [:en_title, :en_body]
has_paper_trail :only => [:es_title, :es_body]
Any easiest solution for this? I really don't want to move my guides into separate models with a one to one relationship just to achieve this.
Although I'd personally extract translations into their own model to avoid this, you should still be able to achieve this without too much trickery.
PaperTrail creates a new Version object every time a record is changed. Every version is accessible through the object.versions array.
You'll have to come up with a way to decide which attribute you want to revert and which version you want to revert it back to. Once you have this, reverting it shouldn't be very difficult.
For example, in your model:
# Arguments:
# attr - Must respond to to_s with name of attribute to revert
# ver - Integer (e.g. -1 for previous version) or actual Version object
def revert_attribute(attr, ver)
ver = self.versions[ver] if ver.class == Integer
ver = ver.reify
self.write_attribute( attr, ver.read_attribute(attr) )
self.save
end
Code isn't tested or syntax checked, but ought to work from what I've seen in the PaperTrail source.
It's not a perfectly integrated solution, but it should be adequate for most needs.

Resources