Ruby On Rails help on Paper_trail gem - ruby-on-rails

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' })

Related

rails active record has_many foreign key after custom function

Consider following models
class User < AR
has_many :resources
end
class Resource < AR
belongs_to :user
end
I have a requirement where the foreign key is saved after applying some function on it. So the value of user_id in resources table does not match id in users table but it can be calculate again from id.
How can I define the association? Let's say that function is dummy_func().
Since belongs_to returns class instance instead of association, you can define methods in Resource class
class Resource < ApplicationRecord
def user
User.find(user_id)
end
def user=(user)
user_id = user.id
end
end
Similar for has_many result in user can be achieved by creating common relation in resources method
class User < ApplicationRecord
def resources
Resource.where(user_id: id)
end
end
So, if you use this code, you can replace any ids in Resource model, and behavior will exactly same as in belongs_to (Maybe there is some difference in depths). And you can achieve very similar behavior in User model, by writing methods by yourself.
Perhaps you can you can use a callback in order to modify the current user_id somehow before saving it: callbacks.
I'd suggest something like :before_save or something of that nature where you define how you want the user_id to be modified in the resources table and then have a way of decrypting it as well.
Maybe you can use an encryption gem to encrypt and decrypt your attribute like attr-encrypted.
Hope this helps a bit!
In the User model, you can override the setter. If you want to encrypt and decrypt the user ID (using attr_encrypted)...
You could try something like this:
attr_encrypted :id, key: ENCRYPTION_KEYS[:value]
def id=(value)
send("encrypted_value=", encrypt(:id, value))
instance_variable_set(:#id, value)
end
Then you can make a method that decrypts the ID
def decrypted_id
decrypt(:id, encrypted_value)
end
Now, when the User is created, the database will set the ID as usual. But it will also create an encrypted_value which stores the id as an encrypted ID. You can use this encrypted value around your app to keep the database ID secret from the interface.
Here is an example in console...

manage roles in rails

I want create roles in my project. Each user can be: admin, registered or demo. Each role see different things.
How can I do that? What is the best gem to do roles?
This is a example in 'bad programming" of what I want:
def index
if current_user.role[:name] == 'admin'
#installations = Installation.all
elsif current_user.role[:name] == 'registered'
#installations = current_user.installations
elsif current_user.role[:name] == 'demo'
#installations = current_user.installations.first
else
end
end
Some gems that might be interesting for you :
rolify
role_model
If you decide to implement it yourself, then within some page you might want to change the content, for that you might want to do something like this :
Add a role to the user model using a migration :
class AddRoleToUsers < ActiveRecord::Migration
def change
add_column :users, :role, :string, default: :demo
end
end
Then in your app you can use it as follows:
def index
case current_user.role
when :admin
#installations = Installation.all
when :registered
#installations = current_user.installations
else
#installations = current_user.installations.first
end
end
You can also simply create a boolean admin for instance.
What you might want to do also is create some methods in your model so that you can call current_user.admin? or current_user.registered? . You can do that by doing (if you chose to use a string to store the role):
class User < ActiveRecord::Base
def admin?
self.role == "admin"
end
def registered?
self.role == "registered"
end
end
One advantage I see of having a role stored in a string is that if you have 5 roles for instance then you do not have 4 booleans (as when you store admin in a boolean) but only one string. On the long run you might want to store actually a role_id instead of a string and have a separate role model.
An excellent alternative pointed out by Jorge de Los Santos (another answer) is to use enum :
class User < ActiveRecord::Base
enum role: [:demo, :admin, :registered]
end
It is an excellent alternative because it will automagically add the methods described above such as current_user.admin? without hard coding them.
With your roles, you might want to do some authorization (admins can have access to specific pages, demo users are restricted to only a subset of pages, etc.). For this, you can use the gem called cancancan. You can look at this railscast to learn more about it. Also, you can have some infos here : How to use cancancan? .
There are plenty of solutions available to you.
Starting by gems:
https://github.com/RolifyCommunity/rolify
https://github.com/martinrehfeld/role_model
By using Devise architecture (in case you use it):
https://github.com/plataformatec/devise/wiki/How-To:-Add-a-default-role-to-a-User
By using enums in rails 4:
class AddRolesToUser < ActiveRecord::Migration
#add_column 'role', :integer, default: 0 to the users table
end
class User < ActiveRecord::Base
enum role: [:demo, :admin, :registered]
end
That will enable role methods.
user = User.find(1)
user.role #:demo
user.admin? #false
user.registered? #false
And consequently:
if user.admin?
#somethig
elsif user.registered?
#another something
else
#another another something.
And last but not least, what you are searching is not the manage roles solution, is the manage permissions solutions:
https://github.com/ryanb/cancan
Add a boolean, :admin to your User model.
class AddAdminToUsers < ActiveRecord::Migration
def change
add_column :users, :admin, :boolean, deafult: false
end
end
Create a method for a registered user to separate them from demo users, such as verifying their email, providing a home address and phone number, filling out a profile, etc. This is up to you though, first you need to decide how a registered and demo user should be different.
The CanCan gem adds authorization to your project, and is especially useful if you want to implement multiple roles with differing abilities. When used with an authentication system like devise, you get a full suite of capability for your site.
You're in full control of what roles you want to define and what abilities they have. CanCan manages tracking, assignment, and querying of roles, and then gets out of your way to let you build what you need.
You can find the CanCan gem in Github: https://github.com/ryanb/cancan
It's simple to use, and the documentation is straightforward and easy to follow.

Create User Account Settings Page in Ruby on Rails with devise

I am new to Ruby on Rails and I have created a project that contains a User table (generated by devise) and a AccountSetting table that contains user specific account settings (this table has a foreign key that relates to the id in the User table thus each User has zero or one AccountSettings). I have my seed data working fine, and I can seed the database with users that have user specific account settings. The User table is related to the AccountSetting table with a "has_one :accountsetting" and the AccountSettings table "belongs_to :user". This all works and makes sense. However, I have a method called "show_user_setting" in my UserSettings controller, and I do not know how to ONLY SHOW the account settings for that specific authenticated user.
So, how can I only display the user setting for the currently logged in user? Again, I am using devise.
My general idea of how to do this would be something like this. However I know this is incorrect, but for the purpose of an explanation, here it is.
def show_user_setting
#setting = AccountSetting.find(current_user)
end
My idea is that the #setting will contain the setting for the currently logged in user. Thanks in advance!
You should do this:
#app/models/account_setting.rb
class AccountSetting < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
class User < ActiveRecord::Base
has_one :account_setting
end
This will allow you to call the following:
#setting = current_user.account_setting
Our Setup
For what it's worth, we do something similar:
#app/models/user.rb
class User < ActiveRecord::Base
before_create :build_profile #-> builds a blank profile on user create
has_one :profile
end
#app/models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
end
This allows us to put all sorts of different options inside the profile model (we have homepage etc):
The important thing to note here is that the above allows you to delegate various methods to the profile model, allowing you to call the following:
current_user.profile_name
current_user.profile_signin_redirect?
current_user.profile_avatar
etc
Have you tried
def show_user_setting
#setting = AccountSetting.find_by(user_id: current_user.id)
end
The way .find() works is it searches the model for the id passed. So, the way you currently have it is your going to try to search for the id of the model, when you want to find the foreign key. So use Model.find_by(column_name: param). You'll what to change user_id: to the column name of what you're storing the foreign key in, I'm just assuming it's something similar to that.
I'm guessing the show_user_setting function is part of a controller, if it is on a model then read this: accessing devise current_user within model
to set the #setting variable you should be able to do this
#setting = AccountSetting.find(user_id: current_user.id)
or
#setting = AccountSetting.find(user: current_user)

Conditional email trigger on Model attribute update (Devise & Rails)

I'm still learning Rails, and using Devise. Currently I am working on a bug/ticket logging system. I'n this system we have tickets created by a user, assigned to another user and all users that can view it can post a reply on it.
I want to trigger an email when a user changes the status of a ticket to closed.
HOWEVER, if you are the creator (of the ticket) and you closed it, you do not want an email, but you want to email the user its assigned to. Likewise, if you are the assignee and you close it, you do not want to email, but you do want to email the creator. If you are neither creator or assignee, you still do not want an email, but you do want to email the other two.
The email will be a small notification noting ticket #_ is closed.
I am a bit tripped up as to where this code should go. There is no new code in the controller but I added a before_update :email_update in my ticket model.
def email_update
#status field is changed
if status_changed? && status.description == "Closed"
if(current_user != assigned_to)
UserMailer.new_ticket_admin(assigned_to, self).deliver
end
if(current_user != user)
UserMailer.new_ticket_admin(user, self).deliver
end
end
end
But, is this not bad practice to access the current user in one of the models? What would be a better approach?
Pretty sure, but I don't think that you can access current_user in the model. Even if you could, might I suggest an alternative. Instead, I would use a closed_by_id attribute where it is the current_user's ID. This way you can also track who closed a ticket. From here, you can check to see if the ticket is closed and if the creator of the ticket's ID is equal to the closed_by_id.
As you mentioned you have a creator and a 'closer' (or whatever you want to call that user). Within your user model you want to have something like this:
class Ticket < ActiveRecord::Base
belongs_to :requested_by, class_name: 'User' # foreign_key requested_by_id
belongs_to :closed_by, class_name: 'User' # foreign_key closed_by_id
def close(user)
self.closed_by = user
self.save
end
# bonus method
def closed?
closed_by?
end
end
def User < ActiveRecord::Base
has_many :tickets, foreign_key: 'requested_by_id'
has_many :closed_tickets, foreign_key: 'closed_by_id'
end
And for your controller something like:
class TicketController < ApplicationController
def create
#ticket = current_user.tickets.build params[:ticket]
end
def close
#ticket = Ticket.find(params[:id])
#ticket.close current_user
end
end
This way there is no need to have current_user within your model. Which probably solves your challege.

Unsure how to model a many-to-many relationship with a flag in Mongoid

I have two entities, projects and users. These are modeled in Rails using Mongoid with two Document instances, User and Project.
In this system, one user can create one project, but many users can follow many projects. For example, as user_id 1 I've created project_id 1. But user_ids 10, 11, 40, and 60 all follow project_id 1. I need to represent a many-to-many relationship between users and projects, and represent a specific user_id as the creator of the project, to assign him editing rights.
Practically speaking, when a user logs-in, he needs to be able to see all projects that he is following, including any that he created, commingled with other projects created by other users. His special creator status wont influence this list at all. When a user looks at a specific project, he needs to be able to view all users following a project, and if he's a creator, he can add new followers and delete existing ones.
In a RDBMS I would represents this with tables users, projects and a users_projects join table with a flag of is_creator. This would easily let me select which projects a user can see, and which users are followers, including which users are creators, of projects.
Mongoid supports many-to-many relationships, but unlike in an RDBMS there's no way for me to put a flag on the relationship. Instead, I'm thinking I'll add a creator field to the projects document, which will contain a link back to an _id field on the users document.
The user->projects relationship might look like this
class User
has_and_belongs_to_many :projects
end
class Project
has_and_belongs_to_many: users
end
But I can't figure out how to map the creator->created_projects relationship. I believe I can reference a user creator in Project like belongs_to :creator, :class_name => 'User' but I'm not sure how to set up the other side.
How best can I model these relationships in Mongoid?
the second version uses less space but you need an extra query to get the user details like usernames. An object ID has 12bytes, max. document size is 16mb so the array could hold around 1.3M user ids (theoretically!)..
here you go:
user.rb
class User
# projects this user owns
has_many :projects
has_many :followed_projects,
:class_name => 'Project',
:foreign_key => :follower_ids
# Uncomment if the relation does not work
#def followed_projects
# Project.where(:follower_ids => self.id)
#end
# get projects that this user has created and projects he is following
def related_projects
Project.any_of({:user_id => self.id}, {:follower_ids => self.id})
end
end
project.rb
class Project
# creator
belongs_to :user
field :follower_ids, :type => Array
# adds a follower
def add_follower!(user_obj)
# adds a user uniquely to the follower_ids array
self.add_to_set(:follower_ids, user_obj.id)
end
def remove_follower!(user_obj)
# remove the user
self.pull(:follower_ids, user_obj.id)
end
end
How to work with it:
#project = Project.first
#some_user = User.last
#project.add_follower!(#some_user)
#some_user.followed_projects
#some_user.related_projects
# create hash like #ids_to_user[user_id] = user
#ids_to_users = User.find(#project.follower_ids).inject({}) {|hsh, c_user| hsh[c_user.id] = c_user; hsh}
#project.followers.each do |c_follower|
puts "I'm #{#ids_to_users[c_follower].username} and I'm following this project!"
end
#project.remove_follower!(#some_user)
create an embedded document which holds all followers with their user_id and their username so you won't have to query the follower's usernames.
The benefits:
Not a single query to lookup a project's followers
Only a single query to lookup a user's followed projects
The downside:
If a user changes his name, you'll have to update all his "followships" but how often
do you change your name compared to how often you lookup your followed projects ;)
If you have many thousand followers per project you may reach the document limit of 16mb
user.rb
class User
# projects this user owns
has_many :projects
def followed_projects
Project.where('followers.user_id' => self.id)
end
# get projects that this user has created and projects he is following
def related_projects
Project.any_of({:user_id => self.id}, {'followers.user_id' => self.id})
end
end
project.rb
class Project
# creator
belongs_to :user
embeds_many :followers
# add an index because we're going to query on this
index 'followers.user_id'
# adds a follower
# maybe you want to add some validations, preventing duplicate followers
def add_follower!(user_obj)
self.followers.create({
:user => user_obj,
:username => user_obj.username
})
end
def remove_follower!(user_obj)
self.followers.destroy_all(:conditions => {:user_id => user_obj.id})
end
end
follower.rb
class Follower
include Mongoid::Document
include Mongoid::Timestamps
embedded_in :project
# reference to the real user
belongs_to :user
# cache the username
field :username, :type => String
end
How to work with it:
#project = Project.first
#some_user = User.last
#project.add_follower!(#some_user)
#some_user.followed_projects
#some_user.related_projects
#project.followers.each do |c_follower|
puts "I'm #{c_follower.username} and I'm following this project!"
end
#all_follower_user_ids = #project.followers.map{|c| c.user_id}
# find a specific follower by user_id
#some_follower = #project.followers.where(:user_id => 1234)
# find a specific follower by username
#some_follower = #project.followers.where(:username => 'The Dude')
#project.remove_follower!(#some_user)
PS: If you want a simpler solution, you could just embedd an array of ObjectIDs (user_ids) in the project and use the atomic updates $addToSet and $pullAll to add/remove a follower. But you'd need an extra query like User.where(:user_id.in => #project.follower_ids) (assuming the array is called follower_ids) to grab all users and their names ;)

Resources