Rails - destroying membership/ownership in user group - ruby-on-rails

I need to be able to "destroy" group ownership when I "destroy" membership of a group if a user is the owner of the group. I have "User", "Group/Cliq", and "Group/CliqMembership" models. When a user creates the group they are the "owner" of the group. Other users can then join the group. When a user leaves the group the membership association for that user and group is destroyed. However, when an owner leaves the group it only removes the "membership" and not the "ownership". I feel like there should be an easy solution, but I'm kind of stuck.
For clarity: Cliqs = Groups; the question is: how do I delete the ownership association and the membership association at the same time? When an "owner" leaves a group I want it to destroy their "group ownership" and their "group membership". As an aside: how would I make the owned group "destroy dependent" when the "owner" leaves?
Here are my models:
class Cliq < ActiveRecord::Base
belongs_to :owner, class_name: 'User'
has_many :cliq_memberships
has_many :members, through: :cliq_memberships, source: :user
end
class CliqMembership < ActiveRecord::Base
belongs_to :cliq
belongs_to :user
end
class User < ActiveRecord::Base
has_one :owned_cliq, foreign_key: 'owner_id', class_name: 'Cliq', dependent: :destroy
has_many :cliq_memberships
has_many :cliqs, through: :cliq_memberships
.
.
.
end
And my controllers:
class CliqsController < ApplicationController
def show
#cliq = Cliq.find(params[:id])
end
def new
#cliq = Cliq.new(params[:id])
end
def create
#cliq = current_user.build_owned_cliq(cliq_params)
#cliq.members << current_user
if #cliq.save
redirect_to current_user
else
redirect_to new_cliq_path
end
end
def destroy
#cliq = current_user.owned_cliq.find(params[:id])
flash[:alert] = "Are you sure you want to delete your Cliq? Your Cliq and all of its associations will be permanently deleted."
#cliq.destroy
if #cliq.destroy
redirect_to current_user
flash[:notice] = "You deleted the Cliq."
else
redirect_to current_user
#set up error handler
flash[:notice] = "Failed to delete Cliq."
end
end
def cliq_params
params.require(:cliq).permit(:name, :cliq_id)
end
class CliqMembershipsController < ApplicationController
def show
end
def create
#Cliq or Cliq_ID?
#cliq = Cliq.find(params[:cliq])
#cliq_membership = current_user.cliq_memberships.build(cliq: #cliq)
#cliq.members << current_user
if #cliq_membership.save
flash[:notice] = "Joined #{#cliq.name}"
else
#Set up multiple error message handler for rejections/already a member
flash[:notice] = "Not able to join Cliq."
end
redirect_to cliq_url
end
def destroy
#cliq_membership = current_user.cliq_memberships.find(params[:id])
#cliq_membership.destroy
if #cliq_membership.destroy
flash[:notice] = "You left the Cliq."
redirect_to user_path(current_user)
else
end
end
end
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
#uploads = Upload.all
#cliq_memberships = CliqMembership.all
#cliqs = Cliq.all
end
end

