Rails association build with strong parameters and argument - ruby-on-rails

In my messages controller I wish to build a message that belongs_to a #sender (class Character). However, messages belong_to not only Characters but also Conversations, and this is causing the problem. The build method of the message needs to be passed the Conversation id to which it belongs so the message can pass validations. My code successfully finds the #conversation via an sql search, but how do I correctly pass both the #conversation.id and the message_params to the #sender.messages.build as strong parameters?
messages_controller.rb
def create
#sender = Character.find_by(callsign: params[:callsign])
#recipient = Character.find_by(callsign: params[:recipient])
sender_id = #sender.id
recipient_id = #recipient.id
conversationID = Conversation.find_by_sql("
SELECT senderConversations.conversation_id FROM Chats AS senderConversations
INNER JOIN Chats AS recipientConversations
ON senderConversations.conversation_id=recipientConversations.conversation_id
WHERE senderConversations.character_id='#{sender_id}'
AND recipientConversations.character_id='#{recipient_id}'
GROUP BY senderConversations.conversation_id
HAVING count(distinct senderConversations.character_id) = 2
; ")
#conversation = Conversation.find_by(id: conversationID)
if(#conversation.nil?)
#conversation = #sender.conversations.create
#recipient.conversations << #conversation
end # It all works great up to here!
#message = #sender.messages.build(message_params, #conversation) # Can't get this working
if #message.save
#conversation.messages << #message
respond_to do |format|
format.html do
end
format.js do
end
end
else
redirect_to request.referrer || root_url
end
end
private
def message_params
params.require(:message).permit( :content, :picture )
end
character.rb
has_many :chats, foreign_key: "character_id",
dependent: :destroy
has_many :conversations, through: :chats, source: :conversation
has_many :messages
conversation.rb
has_many :messages
chat.rb
belongs_to :character
belongs_to :conversation
message.rb
belongs_to :character
belongs_to :conversation
validates :character_id, presence: true
validates :conversation_id, presence: true

To add conversation to the message, try
#message = #sender.messages.build(message_params.merge(:conversation => #conversation))
A couple suggestions -
First, move all the querying code to the Message model. Given the params you're setting, the sender and recipient, you should be able to find or create the conversation and attach it correctly. Reduce the complexity of the controller.
Also, try to move that sql into a rails query if you can and set a scope. Not sure I completely understand the schema, but something like...
class Conversation < ActiveRecord::Base
scope :between, (sender, recipient) -> { joins(:chats).where(:charcter_id => [sender, recipient]).group('chats.conversations_id').having("count(distinct senderConversations.character_id) = 2") }
Then you can look for the conversation on a before_save or before_validation callback, ensuring the conversation exists. In a scope, you'll be able to reuse the sql more easily to find the conversation in other situations.

Related

How to structure a Conversation between two models?

I have a Store object that has an email_address attribute. Using the logic from and How To Build A Form and Handling Inbound Email Parsing with Rails, I'm trying to figure out how to structure a Conversation where a visitor can email the Store, and the Store can reply through email - their replies would post a Message to the Conversation.
When a visitor inquires to the store (via form), I create a Reservation record with their name and email, and start a Conversation like this:
#conversation = Conversation.create(sender_id: self.id, recipient_id: self.store_id)
I wanted to model the notifications similar to this, where everyone but the sender receives an email, but I'm stumped on how to map the User, since it's two different objects (Reservation and Store):
def send_notifications!
(forum_thread.users.uniq - [user]).each do |user|
UserMailer.new_post(user, self).deliver_now
end
end
The Conversation model looks like this, may be wrong, any guidance on what I could use to make the messages unique and structure the notifications?
class Conversation < ApplicationRecord
belongs_to :sender, :foreign_key => :sender_id, class_name: "Reservation"
belongs_to :recipient, :foreign_key => :recipient_id, class_name: "Store"
belongs_to :reservation
has_many :messages, dependent: :destroy
end
The simplest and most flexible way would be to set this up as a many-to-many association:
class User < ApplicationRecord
has_many :messages
has_many :conversations, through: :messages
end
class Message < ApplicationRecord
belongs_to :user
belongs_to :conversation
end
class Conversation < ApplicationRecord
has_many :messages
has_many :users, through: :messages
end
Here Message actually works as the join table that ties it together. Conversion is the recipient. When sending an initial message to a user you would POST to /users/:user_id/messages:
<%= form_with(model: [#user, #message || Message.new]) do |f| %>
# ...
<% end %>
module Users
class MessagesController < ApplicationController
# POST /users/:user_id/messages
def create
#user = User.find(params[:user_id])
#conversation = Conversation.joins(:users)
.where(users: { id: [current_user, #user]})
.first_or_create
#message = #conversation.messages.new(message_params.merge(user: current_user))
if #message.save
redirect_to #coversation
else
render :new
end
end
end
end
And then you would handle the views and controllers (such as a chat window) for conversations in a separate controller:
<%= form_with(model: [#conversation, #message || #conversation.messages.new]) do |f| %>
# ...
<% end %>
module Conversations
class MessagesController < ApplicationController
# POST /conversations/:conversation_id/messages
def create
#conversation = Conversation.find(params[:conversation_id])
#message = #conversation.messages.new(message_params.merge(user: current_user))
if #message.save
redirect_to #coversation
else
render :new
end
end
end
end

private messaging system in rails

I am working on a messaging system in my rails app. I already have it working properly for sending messages between 2 users(sender and recipient). This setup is fine but how can I make a new conversation for each room so the uniqueness checking will be only between an user and a room or viceversa?? Each user is only allowed to send message to a room from the room show page. So room_id can be fetched there. A single user can have many listings which makes it complicated for me.So am confused on what change to make in the below code to accomplish that??Or do I have to make a different design approach for the models?
I have a user, listing, conversation and message model
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
Message.rb
class Message < ActiveRecord::Base
belongs_to :conversation
belongs_to :user
validates_presence_of :content, :conversation_id, :user_id
def message_time
created_at.strftime("%v")
end
end
conversations_controller.rb
class ConversationsController < ApplicationController
before_action :authenticate_user!
def index
#conversations = Conversation.involving(current_user)
end
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
redirect_to conversation_messages_path(#conversation)
end
private
def conversation_params
params.permit(:sender_id, :recipient_id)
end
end
messages_controller.rb
class MessagesController < ApplicationController
before_action :authenticate_user!
before_action :set_conversation
def index
if current_user == #conversation.sender || current_user == #conversation.recipient
#other = current_user == #conversation.sender ? #conversation.recipient : #conversation.sender
#messages = #conversation.messages.order("created_at DESC")
else
redirect_to conversations_path, alert: "You don't have permission to view this."
end
end
def create
#message = #conversation.messages.new(message_params)
#messages = #conversation.messages.order("created_at DESC")
if #message.save
redirect_to conversation_messages_path(#conversation)
end
end
private
def set_conversation
#conversation = Conversation.find(params[:conversation_id])
end
def message_params
params.require(:message).permit(:content, :user_id)
end
end
Your relations are off. A conversation where the sender and recipient are fixed is no good - in fact thats just a monolog!
Instead we need a real many to many relation. That means we need a third table to store the link between users and converstations
So lets start by generating a model:
rails g model UserConversation user:belongs_to conversation:belongs_to
This will generate a model and a migration for a join table which will link users and conversations. We should now also take care of the uniqueness requirement. Open up the migration:
class CreateUserConversations < ActiveRecord::Migration
def change
create_table :user_conversations do |t|
t.belongs_to :user, index: true, foreign_key: true
t.belongs_to :conversation, index: true, foreign_key: true
t.timestamps null: false
end
# Add this constraint
add_index :user_conversations, [:user_id, :conversation_id], unique: true
end
end
That constraint that ensures the uniqueness on the database level and protects against race conditions. We also want a validation on the software level.
class UserConversation < ActiveRecord::Base
belongs_to :user
belongs_to :conversation
validates_uniqueness_of :user_id, scope: :conversation_id
end
Now we setup the relations in User and Conversation so that they go through the join model:
class User < ActiveRecord::Base
has_many :user_conversations
has_many :conversations, through: user_conversations
def has_joined?(conversation)
conversations.where(id: conversation).exist?
end
end
class Conversation < ActiveRecord::Base
has_many :user_conversations
has_many :messages
has_many :users, through: user_conversations
def includes_user?(user)
users.where(id: user).exist?
end
end
This lets us do #user.conversations or #conversation.users. We don't need the hacky scopes.
This is an example of how you could possibly add a user to a conversation on the fly:
class MessagesController < ApplicationController
# ...
def create
unless current_user.has_joined?(conversation)
# #todo handle case where this fails
#conversation.users << current_user
end
#message = #conversation.messages.new(message_params) do |m|
# get the current user from the session or a token
# using params is an open invitation for hacking
m.user = current_user
end
if #message.save
redirect_to conversation_messages_path(#conversation)
else
render :new
end
end
# ...
end
But note that you still have quite a way to go and will likely need several different controllers to properly represent messages in different contexts:
/messages/:id => MessagesController
/users/:user_id/messages => Users::MessagesController
/conversations/:id/messages => Conversations::MessagesController

Passing in, retrieving, and setting restrictions of User from Post - Comment model in Rails

I'm attempting to set limits on the amount of commenting users can do on particular post during the day. I have implemented the following (successfully) in my Post model to limit the amount of Posts they can create.
class Post < ActiveRecord::Base
validate :daily_limit, :on => :create
def daily_limit
# Small limit for users who just sign up
if author.created_at >= 14.days.ago
if author.created_posts.today.count >= 4
errors.add(:base, "Exceeds Your Daily Trial Period Limit(4)")
end
else
if author.created_posts.today.count >= author.post_limit_day
errors.add(:base, "Exceeds Your Daily Limit")
end
end
end
end
But, when I attempt to add similar restrictions to my Comment model
class PostComment < ActiveRecord::Base
validate :daily_limit, :on => :create
belongs_to :post, :counter_cache => true
belongs_to :user
def daily_limit
# Small limit for users who just sign up
if user.posted_comments.today.count >= 2
errors.add(:base, "Exceeds Your Daily Trial Period Limit(4)")
end
end
end
I am greeted with a undefined method 'posted_comments' for nil:NilClass error. I don't believe my user_id is being passed into my model correctly in order to access it with something like user.posted_comments.today.count>=2
My create action in my post_comments controller is as follows:
class PostCommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#post_comment = #post.post_comments.create(post_comment_params)
#post_comment.user = current_user
if #post_comment.save
redirect_to #post
else
flash[:alert] = "Comment Not Added"
redirect_to #post
end
end
end
and the my hacked down User model is as follows:
class User < ActiveRecord::Base
has_many :created_posts, class_name: 'Post', :foreign_key => "author_id",
dependent: :destroy
has_many :posted_comments, class_name: 'PostComment', :foreign_key =>"user_id", dependent: :destroy
end
Thanks.
You are assigning the user after "create" in your controller
#post_comment = #post.post_comments.create(post_comment_params)
#post_comment.user = current_user
Try this:
#post_comment = #post.post_comments.build(post_comment_params)
#post_comment.user = current_user

Rails create action with multiple belongs_to

Trying to figure out a better way of assigning a review it's associated models.
I have the following classes:
class User < ActiveRecord::Base
has_many :reviews, dependent: :destroy
end
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :restaurant
end
class Restaurant < ActiveRecord::Base
has_many :reviews, dependent: :destroy
end
Pretty straightforward stuff. A review must have a restaurant and a user. My create action looks like this:
def create
#restaurant = Restaurant.find(params[:restaurant_id])
#review = #restaurant.reviews.build(review_params)
#review.user = current_user
if #review.save
redirect_to #restaurant
else
render 'new'
end
end
private
def review_params
params.require(:review).permit(:content)
end
Currently I build the review for the restaurant and then I assign the review's user to the current user.
This all works fine but is there a cleaner way to build the associations?
Is there a way to add additional arguments to the build method alongside the strong params?
I looked at accepts_nested_attributes_for but I couldn't get it to work.
Thanks!
You can use merge in the review_params like below
def review_params
params.require(:review).permit(:content).merge(user_id: current_user.id)
end
so that you can erase this line #review.user = current_user in the create method
In your form, you can put a hidden field with the user_id that you want to assign:
<%= f.hidden_field :user_id, value: #user.id %>
Then, add it to your review_params:
params.require(:review).permit(:content, :user_id)

Rails undefined method 'each' for nil:NilClass

I'm setting up an internal messaging system in my rails app and I'm having trouble getting the message to actually send to another user.
class User < ActiveRecord::Base
# messages and conversations
has_many :user_conversations
has_many :conversations, through: :user_conversations
has_many :messages
class UserConversation < ActiveRecord::Base
belongs_to :user
belongs_to :conversation
before_create :create_user_conversations
accepts_nested_attributes_for :conversation
delegate :subject, to: :conversation
delegate :users, to: :conversation
attr_accessor :to
private
def create_user_conversations
to.each do |recip|
recipient = User.find(recip)
UserConversation.create(user_id: recip, conversation_id: 1)
end
end
end
class Conversation < ActiveRecord::Base
has_many :user_conversations
has_many :users, through: :user_conversations
has_many :messages
accepts_nested_attributes_for :messages
class Message < ActiveRecord::Base
belongs_to :user_conversation
belongs_to :user
And here is my user_conversation_controller:
class UserConversationsController < ApplicationController
def new
#user = User.find(params[:user_id])
#conversation = #user.user_conversations.build
#conversation.build_conversation.messages.build
end
def create
#conversation = UserConversation.new(conversation_params)
#conversation.user = current_user
#conversation.conversation.messages.first.user = current_user
if #conversation.save
redirect_to user_conversation_path(current_user, #conversation)
else
flash[:error] = "There was an error"
render 'new'
end
end
private
def conversation_params
params.require(:user_conversation).permit(:to => [],
conversation_attributes: [:subject,
messages_attributes: [:body]])
end
The error comes in the create_user_conversations method in the UserConversation model. When I try to run
to.each do |recip|
I get an "undefined method 'each' for nil:NilClass" error. However, the "to" array has a value in it, in this case the parameters looked like this:
{"utf8"=>"✓",
"user_conversation"=>{"to"=>["2"],
"conversation_attributes"=>{"subject"=>"Hey",
"messages_attributes"=>{"0"=>{"body"=>"hey"}}}},
"commit"=>"Create User conversation",
"user_id"=>"1"}
Any ideas on why that array isn't getting passed in correctly? Thanks.
You define to as an attr_accessor, which will create get/set methods for an instance variable #to. You're using to as a local variable in your private method create_user_conversations though. This explains the nil:NilClass error.
Try changing the local variable to be an instance variable instead.
I solved my problem by going ahead and adding a recipients_id column to my user_conversations table, then in my UserConversations controller I was able to do
def create
#conversation = UserConversation.new(user_conversation_params)
#conversation.user = current_user
#conversation.conversation.messages.first.user_id = current_user.id
if #conversation.save
UserConversation.recipient_id = #conversation.recipient_id
redirect_to user_conversation_path(current_user, #conversation)
create_user_conversations
else
flash[:error] = "There was an error"
render 'new'
end
end
With the private method create_user_conversations also in my UserConversations controller:
def create_user_conversations
UserConversation.recipient_id.each do |recip|
recipient = User.find(recip)
UserConversation.create(user: recipient, conversation: #conversation.conversation)
end
end
I doubt this is the most elegant way to do this, but it at least gets the job done.

Resources