Soft-delete on has_many through association - ruby-on-rails

What is the easiest way to implement soft-deletes on has_many through association?
What I want is something like this:
class Company > ActiveRecord::Base
has_many :staffings
has_many :users, through: :staffings, conditions: {staffings: {active: true}}
end
I want to use Company#users the following way:
the Company#users should be a normal association so that it works with forms and doesn't break existing contract.
when adding a user to the company, a new Staffing with active: true is created.
when removing a user from a company, the existing Staffing is updated active: false (currently it just gets deleted).
when adding a previously removed user to the company (so that Staffing#active == false) the Staffing is updated to active: true.
I thought about overriding the Company#users= method, but it really isn't good enough since there are other ways of updating the associations.
So the question is: how to achieve the explained behaviour on the Company#users association?
Thanks.

has_many :through associations are really just syntactic sugar. When you need to do heavy lifting, I would suggest splitting up the logic and providing the appropriate methods and scopes. Understanding how to override callbacks is useful for this sort of thing also.
This will get you started with soft deletes on User and creating Staffings after a User
class Company < ActiveRecord::Base
has_many :staffings
has_many :users, through: :staffings, conditions: ['staffings.active = ?', true]
end
class Staffing < ActiveRecord::Base
belongs_to :company
has_one :user
end
class User < ActiveRecord::Base
belongs_to :staffing
# after callback fires, create a staffing
after_create {|user| user.create_staffing(active: true)}
# override the destroy method since you
# don't actually want to destroy the User
def destroy
run_callbacks :delete do
self.staffing.active = false if self.staffing
end
end
end

Related

Rails has_many using association ids not calling callback on destroy

The after_commit callback is not being triggered when the has_many relationship is updated and a record is destroyed.
I have a relationship
class Expertise
has_many :doctor_expertises
has_many :doctor_profiles, through: :doctor_expertises
class DoctorExpertise
belongs_to :doctor_profile
belongs_to :expertise
after_commit :do_something
def do_something
# not called when record destroyed
end
in my controller I use the following method to update the has_many relationship
def create
doc = DoctorProfile.find(params[:doctor_id])
doc.expertise_ids = params[:expertise_ids].select do |x|
x.to_i > 0
end
doc.save!
render json: doc.expertises
end
I understand that I should be using update and destroy on the relationship. However, why is after_commit not getting called on a record when it is destroyed?
I'm guessing it has something to do with the way I'm setting doc.expertise_ids not triggering the callback. However I'm unable to find any documentation about this method except for briefly here. Is there documentation that confirms or denies this suspicion or is there something else going on?
From the RailsGuides that you linked:
Automatic deletion of join models is direct, no destroy callbacks are triggered.
Although it doesn't state about after_commit it's quite likely that isn't being fired too
I think the answer you are looking for is here:
Howto use callbacks in a has_many through association?
You need to user after_remove within the has_many declaration
Updating joins model associations, Rails add and remove records on the collection. To remove the records, Rails use the delete method and this one will not call any destroy callback.
Solution 1
One way to invoke some callbacks when add or remove association is using Association Callbacks.
class Expertise
has_many :doctor_expertises
has_many :doctor_profiles, through: :doctor_expertises
before_remove: :my_before_remove,
after_remove: my_after_remove
def my_before_remove(doctor_profile)
...
end
def my_after_remove(doctor_profile)
...
end
end
Solution 2
Force Rails call destroy instead delete when is removing records.
To do that, install the gem replace_with_destroy and pass the option replace_with_destroy: true to the has_many association.
class Expertise
has_many :doctor_expertises
has_many :doctor_profiles, through: :doctor_expertises,
replace_with_destroy: true
...
end
class DoctorExpertise
belongs_to :doctor_profile
belongs_to :expertise
after_commit :do_something
def do_something
# this will be called when updating Expertise.doctor_profiles
# because use destroyed instead delete to remove the record
end
With this, you ensure Rails invoke all the destroy callbacks.
Just add dependent: :destroy like so
has_many :doctor_profiles, through: :doctor_expertises, dependent: :destroy
I know it's somewhat misleading to have dependent destroy on has_many through, but it will trigger destroy callback and it will not destroy the base records, just the join records. Reference: https://github.com/collectiveidea/audited/issues/246

group creator transfered to other user if group creator leaves group

First question: I have a group model that belongs_to :creator, :class_name => "User" and if the creator leaves I want someone else in the group to become the "creator". How would I do this?
As a second question, I would also like to give the leaving creator to choose a new creator when leaving, or let it be automatically assigned.
as of right now these are what my models look like:
class Group < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :members, :through => :memberships
has_many :memberships, :foreign_key => "new_group_id"
and my User model:
class User < ActiveRecord::Base
has_many :groups, foreign_key: :creator_id
has_many :memberships, foreign_key: :member_id
has_many :new_groups, through: :memberships
As a third question, I would like the group to be destroyed when the creator leaves. How can I set up this kind of relation?
These are 3 questions, and the first two are quite open, so I'd try to answer all of them in order making some assumptions down the road.
First question
This depends on what do you want the behavior for choosing a new creator to be. Do you want it to be automatically assigned from the current members? Do you want to give other members to have the change to auto-assign themselves as creator? In the latter you need to provide your members a full UI (routes, controllers, views) for that purpose, so I'll show you how would I code the first option.
First, I'd encapsulate the group leaving logic into its own method on the Group model, that we'll use in the controller for this purpose. On Group we define the logic for assigning as new creator. The logic here will be to pass the method a new_creator if we have it, or default to the first of the members if not given.
class Group < ActiveRecord::Base
def reassign(new_creator = nil)
new_creator ||= members.first
if new_creator
self.creator = new_creator
save
else
false
end
end
end
As an alternative approach, you can move this logic into an Observer (or any alternative) that will observe on Group model for the attribute creator_id to be nulled.
Second question
This one will involve a full UI that you'll need to figure out yourself according to your specifications. In short, I'd create a new action in your GroupsController for members to leave groups like this:
# config/routes.rb
resources :groups do
member do
get :leave
patch :reassign
end
end
# app/controllers/groups_controller.rb
class GroupsController < ApplicationController
def leave
#group = Group.find(params[:id])
end
def reassign
#group = Group.find(params[:id])
if #group.reassign(params[:new_creator_id])
redirect_to my_fancy_path
else
render :leave
end
end
end
In your views you'll have your form_for #group with the new member candidates (possible with a select_tag :new_creator_id) and the rest of your UI as you prefer.
Third question
This is the easiest. You just define your association like this and you'll get all User groups destroyed after the user is deleted.
class User < ActiveRecord::Base
has_many :groups, foreign_key: :creator_id, dependent: :destroy
end

Simulating has_many :through with Mongoid

I'm trying to create an event platform using MongoDB as the db. I want a many-to-many relationship between Events and Users. The thing is, I want there to be properties in the relationship (e.g., Users can either be confirmed or unconfirmed for a specific Event). I realize this would be ideally suited for an RDBMS, but I'm using MongoDB for reasons that I'm taking advantage elsewhere and I would prefer to continue using it.
What I would like is for each Event to embed many Guests, which belong to Users. That way, I can see which users are attending an event quickly and with only one query. However, I would also like to see which Events a User is attending quickly, so I would like each User to have an array of Event ids.
Here is a code summary.
# user of the application
class User
has_many :events
end
# event that users can choose to attend
class Event
embeds_many :guests
has_many :users, :through => :guests # Won't work
end
# guests for an event
class Guest
field :confirmed?, type: Boolean
embedded_in :event
belongs_to :user
end
# Ideal use pattern
u = User.create
e = Event.create
e.guests.create(:user => u, :confirmed => true)
With the ideal use pattern, e has a Guest with a reference to u and u has a reference to e.
I know the has_many :through line won't work. Any suggestions as to how to get similar functionality? I was thinking of using an after_create callback in Guest to add a reference to the Event in User, but that seems pretty hacky.
Maybe I've gone down the wrong path. Suggestions? Thanks.
You can just store the event ids in a array on the user.
You have to manage the array when the event changes or the user is removed from the event for some reason. But that is the trade off.
User.events can then be found with a single db call.
Look at observers to manage the association.
I ended up using callbacks in the models to accomplish I wanted. Here's what it looks like.
Edit: I just saw nodrog's answer. Yeah, using observers would probably have been neater, I didn't know about them. Thanks!
# user of the application
class User
has_and_belongs_to_many :events, inverse_of: nil, dependent: :nullify
end
# event that users can choose to attend
class Event
embeds_many :guests
index 'guests.user_id', unique: true
before_destroy :cleanup_guest_references
def cleanup_guest_references
self.guests.each do |guest|
guest.destroy
end
end
end
# guests for an event
class Guest
field :confirmed?, type: Boolean
embedded_in :event, :inverse_of => :guests
belongs_to :user
after_create :add_event_for_user
before_destroy :remove_event_for_user
private
def add_event_for_user
self.user.events.push(self.event)
end
def remove_event_for_user
self.user.events.delete self.event
self.user.save
end
end

Making record available only to certain models in Rails 3

I have a weird design question. I have a model called Article, which has a bunch of attributes. I also have an article search which does something like this:
Article.project_active.pending.search(params)
where search builds a query based on certain params. I'd like to be able to limit results based on a user, that is, to have some articles have only a subset of users which can see them.
For instance, I have an article A that I assign to writers 1,2,3,4. I want them to be able to see A, but if User 5 searches, I don't want that user to see. Also, I'd like to be able to assign some articles to ALL users.
Not sure if that was clear, but I'm looking for the best way to do this. Should I just store a serialized array with a list of user_id's and have -1 in there if it's available to All?
Thanks!
I would create a join table between Users and Articles called view_permissions to indicate that a user has permission to view a specific article.
class ViewPermission
belongs_to :article
belongs_to :user
end
class User
has_many :view_permissions
end
class Article
has_many :view_permissions
end
For example, if you wanted User 1 to be able to view Article 3 you would do the following:
ViewPermission.create(:user_id => 1, :article_id => 3)
You could then scope your articles based on the view permissions and a user:
class Article
scope :viewable_by, lambda{ |user| joins(:view_permissions).where('view_permissions.user_id = ?', user.id) }
end
To search for articles viewable by a specific user, say with id 1, you could do this:
Article.viewable_by(User.find(1)).project_active.pending.search(params)
Finally, if you want to assign an article to all users, you should add an viewable_by_all boolean attribute to articles table that when set to true allows an article to be viewable by all users. Then modify your scope to take that into account:
class Article
scope :viewable_by, lambda{ |user|
joins('LEFT JOIN view_permissions on view_permissions.article_id = articles.id')
.where('articles.viewable_by_all = true OR view_permissions.user_id = ?', user.id)
.group('articles.id')
}
end
If an Article can be assigned to multiple Writers and a Writer can be assigned to multiple Articles, I would create an Assignment model:
class Assignment < AR::Base
belongs_to :writer
belongs_to :article
end
Then you can use has_many :through:
class Article < AR::Base
has_many :assignments
has_many :writers, :through => :assignments
end
class Writer < AR::Base
has_many :assignments
has_many :articles, :through => :assignments
end

How do I create/maintain a valid reference to a particular object in an ActiveRecord association?

Using ActiveRecord, I have an object, Client, that zero or more Users (i.e. via a has_many association). Client also has a 'primary_contact' attribute that can be manually set, but always has to point to one of the associated users. I.e. primary_contact can only be blank if there are no associated users.
What's the best way to implement Client such that:
a) The first time a user is added to a client, primary_contact is set to point to that user?
b) The primary_contact is always guaranteed to be in the users association, unless all of the users are deleted? (This has two parts: when setting a new primary_contact or removing a user from the association)
In other words, how can I designate and reassign the title of "primary contact" to one of a given client's users? I've tinkered around with numerous filters and validations, but I just can't get it right. Any help would be appreciated.
UPDATE: Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
My solution is to do everything in the join model. I think this works correctly on the client transitions to or from zero associations, always guaranteeing a primary contact is designated if there is any existing association. I'd be interested to hear anyone's feedback.
I'm new here, so cannot comment on François below. I can only edit my own entry. His solution presumes user to client is one to many, whereas my solution presumes many to many. I was thinking the user model represented an "agent" or "rep" perhaps, and would surely manage multiple clients. The question is ambiguous in this regard.
class User < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :clients, :through => :user_client
end
class UserClient < ActiveRecord::Base
belongs_to :user
belongs_to :client
# user_client join table contains :primary column
after_create :init_primary
before_destroy :preserve_primary
def init_primary
# first association for a client is always primary
if self.client.user_clients.length == 1
self.primary = true
self.save
end
end
def preserve_primary
if self.primary
#unless this is the last association, make soemone else primary
unless self.client.user_clients.length == 1
# there's gotta be a more concise way...
if self.client.user_clients[0].equal? self
self.client.user_clients[1].primary = true
else
self.client.user_clients[0].primary = true
end
end
end
end
end
class Client < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :users, :through => :user_client
end
Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
I would do this using a boolean attribute on users. #has_one can be used to find the first model that has this boolean set to true.
class Client < AR::B
has_many :users, :dependent => :destroy
has_one :primary_contact, :class_name => "User",
:conditions => {:primary_contact => true},
:dependent => :destroy
end
class User < AR::B
belongs_to :client
after_save :ensure_only_primary
before_create :ensure_at_least_one_primary
after_destroy :select_another_primary
private
# We always want one primary contact, so find another one when I'm being
# deleted
def select_another_primary
return unless primary_contact?
u = self.client.users.first
u.update_attribute(:primary_contact, true) if u
end
def ensure_at_least_one_primary
return if self.client.users.count(:primary_contact).nonzero?
self.primary_contact = true
end
# We want only 1 primary contact, so if I am the primary contact, all other
# ones have to be secondary
def ensure_only_primary
return unless primary_contact?
self.client.users.update_all(["primary_contact = ?", false], ["id <> ?", self.id])
end
end

Resources