Mutual relationships in rails 4 - ruby-on-rails

I'm trying to allow users in my app to be mutual friends with one another via friend requests and I'm a little confused with the how the relationships work... When a friendship is created by one user and accepted by the other, I would like the friendship to be visible from both users (obviously).
I'd like to achieve an implementation that allows me to do something similar to the following:
user1 friend requests user2
user2 accepts
user1.friends now contains user2
user2.friends now contains user1
Here's what I have so far, but I've read some weird things about nested has_many :through relationships
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :class_name => "User", :through => :friendships
end
class Friendship < ActiveRecord::Base
has_many :users, :limit => 2
end
Is this a viable implementation? If not, what could I change/improve? I'd like to avoid 2 rows representing one relationship if possible.

What you're looking for is a has_and_belongs_to_many relation, but to the same table, kind of like as described in detail by Many-to-many relationship with the same model in rails?. However, since you want the relation to be bi-directional ("my friends are all also friends with me"), you have two options:
Use a single join table, each row of which links two user_ids, but insert two rows for each friendship.
# no need for extra columns on User
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, through: :friendships
end
# t.belongs_to :user; t.belongs_to :friend
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: "User"
end
u1 = User.create!
u2 = User.create!
u3 = User.create!
# make users 1 and 2 friends
u1.friendships.create(friend: u2)
u2.friendships.create(friend: u1)
# make users 2 and 3 friends
u2.friendships.create(friend: u3)
u3.friendships.create(friend: u2)
# and now, u1.friends returns [u1],
# u2.friends returns [u1, u3] and
# u3.friends returns [u2].
Use a single record, but hackery to locate who you're friends with:
# no need for extra columns on User
class User < ActiveRecord::Base
has_many :friendships_as_a, class_name: "Friendship", foreign_key: :user_a_id
has_many :friendships_as_b, class_name: "Friendship", foreign_key: :user_b_id
def friends
User.where(id: friendships_as_a.pluck(:user_b_id) + friendships_as_b.pluck(:user_a_id))
end
end
# t.belongs_to :user_a; t.belongs_to :user_b
class Friendship < ActiveRecord::Base
belongs_to :user_a, class_name: "User"
belongs_to :user_b, class_name: "User"
end
That's not the cleanest way to do it, but I think you'll find there isn't really a particularly clean way when set up like that (with a denormalized table). Option 1 is a much safer bet. You could also use a SQL view to hit the middle ground, by generating the mirror entries for each friendship automatically.
Edit: Migration & usage in an API
Per the OP's comment below, to use Option 1 fully, here's what you'd need to do:
rails g migration CreateFriendships
Edit that file to look like:
class CreateFriendships < ActiveRecord::Migration
create_table :friendships do |t|
t.belongs_to :user
t.belongs_to :friend
t.timestamps
end
end
Create the Friendship model:
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: "User"
end
Then on your User model:
class User < ActiveRecord::Base
# ...
has_many :friendships
has_many :friends, through: :friendships, class_name: 'User'
# ...
end
And in your API, say a new FriendshipsController:
class FriendshipsController < ApplicationController
def create
friend = User.find(params[:friend_id])
User.transaction do # ensure both steps happen, or neither happen
Friendship.create!(user: current_user, friend: friend)
Friendship.create!(user: friend, friend: current_user)
end
end
end
Which your route for looks like (in config/routes.rb):
resource :friendships, only: [:create]
And a request to would look like:
POST /friendships?friend_id=42
Then you can refer to current_user.friends whenever you want to find who a user is friends with.

You'd use a has_many :through:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, through: :friendships, -> { where(status: "accepted") }
end
#app/models/friendship.rb
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: "User"
enum status: [:pending, :accepted]
validates :user, uniqueness: { scope: :friend, message: "You can only add a friend once" }
def decline
self.destroy
end
def accept
self.update status: "approved"
end
end
The above is a self-referential join, allowing the following:
#user = User.find params[:id]
#friend = User.find params[:friend_id]
#user.friends << #friend
--
This will add a new friendship for the user, with its default status set to pending. The #user.friends association is set so that only accepted friends appear from a call.
Thus, you'll be able to do the following:
#config/routes.rb
resources :users do
resources :friendships, only: [:index, :destroy, :update], path_names: { destroy: "remove", update: "accept" }
end
#app/controllers/Frienships_controller.rb
class FriendshipsController < ApplicationController
def index
#user = User.find params[:user_id]
#friendship = #user.friendships
end
def update
#user = User.find params[:user_id]
#friendship = #user.friendships.find params[:id]
#friendship.accept
end
def destroy
#user = User.find params[:user_id]
#friendship = #user.friendships.find params[:id]
#friendship.decline
end
end
#app/views/friendships/index.html.erb
<%= #friendships.pending.each do |friendship| %>
<%= link_to "Accept", user_friendships_path(user, friendship), method: :put %>
<%= link_to "Decline", user_friendships_path(user, friendship), method: :delete %>
<% end %>

