Introduction
While some logic in Rails views is inevitable, recently I have been trying to move any unnecessary logic in the view to the model or controller--especially if it's a database query. This problem came into view while I was coding a Friendship "Accept" button in a Rails Facebook clone.
I had it setup so that users could accept or decline friend requests from the Notifications#index page. The problem was I had to locate the respective friendship object from within Notifications#index. This required a database query. Once I wrote the correct database query in the view to find the Friendship object associated with a notification, I started thinking of ways to remove it from the view. Below is how I approached the problem.
Every notification references a sender
and a receiver
, both belonging to the User class. Similarly, a Friendship object is initiated with a sender
and a receiver
. If a user has many friend requests on their notifications page, it is necessary to determine which specific friend request is being accepted or declined when the receiver clicks "Accept" or "Decline", respectively.
Therefore, when rendering the notifications collection, we can find the soon-to-be-accepted or soon-to-be-declined Friendship with notification.sender
and
notification.receiver
. The "Accept" button is as follows:
Initial "Accept" Button
<% friendship = Friendship.find_by(sender_id: notification.sender.id,
receiver_id: notification.receiver.id) %>
<%= button_to "Accept",
friendship_path(friendship),
method: :put,
params: { friendship: { status: 'accepted' } } %>
We can eliminate the need for the local variable by creating a hash of all friend requests sent to the current user, mapped to the sender_id
. The current user will always be the receiver in this situation and to find the Friendship request sent to the current user, we only need the sender_id
. I thought it made sense to make this method an instance method on User but there are probably better ways to do this. Let me know in the comments! Here are the relevant models and associations.
Relevant Models
User, Friendship, Notification
class User < ApplicationRecord
has_many :sent_notifications,
class_name: 'Notification',
foreign_key: 'sender_id',
dependent: :destroy
has_many :received_notifications,
class_name: 'Notification',
foreign_key: 'receiver_id',
dependent: :destroy
has_many :sent_pending_requests, -> { friendship_pending },
class_name: 'Friendship',
foreign_key: 'sender_id',
dependent: :destroy
has_many :received_pending_requests, -> { friendship_pending },
class_name: 'Friendship',
foreign_key: 'receiver_id',
dependent: :destroy
# ...
end
class Notification < ApplicationRecord
belongs_to :sender, class_name: 'User'
belongs_to :receiver, class_name: 'User'
#...
end
class Friendship < ApplicationRecord
enum status: %i[pending accepted declined]
belongs_to :sender, class_name: 'User'
belongs_to :receiver, class_name: 'User'
scope :friendship_pending, -> { where(status: :pending) }
#...
end
And now the method that maps a sender id to a Friendship object:
class User
#...associations, etc.
def requests_via_sender_id
requests = received_pending_requests
sender_ids = requests.pluck(:sender_id)
sender_ids.to_h do |id|
[id, requests.find_by(sender_id: id)]
end
end
end
Let's say received_pending_requests.count == 1
at the moment. A user with an id of 3 has sent current_user
a request.
current_user.requests_via_sender_id =>
{3=>
#<Friendship:0x000055f71022ec20
id: 16,
status: "pending",
sender_id: 3,
receiver_id: 1 }
The good thing about doing it this way is that the controller can now request this information from the User model prior to rendering the Notifications#index view:
class NotificationsController < ApplicationController
def index
@notifications = current_user.received_notifications.includes(%i[sender receiver])
@friendships = current_user.requests_via_sender_id # => returns a hash
end
end
Finally, we can return to our "Accept" button which has access to the current notification being rendered.
Note the absence of the friendship
local variable:
Final "Accept" Button
<%= button_to "Accept",
friendship_path(@friendships[notification.sender.id]),
method: :put,
params: { friendship: { status: 'accepted' } }%>
Conclusion
Another added benefit of this approach is improved readability. Which friendship are we updating? The one the sender of a notification requested -->
friendship_path(@friendships[notification.sender.id])
. This is arguably more expressive than friendship_path(friendship)
, because it provides the context in which we are searching for a Friendship object.
Balancing responsibilities between the Model, View, and Controller is sometimes challenging, but making your way towards a solution piece by piece can be very educational and rewarding.
This was my first blog post. Thank you so much for reading. Having finished it I realize the benefit it has in regards to understanding. I hope to write more posts in the near future.
Photo by Lawrence Hookham on
Unsplash
Top comments (0)