How to structure a Conversation between two models? - ruby-on-rails

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

Related

Rails polymorphic form for message system - user <-> admin

I used this guide as a starting point for creating a messaging system from scratch. The idea is to have conversations between User and AdminUser. I want the AdminUser respond to questions sent it by User. AdminUser can have many conversations with the same User (there can be several topics of conversation, each topic is a separate conversation).
The Admin should be able to see the previous conversations with a reply box.
Like in the article I've created below db structure:
class User < ApplicationRecord
has_many :conversations, as: :sendable
has_many :conversations, as: :recipientable
end
class AdminUser < ApplicationRecord
has_many :conversations, as: :sendable
has_many :conversations, as: :recipientable
end
class Conversation < ApplicationRecord
belongs_to :sendable, polymorphic: true
belongs_to :recipientable, polymorphic: true
has_many :messages, dependent: :destroy
validates :sendable_id, uniqueness: { scope: :recipientable_id }
end
class Message < ApplicationRecord
belongs_to :conversation
belongs_to :messageable, polymorphic: true
validates_presence_of :body
end
Inside the article provided earlier I read that:
We also added “messageable” as polymorphic. This way, both Admin and User can send messages to conversation with their respective references.
What "messageable" is responsible for? because to create new message I need to provide this messageble_id and I don't now what it is.
# app/views/messages/index.html.erb
<% #messages.each do |message| %>
<%= message.body %>
<% end %>
<%= form_for [#conversation, #message] do |f| %>
<%= f.text_area :body %>
<%= f.submit "Send Reply" %>
<% end %>
controller:
class MessagesController < ApplicationController
before_action :fetch_conversation
def index
#messages = #conversation.messages
#message = #conversation.messages.new
end
def new
#message = #conversation.messages.new
end
def create
#message = #conversation.messages.new(message_params)
redirect_to conversation_messages_path(#conversation) if #message.save
end
private
def message_params
params.require(:message).permit(:body, :messageable_id)
end
def fetch_conversation
#conversation = Conversation.find(params[:conversation_id])
end
end
With above code I've got an error:
ActiveRecord::RecordInvalid: Validation failed: Messageable must exist
Which means I need to provide messageable_id and probably messageable_type but what are these values?

has_many :through add extra param in join table in one call (object creation)

I have the following code letting a user to create a new album through a join table with an extra params (creator).
In order to do it, my controller does 2 requests (one for creating the album object and the collaboration object / the other to update the collaboration object with the extra params).
I would like to know if there is a way to do this call with only one request. (add the extra "creator" params in the same time than the album creation)
Thank you.
albums_controller.rb
class AlbumsController < ApplicationController
def new
#album = current_user.albums.build
end
def create
#album = current_user.albums.build(album_params)
if current_user.save
#album.collaborations.first.update_attribute :creator, true
redirect_to user_albums_path(current_user), notice: "Saved."
else
render :new
end
end
private
def album_params
params.require(:album).permit(:name)
end
end
Album.rb
class Album < ApplicationRecord
# Relations
has_many :collaborations
has_many :users, through: :collaborations
end
Collaboration.rb
class Collaboration < ApplicationRecord
belongs_to :album
belongs_to :user
end
User.rb
class User < ApplicationRecord
has_many :collaborations
has_many :albums, through: :collaborations
end
views/albums/new
= simple_form_for [:user, #album] do |f|
= f.input :name
= f.button :submit
You can just add associated objects on the new album instance:
#album = current_user.albums.new(album_params)
#album.collaborations.new(user: current_user, creator: true)
When you call #album.save ActiveRecord will automatically save the associated records in the same transaction.
class AlbumsController < ApplicationController
def new
#album = current_user.albums.new
end
def create
#album = current_user.albums.new(album_params)
#album.collaborations.new(user: current_user, creator: true)
if #album.save
redirect_to user_albums_path(current_user), notice: "Saved."
else
render :new
end
end
private
def album_params
params.require(:album).permit(:name)
end
end
You are also calling current_user.save and not #album.save. The former does work due to fact that it causes AR to save the associations but is not optimal since it triggers an unessicary update of the user model.

has_many :through 4r post contributions

I'm having a bit of trouble understanding how to setup the contributions controller and the form in the view. I've set some forms in the view so i know the join tables work.
As of right now a post belongs_to user && a user has_many posts
Objective:
1. user1 creates post - which belongs to user1
2. user2 requesting to join the user1_post as a contributor
3. user1 accepts or declines request
4. user2 is now a contributor to user1_post
5. user1 can remove user2 as a contributor
Got the has_many :through setup properly and have tested it in the console
contribution.rb
class Contribution < ActiveRecord::Base
belongs_to :post
belongs_to :user
def accept
self.accepted = true
end
end
post.rb
class Post < ActiveRecord::Base
belongs_to :author, class_name: 'User'
has_many :contribution_requests, -> { where(accepted: false) }, class_name: 'Contribution'
has_many :contributions, -> { where(accepted: true) }
has_many :contributors, through: :contributions, source: :user
end
user.rb
class User < ActiveRecord::Base
has_many :posts, foreign_key: 'author_id'
has_many :contribution_requests, -> { where(accepted: false) }, class_name: 'Contribution'
has_many :contributions, -> { where(accepted: true) }
has_many :contributed_posts, through: :contributions, source: :post
end
contributions_controller.rb
class ContributionsController < ApplicationController
def create
#contribution = current_user.contributions.build(:user_id => params[:id])
if #contribution.save
flash[:notice] = "Added contributor."
redirect_to posts_path(#post)
else
flash[:error] = "Unable to add contributor."
redirect_to posts_path(#post)
end
end
def destroy
#contribution = current_user.contributions.find(params[:id])
#contribution.destroy
flash[:notice] = "Removed contributor."
redirect_to root_url
end
end
Without much context, this is what I'd do:
#config/routes.rb
resources :posts do
resources :contributions, only: [:create, :destroy] #-> can use posts#edit to add extra contributions
end
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def edit
#post = Post.find params[:id]
end
end
#app/views/contributions/edit.html.erb
<%= form_for #post do |f| %>
# #post form
<% end %>
## contributor add / remove form (select boxes)
#app/controllers/contributions_controller.rb
class ContributionsController < ApplicationController
def create
#post = Post.find params[:post_id]
#contribution = current_user.contributions.new contribution_params
#contribution.post = #post
notice = #contribution.save ? "Added Contributor" : "Unable to add contributor"
redirect_to #post, notice: notice
end
def destroy
#contribution = current_user.contributions.find params[:id]
#contribution.destroy
redirect_to root_url, notice: "Removed Contributor"
end
private
def contribution_params
params.require(:contribution).permit(:user, :post, :accepted)
end
end
As an aside, you should look at an ActiveRecordExtension to give you some methods for your conbtributions association (instead of having multiple associations):
#app/models/post.rb
class Post < ActiveRecord::Base
has_many :contributions, -> { extending ContributionExtension }
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :contributions, -> { extending ContributionExtension }
end
#app/models/concerns/contribution_extension.rb
class ContributionExtension
def requests(status=false)
where accepted: status
end
def accepted(status=true)
where accepted: status
end
end
#post.contirbutions.requets
#post.contributions.accepted
#user.contributions.requests
#user.contributions.accepted
--
And also, you should look at implementing a state_machine for your Contribution model:
#app/models/contribution.rb
class Contribution < ActiveRecord::Base
state_machine :accepted, initial: :pending do
event :accept do
transition [:pending, :denied] => :accepted
end
event :deny do
transition [:pending, :accepted] => :denied
end
end
end
Great article about it here.
This will allow you to call:
#contribution = current_user.contributions.find params[:id]
#contribution.accept
It will also give you several other cool methods:
#contribution.accepted?
#contribution.state

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)

Not showing user during iteration if user already belongs to "group" relationship

I have this relationship where User can create a document(trip) and invite other users to a group that belongs to that document. My relationship indicates that "Group" has a user_id and trip_id column, so for every user I invite, a new Group record will be created in the database.
When I am inviting other users, I only want users who are NOT in the group to appear. Users who are already in the group should not show up, but my view still shows the users.
I've been playing around with <% if !friend.trips.include?(#trip)%>, but I can't seem to get the correct view. The record is being created in the database correctly.
Also, when I am viewing groups/new.html.erb, this is the url http://localhost:3000/groups/new?id=2, where the id is the trip_id.
My question:
Am I using restful convention? That is, should I be using the new method here (as is) or should I be using the index method instead?
How do I iterate through each friend's groups to make sure that none of the group's trip_id is equivalent to #trip.id?
Thanks!
view (/groups/new.html.erb)
<% if !#friends.blank? %>
<% #friends.each do |friend| %>
<% if !friend.trips.include?(#trip)%>
<%= link_to groups_path(:user_id => friend.id, :trip_id => #trip.id),
:method => :post, :action => 'create' do %>
<div id="addfriend_totrip_button_groupsnew">add friend to trip</div>
<% end %>
<% end %>
<% end %>
<% end %>
groups_controller.rb
class GroupsController < ApplicationController
before_filter :authenticate, :only => [:update, :create, :destroy]
def new
#trip = Trip.find(params[:id])
#user = User.find(current_user)
#group = Group.new
#friends = #user.friends.all
end
def create
#trip = Trip.find(params[:trip_id])
#user = User.find(params[:user_id])
#group = Group.create(:user_id => #user.id, :trip_id => #trip.id)
if #group.save
flash[:success] = "Friend added to group."
redirect_to groups_path(:id => #trip.id)
else
flash[:error] = "Could not add friend."
redirect_to root_path
end
end
end
user.rb
class User < ActiveRecord::Base
has_many :trips, :through => :groups
has_many :trips, :dependent => :destroy
has_many :groups
end
trip.rb
class Trip < ActiveRecord::Base
belongs_to :user
belongs_to :traveldeal
has_many :groups
has_many :users, :through => :groups
end
group.rb
class Group < ActiveRecord::Base
belongs_to :trip
belongs_to :user
end
First of all, you have has_many :trips called twice in your User model. I understand you have two different types of User-Trip relationships (one directly, and one through Group), but you can't give both the same name, otherwise one will hide the other. Try defining your User model like this:
class User < ActiveRecord::Base
has_many :group_trips, :through => :groups,
:class_name => "Trip"
has_many :trips, :dependent => :destroy
has_many :groups
def all_trips
Trip.joins(:groups).where({:user_id => self.id} | {:groups => {:user_id => self.id}})
end
end
There's also the problem that you're searching the friend's list of groups for a Trip object. Try changing that line to:
<% if !friend.all_trips.include?(#trip) %>
Or without the new method, something like this should work:
<% if !friend.groups.where(:trip_id => #trip.id).first %>
I don't see anything un-RESTful about your approach. RESTful in general means stateless. I.e. the only thing a response depends on is the HTTP method and the address. So as long as your not keeping state information in, say, the session, you should be following REST.

Resources