Asso
Asso

Reputation: 53

Rails 5.1, delete multiple records with conditions

I try to delete several records in my DB according to a few conditions. Before I talk about my problem I will explain how my app is working.

A User can create groups and links. This User own the groups he created same for the links. Other Users can then join this group by providing a token (which is created automatically when the group is created) via a Member model. Where member has the user_id and the group_id in the DB. Then once the User is part of a group he can share the links he created in the groups via Grouplink model. Where grouplink has groupd_id and link_id.

I managed to code the fact that if there is no member in a group the group is destroyed. But how do I manage to remove the links a user shared in the group if he leaves the group ? (when the user is destroyed automatically all the links are deleted so I think that the sharing should be gone as well, I have to try that tho'). When a User leaves the group I destroy his member record and he is gone but the links remain. I display the shared links, the fact that you can kick users (member destroy) and the list of the members of a group in the group show btw.

I thought about a few things. I write down what I did in the console

g = Group.find(66)
u = g.users.find(1)
u.links

and that give me all the links from the user in the group. Next to that

g.grouplinks

would give me all the shared links in the group.

g.grouplinks.map(&:link_id) returns [16, 17, 14, 13, 15]
u.links.map(&:id) returns [13, 15]

Now what can I do here ? My user is leaving the group. The member record is destroyed and how can I destroy those grouplinks according to the links the users has ?
Is there a magic trick I don't know yet ?

Thanks for your help.

EDIT

class User < ApplicationRecord

    has_secure_password


  has_many :links, dependent: :destroy
  has_many :grouplinks, dependent: :destroy

  has_many :members, :dependent => :destroy
  has_many :groups, :through => :members
  has_one :owned_group, foreign_key: "owner_id", class_name: "Group"
end

class Member < ApplicationRecord

  belongs_to :user
  belongs_to :group

  validates :user_id, :presence => true
  validates :group_id, :presence => true
  validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}

end

class Link < ApplicationRecord

    has_many :grouplinks, :dependent => :destroy
    belongs_to :user

end

class Grouplink < ApplicationRecord

    belongs_to :group
    belongs_to :link

end

class Group < ApplicationRecord
has_secure_token :auth_token

    has_many :members, :dependent => :destroy
    has_many :users, through: :members, source: :user
    belongs_to :owner, class_name: "User"

    has_many :links, through: :grouplinks
    has_many :grouplinks, :dependent => :destroy

    def to_param
        auth_token
    end

end

I thought that actually I could add the user_id in the grouplinks so I could delete_all according to the user_id in the links and in the groupslinks. Not sure how to that tho' and don't know if there is a better solution.

EDIT 2

I tried your solution within the models. Actually it is smart and I didn't think about that... Problem is now with the creation of my grouplink (share the link). I had this :

  def create
    user = current_user if current_user
    group = user.groups.find_by(auth_token: params[:auth_token])
    share = group.id
    group_link = group.grouplinks.build(link_id: params[:link_id])
    gl = group.grouplinks
    if gl.where(group_id: share).where(link_id: params[:link_id]).exists?
        flash[:error] = "You shared this link in '#{group.name}' already."
        redirect_to mylinks_path
    else
        if group_link.save
          group_link.toggle!(:shared)
          flash[:success] = "You shared your link in '#{group.name}'."
          redirect_to mylinks_path
        else
          render 'new'
        end
    end
  end

And this is obviously not working anymore and I have this error when I try to share a link : First argument in form cannot contain nil or be empty <%= form_for @grouplink do |f| %>.

I tried to change it like this :

def create
    group = Group.find_by(auth_token: params[:auth_token])
    share = group.id
    group_link = group.grouplinks.build(link_id: params[:link_id])
    gl = group.grouplinks
    if gl.where(group_id: share).where(link_id: params[:link_id]).exists?
        flash[:error] = "You shared this link in '#{group.name}' already."
        redirect_to mylinks_path
    else
        if group_link.save
          group_link.toggle!(:shared)
          flash[:success] = "You shared your link in '#{group.name}'."
          redirect_to mylinks_path
        else
          render 'new'
        end
    end
  end

But it is not working either

Upvotes: 0

Views: 976

Answers (4)

amrrbakry
amrrbakry

Reputation: 599

In your Member model, you could use an after_destroy callback to destroy all user links in the group after membership record is destroyed:

