Create an audit trail using paper_trail gem - ruby-on-rails

I'm pretty new to rails, and brand new to using the paper_trail gem.
I would like to show a list of each change made to the location or tag number of my assets model. I have installed paper_trail and it seems to be working correctly. In my view I have this:
<% #asset.versions.order('created_at DESC').each do |version| %>
On <%= version.created_at %> <%= version.whodunnit %> updated this asset.<br>
<ul>
<li>Asset relocated from "<%= version.last.locaton %>" to "<%= version.location %> "</li>
<li>User changed from "<%= version.last.tag_number %>" to "<%= version.tag_number %> "</li>
</ul>
<% end %>
This gives me the correct information for when the change was made and who made it, but I cannot figure out to display the lines that say (for instance): Asset relocated from "223" to "258 or Tag Number changed from "1173" to "1175".
Ideally I would only show information for things that have changed. So if the location changed but the tag number did not, I would show only the location line.
I don't really even know where to start. Any help would be appreciated. Thanks!

From The PaperTrail Documentation:
PaperTrail has an optional table column that can be used when creating versions called object_changes. This will store ONLY the changes made between versions. Which will allow you to just display the changes between each version. It works like this.
>> widget = Widget.create :name => 'Bob'
>> widget.versions.last.changeset # {'name' => [nil, 'Bob']}
>> widget.update_attributes :name => 'Robert'
>> widget.versions.last.changeset # {'name' => ['Bob', 'Robert']}
>> widget.destroy
>> widget.versions.last.changeset # {}
If you recently added paper_trail to your model then you can run a migration to add this column. This will track future changes.
class AddObjectChangesColumnToVersions < ActiveRecord::Migration
def self.up
add_column :versions, :object_changes, :text
end
def self.down
remove_column :versions, :object_changes
end
end
I recently had a need to get the changes, however, we have years of version data for 20+ models and adding that migration would only help in the future and not give me all of the data changes in the past.
I wound up creating a patch for PaperTrail that added methods like diff_previous, diff_next, diff_live which would give me the difference between versions. It was based off the idea of this patch of paper_trail(https://github.com/jeremyw/paper_trail/blob/master/lib/paper_trail/has_paper_trail.rb#L151-L156) but I implemented it quite differently. If you want more details I will see if I can open source that patch.

Related

Displaying existing content in Rails 5.2 with ActionText

I have upgraded my app to rails 5.2 and inclined to use ActionText since old trix-editor/gem is no longer working.
Now new posts display their "descriptions" but how can I display my old posts' DESCRIPTIONS with the new installed ActionText?
post.rb has_rich_text :description
posts_controller.rb
...params.require(:post).permit(:description)
_form.html.erb
<%= f.rich_text_area :description %>
show.html.erb
<%= #post.description %>
Descriptions are only fetching from new records in ActionText but not displaying from existing "description" columns for old posts
I had a similar issue and I couldn't find a clean solution in the rails repo or anywhere. As a workaround, in your case, I would try:
show.html.erb: .
<%= #post.try(:description).body || #post[:description] %>
That wont solve the issue, but it would help you to populate old post values.
This answer worked for me. It also has the added bonus of cleaning up your database of the tables used for the normal (non-rich) text content.
"Assuming there's a 'content' in your model and that's what you want to migrate, first, add to your model: "
has_rich_text :content
"then create a migration"
rails g migration MigratePostContentToActionText
class MigratePostContentToActionText < ActiveRecord::Migration[6.0]
include ActionView::Helpers::TextHelper
def change
rename_column :posts, :content, :content_old
Post.all.each do |post|
post.update_attribute(:content, simple_format(post.content_old))
end
remove_column :posts, :content_old
end
end
You can find the original solution I used here
https://github.com/rails/rails/issues/35002#issuecomment-562311492

Migrated a new column to a table and updated its form in Rails but fails to update

I am using a blog engine in my Rails project called Lines, and blog posts are contained in a table called LinesArticle.
I want to add a custom attribute to hide certain posts behind a paywall, so I ran the following migration and raked it successfully:
class AddPaywallToLinesArticles < ActiveRecord::Migration
def change
add_column :lines_articles, :paywall, :string
end
end
Each blog post added to this table is posted through a form in the UI contained in my views/Lines/articles folder. I added a new text_field (where the user would write yes or no, I know I could do this through a Boolean but wanted to use a string just to test it).
<div class="input-field checkbox">
<%= f.label :paywall %><br>
<%= f.text_area :paywall%>
The form now loads and shows a text_area; however, that text is not saved when I submit the form. Example output if I check the last record in the table:
<LinesArticle id: 5, title: "Test 2 with False", teaser: nil, insider: nil, paywall: nil>
So the paywall column is not updating with the string I am typing in. I don't think it's an issue with the form -- I am wondering if the real issue is that I need to set some sort of permits: -- this blog engine does not have a Controller associated with it though.
Any ideas?
Your problem is most likely due to a missing strong parameter in your controller:
def create
...
#lineArticle = LinesArticle.create(line_article_params)
...
end
private
def line_article_params
params.require(:line_article).permit(:title, :teaser, :insider, :paywall)
end

Automatically creating a record for the user in rails

I am working on status feature for my rails app. I have everything set up and it works perfectly fine. For the interface, I am using the best_in_place gem to allow users to edit in place for the status. The code is given below.
<% if #scoreboard.status.content.present? %>
<div id="statuscontent">
<%= render 'statusedit' %>
</div>
<% else %>
<%= render 'statusupdate' %>
<% end %>
The statusedit partial contains the best_in_place code and the statusupdate partial contains the normal form for creating the status. Codes for both partials are given below.
statusedit
<%= best_in_place [#scoreboard, #status], :content, place_holder: "Enter a status", as: "textarea" %>
<%= link_to "delete", [#scoreboard, #status], method: :delete, data: {confirm: "Are you sure"} %>
statusupdate
<%= form_for [#scoreboard, #status] do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.text_area :content, class: "form-control", id:"status" %>
<%= f.submit "Post", class: "btn btn-primary", id: "status-button" %>
<% end %>
The above code is working perfectly fine, however, I am struggling to set up the interface I want. The problem is that best_in_place editing only works if the status has been created before. Therefore, I have to create the status using the form and then I can use in place editing. However, I want the interface such that users can use in place editing without the need of physically creating a status first. Is there a way to create a default status for every user? Or a before_update function that maybe creates a status before the user tries to update.
I tried using Ajax for creating status to achieve the interface but it didn't work as intended. Also, I feel there must be a simpler solution to this problem. I tried setting :default => "please upload status" in the database but that doesn't actually create a default record. It acts like a placeholder but you still have to click post for the record to be created. Is there a way to somehow automatically create a default status for each user or set a value in the database. I have read a few stack-overflow posts on this but nothing really points to the right direction. There must be a simpler way of doing this in rails. Any documentation or suggestions would be a great help. As always, any help is always greatly appreciated!! Thanks
I have added the relevant migration and model files.
status model
class Status < ActiveRecord::Base
belongs_to :scoreboard
end
scoreboard model
class Scoreboard < ActiveRecord::Base
has_one :status
end
Status migration file
class CreateStatuses < ActiveRecord::Migration
def change
create_table :statuses do |t|
t.text :content
t.references :scoreboard, index: true
t.timestamps null: false
end
add_foreign_key :statuses, :scoreboards
end
end
Answer for your questions.
Is there a way to create a default status for every user ?
Yes
You could create a default record in database after change_column.
def up
change_column :users, :admin, :boolean, default: false # I'm assuming you are saving default as false in users table but you could change accordingly all thing.
end
before_update function that maybe creates a status before the user tries to update.
instead of before_update one could use after_save callback.
As mentioned in official Document
after_save runs both on create and update, but always after the more
specific callbacks after_create and after_update, no matter the order
in which the macro calls were executed.
And lastly
There must be a simpler way of doing this in rails
Use ActiveRecord Migrations.
Apologies if this answer is not specific; you wrote a lot.
You're best setting a default value for an attribute at the database level.
Yes, you can use the before_create callback:
#app/models/scoreboard.rb
class Scoreboard < ActiveRecord::Base
before_create :set_default_status
private
def set_default_status
self[:status] = "default"
end
end
The problem with this is that it adds application-level logic which can be handled by the db:
$ rails g migration CreateStatusDefault
# db/migrate/create_status_default________.rb
class CreateStatusDefault < ActiveRecord::Migration
def change
change_column :scoreboards, :status, default: "Default"
end
end
$ rake db:migrate
Creating a default value in the db basically means that unless you send a value from your model at create / update, you're going to get the default.
The beauty of setting it in the DB is that your Rails app has absolutely zero work to do for it, making it much more efficient.
--
I subsequently found that your status "object" is actually a model in itself, which is probably where your issues are coming from.
If that is the case, you'll need to build the associative object before you create your main/parent one:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :statuses
before_create -> (model) { model.statuses.build }
end
We use this to build a profile for each User who signs up to our applications; it creates a "blank" associative record for your parent model.
This is something which would give you a "blank" status object, but as you've not posted your code, I really don't know.
A good tip with SO is code > text.
If in doubt, post code.
Is there a way to create a default status for every user?
Yep you can write it yourself. Something like this ...
User < ActiveRecord::Base
has_many :statuses
after_create :create_first_status
def create_first_status
self.statuses.create some_status_attribute: "some value"
end
end

Ruby Rails - how to update database with a calculation?

I'm working on a simple rails app. The user enters two separate integers and when they press 'create', the 'show' page displays the two values and a third value which is calculated from the user input. I have put the arithmetic to calculate the third value in the 'show' html.erb view, but I can't work out how to add the third value into the database. Heres my code for the 'show' view:`
Name:
<%= #startup.name %>
<p>
<strong>Revenue:</strong>
<%= #startup.revenue %>
</p>
<p>
<strong>Costs:</strong>
<%= #startup.costs %>
</p>
<h2>
<strong>Profit:</strong>
<%= #startup.profit = #startup.revenue - #startup.costs %>
You could add a column to the database for this calculated value, and update it upon completing the calculation.
Create a migration to add the column profit to the startup model:
rails g migration AddProfitToStartups profit:decimal
with a note that you may choose a different datatype for profit depending on what datatype your revenue and costs columns are. There are other helpers for currency, see here.
This will generate:
class AddProfitToStartups < ActiveRecord::Migration
def change
add_column :startups, :profit, :decimal
end
end
now run rake db:migrate to update the database.
After this, the code you have in your view should work as is.
Create a variable in the Startup model call profit.
In the create action have something like this:
def create
#startup = Startup.new(startup_params)
#startup.profit = #startup.revenue - #startup.costs
#startup.save
#redirect
end
Apparently the above answer is bad practice. So, here is what I hope is a better more general solution.
At the top of your Startup model have a call to before_save that will look like this:
before_save :calculate_profit
and then write a function in the Model called calculate profit. It will look something like this
def calculate_profit
self.profit ||= self.revenue - self.costs
end

Composite keys with ActiveScaffold in Ruby On Rails

I am developing RoR application that works with legacy database and uses ActiveScaffold plugin for fancy CRUD interface.
However one of the tables of my legacy db has composite primary key. I tried using Composite Keys plugin to handle it, but it seems to have conflicts with ACtiveScaffold: I get the following error:
ActionView::TemplateError (Could not find column contact,type) on line #3 of ven
dor/plugins/active_scaffold/frontends/default/views/_form.rhtml:
1: <ol class="form" <%= 'style="display: none;"' if columns.collapsed -%>>
2: <% columns.each :for => #record do |column| -%>
3: <% if is_subsection? column -%>
4: <li class="sub-section">
5: <h5><%= column.label %> (<%= link_to_visibility_toggle(:default_visible =
> !column.collapsed) -%>)</h5>
6: <%= render :partial => 'form', :locals => { :columns => column } %>
vendor/plugins/active_scaffold/lib/data_structures/sorting.rb:16:in `add'
while having in model code smth like:
set_primary_keys :contact, :type
I highly appreciate any idea how I can get composite keys capability with ActiveScaffold.
I think your best bet may be checking the ActiveScaffold Google Group as it's monitored by core developers of ActiveScaffold and they would ultimately be able to solve your problem and explain why composite keys with the plugin won't work with ActiveScaffold.
Good luck and be sure to post a follow-up if you do get results from the Google Group (which I have posted on before and received feedback very quickly).
One quick result I did find was this.
What I did was to create a facade class that does not inherit from
ActiveRecord then make the "id" show the primary key. In my case the
primary key was computed from other data and could change as a result
of an edit, so I had to override ActiveScaffold in a few places to
allow for the primary key changing after an update. But, all in all
it works and is fairly straightforward. Start with an empty class
and just resolve messages that are not understood. In your case you
could even just redirect all messages to a wrapped ActiveRecord while
replacing the id and id= methods, and filtering the [] and []= methods.
That may do the trick for you.
No, I have not received any reply from the group and I am not sure if ActiveScaffold is actively maintained yet.
After some time playing with ActiveScaffold, I ended up implementing my own CRUD interface from the scratch.
I have this working, with read-only models, using ActiveScaffold on a legacy DB.
The trick was to override the default 'id' field in the model and return a concatenated PK string.
If that's good enough, then here you go:
class CPKReadonlyModel < ActiveRecord::Base
set_primary_key :id_one # only half of it, but id overridden below...
def id
self.id_one.to_s + ',' + self.id_two.to_s
end
def readonly?
true
end
def before_destroy
raise ActiveRecord::ReadOnlyRecord
end
def delete
raise ActiveRecord::ReadOnlyRecord
end
def self.delete_all
raise ActiveRecord::ReadOnlyRecord
end
end
The controller has the following in the active_scaffold config block:
config.actions.exclude :create, :update, :delete

Resources