Using PaperTrail in Rails to approve/deny edits - ruby-on-rails

Here is my code so far:
class Video < ActiveRecord::Base
has_paper_trail meta: { athlete_id: :athlete_id, approved: false },
if: Proc.new { |v| v.needs_approval? }
validate :should_be_saved?
def should_be_saved?
errors.add(:base, 'added for approval') if needs_approval?
end
def needs_approval
#needs_approval ||= false
end
def needs_approval?
#needs_approval
end
end
# ApplicationController
class ApplicationController < ActionController::Base
def user_for_paper_trail
return unless user_signed_in?
original_user.present? ? original_user : current_user
end
# Used to determine the contributor
# When contributor logs in, warden saves the contributor_id in session
def original_user
return nil unless remember_contributor_id?
#original_user ||= User.find(remember_contributor_id)
end
def info_for_paper_trail
{ athlete_id: current_user.id } if current_user
end
end
The problem I am running into currently is when the Video object is saved the validation fails (because I told it too), but I need for the validation to fail but the version object continue with its creation. Just not too sure how to go about doing that.
EDITS
Here is my code (the code below is still using the ApplicationController code from above):
class Video < ActiveRecord::Base
# .. other methods
include Contributable
attr_accessible :video_type_id, :athlete_id, :uploader_id, :created_at, :updated_at, :uniform_number, :featured,
:name, :panda_id, :date, :thumbnail_url, :mp4_video_url, :from_mobile_device, :duration, :sport_id,
:delted_at, :approved
end
module Contributable
extend ActiveSupport::Concern
included do
has_paper_trail meta: { athlete_id: :athlete_id, approved: false },
unless: Proc.new { |obj| obj.approved? },
skip: [:approved]
end
def log_changes_or_update(params, contributor = nil)
update_attribute(:approved, false) unless contributor.blank?
if contributor.blank?
update_attributes params
else
self.attributes = params
self.send(:record_update)
self.versions.map(&:save)
end
end
end
class VideosController < ApplicationController
def update
# ... other code
# original_user is the contributor currently logged in
#video.log_changes_or_update(params[:video], original_user)
end
end
The app I am working on has a small layer of complexity that allows for users with a certain role to edit profiles they have access too. I am trying to save the versions of each change out (using paper_trail) without affecting the existing object.
The code above works exactly how I want it to, however, I am just curious to know if in my log_changes_or_update method is not the correct way to go about accomplishing the overall goal.

Why not just remove the validation and add an approved attribute with a default value of false to the Video model? That way the Video object is saved and a paper_trail version is created. Later when the video gets approved paper_trail will note that change too.

Related

Serializing Rails 6 models based on current_user