Related

Button to connect an association in Rails is not working. Transfer of ownership

I am working my first Rails project, an adoption app and trying to bridge an association to a new potential owner in Rails. My controller action is moving through my adoption_request method, but no changes are being persisted to my join table in ActiveRecord. Can someone please tell me what I am missing here?
The app:
Owners sign up or log in to their account. They can add their Ferret using a form. Later, the Owner may want to create an Opportunity listing to adopt/rehome their animal. People browsing should be able to click on an Opportunity they are interested in, which should establish an association in the join table Opportunity, :adopter_id.
My Models:
class Owner < ApplicationRecord
has_secure_password
has_many :ferrets, dependent: :destroy
has_many :opportunities, dependent: :destroy
has_many :ferret_adoptions, through: :opportunities, source: :ferret
accepts_nested_attributes_for :ferrets, :opportunities
end
class Ferret < ApplicationRecord
belongs_to :owner
has_many :opportunities
has_many :owners, through: :opportunities
end
class Opportunity < ApplicationRecord
belongs_to :ferret
belongs_to :owner
end
In Opportunities Controller, my adoption_request method:
def adoption_request
#owner = Owner.find(session[:owner_id])
#opportunity = Opportunity.find(params[:id])
#opportunity.adopter_id = [] << current_user.id
current_user.req_id = [] << #opportunity.id
flash[:message] = "Adoption request submitted."
redirect_to questions_path
end
I am using a button to do this, but I am open to change that if something may work better:
<button><%= link_to 'Adoption Request', adoption_request_path, method: :post %> <i class='fas fa-heart' style='color:crimson'></i></button>
As an Owner when I click the button to make an Adoption Request, I am seeing all the working parts in byebug, and I am being redirected to the next page with the success message as if everything worked, but there is no Association actually being persisted to the database.
I appreciate any feedback you can offer.
I'm assuming here that Opportunity should represent something like a listing (it needs a less vague name).
If so you're missing a model and its table if ever want more then one user to be able to respond to an Opportunity:
class Owner < ApplicationRecord
has_secure_password
has_many :ferrets, dependent: :destroy
has_many :opportunities, dependent: :destroy
has_many :adoption_requests_as_adopter,
foreign_key: :adopter_id,
class_name: 'AdoptionRequest'
has_many :adoption_requests_as_owner,
through: :opportunities,
source: :adoption_requests
accepts_nested_attributes_for :ferrets, :opportunities
end
class Ferret < ApplicationRecord
belongs_to :owner
has_many :opportunities
has_many :owners, through: :opportunities
has_many :adoption_requests, through: :opportunities
end
class Opportunity < ApplicationRecord
belongs_to :ferret
belongs_to :owner
has_many :adoption_requests
end
class AdoptionRequest < ApplicationRecord
belongs_to :adopter, class_name: 'Owner' # ???
belongs_to :opportunity
has_one :ferret, through: :opportunity
has_one :owner, through: :opportunity
end
If you just have a adopter_id on your opportunities table it can only ever hold a single value.
I would just set the route / controller up as a normal CRUD controller for a nested resource:
# routes.rb
resources :opportunities do
resources :adoption_requests, only: [:create, :index]
end
<%= button_to "Adopt this ferret", opportunity_adoption_requests_path(#opportunity), method: :post %>
class AdoptionRequestsController < ApplicationController
before_action :set_opportunity
# #todo authorize so that it can only be viewed by the owner
# GET /opportunities/1/adoption_requests
def index
#adoption_requests = #opportunity.adoption_requests
end
# #todo authorize so that a current owner can't create adoption_requests
# for their own ferrets
# POST /opportunities/1/adoption_requests
def create
#adoption_request = #opportunity.adoption_requests.new(
adopter: current_user
)
if #adoption_request.save
redirect_to #opportunity, notice: 'Thank you for your reply! The owner of the ferret will be notified.'
# #todo send notification to owner
else
redirect_to #opportunity, notice: 'Oh noes!'
end
end
private
def set_opportunity
#opportunity = Opportunity.find(params[:opportunity_id])
end
end
Its only later when the owner actually accepts a adoption_request that you will actually update the opportunity and this is a seperate question for a later time.

