Passing a variable in an after_update action - ruby-on-rails

I have an UserProfile model:
class UserProfile < ApplicationRecord
after_update :check_changes
def check_changes
AuditRecord.create(account_id: self.id, fields: self.saved_changes , account_type: 'UserProfile', admin_id: 3) if self.saved_changes?
end
id
user_id
name
last_name
1
1
john
doe
2
2
foo
bar
and AuditRecord model:
id
account_type
account_id
field
admin_id
1
UserProfile
1
{}
3
2
UserProfile
2
{}
3
This AuditRecord saves all of the updates of the profiles, but could be updated by different admins.
How can I send to check_changes function the admin_id? Because right now always is going to be 3.

If I understand correctly, you just want to pass an admin_id to AuditRecord, which means you need another model Admin, which is related to AuditRecord in some way.
Start by generating Admin model, set up the appropriate active record association and references.
Rails docs for this
You should be able to access associations now, with something like admin.auditrecords depending upon how you set up the associations.

after_update is called automatically. You can't pass some variables there dynamically
But you can evenly distribute tasks between admins
class AuditRecord < ApplicationRecord
class << self
def most_free_admin_id
select(:admin_id).
group(:admin_id).
order('COUNT (admin_id)').
first.
admin_id
end
end
end
AuditRecord.most_free_admin_id will generate SQL like this
SELECT admin_id
FROM audit_records
GROUP BY admin_id
ORDER BY COUNT (admin_id)
LIMIT 1;
It will return id of the admin who has the least audit records
Also it's better to create such records not after update, but after commit when record is 100% saved in DB. For this purpose after_update_commit is better
Also in Ruby we use self. only in situations where it is impossible without it
Finally you can apply it in the model
class UserProfile < ApplicationRecord
after_update_commit :check_changes
private
def check_changes
AuditRecord.create(
account_id: id,
fields: saved_changes,
account_type: 'UserProfile',
admin_id: AuditRecord.most_free_admin_id
)
end
end

Related

Select highest ID from group

I'm trying to group a table (oauth_access_tokens) by application_id and select the highest record by ID from the corresponding group (see the complete model below). I've seen this post, which explains how to get the highest ID from the group, but sadly it's not working for my case.
I have a table called oauth_access_tokens with the following attributes: id, resource_owner_id, application_id, token, refresh_token, expires_in, scopes and revoked_at.
The method I have in my model:
def last_token
Doorkeeper::AccessToken.group(:application_id).having('id = MAX(id)')
end
After calling it like so: User.first.last_token (I made sure that there are a few records in the database present).
sql output:
Doorkeeper::AccessToken Load (0.5ms) SELECT `oauth_access_tokens`.* FROM `oauth_access_tokens` GROUP BY `oauth_access_tokens`.`application_id` HAVING id = MAX(id)
Output: #<ActiveRecord::Relation []>
Why don't I get the record with the highest ID? When I run User.first.last_token I expect to see the access_token with the id of 28.
Happy holidays!
Given that id is the primary key of :users table and oauth_access_tokens.resource_owner_id points to users.id, something like this should work:
class User < ApplicationRecord
...
def last_token
# 'id' (or, self.id) in 'resource_owner_id: id' points to the user id, i.e. 'User.first.id' in 'User.first.last_token'
Doorkeeper::AccessToken.group(:application_id).where(resource_owner_id: id).pluck('max(id)').first
end
...
end
Though the above solution should work, but you can improve the query writing to a more readable way by defining the associations inside corresponding models like below:
class User < ApplicationRecord
...
has_many :access_tokens,
class_name: 'Doorkeeper::AccessToken',
foreign_key: :resource_owner_id
...
def last_token
access_tokens.group(:application_id).pluck('max(id)').first
end
...
end
class Doorkeeper::AccessToken < ApplicationRecord
...
self.table_name = 'oauth_access_tokens'
belongs_to :resource_owner, class_name: 'User'
...
end
In both cases, User.first.last_token will return 28 as you would expect.
Update
You are only 1 query away to get the access_token instances instead of mere ids. The method will now return an ActiveRecord::Relation of Doorkeeper::AccessToken instances which meet the defined criteria.
def last_tokens # pluralized method name
token_ids = access_tokens.group(:application_id).pluck('max(id)')
Doorkeeper::AccessToken.where(id: token_ids)
end

Increase a counter in parent when a child is added in has_many association

I have a schema where User has many Student with a user_id field.
In the User table, I am saving a counter next_student_number with default value as 1, and there is a roll_number column in Student.
In the Student class, I have before_create :generate_roll_number callback which sets the student's roll number to next_student_number and increments the value in User class.
Some thing like this :-
def generate_roll_number
self.roll_number = user.next_roll_number
user.increment! :next_roll_number
end
I feel there will be an issue when two records are trying to save at the same time here. Either they'll have a clash, or some roll numbers will be skipped.
What is the best way to implement this?
I think this should work fine:
Controller
def create
Student.transaction do
Student.create(user_id: current_user, ...)
end
end
Student Model
before_create :generate_roll_number
def generate_roll_number
user.increment! :next_roll_number
# Fires query like
# UPDATE users SET next_roll_number=2, WHERE id=xxx
self.roll_number = user.next_roll_number
end
Now, if any error happens while Student record is saved, the transaction will also rollback the incremented next_roll_number value in User table