class Member < ApplicationRecord
  after_destroy do |record|
    record.user.links.each { |link| link.grouplinks.where(group_id: record.group.id).destroy_all }
  end

  belongs_to :user
  belongs_to :group

  ...

end

Also, I suggest you change Member to Membership to be more clear.

Upvotes: 1

jvillian
jvillian

Reputation: 20263

How about:

class Member < ApplicationRecord

  belongs_to :user
  belongs_to :group
  has_many :group_links, dependent: :destroy

  validates :user_id, :presence => true
  validates :group_id, :presence => true
  validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}

end

and

class Grouplink < ApplicationRecord

  belongs_to :link
  belongs_to :member

end

Now, when a Member record is destroyed (i.e., the user is kicked out of or leaves the group), any links shared with the group (i.e., group_links) are also destroyed. But, if the user has shared the link in another group, the link will continue to be shared with the other groups.

As mentioned by @Pablo in the comments, you probably also want to do:

class Group < ApplicationRecord
  has_secure_token :auth_token

  has_many :members, :dependent => :destroy
  has_many :grouplinks, through: :members
  has_many :users, through: :members, source: :user
  belongs_to :owner, class_name: "User"

  has_many :links, through: :grouplinks

  def to_param
      auth_token
  end

end

Which will allow you to do:

group.grouplinks

I also agree with @amr-el-bakry that Member is a bit confusing. I suggest GroupUser as it makes it quite clear that it is an association between Group and User.

Also, I think it might be a bit more conventional to say GroupLink instead of Grouplink. Or, if you want to stick with naming based on associated classes, perhaps MemberLink. If you change Member to GroupUser, then perhaps GroupUserLink.

I'm thinking your create code should probably look something like:

  def create
    if group
      if member
        if link
          unless group_link
            @group_link = member.group_links.build(link: link)
            if group_link.save
              group_link.toggle!(:shared)
              flash[:success] = "You shared your link in '#{group.name}'."
              redirect_to mylinks_path
            else
              render :new
            end
          else
            flash[:error] = "You shared this link in '#{group.name}' already."
        else
          flash[:error] = "That link does not exist."
          redirect_to somewhere #fix this
        end
      else
        flash[:error] = "You must be a member of this group to add a link."
        redirect_to somewhere #fix this
      end
    else
      flash[:error] = "There is no group with that token."
      redirect_to somewhere #fix this
    end
  end

private

  def group
    @group ||= Group.find_by(auth_token: params[:auth_token])
  end

  def member 
    @member ||= current_user.members.where(group: group)
  end

  def link
    @link ||= Link.find_by(id: params[:link_id])
  end

  def group_link
    @group_link ||= member.group_links.where(link: link)
  end

You may be able to write this as:

  def create

    flash[:error] = "There is no group with that token."
    redirect_to somewhere unless group

    flash[:error] = "You must be a member of this group to add a link."
    redirect_to somewhere unless member

    flash[:error] = "That link does not exist."
    redirect_to somewhere unless link

    flash[:error] = "You shared this link in '#{group.name}' already."
    redirect_to mylinks_path if group_link

    flash[:error] = nil

    @group_link = member.group_links.build(link: link)
    if group_link.save
      group_link.toggle!(:shared)
      flash[:success] = "You shared your link in '#{group.name}'."
      redirect_to mylinks_path
    else
      render :new
    end

  end

But I can't remember if those redirects will give you heartache.

Upvotes: 2

Pablo
Pablo

Reputation: 3005

I think the best idea is that the group_links belong to a member and a link (and not a group and a link). And a member (not a user) has many group_links. When your destroy the member, it will destroy the group_links.

EDIT

This is what jvillian suggested in his answer, just before I did. So I believe his answer is the right one (with some minor enhancements I suggested in comments that jvillian will surely accept and add :-).

EDIT2

Regarding the problem you faced after applying jvillian suggestion, when creating a new grouplink, it must be done from a member (not a group). So in the create action you must search the member (by user_id and group_id) and create the grouplink as member.grouplinks.build

Upvotes: 0

gwalshington
gwalshington

Reputation: 1505

What you're looking for is dependent :delete_all

In your Group Model, you want to you should have a line like:

has_many :links, dependent :delete_all

This says, the group has many links and if you destroy the group, destroy all the related links.

Upvotes: 1

Related Questions