How do I incorporate a "matching feature" in rails activerecord relationship

I am currently working on a project that is similar to dating apps like Tinder. A user (named Owner in my program) swipes on other owners and if they both swipe on each other it creates a "match". I have been looking into finding a solution to this such as friend requests similar to Facebook friends. I am seeing people using a "confirmed" column that is boolean defaulted to false and changing it to true but I cannot figure out the logic for this. Any advice on how to accomplish this would be appreciated. My only experience in this has been following or followers which doesn't require mutual requests to accomplish.
Owner class: (the user)
class Owner < ApplicationRecord
has_many :matches
has_many :friends, :through => :matches
end
Match class:
class Match < ApplicationRecord
belongs_to :owner
belongs_to :friend, :class_name => "Owner"
end
Thank you for any help! Self joins have been a complicated topic for me to understand.
You can add more fields to your join table. You could add something like owner_accepted and friend_accepted. Although I think that just one accepted field is enough.
Example solution:
class AddAcceptedToMatches < ActiveRecord::Migration[6.0]
def change
add_column :matches, :accepted, :boolean, default: false
end
end
class Owner < ApplicationRecord
has_many :matches
has_many :friends, :through => :matches
def send_request_to(friend)
friends << friend
end
def accept_request_from(owner)
matches.find_by(owner_id: owner.id).accept
end
def is_friends_with?(stranger)
match = matches.find_by(friend_id: stranger.id)
return false unless match
match.accepted?
end
class Match < ApplicationRecord
belongs_to :owner
belongs_to :friend, :class_name => "Owner"
def accept
update(accepted: true)
end
end
then you can do something like:
owner = Owner.new
friend = Owner.new
owner.send_request_to(friend)
owner.is_friends_with?(friend)
# false
friend.accept_request_from(owner)
owner.is_friends_with?(friend)
# true

Define owner after create

Walls belong to users through a WallAssignments association.
class Wall < ApplicationRecord
belongs_to :user
has_many :wall_assignments
has_many :users, :through => :wall_assignments
end
class User < ApplicationRecord
has_many :wall_assignments
has_many :walls, :through => :wall_assignments
end
class WallAssignment < ApplicationRecord
belongs_to :user
belongs_to :wall
end
In the create action, I'm associating the current user with the new wall record.
def create
#wall = Wall.new
#wall.wall_assignments.build(user_id: current_user.id)
if #wall.save
redirect_to #wall
else
redirect_to current_user
end
end
However, aside from allowing many users to belong to the wall, I'd like to have one user (the user who created it) own the wall.
I'm attempting something like this:
class Wall < ApplicationRecord
after_create { owner }
belongs_to :user
has_many :wall_assignments
has_many :users, :through => :wall_assignments
private
def owner
self.owner = Wall.users.first
end
end
Eventually, I'd like to be able to call #wall.owner.name and #wall.owner.id in my views.
I guess you want to have has_many(as users) and has_one(as owner) with same table User.
In this scenario, your Wall model will be:
class Wall < ApplicationRecord
belongs_to :owner, class_name: 'User', foreign_key: :owner_id
has_many :wall_assignments
has_many :users, :through => :wall_assignments
end
You need to add owner_id column in walls table.
So when you create Wall record, it will
class Wall < ApplicationRecord
after_create { add_owner }
private
def add_owner
self.update_column(:owner_id, self.users.first.id) if self.users.present?
end
end
You can also modify controller's create code(I assumed, create method will get called only once.)
def create
#wall = Wall.new(wall_params)
#wall.owner_id = current_user.id
#wall.wall_assignments.build(user_id: current_user.id)
if #wall.save
redirect_to #wall
else
redirect_to current_user
end
end
with this, you don't need to add after_create callback in Wall model.
And then you can call #wall.owner.name and #wall.owner_id

Rails has many through association setting multiple attributes

