Rails polymorphic form for message system - user <-> admin - ruby-on-rails

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?

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

Add Record to Existing Collection in a Rails App

I am attempting to develop a model in which a user can add the recipe they are viewing to an existing menu of recipes they have created, similar to adding a song to a custom playlist. I believe I have the models set up correctly (using a many to many through relationship) however I am unsure how to go about the adding of the actual records to a selected collection. Any guidance would be helpful. My code is as below.
Menus Controller
class MenusController < ApplicationController
before_action :set_search
def show
#menu = Menu.find(params[:id])
end
def new
#menu = Menu.new
end
def edit
#menu = Menu.find(params[:id])
end
def create
#menu = current_user.menus.new(menu_params)
if #menu.save
redirect_to #menu
else
render 'new'
end
end
def update
#menu = Menu.find(params[:id])
if #menu.update(menu_params)
redirect_to #menu
else
render 'edit'
end
end
def destroy
#menu = Menu.find(params[:id])
#menu.destroy
redirect_to recipes_path
end
private
def menu_params
params.require(:menu).permit(:title)
end
end
Menu Model
class Menu < ApplicationRecord
belongs_to :user
has_many :menu_recipes
has_many :recipes, through: :menu_recipes
end
menu_recipe Model
class MenuRecipe < ApplicationRecord
belongs_to :menu
belongs_to :recipe
end
Recipe Model
class Recipe < ApplicationRecord
belongs_to :user
has_one_attached :cover
has_many :menu_recipes
has_many :menus, through: :menu_recipes
end
User Model
class User < ApplicationRecord
has_secure_password
has_one_attached :profile_image
has_many :recipes
has_many :menus
end
You can do something like :
def add_recipe_to_menu
menu = current_user.menus.find params[:id]
recipe = current_user.recipes.find params[:recipe_id]
menu.recipes << recipe
end
It will add a viewing recipe to existing menu of recipes.
First make sure you build the new record off the user:
class MenusController < ApplicationController
# make sure you authenticate the user first
before_action :authenticate_user!, except: [:show, :index]
def new
#menu = current_user.menus.new
end
def create
#menu = current_user.menus.new(menu_attributes)
# ...
end
end
Then we can just add a select to the form where the user can select from his recipes:
# use form_with in Rails 5.1+
<%= form_for(#menu) do |f| %>
... other fields
<div class="field">
<%= f.label :recipe_ids %>
<%= f.collection_select :recipe_ids, f.object.user.recipies, :id, :name, multiple: true %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
f.object accesses the model instance wrapped by the form builder.
recipe_ids is a special setter/getter created by ActiveRecord for has_many associations. As you may have guesses it returns an array of ids and lets the association be set with an array of ids - automatically inserting/deleting rows in the join table in the process.
You then just need to whitelist the recipe_ids param:
def menu_attributes
params.require(:menu)
.permit(:foo, :bar, recipe_ids: [])
end
recipe_ids: [] whitelists an array of permitted scalar types. Since this is a hash option it must be listed after any positional arguments to be syntactically valid.
rb(main):003:0> params.require(:menu).permit(:foo, recipe_ids: [], :bar)
SyntaxError: (irb):3: syntax error, unexpected ')', expecting =>

Setting attribute in join model when creating two new parents

I have two models: User and Company. A company can have many users and a user can have many companies. As you might suggest, this is the perfect place to use a join table. I'm actually using a full blown model to join User and Company so that I can specify the role that each user has. The table, companies_users, therefore has the following columns: user_id, company_id and company_role.
The situation I'm trying to negotiate is one in which I'm creating both a Company and a User and would like to specify the company_role while doing so.
My new method is as follows:
def new
#user=User.new
#company=#user.companies.build
end
This creates an entry in the companies_users join table but (obviously) does so in leaving the company_role blank.
How might I add this bit of info?
Thanks in advance!
You can pass the attributes through the build / create methods:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
#user = User.new
#user.company_users.build.build_company
end
def create
#user = User.new user_params
#user.save
end
private
def user_params
params.require(:user).permit(company_users_attributes: [company_attributes:[:name]])
end
end
#app/views/users/new.html.erb
<%= form_for #user do |f| %>
<%= f.fields_for :company_users do |cu| %>
<%= cu.text_field :company_role %>
<%= cu.fields_for :company do |c| %>
<%= c.text_field :name %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
The above looks complicated, I'll explain in a second.
You need the following models:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :company_users
has_many :companies, through: :company_users
accepts_nested_attributes_for :company_users
end
#app/models/company_user.rb
class CompanyUser < ActiveRecord::Base
belongs_to :company
belongs_to :user
accepts_nested_attributes_for :company
end
#app/models/company.rb
class Company < ActiveRecord::Base
has_many :company_users
has_many :users, through: :company_users
end
If you want to create a company and company_user, you'll have to pass params for both. Although it looks messy, all you're doing is passing each nested object to their respective models.
If you want to set the "role", you have to pass the attributes to company_users. If you want to also create a new company (rather than just assigning an existing one), you need to also pass the respective params for that too.
You could explicitly reference the join table...
def new
#user = User.new
#company = Company.new
#companies_user=#user.companies_user.build(company: #company, company_role: 'default role')
end

Rails 4: First argument in form cannot contain nil or be empty

In our Rails 4 app, there are four models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
end
class Post < ActiveRecord::Base
belongs_to :calendar
end
We have routed the corresponding resources with Routing Concerns, as follows:
concern :administratable do
resources :administrations
end
resources :users, concerns: :administratable
resources :calendars, concerns: :administratable
To create a new #calendar, we use the following form:
<h2>Create a new calendar</h2>
<%= form_for(#calendar) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_field :name, placeholder: "Your new calendar name" %>
</div>
<%= f.submit "Create", class: "btn btn-primary" %>
<% end %>
When we embed this form into the user's show.html.erb (so that a user can create a new calendar from his profile), everything works fine.
However, we would like to have a special page, call dashboard, where a user could see all his calendars and create a new one.
We believe the logical url should be /users/:id/administrations.
So, we embedded the above form in the Administration's index.html.erb.
And as soon as we do that and visit for instance /users/1/administrations, we get the following error:
ArgumentError in AdministrationsController#index
First argument in form cannot contain nil or be empty
Extracted source (around line #432):
else
object = record.is_a?(Array) ? record.last : record
raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
object_name = options[:as] || model_name_from_record_or_class(object).param_key
apply_form_for_options!(record, object, options)
end
EDIT: here is also our Administrations#Controller:
class AdministrationsController < ApplicationController
def to_s
role
end
def index
#user = current_user
#administrations = #user.administrations
end
def show
#administration = Administration.find(params[:id])
end
end
Any idea of what we are doing wrong?
First argument in form cannot contain nil or be empty
The reason is #calendar is not initialised in your controller action, so obviously #calendar is nil. So is the error.
To get your form to work in administrations/index.html.erb, you should be having the below code in your index action of your administrations_controller.
def index
#user = current_user
#administrations = #user.administrations
#calendar = Calendar.new # this one.
end

Rails 4 Attendance system

Hi I'm actually struggling with building a Rails 4 application implementing an attendance model on rails 4, I found maybe two question on stackoverflow but they're posted in 2012 and it failed when i tried following them.
This is the closest one i got on stackoverflow
Edit: I already have a view of classroom and list out the students. And could assign students into the classroom but the problem would be to GET the students into a new attsheet and saving them into attendance
Here's what I currently have now
# Attendance
# take student as a single entry for :attsheet and
# has a :attended (boolean) and remarks as well
class Attendance < ActiveRecord::Base
belongs_to :student
belongs_to :attsheet
end
#Attsheet which means attendance sheet
#has :post_date and :remark
class Attsheet < ActiveRecord::Base
belongs_to :classroom
has_many :attendances
accepts_nested_attributes_for :attendances
end
class Student < ActiveRecord::Base
belongs_to :school
has_and_belongs_to_many :classrooms
has_many :attendances
end
class Classroom < ActiveRecord::Base
belongs_to :school
has_and_belongs_to_many :students
has_many :attsheets
validates :class_name, presence: true
end
I want the classroom to be able to create a new attendance or view attendance archives for each student.
I am able to do this in classroom right now but I'm stuck at what to do next for the controller and view
$ = link_to "New Attendance", new_school_classroom_attsheet_path(#school, #classroom, #attsheet)
In attandances_controller,
class AttendancesController < ApplicationController
before_filter :set_parents
def new
#attendance= #classroom.attendances.new
end
def create
#attendance= #classroom.attendances.new(params[:milestone])
if #attendance.save
redirect_to ....
else
render :action=>:new
end
end
def set_parents
#school= School.find(params[:school_id])
#classroom= #school.classrooms.find(params[:classroom_id])
end
end
and in _form.html.erb of attendacen,
<%= form_for(#school, #classroom, #attendance]) do |f|%>
<% if #attendance.errors.present? %>
<ul class="warning">
<% #attendance.errors.full_messages.each do |message| %>
<li><%= message%></li>
<% end %>
</ul>
<% end %>
<h2>Attendance</h2>
.........
<%= f.submit button %>
<% end %>
this will submit fotm to create action of attendance
I found the solution by doing
I changed attsheet to attendance_list
def new
#attendance_list = #classroom.attendance_lists.new
#attendance_list.attendances = #classroom.student_ids.map do |student_id|
#attendance_list.attendances.build(student_id: student_id)
end
end
def create
#attendance_list = #classroom.attendance_lists.new(attendance_list_params)
#attendance_list.classroom_id = params[:classroom_id]
respond_to do |format|
if #attendance_list.save
format.html {redirect_to school_classroom_path(#school, #classroom), notice: "You added the attendance!" }
else
redirect_to new_school_attendance_list_path(attendance_list_params)
end
end
end
with simple fields
= f.simple_fields_for :attendances do |g|
= g.input :student_id, as: :hidden
...... more fields ...

Resources