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
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.
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.
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.
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
Related
In our web app, a composition has many authors through a table named contributions. We want to check that an admin does not accidentally delete all the authors of one composition in activeadmin (at least one should remain). If this happens, the update fails with an error message and the edit view for a composition is rendered again.
Calling the model validation with
validates_presence_of :authors, on: :update
is not suitable here, because the addition of new contributions (thus authors) is done while calling the success.html on the update function of the activeadmin controller, to prevent some previous bugs that created double entries for authors.
Models are:
class Composition < ApplicationRecord
has_many :contributions
has_many :authors, through: :contributions
end
----
class Contribution < ApplicationRecord
belongs_to :composition
belongs_to :author
end
----
class Author < ApplicationRecord
has_many :author_roles, dependent: :delete_all
has_many :contributions
has_many :compositions, through: :contributions
end
Our code in admin has some background logic to handle what has been described before:
ActiveAdmin.register admin_resource_name = Composition do
...
controller do
def update
author = []
contribution_delete = []
params[:composition][:authors_attributes].each do |number, artist|
if artist[:id].present?
if artist[:_destroy] == "1"
cont_id = Contribution.where(author_id: artist[:id],composition_id: params[:id]).first.id
contribution_delete << cont_id
end
else
names = artist[:full_name_str].strip.split(/ (?=\S+$)/)
first_name = names.size == 1 ? '' : names.first
exist_author = Author.where(first_name: first_name, last_name: names.last, author_type: artist[:author_type]).first
author << exist_author.id if exist_author.present?
end
end if params[:composition][:authors_attributes] != nil
params[:composition].delete :authors_attributes
update! do |success, failure|
success.html do
if author.present?
author.each do |id|
Contribution.create(author_id: id, composition_id: params[:id])
end
end
if contribution_delete.present?
contribution_delete.each do |id|
Contribution.find(id).destroy
end
end
...
redirect_to admin_composition_path(#composition.id)
end
failure.html do
render :edit
end
end
end
end
...
end
Do you have any idea how I can control the authors_attributes and throw a flash message like "There must be at least one author" if the number of to-be-deleted authors is equal to the number of existing authors?
I thought maybe it's possible to handle this before the update! call so to convert the success into a failure somehow, but I have no idea how.
I have classic has_many: through relationship:
class UserGroup < ApplicationRecord
has_many :user_groups_users
has_many :users, through: :user_groups_users
end
class UserGroupsUser < ApplicationRecord
belongs_to :user_group
belongs_to :user
end
class User < ApplicationRecord
has_many :user_groups_users
has_many :user_groups, through: :user_groups_users
end
and in order to destroy UserGroup record, I need to destroy appropriate records in UserGroupsUser, which both is part of a gem. Otherwise I will get back error that there are Users tied to UserGroups and I cannot destroy particular UserGroup.
At the moment in my Controller I have this:
def destroy
#user_group = UserGroup.find(params[:id])
UserGroupsUser.where(user_group_id: #user_group).destroy_all
respond_to do |format|
if #user_group.destroy
format.js { flash.now[:notice] = "User group #{#user_group.name} deleted!" }
format.html { redirect_to user_groups_url }
format.json { head :no_content }
else
format.js { flash[:danger] = "User group #{#user_group.name} cannot be deleted because
#{#user_group.users.size} users belong to it" }
end
end
end
however when I click Delete button in View, it destroys a record before I accept that in my modal window. How do I make it do destroy action, after accept in view, please? As I undestand it would require that after accept, it would firs destroy records in through models and then UserGroup.
My "Delete" action in View is quite regular:
<%= link_to 'Delete', user_group, method: :delete, remote: true,
data: { confirm: "Do you confirm deleting #{user_group.name}?" }, class: 'btn-danger btn btn-xs' %>
To simplify the whole thing, you could just add a before_destroy callback to your UserGroup. It'll only execute when you run #user_group.destroy
class UserGroup < ApplicationRecord
has_many :user_groups_users
has_many :users, through: :user_groups_users
before_destroy do
user_groups_users.destroy_all
end
end
Read ActiveRecord Callbacks
Just change has_many :user_groups_users to has_many :user_groups_users, :dependent => :destroy
See more at Association Basics.
Edit: You said it was in a gem. Not an issue, still! Find the class, and add this in an initializer (I know, I know, there are better places, but for the sake of moving on from this):
Whatever::To::UserGroupThing.class_eval do
has_many :user_group_users, :dependent => :destroy
end
But maintenance may not be your friend here if there's some sort of change to the association made down the line by the maintainer.
You could also use a before_destroy hook in user_group.rb
before_destroy do
UserGroupUser.where(:user_group => self).destroy_all
end
I'm trying to create an app in Ruby on Rails where I can:
Create conferences (working)
See a listing of conferences (working)
Follow and unfollow a conference (not working)
View a following conference (not working)
I've started working with Ruby on Rails back in December and used Micheal Hartl's tutorial to get started. In chapter 14 (https://www.railstutorial.org/book/following_users) Micheal introduces following and unfollowing users, trough relationships.
I'm trying to apply his techniques, but adjusting it to where you have a relation between one model and one controller, to where there are two models and two controllers.
One controller & model is the User_controller and the User Model, the other controller & model are the Conference_controller and the Conference Model.
I started by adding active relations to the User Model, since it's the party that's following the conferences
user.rb
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
I've done the opposite in the Conference Model, because i'ts the party thats being followed
Conference.rb
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent: :destroy
has_many :followers, through: :passive_relationships, source: :follower
To make the structure cleared I've added to following line of code to the Relationship model
Relationship.rb
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "Conference"
validates :follower_id, presence: true
validates :followed_id, presence: true
When trying to see if the user is actually following the conference an error occurs in the User model stating:
ActiveRecord::RecordNotUnique in RelationshipsController#create
SQLite3::ConstraintException: UNIQUE constraint failed: relationships.follower_id, relationships.followed_id: INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)
The root of the problem lies in:
app/models/user.rb:116:in `follow'
app/controllers/relationships_controller.rb:6:in `create'
I understand that the problem occurs when a record cannot be inserted because it would violate a uniqueness constraint, but I don't know what uniqueness constraint is being violated.
Now the first problem happens in the user.rb, when an active_relationship is being created between user and conference.
# Returns true if the current user is following the other user.
def following?(other_conference)
following.include?(other_user)
end
# Follows a conference.
def follow(other_conference)
active_relationships.create(followed_id: other_conference.id)
end
# Unfollows a conference.
def unfollow(other_conference)
active_relationships.find_by(followed_id: other_conference.id).destroy
end
The second problem is in the Relationships_controller, where the current_user should follow the conference.
def create
#conference = Conference.find(params[:followed_id])
current_user.follow(#conference)
respond_to do |format|
format.html { redirect_to #conference }
format.js
end
end
Now i'm not sure what the cause of the problem is and how to solve it. I hope I've made my problem clear and what i'm trying to achieve. If not I would gladly give more information concerning my problem.
You're following an example that handles a more complex case (where you're joining the same table twice) and your solution is a bit more complicated than it needs to be:
class User
has_many :subscriptions
has_many :conferances, though: :subscriptions
def subscribed_to?(conference)
conferances.include?(conference)
end
def find_subscription(conference)
subscriptions.find_by(conference: conference)
end
end
class Conferance
has_many :subscriptions
has_many :users, though: :subscriptions
end
# Its better to name join models after an actual thing
class Subscription
belongs_to :user
belongs_to :conference
end
resources :conferences, shallow: true do
resource :subscriptions, only: [:create, :destroy]
end
class SubscriptionsController
before_action :set_conferance, only: :create
def create
if current_user.subsciptions.create(conferance: #conferance)
flash[:success] = "You are now subscribed to { #conferance.name }"
else
flash[:error] = "Could not create subscription."
end
redirect_to #conferance
end
def destroy
#subscription = current_user.subsciptions.find(params[:id])
if #subscription.destroy
flash[:success] = "You are no longer subscribed to { #conferance.name }"
else
flash[:error] = "Oh noes"
end
redirect_to #subscription.conferance
end
def set_conferance
#conferance = Conferance.find(params[:conferance_id])
end
end
<% if current_user.subscribed_to?(#conferance) %>
<%= button_to "Subscribe", conferance_subsciptions_path(#conferance), method: :post %>
<% else %>
<%= button_to "Unsubscribe", subscription_path(current_user.find_subscription(#conferance)), method: :delete %>
<% end %>
I have a group model that has_many :members, and has_many :memberships. What I would like to do is make it so that in some groups the creator of the group would make it so that you have to request membership in order to join that specific group. How could I set this up in my rails application?
I have added a boolean field to the memberships ActiveRecord but I dont know how to set it up in a way that would allow me to join groups that dont require the "request a membership" function but also to create a "request a membership" function.
as of right now my models look like this:
class Group < ActiveRecord::Base
belongs_to :creator, :class_name => "User"
has_many :members, :through => :memberships
has_many :memberships, :foreign_key => "new_group_id"
has_many :events
end
class User < ActiveRecord::Base
has_many :groups, foreign_key: :creator_id
has_many :memberships, foreign_key: :member_id
has_many :new_groups, through: :memberships
end
class Membership < ActiveRecord::Base
belongs_to :member, class_name: "User"
belongs_to :new_group, class_name: "Group"
validates :new_group_id, uniqueness: {scope: :member_id}
has_many :accepted_memberships, -> {where(memberships: { approved: true}) }, :through => :memberships
has_many :pending_memberships, -> {where(memberships: { approved: false}) }, :through => :memberships
end
and my membership controller:
class MembershipsController < ApplicationController
def create
#group = Group.find(params[:new_group_id])
#membership = current_user.memberships.build(:new_group_id => params[:new_group_id])
if #membership.save
flash[:notice] = "Joined #{#group.name} "
else
flash[:notice] = "You're already in this group"
end
redirect_to groups_url
end
def destroy
#group = Group.find(params[:id])
#membership = current_user.memberships.find_by(params[membership_id: #group.id]).destroy
redirect_to groups_url
end
end
I believe that you are already very close to your solution, and that it is more of a business problem than a technical one. First I would add a boolean to the group to indicate that approval is required. e.g.
rails generate migration add_require_approval_to_groups require_approval:boolean
This would get set when the creator first creates the group depending upon the type of group that they have created.
Now, somehow a user has to be able to discover that there are groups that they can join, and you need to communicate an awareness to them that for some groups, membership is not automatic, but must be approved by the group creator.
So, assuming that you have communicated this to the user, and that they are on a page with a selection box listing all of the groups that they can become a member of (not necessarily the best design choice, but will do for this example). You need to have a query in your model that will gather all of the available groups that a user can still join.
def self.available_groups(user_id)
where("id not in (select group_id from group_members where user_id = ?)", user_id)
.select("id, name")
.collect{ |g| [g.name, g.id] }
end
In your controller:
#available_groups = Group.available_groups(#current_user)
And in your view:
<h2>Please select the group to join:</h2>
<p>
<%= form_tag :action => 'join_group' do %>
<%= select("group", "id",
#available_groups) %>
<%= submit_tag "Join" %>
<% end %>
</p>
Now, when you process the "post" in your membership_controller, you need to inform the creator that someone is trying to join the group that requires approval (perhaps a mailer). If the require_approval boolean is not set, then you need to automatically approve the user so that they can access the group immediately.
I have 3 models: account, player_team and team. Player team serves to associate accounts and teams. Player_team table has account_id and team_id attributes. When I create the team, i should at least have the account who created it belonging to the team. What am i doing wrong ?Any help would be appreciated, Thanks.
def create
#team = Team.new(team_params)
#team.save
#team_player = current_account.player_teams.build(:account_id => current_account.id, :team_id => #team.id)
#team_player.save
respond_with(#team)
end
class Account < ActiveRecord::Base
has_many :player_teams
has_many :teams, through: :player_teams
class Team < ActiveRecord::Base
has_many :player_teams
has_many :accounts, through: :player_teams
end
class PlayerTeam < ActiveRecord::Base
belongs_to :account
belongs_to :team
end
Because you are creating the object right into the controller (instead of just declaring it and opening a form in the view to enter parameters) , you have to use the
new
keyword.
A solution of your problem would be
#team_player = current_account.player_teams.new(:account_id => current_account.id, :team_id => #team.id)
This should work:
def create
#team = Team.new(team_params)
#team.save
#team_player = current_account.build_player_team(:account_id => current_account.id, :team_id => #team.id)
#team_player.save
respond_with(#team)
end
Build on it's own won't save, and saving the parent won't do anything. You need to use build_player_team, or use create() instead of build. Either would work.
def create
#team = Team.new(team_params)
#team.save
#team_player = current_account.player_teams.create(:account_id => current_account.id, :team_id => #team.id)
#team_player.save
respond_with(#team)
end
Note that there's no need to go through all this trouble manually. You could have just said:
respond_with(#team = current_account.teams.create(team_params))