Creating a Conversation between 3 users in a rails chat app - ruby-on-rails

Currently, it requires recipient_id and sender_id to start a Conversation between 2 users.
How would the associations work if I wanted to allow a 3rd user to join a Conversation?
When a user starts a conversation, the conversation.id belongs to sender_id and recipient_id.
I want visitor_id(which is the current_user) to be able to join any conversation and all users to be able to view all conversations.
conversation.rb
class Conversation < ActiveRecord::Base
belongs_to :sender, :foreign_key => :sender_id, class_name: 'User'
belongs_to :recipient, :foreign_key => :recipient_id, class_name: 'User'
has_many :messages, dependent: :destroy
validates_uniqueness_of :sender_id, :scope => :recipient_id
scope :involving, -> (user) do
where("conversations.sender_id =? OR conversations.recipient_id =?",user.id,user.id)
end
scope :between, -> (sender_id,recipient_id) do
where("(conversations.sender_id = ? AND conversations.recipient_id =?) OR (conversations.sender_id = ? AND conversations.recipient_id =?)", sender_id,recipient_id, recipient_id, sender_id)
end
end
user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :conversations, :foreign_key => :sender_id
after_create :create_default_conversation
I'm curious is if it's easy as adding
belongs_to :visitor, :foreign_key => :visitor_id, class_name: 'User'
to the conversation.rb model. But i'm not sure how i can get visitor_id which belongs to current_user to join a specific conversation (or make all conversations viewable to everyone).
EDIT: Added message.rb and controllers.
message.rb
class Message < ActiveRecord::Base
belongs_to :conversation
belongs_to :user
validates_presence_of :body, :conversation_id, :user_id
end
conversations_controller.rb
class ConversationsController < ApplicationController
before_filter :authenticate_user!
layout false
def create
if Conversation.between(params[:sender_id],params[:recipient_id]).present?
#conversation = Conversation.between(params[:sender_id],params[:recipient_id]).first
else
#conversation = Conversation.create!(conversation_params)
end
render json: { conversation_id: #conversation.id }
end
def show
#conversation = Conversation.find(params[:id])
#reciever = interlocutor(#conversation)
#messages = #conversation.messages
#message = Message.new
end
private
def conversation_params
params.permit(:sender_id, :recipient_id)
end
def interlocutor(conversation)
current_user == conversation.recipient ? conversation.sender : conversation.recipient
end
end
messages_controller.rb
class MessagesController < ApplicationController
before_filter :authenticate_user!
def create
#conversation = Conversation.find(params[:conversation_id])
#message = #conversation.messages.build(message_params)
#message.user_id = current_user.id
#message.save!
##path = conversation_path(#conversation)
end
private
def message_params
params.require(:message).permit(:body)
end
end

Adding :visitor would allow for the third party with minimal changes, but would not allow for a 4th or kth additional visitor easily. This seems a likely scenario and for that you'd need a join table that tracks all users_conversations.
First create a migration for the join table:
class CreateUsersConversationsJoinTable < ActiveRecord::Migration
def change
create_table :users_conversations do |t|
t.integer :user_id, null: false
t.integer :conversation_id, null: false
end
add_index :users_conversations, [:user_id, :conversation_id], unique: true
end
end
Then create a model along these lines:
class UsersConversations
belongs_to :users, class_name: User, inverse_of: :users_conversations
belongs_to :conversations, class_name: Conversation, inverse_of: :users_conversations
validates :user_id, :conversation_id, presence: true
end
You'd need to also create migration to move sender and receiver into this new model.
You'd need to move the sender attributes from conversation to message and make conversations know about senders via messages: has_many :senders through: message, foreign_key: sender_id. That way each message tracks its senders.
Your receiver becomes all users in a conversation that didn't send a particular message. Your message class would need something like this added:
has_many :users_conversations, through: :conversations
has_many :receivers, -> { where('users_conversations.user_id != messages.sender_id') }, through: :users_conversations, foreign_key: :user_id, class_name: 'User'
While your conversation class would need to change :between to use/be replaced by users and your scope would look like:
scope :involving, -> (user) do
users_conversations.where(user: user)
end
To get all conversations you can simply assign #conversations = Conversation.all in the relevant controller, or use the controller index route.

Related

Ruby on Rails - Follower notification using noticed

My goal is to display in the notifications modal that a follower has started following the current user whenever the user clicks the follow button.
I am also using the noticed gem, but it seems a bit complicated for me to implement with my relationship model (Which is the follower/following model).
Whenever I follow someone, I see in the console that it is inserting the notification, but when I click unfollow I get an error that there are "too many has_many associations". And when I log in as the user that gets followed the notification does not appear. I am assuming because I have implemented the notify recipient function wrong.And I cannot seem to find any resources only for follow notifications.
Here is my code:
FollowNotification.rb
def message
#user = User.find(follower_id: params[:user_id])
"#{#user.username} has started following you"
end
#
def url
show_user_path(#user)
end
Relationships Controller
class RelationshipsController < ApplicationController
before_action :authenticate_user!
def create
#user = User.find(params[:user_id])
# if statement prevents user from forcing both users to follow each other after accepting request
if current_user.Is_private? && !#user.pending_requests
following = #user.relationships.build(follower_id: current_user.id)
following.save
redirect_to request.referrer || root_path
else
following = current_user.relationships.build(follower_id: params[:user_id])
following.save
redirect_to request.referrer || root_path
end
end
def destroy
following = current_user.relationships.find_by(follower_id: params[:user_id])
following.destroy
redirect_to request.referrer || root_path
end
end
Relationship model
class Relationship < ApplicationRecord
belongs_to :following, class_name: 'User'
belongs_to :follower, class_name: 'User'
has_noticed_notifications model_name: 'Notification'
has_many :notifications, through: :user, dependent: :destroy
after_create_commit :notify_recipient
before_destroy :cleanup_notifications
private
def notify_recipient
FollowNotification.with(follower: self).deliver_later(following.id)
end
def cleanup_notifications
notifications_as_follow.destroy_all
end
end
User model
class User < ApplicationRecord
has_merit
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_one_attached :avatar
validates :avatar, file_size: { less_than_or_equal_to: 5.megabytes },
file_content_type: { allow: ['image/jpg', 'image/png', 'image/jpeg'] }
has_many(
:posts,
class_name: 'Post',
foreign_key: 'user_id',
inverse_of: :user
)
has_many :likes
has_many :comments
validates :username, presence: true, length: {maximum: 30}
validates_uniqueness_of :username
has_many :relationships, foreign_key: :following_id
has_many :followings, through: :relationships, source: :follower
has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: :follower_id
has_many :followers, through: :reverse_of_relationships, source: :following
def is_followed?(user)
reverse_of_relationships.find_by(following_id: user.id).present?
end
has_many :notifications, as: :recipient, dependent: :destroy
end

Rails ActiveModel designing belongs_to and has_many with single class

I need some help modeling my models and controller. Here is what I want to achieve:
I want to have a devise user named User (as usual) and a second model named Project. A Project should belong to a single User and at the same time should have many participants. The participants in a project should also be users (with devise registration/login) but the user, that created the project should not be able to participate.
So far, so good. Here comes the tricky part: In my controller I want to be able to write:
def participate
p = Project.find(id: params[:id])
p.participants << current_user unless p.participants.includes?(current_user) && !p.user_id.equal(current_user.id)
if p.save
redirect_back
else
render :project
end
end
This doesn't work because p.participants is not an array and the query (I tried it in rails console) does not check my n:m table.
Here is my current model setup:
class Project < ApplicationRecord
before_validation :set_uuid, on: :create
validates :id, presence: true
belongs_to :user
has_and_belongs_to_many :participants, class_name: "User"
end
class User < ApplicationRecord
before_validation :set_uuid, on: :create
validates :id, presence: true
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_and_belongs_to_many :projects
end
Finally my migrations:
class CreateProjects < ActiveRecord::Migration[6.0]
def change
create_table :projects, id: false do |t|
t.string :id, limit: 36, primary_key: true
t.string :title
t.belongs_to :user, index: true, foreign_key: true, type: :uuid
t.datetime :published_at
t.timestamps
end
end
end
class CreateJoinTableProjectsUsers < ActiveRecord::Migration[6.0]
def change
create_join_table :users, :projects do |t|
t.index :project_id
t.index :user_id
end
end
end
It is better to use has_many: through instead of has_and_belongs_to_many. This allows you to write cleaner code for validation.
Remove has_and_belongs_to_many from User and Project models
Add has_many :through to User and Project models
rails g model UserProject user:references project:references
rails db:migrate
class User < ApplicationRecord
..
has_many :user_projects
has_many :projects, through: :user_projects
..
end
class Project < ApplicationRecord
..
has_many :user_projects
has_many :participants, through: :user_projects, source: 'user'
..
end
class UserProject < ApplicationRecord
belongs_to :user
belongs_to :project
end
Add validation to UserProject model
class UserProject < ApplicationRecord
belongs_to :user
belongs_to :project
validate :check_participant
private
def check_participant
return if project.participants.pluck(:id).exclude?(user.id) && project.user != user
errors.add(:base, 'You cannot be participant')
end
end
Update participate method
def participate
p = Project.find(id: params[:id])
begin
p.participants << current_user
redirect_back
rescue ActiveRecord::RecordInvalid => invalid
puts invalid.record.errors
render :project
end
end

Undefined method 'about' for nill class

I have two table: User has one Account, Account belongs_to User. When I want to get some value from field 'about' (table Accounts), I have a problem 'undefined method about' for nil:NilClass'. The challenge is to get the list of followers and gain their avatar or information 'about' from another table and output it in View.
My method in controller
def list_of_follower
#followers_id = Follow.select("follower_id ").where("followable_id = ?", current_user)
#followers = User.where("id in (?)", #followers_id)
#followables_id = Follow.select("followable_id").where("follower_id = ?", current_user)
#followables = User.where("id in (?)", #followables_id)
end
View list_of_follower.html.haml
%h1 My followers
- #followers.each do |f|
%ul.list-group
%li.list-group-item
%p=f.name
%p=f.account.about
%h1 I'm follower
- #followables.each do |followable|
%ul.list-group
%li.list-group-item
%p=followable.name
%p=followable.account.about
Create_Accounts.rb
class CreateAccounts < ActiveRecord::Migration
def change
create_table :accounts do |t|
t.belongs_to :user, index: true
t.text :about
t.timestamps null: false
end
end
end
User.rb
class User < ActiveRecord::Base
acts_as_followable
acts_as_follower
acts_as_liker
has_one :account
has_many :posts
has_many :comments
accepts_nested_attributes_for :account
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
def email_required?
false
end
def email_changed?
false
end
validates :login, :email, uniqueness: true
end
Account.rb
class Account < ActiveRecord::Base
belongs_to :user
mount_uploader :avatar, AvatarUploader
end
table User content is displayed without problems (work requests), but the contents of the table associated with it does not displayed, what is the problem?
I think, a problem is not every user has an account.
You can try this:
%p=f.account.try(:about)

Rails User Groups - Set Group Owner In Another Model

I have user created groups in my application. I'm confused as to how to set the user that creates the group as an owner. I want there to be able to be multiple owners so it's a 'has-many-through' relationship. I can create/edit/delete a group.
So my question is how do I insert the current user_id and the group_id into the group_owners table at the time that the group is created?
Here is what I have that works so far:
User Model
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable
has_many :group_owners
has_many :user_groups, through: :group_owners
end
Group Model
class UserGroup < ActiveRecord::Base
has_many :goup_owners
has_many :users, through: :groups_owners
validates :name, presence: true, length: {minimum: 5}
validates :visibility, presence: true, length: {minimum: 5}
VISIBILITY_TYPES = ["Public", "Private"]
end
Group Owner Model
class GroupOwner < ActiveRecord::Base
belongs_to :user
belongs_to :user_group
end
User Groups Controller - Create Action
def create
#usergroup = UserGroup.new(usergroup_params)
if #usergroup.save
redirect_to user_groups_path
else
render 'new'
end
end
I assume something needs to go in the user group create method but I'm not sure what.
Thanks for any help you can offer.
You should create UserGroup like
def create
#usergroup = current_user.user_groups.build(usergroup_params)
if #usergroup.save
redirect_to user_groups_path
else
render 'new'
end
end
This way the user group will be created with current users id and the group id into the group owners table.
In you UserGroup model, set a boolean for owner.
create_table |t|
t.references :user
t.references :group
t.boolean :owner
end
class UserGroup < ActiveRecord::Base
belongs_to :user
belongs_to :owner
scope :groups, ->(*g) {where(group_id: g.flatten.compact.uniq)}
scope :users, ->(*u) { where(user_id: u.flatten.compact.uniq)}
scope :owners, ->{where owner:true}
end
class User < ActiveRecord::Base
has_many :user_groups, dependent: :destroy, inverse_of: user
has_many :groups, through: :user_groups
def owned_groups
groups.merge(UserGroup.owners)
end
end
The following changes to the user group controller create method fixed my issue.
def create
#user_group = current_user.user_groups.build(usergroup_params)
if #user_group.save
#user_group.users << current_user
redirect_to user_groups_path
else
render 'new'
end
end

Rails destroy action destroying the wrong record

I posted a similar question earlier and I thought that I had fixed this problem but I was wrong. I thought that the activity wasn't being deleted but turns out that it was deleting activity it was just the wrong one. I don't have this problem with any of my other models and this only happens on one of my apps. My forked app which has the exact same code works correctly. I don't know why this is happening.
projects_controller.rb
class ProjectsController < ApplicationController
before_filter :find_project, only: [:edit, :update, :destroy]
def create
params[:project][:about] = sanitize_redactor(params[:project][:about])
#project = current_member.projects.new(params[:project])
respond_to do |format|
if #project.save
current_member.create_activity(#project, 'created')
format.html { redirect_to #project }
format.json { render json: #project, status: :created, location: #project }
else
format.html { render action: "new" }
format.json { render json: #project.errors, alert: 'Please make sure all required fields are filled in and all fields are formatted correctly.' }
end
end
end
def destroy
#project = Project.find(params[:id])
#activity = Activity.find_by_targetable_id(params[:id])
if #activity
#activity.destroy
end
#project.destroy
respond_to do |format|
format.html { redirect_to profile_projects_path(current_member) }
format.json { head :no_content }
format.js
end
end
def find_project
#project = current_member.projects.find(params[:id])
end
end
member.rb
class Member < ActiveRecord::Base
def create_activity(item, action)
activity = activities.new
activity.targetable = item
activity.action = action
activity.save
activity
end
end
Migration
class CreateActivities < ActiveRecord::Migration
def change
create_table :activities do |t|
t.integer :member_id
t.string :action
t.integer :targetable_id
t.string :targetable_type
t.timestamps
end
add_index :activities, :member_id
add_index :activities, [:targetable_id, :targetable_type]
end
end
****EDIT****
activity.rb
class Activity < ActiveRecord::Base
belongs_to :member
belongs_to :targetable, polymorphic: true
acts_as_votable
self.per_page = 36
def self.for_member(member, options={})
options[:page] ||= 1
following_ids = member.following_members.map(&:id).push(member.id)
where("member_id in (?)", following_ids).
order("created_at desc").
page(options[:page])
end
end
project.rb
class Project < ActiveRecord::Base
belongs_to :member
attr_accessible :about, :blurb, :category, :markers, :video, :website, :name, :avatar, :banner, :marker_list, :city
acts_as_votable
acts_as_followable
acts_as_ordered_taggable
acts_as_ordered_taggable_on :markers
acts_as_messageable
has_many :comments, as: :commentable, :dependent => :destroy
has_many :uploads, as: :uploadable, :dependent => :destroy
has_many :updates, as: :updateable, :dependent => :destroy
def to_param
"#{id}-#{name.parameterize}"
end
end
member.rb
class Member < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :email_confirmation, :password, :password_confirmation, :remember_me,
:full_name, :user_name, :pursuits, :avatar, :bio, :city, :state, :country, :pursuit_list,
:facebook, :twitter, :linkedin, :soundcloud, :youtube, :vimeo, :instagram, :flickr, :google, :pinterest, :blog, :website, :banner
has_many :medium, :dependent => :destroy
has_many :projects, :dependent => :destroy
has_many :events, :dependent => :destroy
has_many :statuses, :dependent => :destroy
has_many :activities, :dependent => :destroy
has_many :listings, :dependent => :destroy
has_many :comments, :dependent => :destroy
has_many :uploads, :dependent => :destroy
has_many :updates, :dependent => :destroy
has_many :assets, :dependent => :destroy
acts_as_follower
acts_as_followable
acts_as_ordered_taggable
acts_as_ordered_taggable_on :pursuits
acts_as_voter
acts_as_messageable
def to_param
user_name
end
def name
user_name
end
def mailboxer_email(object)
return user_name
end
def create_activity(item, action)
activity = activities.new
activity.targetable = item
activity.action = action
activity.save
activity
end
end
Interesting...
--
Activity Model
You mention it's the Activity model which doesn't destroy correctly. With this in mind, you'll want to look at all the steps contributing to the destroy mechanism:
def destroy
...
#activity = Activity.find_by_targetable_id(params[:id])
#activity.destroy if #activity
First things first - what is targetable_id? Also, if you're using Rails 4, you'll be able to use the find_by method with a hash of attributes:
#activity = Activity.find_by targetable_id: params[:id]
This could be the main cause of the issue - the above code will basically look for any Activity records with the targetable_id attribute having the same id parameter as you passed from your request
I'm not a betting man, but I'd surmise this is where the issue lies
Polymorphic
Okay, I found the problem.
You're trying to call a polymorphic association without referencing it correctly. Polymorphic associations allow you to create an ActiveRecord association for multiple models, storing the associative data in a single table / model:
Notice how the above example (as your code) includes ______type column? This is VITAL to your solution - as it what stores the model you saved the ID for:
When you save data in ActiveRecord associations, it uses something called a foreign_key to define the associative data in your model. This foreign key is normally something like activity_id or similar, as you know already.
The difference is that your association is a polymorphic association, meaning you can store multiple model types in a single table. This means that ActiveRecord / Rails will have to refer to the model you're saving, which is where the _type column comes in
My theory is that your local database will have several models saved in your "polymorphic" datatable - meaning when you look for an ID, it's bringing back a record which isn't the correct model
Fix
I would recommend the following:
#app/models/project.rb
Class Project < ActiveRecord::Base
has_many :activities, as: :targetable, dependent: :destroy
end
#app/models/activity.rb
Class Activity < ActiveRecord::Base
belongs_to :targetable, polymorphic: true
end
I don't know your model associations, so I just made the above up. If you set up your associations as above, you'll be able to call the following:
#app/controllers/projects_controller.rb
Class ProjectController < ApplicationController
def destroy
#project = Project.find params[:id] # should find project.activities too
#project.destroy
end
end
I used the above for several important reasons:
You're using the same ID for both objects (means they're associated)
You can use the dependent: :destroy method
The bottom line is you will be able to destroy just the Project object, and have its dependants destroyed too, achieving your desired outcome

Resources