In your CliqMemberships controller's destroy action, right before destroying the CliqMembership, you can check for the current_user's ownership of the group just like you're checking for his/her membership. I'm assuming you want an implementation wherein if the owner leaves the group, you want the group to automatically be destroyed too, along with all its memberships. (Correct me if I'm wrong on that.) In that case, you can add dependent: :destroy to has_many :cliq_memberships
If the current_user happens to be the owner, you can destroy the cliq altogether which would in turn destroy its cliq_memberships too.
You can do it like this in CliqMemershipsController's destroy action.
def destroy
#cliq_membership = current_user.cliq_memberships.find(params[:id])
#cliq = #cliq_membership.cliq
if #cliq.owner == current_user
#cliq.destroy
flash[:notice] = "The Cliq is destroyed"
redirect_to user_path(current_user)
else
# destroy only the membership
flash[:notice] = "You left the Cliq."
redirect_to user_path(current_user)
end
end

Related

Create record for another model when account is created?

I have a multi-tenant application. When an account is created, the account belongs to an owner and also creates a user record for the owner. This owner can invite other users through a memberships join table. All users except for account owners have memberships to the account.
For users with memberships (not owners of accounts) the account/users/:id show page shows up. I would like the same for account owners, but am receiving the following error message:
ActiveRecord::RecordNotFound in Accounts::UsersController#show
Couldn't find User with 'id'=2 [WHERE "memberships"."account_id" = $1]
def show
#user = current_account.users.find(params[:id])
end
I can add a membership to the owner user in the admin panel and this error goes away, however I would like to add the membership to the owner/user when they create their account.
Any ideas?
Adding #account.memberships.build(user_id: current_user, account_id: current_account) before if #account.save in the accounts controller below does not seem to work.
controllers
user.rb
module Accounts
class UsersController < Accounts::BaseController
before_action :authorize_owner!, only: [:edit, :show, :update, :destroy]
def show
#user = current_account.users.find(params[:id])
end
def destroy
user = User.find(params[:id])
current_account.users.delete(user)
flash[:notice] = "#{user.email} has been removed from this account."
redirect_to users_path
end
end
end
accounts_controller.rb
class AccountsController < ApplicationController
def new
#account = Account.new
#account.build_owner
end
def create
#account = Account.new(account_params)
if #account.save
sign_in(#account.owner)
flash[:notice] = "Your account has been created."
redirect_to root_url(subdomain: #account.subdomain)
else
flash.now[:alert] = "Sorry, your account could not be created."
render :new
end
end
private
def account_params
params.require(:account).permit(:name, :subdomain,
{ owner_attributes: [:email, :password, :password_confirmation
]}
)
end
end
Models
user.rb
class User < ApplicationRecord
has_many :memberships
has_many :accounts, through: :memberships
def owned_accounts
Account.where(owner: self)
end
def all_accounts
owned_accounts + accounts
end
end
account.rb
class Account < ApplicationRecord
belongs_to :owner, class_name: "User"
accepts_nested_attributes_for :owner
validates :subdomain, presence: true, uniqueness: true
has_many :memberships
has_many :users, through: :memberships
end
membership.rb
class Membership < ApplicationRecord
belongs_to :account
belongs_to :user
end
Have you tried callback after_create?
If it works, you will need to figure it on client and admin create an account, self assigning (admin) against on create assigning (client).
# models/account.rb
after_create do
self.memberships.create(user_id: self.owner, account_id: self.id)
end
Ended up answering this question by putting this line under if #account.save:
if #account.save
#account.memberships.create(user_id: #account.owner.id, account_id: #account.id)
Probably not ideal, but it works for now. I might end up making a service or something like that for it, though.

Error getting user's name and email from the associated model in Ruby on Rails

I am creating a webiste where people can debate with each other. It has 4 main models - post, for_the_motion, against_the_motion, and user( added in the respective order). I ran a migration and made a association between for model and against model.
For each view in "for" model I want to show which user added that particular motion. But I am getting an error
undefined method `image_url' for nil:NilClass
Stuck from long time on this. This is how the models look
user.rb
class User < ApplicationRecord
has_many :posts
has_many :fors
has_many :againsts
class << self
def from_omniauth(auth_hash)
user = find_or_create_by(uid: auth_hash['uid'], provider: auth_hash['provider'])
user.name = auth_hash['info']['name']
user.image_url = auth_hash['info']['image']
user.url = auth_hash['info']['urls'][user.provider.capitalize]
user.save!
user
end
end
end
for.rb
class For < ApplicationRecord
belongs_to :post, optional: true
belongs_to :user,optional: true
end
post.rb
class Post < ApplicationRecord
has_many :fors, dependent: :destroy
has_many :againsts, dependent: :destroy
belongs_to :user, optional: true
end
against.rb
class Against < ApplicationRecord
belongs_to :post, optional: true
belongs_to :user, optional:true
end
CONTROLLERS
posts_controller.rb
class PostsController < ApplicationController
def index
#posts = Post.all
end
def land
end
def show
#post = Post.find(params[:id])
end
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
#post.user = current_user
if #post.save
redirect_to #post
else
render 'new'
end
end
private
def post_params
params.require(:post).permit(:title)
end
end
fors_controller.rb
class ForsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#for = #post.fors.create(fors_params)
#for.user = current_user
redirect_to post_path(#post)
end
private
def fors_params
params.require(:for).permit(:content)
end
end
sessions_controller.rb
class SessionsController < ApplicationController
def create
begin
#user = User.from_omniauth(request.env['omniauth.auth'])
session[:user_id] = #user.id
# flash[:success] = "Welcome, #{#user.name}!"
rescue
# flash[:warning] = "There was an error while trying to authenticate you..."
end
redirect_to root_path
def destroy
if current_user
session.delete(:user_id)
# flash[:success] = 'See you!'
end
redirect_to root_path
end
end
end
This is where I am getting the error
<h1><%=#post.title%></h1>
<div class="fort">
<h3>For the motion</h3>
<%#post.fors.each do |f|%>
<p><%=f.content%></p>
<p><%=f.user.image_url%></p>/*This is where errors arise*/
<%end%>
<%= render "fors/form"%>
</div>
<div class="against">
<h3>Against the motion</h3>
<%#post.againsts.each do |f|%>
<p><%=f.content%></p>
<p><%= #post.user.name%></p>
<%end%>
<%= render "againsts/form"%>
</div>
Here is the github link for any other required information
https://github.com/sarfrazbaig/DebatingSociety2
Seems like you missed saving the .user on fors_controller.rb:
class ForsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#for = #post.fors.create(fors_params)
# .create above already will save a new For record in DB
# therefore your #for.user assignation will be only assigned in memory, but not yet in DB
#for.user = current_user
# you'll need to save it again afterwards:
#for.save
redirect_to post_path(#post)
end
# ...
end
Suggestion:
use .new instead of .create to not-yet-save into the DB, and only call save when everything that you need to assign is already assigned.
class ForsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#for = #post.fors.new(fors_params)
#for.user = current_user
#for.save
redirect_to post_path(#post)
end
# ...
end
Take note that you would still encounter that error even if you already updated your code with the above; this is because currently your For records in the DB all are missing the .user value. You'll have to manually assign and save the .user accordingly for each For record, and probably best that you'd write a...
class For < ApplicationRecord
validates :user, presence: true
end
... validation so that this error will be prevented in the future.
One of the #post.fors is lacking a user, which is permitted by the belongs_to :user, optional: true in your For model.
You can restrict your query to showing only fors that have an associated user:
#post.fors.joins(:users) or you can use the safe navigation operator to return nil when attempting to read the image_url for a non-existent user - f.user&.image_url

DB rolls back on create action

I'm trying to create a form with a series of checks to prevent duplicates during the simultaneous creation of three model records: one for the parent (assuming it doesn't exist), one for its child (assuming it doesn't exist), and one for a join table between the child and the User (to allow the User to have their own copy of the Song object).
In the current state of the code, The checks seemingly pass, but
the server logs show ROLLBACK, and nothing gets saved
to the database EXCEPT the parent object (artist).
When I try to use the ids of the object, I get the error undefined method id for nil:NilClass, or "couldn't find object without an ID".
The following code is in my controller:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_by(name: params[:artist][:name].strip.titleize) #look for the artist
#song = Song.find_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize)
if #artist.present? && #song.present?
#user_song = current_user.user_songs.find(#song_id)
if #user_song.present?
render html: "THIS SONG IS ALREADY IN YOUR PLAYLIST"
render action: :new
else
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
end
elsif #artist.present? && !#song.present?
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
elsif !#artist.present?
#artist = Artist.create(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
else
render html: "SOMETHING WENT WRONG. CONTACT ME TO LET ME KNOW IF YOU SEE THIS MESSAGE"
end
end
def index
#songs = Song.all
end
def new
#artist = Artist.new
#artist.songs.build
#user_song = UserSong.new(user_id: current_user.id, song_id: #song_id)
end
def show
#song_id = params["song_id"]
#song = Song.find(params[:id])
end
def destroy
UserSong.where(:song_id => params[:id]).first.destroy
flash[:success] = "The song has been from your playlist"
redirect_to root_path
end
def edit
#song = Song.find(params[:id])
#artist = Artist.find(#song.artist_id)
end
def update
end
private
def set_artist
#artist = Artist.find(params[:id])
end
def artist_params
params.require(:artist).permit(:name, songs_attributes: [:id, :title, :lyrics])
end
def set_song
#song = Song.find(params["song_id"])
end
end
The models:
class Artist < ApplicationRecord
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class Song < ApplicationRecord
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class UserSong < ApplicationRecord
belongs_to :song
belongs_to :user
end
Sorry if I haven't abstracted enough. Not really sure how, given that there's no error message, just a rollback (without any validations present in any of the controllers).
Thanks to #coreyward and his pointing out of the fat-model skinny-controller lemma (never knew that was a thing), I was able to cut the code down and arrive at a solution immediately. In my models, I used validates_uniqueness_of and scope in order to prevent duplication of records. In my controller, I used find_or_create_by to seal the deal.
To whom it may concern, the final code is as follows:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_or_create_by(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.find_or_create_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize) do |song|
song.lyrics = params[:artist][:songs_attributes]["0"][:lyrics].strip
end
#user_song = current_user.user_songs.find_or_create_by(song_id: #song.id) do |user_id|
user_id.user_id = current_user.id
end
redirect_to root_path
end
class Song < ApplicationRecord
validates_uniqueness_of :title, scope: :artist_id
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class Artist < ApplicationRecord
validates_uniqueness_of :name
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class UserSong < ApplicationRecord
validates_uniqueness_of :song_id, scope: :user_id
belongs_to :song
belongs_to :user
end

How to require users to request group membership?

I have a User model, a group model (Cliq), and a group_membership model (Cliq_Membership). Everything seems to be working fine so far. I currently have it so that when a User creates a group they "own" it and when an "owner" leaves the group (destroys their group membership) the entire group is destroyed. A group has one owner and many members. I want to make it so that a User has to request to be a "member". I want the "owner" to be the only one to see the requests and accept/deny the requests.
For Clarity:
I want users to have to request to be group members
I want Cliqs to be able to request Users to be members
Only the owner should be able to see/accept/deny friend requests
I want the relationship to be "two-way/self-referential"; that is, I want the User/Member to be shown as being included in the group and the group as having another member
Cliqs = Groups
How do you accomplish this?
Here is my code so far:
Models:
class User < ActiveRecord::Base
has_many :uploads
has_one :owned_cliq, foreign_key: 'owner_id', class_name: 'Cliq', dependent: :destroy
has_many :cliq_memberships
has_many :cliqs, through: :cliq_memberships
end
class CliqMembership < ActiveRecord::Base
belongs_to :cliq
belongs_to :user
end
class Cliq < ActiveRecord::Base
belongs_to :owner, class_name: 'User'
has_many :cliq_memberships, dependent: :destroy
has_many :members, through: :cliq_memberships, source: :user
end
Controllers:
class CliqMembershipsController < ApplicationController
def create
#Cliq or Cliq_ID?
#cliq = Cliq.find(params[:cliq])
#cliq_membership = current_user.cliq_memberships.build(cliq: #cliq)
#cliq.members << current_user
if #cliq_membership.save
flash[:notice] = "Joined #{#cliq.name}"
else
flash[:notice] = "Not able to join Cliq."
end
redirect_to cliq_url
end
def destroy
#cliq_membership = current_user.cliq_memberships.find(params[:id])
#cliq = #cliq_membership.cliq
if #cliq.owner == current_user
#cliq.destroy
flash[:notice] = "Cliq has been deleted."
redirect_to current_user
else
#cliq_membership.destroy
flash[:notice] = "You left the Cliq."
redirect_to current_user
end
end
end
class CliqsController < ApplicationController
def show
#cliq = Cliq.find(params[:id])
end
def new
#cliq = Cliq.new(params[:id])
end
def create
#cliq = current_user.build_owned_cliq(cliq_params)
#cliq.members << current_user
if #cliq.save
redirect_to current_user
else
redirect_to new_cliq_path
end
end
def destroy
##cliq = current_user.owned_cliq.find(params[:id])
#lash[:alert] = "Are you sure you want to delete your Cliq? Your Cliq and all of its associations will be permanently deleted."
##cliq.destroy
#if #cliq.destroy
#redirect_to current_user
#flash[:notice] = "You deleted the Cliq."
#else
#redirect_to current_user
#set up error handler
#flash[:notice] = "Failed to delete Cliq."
#end
end
def cliq_params
params.require(:cliq).permit(:name, :cliq_id)
end
end
You can create another model and controller for handling user requests
create request.rb model
class Request < ActiveRecord::Base
belongs_to :user
belongs_to :cliq
end
create requests_controller.rb.
class RequestsController < ApplicationController
before_action :set_group
before_action :auth_group_owner
before_action :find_request, except: [:index, :create]
def index
end
def create
#grp.requests.where(user_id: current_user.id).first_or_create
# redirect the user
end
def approv
# add the user to the group
#request.destroy
# redirect
end
def destroy
#delete the request
end
private
def set_group
#find group #grp
end
def auth_group_owner
if current_user != #grp.owner
redirect
end
end
def find_request
#find request
end
end
your routes.rb
resources :groups do
resources :requests, only: [:index, :destroy] do
member do
get 'approv'
end
end
end
The following solution should work without creating a new model for requests. Adding a new boolean field to CliqMembership model to store whether a particular cliq_memberhip is confirmed or not is sufficient. (Let's call that field 'confirmed', for example)
class User < ActiveRecord::Base
has_many :cliq_memberships
has_many :cliqs, through: :cliq_memberships
has_many :confirmed_memberships, -> { confirmed }, class_name: "CliqMembership"
has_many :confirmed_cliqs, through: :confirmed_memberships, source: :cliq
end
class CliqMembership < ActiveRecord::Base
belongs_to :cliq
belongs_to :user
scope :confirmed, -> { where(confirmed: true) }
end
class Cliq < ActiveRecord::Base
has_many :cliq_memberships, dependent: :destroy
has_many :members, through: :cliq_memberships, source: :user
has_many :confirmed_memberships, { confirmed }, class_name: "CliqMembership"
has_many :confirmed_members, through: :confirmed_memberships, source: :user
end
With this, you can set the value of confirmed field to false by default when a new cliq_membership is created by a user. Until the owner update's that particular cliq_membership to change the value of confirmed to true.
Assuming user & cliq are instances of User model & Cliq model respectively, you can now use user.confirmed_cliqs and cliq.confirmed_members.
Edit:
In order to restrict the edit & update actions on cliq_membership to only the cliq owner, you can use a before filter.
class CliqMembershipsController < ApplicationController
before_action :cliq_owner, only: [:edit, :update]
def edit
#cliq_membership = CliqMembership.find(params[:id])
end
def update
#cliq_membership = CliqMembership.find(params[:id])
#cliq_membership.update_attributes(cliq_membership_params)
end
private
def cliq_membership_params
params.require(:cliq_membership).permit(:cliq_id, :user_id, :confirmed)
end
def cliq_owner
#cliq = CliqMembership.find(params[:id]).cliq
redirect_to root_url unless #cliq.owner == current_user
end
end
Hope it works for you.

Update many-to-many association in rails (User-Team relationship aka Membership)

I just created a create feature for adding a team together with adding the members to a said team.
The form contains the following:
Name of the team.
Team's department.
Leader (value is the user id, but displayed as full name)
Members (values are user ids, but displayed also as full names) //I used a special select menu called select2.
Here are the models. I'll only show the associations and some methods related to my problem.
user.rb
class User < ActiveRecord::Base
has_many :memberships
has_many :teams, through: :memberships
accepts_nested_attributes_for :memberships, :teams
end
team.rb
class Team < ActiveRecord::Base
has_many :memberships
has_many :teams, through: :memberships
accepts_nested_attributes_for :memberships, :users
def build_membership(user_ids)
unless user_ids.blank?
user_ids.each do |id|
self.users << User.find_by_id(id)
end
end
end
end
membership.rb
class Membership < ActiveRecord::Base
belongs_to :team
belongs_to :user
#Note that leader and members are both users
end
Here is the controller, with the create and update methods.
class TeamsController < ApplicationController
def create
#team = Team.new(team_params)
#team.build_membership(build_members_array(members_params))
if #team.save
flash.now[:success] = 'Team was successfully created.'
redirect_to #team
else
flash.now[:notice] = #team.errors.full_messages
render "new"
end
end
def update
# TO-DO: update leader and members (i.e. add or remove member)
if #team.update(team_params)
flash.now[:success] = "Team was successfully updated."
redirect_to #team
else
flash.now[:notice] = #team.errors.full_messages
render "edit"
end
end
private
def team_params
params.require(:team).permit(:name,:department)
end
def members_params
params.require(:team).permit(:leader, members:[])
end
def build_user_ids_array(members)
#put ids of leader and members in an array
end
end
It seems that only the name and department attributes are only updated while the leader and the members are not. Should I create my own method again for updating the roster of the team or do something else in mind?
#team.build_membership would be if you are passing in the params to create a new user, but you are using existing User's so this wouldn't apply. You will want to instead fetch all of the User's by their id and then add that the the Team. You also need to permit the id in the members_attributes in your member_params method.
Something like:
def create
#team = Team.new(team_params)
#team.memberships << users_from_params
if #team.save
flash.now[:success] = 'Team was successfully created.'
redirect_to #team
else
flash.now[:notice] = #team.errors.full_messages
render "new"
end
end
def members_params
params.require(:team).permit(:leader, members_attributes: [:id])
end
def users_from_params
#This members_params[:members_attributes].values should be an array of `id`s
User.find(members_params[:members_attributes].values)
end

Resources