Ruby On Rails help on Paper_trail gem

So I have a project with authentication of users, and history tracking of some models. For the history I use the paper_trail gem, I have a problem of filtering the history to show to the user, I have configured it to track the current_user id into the Whodunnit field.
My user has role_id which specifies the role from the Roles table
Also i have another table with some items that have id and user_id fields. And now my problem is how to take specific rows from Versions table according to the role of the user, like for ex: if user role is 'SomeRole' it has to return only those actions done by the users that have the same role 'SomeRole'.
I know that i can take out all the actions by
#versions = PaperTrail::Version.order('created_at')
but have no idea on how to filter to select only those that are satisfying for my uses. Is there an easy way to do it or should i hardcode it like selecting one by one, than check all user_id of their roles and so on so forth? Hope you understood my messy way of explaining
In similar situation I used this solution:
1) Added user_id field to versions
class AddUserIdToVersions < ActiveRecord::Migration
def change
add_column :versions, :user_id, :integer
add_index :versions, :user_id
end
end
(whodunnit is a text field and I didn't want to use it as association key)
2) Defined association in Version model:
# /app/models/version.rb
class Version < PaperTrail::Version
belongs_to :user
# ... some other custom methods
end
and in User model:
has_many :versions
3) Set info_for_paper_trail in ApplicationController (or where you need it)
def info_for_paper_trail
{ user_id: current_user.try(:id) } if user_signed_in?
end
So I'm able to access versions like:
#user.versions
So in your case it would be:
Version.joins(:user).where(users: { role: 'SomeRole' })

Single Table Inheritance with Conditions

I have model User and model Recruiter. Currently, these are two separate tables, but I want to make them one.
Current:
User: id, username, password, name
Recruiter: id, user_id
Ideal:
User: id, username, password, role (recruiter, admin)
I understand the basics of STI. What I'm wondering is, when I perform methods on the new Recruiter controller (that inherits from User) how do I make sure all my methods are calling on users that are only a recruiter? Thus, queries along the lines of... SELECT * FROM users WHERE role = 'recruiter' for everything.
That is something rails takes care of for you, out of the box. You do not have to manually query on a particular type of user, just query on the right model.
I must also mention that by default rails assumes that your sti_column is called type, but can be overridden to role easily.
Let's admit you have your 2 classes:
class User < ActiveRecord::Base
end
class Recruiter < User
end
Rails will automagically add a type column in the users table so that in your controller, if you do something like this:
class RecruitersController < ApplicationController
def index
#recruiters = Recruiter.all
end
end
Rails will automatically fetch the records with type = 'Recruiter' and you don't even have to set this manually. If you do:
Recruiter.new(name: 'John').save
A new User will be created in database with the field type set to 'Recruiter'.
you would define your models something like this:
class User < ActiveRecord::Base
...
end
class Recruiter < User
...
def initialize
# ... special initialization for recruiters / you could put it here
super
end
...
end
and to create a new recruiter, you would do this:
Recruiter.create(:name => "John Smith")
and because of the type attribute in the STI user table (set to 'Recruiter'), the record will be for a recruiter.
You could put the special initialization for the STI models either in the model's initializer, or in a before filter with a if-cascade checking the type.
An initializer is probably much cleaner.
Have you tried has_one association?
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one

Is this a bug in Rails validations or am I doing something wrong?

This involves validations on a join table, validating the activerecord on either side of the join against each other. It seems to not behave as expected, allowing a violation of the validation.
I want to allow users to be able to belong to groups, (or groups to users, as it's a many-to-many). But the user's company must match the group's company. Hence, UserGroup looks like this:
class UserGroup < ActiveRecord::Base
belongs_to :user
belongs_to :group
validate :group_company_matches_user_company
private
def group_company_matches_user_company
if user.company != group.company
self.errors.add(:group, "company must match user company")
end
end
end
Now here is a test showing the failure of the validation:
test 'validation failure example' do
group = groups(:default)
user = users(:manager)
#default user and group have the same company in the DB
assert_equal user.company, group.company
#create a 2nd company
company2 = user.company.dup
assert_difference 'Company.count', 1 do
company2.save!
end
#set the group's company to the new one, verify user and group's company don't match
group.company = company2
assert_not_equal user.company, group.company
#WARNING!!! this passes and creates a new UserGroup. even though it violates
#the UserGroup validation
assert_difference 'UserGroup.count', 1 do
group.users << user
end
#What about when we save the group to the DB?
#no issues.
group.save
#this will fail. we have saved a UserGroup who's user.company != group.company
#despite the validation that requires otherwise
UserGroup.all.each do |ug|
assert_equal ug.user.company.id, ug.group.company.id
end
end
Using this collection << object TL:DR bypasses validation
Adds one or more objects to the collection by setting their foreign
keys to the collection’s primary key. Note that this operation
instantly fires update sql without waiting for the save or update call
on the parent object.

Resources