So for some reason, my client will not drop inactive users from their database. Is there a way to globally exclude all inactive users for all ActiveRecord calls to the users table?
EX: User.where("status != 'Inactive'")
I want that to be global so I don't have to include that in EVERY user statement.
Yes, you can set a default scope:
class User < ActiveRecord::Base
default_scope where("status != 'Inactive'")
end
User.all # select * from users where status != 'Inactive'
... but you shouldn't.
It will only lead to trouble down the road when you inevitably forget that there is a default scope, and are confused by why you can't find your records.
It will also play havoc with associations, as any records belonging to a user not within your default scope will suddenly appear to belong to no user.
If you had a simple setup with posts and users, and users had a default scope, you'd wind up with something like this:
# we find a post called 1
p = Post.first # <#post id=1>
# It belongs to user 2
p.user_id # 2
# What's this? Error! Undefined method 'firstname' for `nil`!
p.user.first_name
# Can't find user 2, that's impossible! My validations prevent this,
# and my associations destroy dependent records. Can't be!
User.find(2) # nil
# Oh, there he is.
User.unscoped.find(2) <#user id=2 status="inactive">
In practice, this will come up all the time. It's very common to find a record by it's ID, and then try to find the associated record that owns it to verify permissions, etc. Your logic will likely be written to assume the associated record exists, because validation should prevent it from not existing. Suddenly you'll find yourself encountering many "undefined method blank on nil class" errors.
It's much better to be explicit with your scope. Define one called active, and use User.active to explicitly select your active users:
class User < ActiveRecord::Base
scope :active, -> where("status != 'Inactive'")
end
User.active.all # select * from users where status != 'Inactive'
I would only ever recommend using a default_scope to apply an order(:id) to your records, which helps .first and .last act more sanely. I would never recommend using it to exclude records by default, that has bitten me too many times.
Sure, in your model define a default scope
see here for more info
eg
class Article < ActiveRecord::Base
default_scope where(:published => true)
end
Article.all # => SELECT * FROM articles WHERE published = true
As an alternative to #meagar's suggestion, you could create a new table with the same structure as the Users table, called InactiveUsers, and move people into here, deleting them from Users when you do so. That way you still have them on your database, and can restore them back into Users if need be.
Related
Is there a way to rewrite the process below, which currently uses find_or_initialize_by, using the joins method?
For context - I have users (employees) who record their attendances in the system (a user has many attendances, and an attendance record belongs to a user).
Attendance.find_or_initialize_by(
user: User.find_by(name: 'Bob'),
date: Time.zone.today
)
.update(...) # Update some columns after this
I'm trying to rewrite it using .joins like this:
Attendance.joins(:user)
.where(users: {name: 'Bob'}, date: Time.zone.today)
.first_or_initialize
.update(...) # Update some columns after this
My tests come back with mixed results:
Test cases where the record only needs to be updated pass
Test cases where the attendance record doesn't exist yet (i.e. cases when I have to initialize) fail
The error message is
ActiveRecord::RecordInvalid: Validation failed: User must exist.
But I'm confused because the user actually exists - if I debug using byebug, the user is there.
Rather than starting from the Attendance model, I would tend to start from the User, like this:
User.find_by(name: 'Bob').attendances.find_or_initialize_by(date: Time.zone.today).update(...)
That keeps things easy to read. You could add an association extension method to make things more convenient:
class User < ActiveRecord::Base
has_many :attendances do
def for_date(date)
find_or_initialize_by(date: Time.zone.today)
end
end
end
# Then call with:
User.attendances.for_date(Time.zone.today)
Depending on what you're doing with that attendance record, you could also have your for_date method take extra arguments.
first_or_initialize has been removed according to: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html. Thanks to #engineersmnky for the correction. The method is undocumented, but that looks likes a mistake.
Are you setting Bob's ID in the update call? Basically when you run this, and the where clause returns empty, an empty new record is instantiated. At this moment, all reference to Bob is gone, so you need to set it again in the update:
Attendance.joins(:user)
.where(users: {name: 'Bob'}, date: Time.zone.today)
.first_or_initialize
.update(user: User.find_by(name: 'Bob'), #other stuff ) # Update some columns after this
I have two models one of them is a User, and another Comments. Comments belong to User.
class User < ActiveRecord::Base
act_as_paranoid
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
end
When I do user.delete in my controller I get the expected result of the deleted_at column being set and the record being hidden.
My problem is the Comment associations for the user are set to null. So now on the site it shows no user owning the comment. I'd like the comment to still show the user's name not be "None" or "Anonymous" etc.
Looking at the source on github https://github.com/goncalossilva/rails3_acts_as_paranoid/blob/rails3.2/lib/acts_as_paranoid/core.rb it seems to call run_callbacks which in turn results in Rails 3 falling back to Nullify default for associations.
In my case I just want the user account to be closed off when deleted. Not showing up in queries anymore so that Authlogic will deny them and the User index page won't show them. But still allowing everything a user owns to still be owned by them (since they may come back, etc.).
Is there a better way to do this then acts_as_paranoid?
Rather then go to the trouble of overriding the destroy method I created a close method that simply sets closed_at to a timestamp. If you set default scope to something like:
default_scope { where("closed_at IS NULL") }
Then the model won't show up to any queries including User.All. You can delete the scope to get a full query essentially I took these ideas from act_as_paranoid but much more simplified. The problem is that then even though the Comments still have user_id set, the default scope runs with any association load. So say
c = Comment.first
c.user
That will output nil if user_id is a closed account. In my case the easiest solusion was to remove default scoping and modify my Authlogic function to:
def self.find_by_username_or_email(login)
u = User.find(:first, :conditions => ["lower(username) = ?", login.downcase]) || User.find_by_email(login)
return u unless u.closed_at
end
This way closed accounts can't login. Anywhere I list out users in my views I used a hide_closed scope.
Not sure if this was the best most elegant solution. But for my purposes it works.
I'm currently using Rails 3 + Devise. I'm soft deleting users by setting a Time.now to User.deleted_at.
This deletes the user. Then in my application_controller, I check to get rid of any soft deleted users.
before_filter :no_deleted_users
def no_deleted_users
if current_user && current_user.deleted_at
flash[:alert] = "Access Denied"
return sign_out_and_redirect('/')
end
end
The problem is that soft deleted users still appear through out my app. I have models like:
User (id, deleted_at)
GroupMember(id,user_id)
Comment(id,user_id)
How can I make it so that soft_deleted users never show up in the app? I tried doing this in User.rb:
default_scope :conditions => 'users.deleted_at IS NULL'
But that scope took no effect when I did something like group.group_members. Group_members with user_id records that were soft_deleted were still returned.
Suggestions on how to elegantly handle removing soft_deleted users in an app?
Thanks
Your GroupMember record won't know about the default scope on User. If you specify that a group has many users through group members, then I would expect group.users to return only existing users with your User default scope. For group.group_members to behave as expected, you would need to either delete the record associating the group to the soft deleted user, or maintain a separate default scope in GroupMember, such as (assuming Rails 3):
class GroupMember < ActiveRecord::Base
belongs_to :user
default_scope joins(:user).where('users.deleted_at IS NULL')
# rest of the class
end
I want to make a record management system. The system will have 4 different user roles: Admin, Viewer, Editor and Reviewer.
While the first two are easy to implement using gems such as cancan and declarative authorization, the other two are not so simple.
Basically each new record is created by an Admin (only an Admin can create new records), and should have its own separate Editor and Reviewer roles. That is, a user can be assigned many different roles on different records but not others, so a user might be assigned Editor roles for Record A and C but not B etc.
Editor: can make changes to the record, and will have access to specific methods in the controller such as edit etc.
Reviewer: will be able to review (view the changes) made to the record and either approve it or submit comments and reject.
Viewer: Can only view the most recent approved version of each record.
Are there any ways of handling such record-specific user roles?
This can be accomplished without too much effort with the cancan gem and a block condition. A block condition checks for authorization against an instance. Assuming your Record class had an editors method that returns an array of authorized editors the cancan ability for updating a Record might look something like this:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
...
can :update, Record do |record|
record.editors.include?(user)
end
...
end
end
See "Block Conditions" on the CanCan wiki:
https://github.com/ryanb/cancan/wiki/Defining-Abilities
Update
Storing which users have which access to which records could be done many ways depending on your specific needs. One way might be to create a model like this to store role assignments:
class UserRecordRoles < ActiveRecord::Base
# Has three fields: role, user_id, record_id
attr_accessible :role, :user_id, :record_id
belongs_to :user_id
belongs_to :record_id
end
Now create a has_many association in the User and Record models so that all role assignments can be easily queried. An editors method might look like this:
class Record < ActiveRecord::Base
...
has_many :user_record_roles
def editors
# This is rather messy and requires lot's of DB calls...
user_record_roles.where(:role => 'editor').collect {|a| a.user}
# This would be a single DB call but I'm not sure this would work. Maybe someone else can chime in? Would look cleaner with a scope probably.
User.joins(:user_record_roles).where('user_record_roles.role = ?' => 'editor')
end
...
end
Of course there are many many ways to do this and it varies wildly depending on your needs. The idea is that CanCan can talk to your model when determining authorization which means any logic you can dream up can be represented. Hope this helps!
I have users in my system that can elect to 'hibernate', at which point they can remove themselves and all of their associated records entirely from the system. I have queries all over my site that search within the User table and its associated tables (separated by as many as 5 intermediate tables), and none explicitly test whether the user is hibernating or not.
Is there a way to redefine the User set to non-hibernating users only, so all my current queries will work without being changed individually?
How can I most elegantly accomplish what I'm trying to do?
This is typically done with default scopes. Read all about them
Code from Ryan's site:
class User < ActiveRecord::Base
default_scope :hibernate => false
end
# get all non-hibernating users
#users = User.all
# get all users, not just non-hibernating (break out of default scope)
#users = User.with_exclusive_scope { find(:all) } #=> "SELECT * FROM `users`