So I have a has_many through association where between two tables posts and users:
class Post < ApplicationRecord
has_many :assignments
has_many :users, :through => :assignments
end
class User < ApplicationRecord
has_many :assignments
has_many :posts, :through => :assignments
end
class Assignment < ApplicationRecord
belongs_to :request
belongs_to :user
end
Now in my association table (assignment) there are additional attributes for creator:boolean and editor:boolean.
My question is what's the best way to set these secondary attributes from within the controller?
Having looked around I've got a current solution:
posts_controller.rb:
class PostsController < ApplicationController
def create
params.permit!
#post = Post.new(post_params)
if #post.save
Assignment.handle_post(#post.id, params[:creator], params[:editors])
redirect_to posts_path, notice: "The post #{#post.title} has been created."
else
render "new"
end
end
assignment.rb:
class Assignment < ApplicationRecord
belongs_to :request
belongs_to :user
def self.handle_post(post_id, creator, assignment)
Assignment.where(:post_id => post_id).delete_all
Assignment.create!(:post_id => post_id, :user_id => creator, :creator => true, :editor => false)
if editors.present?
editors.each do |e|
Assignment.create!(:post_id => post_id, :user_id => e, :creator => false, :editor => true)
end
end
end
end
So what is essentially happening is I'm getting the user_ids from the form via params (creator returns 1 id, editors returns an array), and AFTER creating the post I'm deleting all columns associated with the post and recreating them off the new attributes.
The issue I have here is I can't run post validations on these associations (e.g. check a creator is present).
My two questions are as follows:
Is this the correct way to handle secondary attributes?
Is there a way to set the association up and then save it all at once so validations can be performed?
This is a more Rails way to do this:
Use nested attributes
post.rb
class Post < ApplicationRecord
# Associations
has_many :assignments, inverse_of: :post
has_many :users, through: :assignments
accepts_nested_attributes_for :assignments
# Your logic
end
assignment.rb
class Assignment < ApplicationRecord
after_create :set_editors
belongs_to :request
belongs_to :user
belongs_to :post, inverse_of: :assignments
# I would create attribute accessors to handle the values passed to the model
attr_accessor :editors
# Your validations go here
validates :user_id, presence: true
# Your logic
private
def set_editors
# you can perform deeper vaidation here for the editors attribute
if editors.present?
editors.each do |e|
Assignment.create!(post_id: post_id, user_id: e, creator: false, editor: true)
end
end
end
end
And finally, add this to your PostsController
params.require(:post).permit(..., assignments_attributes: [...])
This allows you to create Assignments from the create Post action, will run validations on Post and Assignment and run callbacks for you.
I hope this helps!

Rails: alternatives to using nested loops to find match in controller

There has to be a better way to do this. My Favorite model belongs to User while Applicant belongs to both Gig and User. I am trying to efficiently determine whether a user has applied for Gig that was favorited (<% if #application.present? %>).
I tried chaining the collection by using something like #favorites.each.gig to no avail. While the below index action for Favorites seems to work, it's really verbose and inefficient. What is a more succinct way of doing this?
def index
#favorites = Favorite.where(:candidate_id => current_candidate)
#applications = Applicant.where(:candidate_id => current_candidate)
#favorites.each do |favorite|
#applications.each do |application|
if favorite.gig.id == application.id
#application = application
end
end
end
end
class User
has_many :applicants
has_many :gigs, :through => :applicants
has_many :favorites
end
class Favorite < ActiveRecord::Base
belongs_to :candidate
belongs_to :gig
end
class Applicant < ActiveRecord::Base
belongs_to :gig
belongs_to :candidate
end
class Candidate < ActiveRecord::Base
has_many :applicants
has_many :gigs, :through => :applicants
has_many :favorites
end
class Gig < ActiveRecord::Base
belongs_to :employer
has_many :applicants
has_many :favorites
has_many :users, :through => :applicants
end
For lack of a better answer, here's my idea:
--
User
Your user model should be structured as such (I just highlighted foreign keys, which I imagine you'd have anyway):
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :applicants
has_many :gigs, :through => :applicants, foreign_key: "candidate_id"
has_many :favorites, foreign_key: "candidate_id"
end
This means you'll be able to call:
current_candidate.favorites
current_candidate.applicants
This will remove the need for your #applications and #favorites queries
--
Favorite
You basically want to return a boolean of whether applicant is part of the favorite model or not. In essence, for each favorite the candidate has made, you'll be able to check if it's got an application
I would do this by setting an instance method on your favorites method using an ActiveRecord Association Extension, like so:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :favorites do
def applied?
self.applicant.exists? proxy_association.owner.gig.id
end
end
end
This will allow you to call:
<%= for favorite in current_candidate.favorites do %>
<%= if favorite.applied? %>
<% end %>
This is untested & highly speculative. I hope it gives you some ideas, though!

Resources