In an API controller, I'd like to limit what fields of a model can be seen depending on who is logged in. ActiveModel Serializers would seem to allow this, but I've had no luck with the following:
class MyModelSerializer < ActiveModel::Serializer
attributes :name, :custom_field, :secret_field
has_many :linked_records
def custom_field
object.do_something
end
def filter(keys)
unless scope.is_admin?
keys.delete :secret_field
keys.delete :linked_records
end
keys
end
end
But, the filtering is never performed and so my output always contains :secret_field and :linked_records even if there's no user logged in.
Perhaps this is because I am using Rails 6, and it would seem that ActiveModel Serializers might no longer be the best tool (e.g. https://stevenyue.com/blogs/migrating-active-model-serializers-to-jserializer).
Please do offer your suggestions for a means to perform this, if you can think of a better means.
EDIT:
Further to all the comments below, here's some different code:
attributes :name, :id, :admin_only_field, :is_admin
$admin_only = %i[:id, :admin_only_field]
def attributes(*args)
hash = super
$admin_only.each do |key|
unless scope.is_admin?
hash.delete(key)
end
end
hash
end
def is_admin
if scope.is_admin?
'admin!'
else
'not an admin!'
end
end
If I then visit the model's index page without being an admin I see that the admin_only_field and id are both present, and is_admin says that I'm not. Bizarre.
class MyModelSerializer < ActiveModel::Serializer
attributes :name, :custom_field, :secret_field
has_many :linked_records
def custom_field
object.do_something
end
private
def attributes
hash = super
unless scope.is_admin?
hash.delete :secret_field
hash.delete :linked_records
end
hash
end
end

Inline `after_commit` Callbacks in Rails

I've got a sidekiq job that needs to be run after the commit, but only in some situations and not all, in order to avoid a common race condition.
For example, the below after_commit will always fire but the code inside will only execute if the flag is true (previously set in the verify method).
class User < ActiveRecord::Base
...
after_commit do |user|
if #enqueue_some_job
SomeJob.new(user).enqueue
#enqueue_some_job = nil
end
end
def verify
#enqueue_some_job = ...
...
save!
end
end
The code is a bit ugly. I'd much rather be able to somehow wrap the callback inline like this:
class User < ActiveRecord::Base
def verify
if ...
run_after_commit do |user|
SomeJob.new(user).enqueue
end
end
...
save!
end
end
Does anything built into Rails exist to support a syntax like this (that doesn't rely on setting a temporary instance variable)? Or do any libraries exist that extend Rails to add a syntax like this?
Found a solution using a via a concern. The snippet gets reused enough that it is probably a better option to abstract the instance variable and form a reusable pattern. It doesn't handle returns (not sure which are supported via after_commit since no transaction is present to roll back.
app/models/concerns/callbackable.rb
module Callbackable
extend ActiveSupport::Concern
included do
after_commit do |resource|
if #_execute_after_commit
#_execute_after_commit.each do |callback|
callback.call(resource)
end
#_execute_after_commit = nil
end
end
end
def execute_after_commit(&callback)
if callback
#_execute_after_commit ||= []
#_execute_after_commit << callback
end
end
end
app/models/user.rb
class User < ActiveRecord::Base
include Callbackable
def verify
if ...
execute_after_commit do |user|
SomeJob.new(user).enqueue
end
end
...
save!
end
end
You can use a method name instead of a block when declaring callbacks:
class User < ActiveRecord::Base
after_commit :do_something!
def do_something!
end
end
To set a condition on the callback you can use the if and unless options. Note that these are just hash options - not keywords.
You can use a method name or a lambda:
class User < ActiveRecord::Base
after_commit :do_something!, if: -> { self.some_value > 2 }
after_commit :do_something!, unless: :something?
def do_something!
end
def something?
true || false
end
end
Assuming that you need to verify a user after create.
after_commit :run_sidekiq_job, on: :create
after_commit :run_sidekiq_job, on: [:create, :update] // if you want on update as well.
This will ensure that your job will run only after a commit to db.
Then define your job that has to be performed.
def run_sidekiq_job
---------------
---------------
end
Hope it helps you :)

How to access attributes from a child module

So i'm working on a sort of custom-rolled history tracking for a RoR application. The part i'm hung up on is getting the logged in users information to tie to the record. I've figured out getting the user, its by a submodule which is attached to the ActionController::Base class. The problem is, I'm having trouble retrieving it from the submodule.
Here is my code:
module Trackable
# This is the submodule
module TrackableExtension
extend ActiveSupport::Concern
attr_accessor :user
included do
before_filter :get_user
end
def get_user
#user ||= current_user # if I log this, it is indeed a User object
end
end
# Automatically call track changes when
# a model is saved
extend ActiveSupport::Concern
included do
after_update :track_changes
after_destroy :track_destroy
after_create :track_create
has_many :lead_histories, :as => :historical
end
### ---------------------------------------------------------------
### Tracking Methods
def track_changes
self.changes.keys.each do |key|
next if %w(created_at updated_at id).include?(key)
history = LeadHistory.new
history.changed_column_name = key
history.previous_value = self.changes[key][0]
history.new_value = self.changes[key][1]
history.historical_type = self.class.to_s
history.historical_id = self.id
history.task_committed = change_task_committed(history)
history.lead = self.lead
# Here is where are trying to access that user.
# #user is nil, how can I fix that??
history.user = #user
history.save
end
end
In my models then its as simple as:
class Lead < ActiveRecord::Base
include Trackable
# other stuff
end
I got this to work by setting a Trackable module variable.
In my TrackableExtension::get_user method I do the following:
def get_user
::Trackable._user = current_user #current_user is the ActionController::Base method I have implemented
end
Then for the Trackable module I added:
class << self
def _user
#_user
end
def _user=(user)
#_user = user
end
end
Then, in any Trackable method I can do a Trackable::_user and it gets the value properly.

Carrierwave delete file from controller

The title says everything. I currently have a model concern with which I'm setting if a model can have attachments or not using include Attachable. So far so good.
Then, when I display the list of files attached to a particular model, I'm adding a link to delete it such as :
DELETE /posts/:post_id/attachments/:id(.:format) attachments#destroy
For that purpose, I created an AttachmentsController with a destroy method. So I have 2 problems here. The first, how can I delete a file from this controller using Carrierwave (for delete the file itself and the table record)?
Second, since my attachable behavior gonna be plug in several model :
DELETE /posts/:post_id/attachments/:id(.:format) attachments#destroy
DELETE /users/:user_id/attachments/:id(.:format) attachments#destroy
...
How can I do in my AttachmentsController to delete a file depending on the associated model dynamically?
class Attachment < ActiveRecord::Base
include Sluggable
belongs_to :attachable, polymorphic: true
mount_uploader :file, AttachmentUploader
validates :name, presence: true, if: :file?
validates :file, presence: true, if: :name?
end
class AttachmentsController < ApplicationController
before_action :authenticate_user!
def destroy
// Don't know how to remove that file
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to root_path
end
end
Hope I was clear.
Thanks
EDIT :
Ok I create a tweak on the params hash in order to get the associated object dynamically within AttachmentsController :
private
def get_attachable_model
params.each do |name, value|
if name =~ /(.+)_id$/
model = name.match(/([^\/.]*)_id$/)
return model[1].classify.constantize
end
end
nil
end
Ok I finally found a solution myself. Here is my destroy method from AttachmentsController :
def destroy
model, param = get_attachable_instance
model_attach = model.find_by slug: params[param.to_sym]
file = model_attach.attachments.find_by slug: params[:id]
file.destroy
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to root_path
end
Not sure if it's the best way to go, but it does works

skip certain validation method in Model

I am using Rails v2.3
If I have a model:
class car < ActiveRecord::Base
validate :method_1, :method_2, :method_3
...
# custom validation methods
def method_1
...
end
def method_2
...
end
def method_3
...
end
end
As you see above, I have 3 custom validation methods, and I use them for model validation.
If I have another method in this model class which save an new instance of the model like following:
# "flag" here is NOT a DB based attribute
def save_special_car flag
new_car=Car.new(...)
new_car.save #how to skip validation method_2 if flag==true
end
I would like to skip the validation of method_2 in this particular method for saving new car, how to skip the certain validation method?
Update your model to this
class Car < ActiveRecord::Base
# depending on how you deal with mass-assignment
# protection in newer Rails versions,
# you might want to uncomment this line
#
# attr_accessible :skip_method_2
attr_accessor :skip_method_2
validate :method_1, :method_3
validate :method_2, unless: :skip_method_2
private # encapsulation is cool, so we are cool
# custom validation methods
def method_1
# ...
end
def method_2
# ...
end
def method_3
# ...
end
end
Then in your controller put:
def save_special_car
new_car=Car.new(skip_method_2: true)
new_car.save
end
If you're getting :flag via params variable in your controller, you can use
def save_special_car
new_car=Car.new(skip_method_2: params[:flag].present?)
new_car.save
end
The basic usage of conditional validation is:
class Car < ActiveRecord::Base
validate :method_1
validate :method_2, :if => :perform_validation?
validate :method_3, :unless => :skip_validation?
def perform_validation?
# check some condition
end
def skip_validation?
# check some condition
end
# ... actual validation methods omitted
end
Check out the docs for more details.
Adjusting it to your screnario:
class Car < ActiveRecord::Base
validate :method_1, :method_3
validate :method_2, :unless => :flag?
attr_accessor :flag
def flag?
#flag
end
# ... actual validation methods omitted
end
car = Car.new(...)
car.flag = true
car.save
Another technique, which applies more to a migration script than application code, is to redefine the validation method to not do anything:
def save_special_car
new_car=Car.new
new_car.define_singleton_method(:method_2) {}
new_car.save
end
#method_2 is now redefined to do nothing on the instance new_car.
Use block in your validation something like :
validates_presence_of :your_field, :if => lambda{|e| e.your_flag ...your condition}
Depending on weather flag is true of false, use the method save(false) to skip validation.

Resources