I am trying to create a question and answer thread in Rails 6 where a User can answer on a question, and then other users can comment on the answer - similar to a reddit or even stackoverflow.
I created a polymorphic association on my Answer model with a 'parent_id' and I am able to post answers on the initial question. However the nested answers do not render below the initial answer, but rather below the main question. I think I have isolated the problem to the corresponding partial view seen below:
Answer View
<li>
<%= answer.body %></br>
<%= link_to answer.user.first_name, answer.user %>
<%= link_to answer.user.last_name, answer.user %>
answered <%= time_ago_in_words(answer.created_at) %> ago.
<div class="comments-container">
<%= render partial: "answers/reply", locals: {commentable: answer.commentable, parent_id: answer.parent.id} %>
</div>
<ul> <%= render partial: "answers/answer", collection: answer.answers %> </ul>
</li>
From my understanding, the last line should render the answers to the answer, however the answers render underneath the initial question, and not the answer. Any ideas on what im doing wrong?
Should I be using a gem like Ancestry to do this? If so how would that work?
For completeness, here are the other components
Question View
<h3><%= #question.title %></h3>
<p> Asked by <%= link_to #question.user.email, #question.user %> <%= time_ago_in_words(#question.created_at) %> ago. </p>
</br>
<span class="body"> <%= #question.body %> </span>
</br>
<h5><strong><%= #question.answers.count %> Answers</strong></h5>
<%= render #answers %></br>
<%= render partial: "answers/form", locals: {commentable: #question} %> </br>
<%= paginate #answers %>
Answer model
belongs_to :user
belongs_to :parent, optional: true, class_name: 'Answer'
belongs_to :commentable, polymorphic: true
has_many :answers, as: :commentable, dependent: :destroy
validates :body, presence: true
validates :user, presence: true
Question model
belongs_to :user
has_many :answers, as: :commentable, dependent: :destroy
validates :body, presence: true
validates :title, presence: true
validates :user, presence: true
AnswerController
class AnswersController < ApplicationController
before_action :set_answer, only: [:edit, :update, :destroy, :upvote, :downvote]
before_action :find_commentable, only: [:create]
def new
#answer = Answer.new
end
def create
#answer = #commentable.answers.new(answer_params)
respond_to do |format|
if #answer.save
format.html { redirect_to #commentable }
format.json { render :show, status: :created, location: #commentable }
else
format.html { render :new }
format.json { render json: #answer.errors, status: :unprocessable_entity }
end
end
end
def destroy
#answer = #commentable.answers.find(params[:id])
#answer.discard
respond_to do |format|
format.html { redirect_to #commentable, notice: 'Answer was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_answer
#answer = Answer.find(params[:id])
end
def answer_params
params.require(:answer).permit(:body).merge(user_id: current_user.id, parent_id: params[:parent_id])
end
def find_commentable
#commentable = Answer.find(params[:answer_id]) if params[:answer_id]
#commentable = Question.find(params[:question_id]) if params[:question_id]
end
end
Question Controller
class QuestionsController < ApplicationController
before_action :set_question, only: [:show, :edit, :update, :destroy, :upvote, :downvote]
def index
#questions = Question.order('created_at desc').page(params[:page])
end
def show
#answer = #question.answers.new
#answers = if params[:answer]
#question.answers.where(id: params[:answer])
else
#question.answers.where(parent_id: nil)
end
#answers = #answers.page(params[:page]).per(5)
end
def new
#question = Question.new
end
def edit
end
def create
#question = Question.new(question_params)
respond_to do |format|
if #question.save
format.html { redirect_to #question, notice: 'You have successfully asked a question!' }
format.json { render :show, status: :created, location: #question }
else
format.html { render :new }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if #question.update(question_params)
format.html { redirect_to #question, notice: 'Question successfully updated.' }
format.json { render :show, status: :ok, location: #question }
else
format.html { render :edit }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
def destroy
#question.discard
respond_to do |format|
format.html { redirect_to #questions_url, notice: 'Question successfully deleted.' }
format.json { head :no_content }
end
end
private
def set_question
#question = Question.find(params[:id])
end
def question_params
params.require(:question).permit(:title, :body, :tag_list).merge(user_id: current_user.id)
end
end
You kind of failed at modeling polymorphism. If you want a true polymorphic association you would model it as so:
class Question
has_many :answers, as: :answerable
end
class Answer
belongs_to :answerable, polymorphic: true
has_many :answers, as: :answerable
end
This lets the "parent" of a question be either a Question or a Answer and you don't need to do ridiculous stuff like #question.answers.where(parent_id: nil). You can just do #answers = #question.answers and this will only include the first generation children.
However polymorphism isn't all its cracked up to be and that will be especially apparent when building a tree hierarchy. Since we actually have to pull the rows out of the database to know where to join you can't eager load the tree effectively. Polymorphism is mainly useful if the number of parent classes in large or unknown or you're just prototyping.
Instead you can use Single Table Inheritance to setup the associations:
class CreateAnswers < ActiveRecord::Migration[6.0]
def change
create_table :answers do |t|
t.string :type
t.belongs_to :question, null: true, foreign_key: true
t.belongs_to :answer, null: true, foreign_key: true
# ... more columns
t.timestamps
end
end
end
Note the nullable foreign key columns. Unlike with polymophism these are real foreign keys so the db will ensure referential integrity. Also note the type column which has a special significance in ActiveRecord.
Then lets setup the models:
class Question < ApplicationRecord
has_many :answers, class_name: 'Questions::Answer'
end
class Answer < ApplicationRecord
has_many :answers, class_name: 'Answers::Answer'
end
And the subclasses of Answer:
# app/models/answers/answer.rb
module Answer
class Answer < ::Answer
belongs_to :answer
has_one :question, through: :answer
end
end
# app/models/questions/answer.rb
module Questions
class Answer < ::Answer
belongs_to :question
end
end
Pretty cool. Now we can eager load to the first and second generation with:
Question.eager_load(answers: :anser)
And we can keep going:
Question.eager_load(answers: { answers: :answer })
Question.eager_load(answers: { answers: { answers: :answers }})
But at some point you'll want to call it quits and start using ajax like reddit does.
Related
When I push submit button in _form, Image should be created and linked to the user, but something went wrong. I use the same Image model for Ads and there almost the same code works right. Maybe the problem can somehow be connected with the inconsistency of devise user controllers and my user controller?
Users controller
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
def index
end
def show
end
def new
#user = User.new
#user.images.build
end
def edit
authorize! :update, #user
#user.images.build
end
def create
#user = current_user
respond_to do |format|
if #user.save
format.html { redirect_to board_url }
format.json { render :show, status: :created, location: #user }
else
format.html { render :new }
format.json { render json: #user.errors, status: :unprocessable_entity}
end
end
end
def update
respond_to do |format|
if #user.update(user_params)
format.html { redirect_to board_url }
format.json { render :show, status: :ok, location: #user }
else
format.html { render :edit }
format.json { render json: #user.errors, status: :unprocessable_entity}
end
end
end
def destroy
authorize! :update, #user
#user.destroy
respond_to do |format|
format.html { redirect_to board_url }
format.json { head :no_content }
end
end
private
def set_user
#user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:email, images_attributes: [:id, :name])
end
end
_form
<%= form_with(model: user, local: true) do |form| %>
<%= form.fields_for :images do |image| %>
<div class="image_fields">
<div class="field">
<%= image.label t('.img_name') %><br>
<%= image.text_field :name %>
</div>
</div>
<% end %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
class Image
class Image < ApplicationRecord
belongs_to :ad
belongs_to :user
validates :name, presence: true
...
class User
class User < ApplicationRecord
has_many :ads
has_many :images, dependent: :destroy
accepts_nested_attributes_for :images,
reject_if: proc { |attributes| attributes['name'].blank? },
allow_destroy: true
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
Migrations
class AddUserIdToImages < ActiveRecord::Migration[5.1]
def change
add_column :images, :user_id, :integer
end
end
class CreateImages < ActiveRecord::Migration[5.1]
def change
create_table :images do |t|
t.string :name
t.references :ad, foreign_key: true
t.timestamps
end
end
end
Please provide error message and backtrace.
I assume it is because "ad" reference is blank for the user.
And according to your code:
t.references :ad, foreign_key: true
So I think the issue is in database constraint
And the simplest solution is to change it to
add_column :images, :ad_id, :integer
Without foreign key.
Or for your case, you can add polimorhic association
Also, a question not related to the topic, but want to ask about
respond_to do |format|
if #user.save
format.html { redirect_to board_url }
format.json { render :show, status: :created, location: #user }
else
format.html { render :new }
format.json { render json: #user.errors, status: :unprocessable_entity}
end
end
Why are you saving inside respond_to method?
So in rails depth, there is a method:
def respond_to(&block)
...
block.call
...
end
And your #user.save will be called there. Are you sure you want it?
I have an error when I try create a product: "Cannot build association 'type_data'. Are you trying to build a polymorphic one-to-one association?"
I don't know what is happend. I saw a lot examples with the same code but mine doesn't work.
Here my product.rb
class Product < ApplicationRecord
belongs_to :type_data, polymorphic: true
accepts_nested_attributes_for :type_data, allow_destroy: true
end
Here my hotel_room.rb
class HotelRoom < ApplicationRecord
has_one :product, as: :type_data, dependent: :destroy
has_many :rates
accepts_nested_attributes_for :rates, allow_destroy: true
end
My products/_form.html.erb
<%= f.fields_for :type_data do |builder| %>
<%= render "edit_hotel_account_fields", f: builder %>
<% end %>
My products/_edit_account_fields.heml.erb
<%= f.fields_for :rates do |builder| %>
<%= render 'rate_fields', f: builder %>
<% end %>
<%= link_to_add_fields "Add rate", f, :rates %>
My products/_rate_fields.html.erb
<div class="form-group">
<label><%= t('products.new.rates.double_price') %></label>
<%= f.number_field :double_price, step: 0.1, class: 'form-control' %>
</div>
<%= f.hidden_field :_destroy %>
<%= link_to "remove", '#', class: "remove_fields" %>
And this is all my products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
# GET /products
# GET /products.json
def index
#products = Product.all
end
# GET /products/1
# GET /products/1.json
def show
end
# GET /products/new
def new
#product = Product.new
end
# GET /products/1/edit
def edit
end
# POST /products
# POST /products.json
def create
#product = Product.new(product_params)
respond_to do |format|
if #product.save
format.html { redirect_to products_path, notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: #product }
else
format.html { render :new }
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /products/1
# PATCH/PUT /products/1.json
def update
respond_to do |format|
if #product.update(product_params)
format.html { redirect_to #product, notice: 'Product was successfully updated.' }
format.json { render :show, status: :ok, location: #product }
else
format.html { render :edit }
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
# DELETE /products/1
# DELETE /products/1.json
def destroy
#product.destroy
respond_to do |format|
format.html { redirect_to products_url, notice: 'Product was successfully destroyed.' }
format.json { head :no_content }
end
end
# AJAX cargas más fechas
def date_range
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product
#product = Product.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:status, :provider_id, :commission, :title, :commercial_title, :title_seo, :description, :seo_description, :slug_web, :slug_redirection, :services, :info, :province, :state, :city, :country, :cancel_conditions, :limit_children_age, :special_alert, :notes, :stock, :iva, :minimum_advance, type_data_attributes: [:room_stock, :room_service, :_destroy, rates_attributes: [:title, :start_date, :end_date, :hotel_room_id, :individual_price, :double_price, :child_bed_price, :adult_bed_price, :adult_breakfast_price, :adult_medium_price, :adult_complete_price, :child_breakfast_price, :child_medium_price, :child_complete_price, :_destroy]], dates: {}, map: {}, address_autocomplete: {})
end
end
Do anyone know what is happend?
Thanks!
As Hotel model has one Product via polymorphic,
class HotelRoom < ApplicationRecord
has_one :product, as: :type_data, dependent: :destroy
accepts_nested_attributes_for :product, allow_destroy: true
accepts_nested_attributes_for :rates, allow_destroy: true
....
end
In hotel_rooms controller new action
def new
#hotel_room = HotelRoom.new
#hotel_room.build_product
end
In Hotel view form
<%= form_for(#hotel_room) do |f| %>
....
<%= f.fields_for(:product) do |p| %>
....
<% end %>
<% end %>
Note:
If you have common data between HotelRoom and Product, then you need to introduce another model like TypeData and set it polymorphic.
I am building an app that allows a user to create a contest. Each contest has many questions and each contests has many entries. Each entry has many answers and each question has many answers. Here are my models:
class Answer < ActiveRecord::Base
belongs_to :entry
belongs_to :question
end
class Contest < ActiveRecord::Base
has_many :entries
has_many :questions
end
class Entry < ActiveRecord::Base
belongs_to :contest
has_many :answers
accepts_nested_attributes_for :answers, allow_destroy: true
end
class Question < ActiveRecord::Base
has_many :answers
belongs_to :contest
end
Everything works except for when I try to create an entry. I get a "param is missing or the value is empty: entry" error. Here is my controller:
class EntriesController < ApplicationController
before_action :set_entry, only: [:show, :edit, :update, :destroy]
before_action :set_contest
# GET /entries
# GET /entries.json
def index
#entries = Entry.all
end
# GET /entries/1
# GET /entries/1.json
def show
end
# GET /entries/new
def new
#entry = Entry.new
end
# GET /entries/1/edit
def edit
end
# POST /entries
# POST /entries.json
def create
#entry = Entry.new(entry_params)
#entry.contest = #contest
respond_to do |format|
if #entry.save
format.html { redirect_to #entry, notice: 'Entry was successfully created.' }
format.json { render :show, status: :created, location: #entry }
else
format.html { render :new }
format.json { render json: #entry.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /entries/1
# PATCH/PUT /entries/1.json
def update
respond_to do |format|
if #entry.update(entry_params)
format.html { redirect_to #entry, notice: 'Entry was successfully updated.' }
format.json { render :show, status: :ok, location: #entry }
else
format.html { render :edit }
format.json { render json: #entry.errors, status: :unprocessable_entity }
end
end
end
# DELETE /entries/1
# DELETE /entries/1.json
def destroy
#entry.destroy
respond_to do |format|
format.html { redirect_to entries_url, notice: 'Entry was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_entry
#entry = Entry.find(params[:id])
end
def set_contest
#contest = Contest.find(params[:contest_id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def entry_params
params.require(:entry).permit(:contest_id, answers_attributes: [:id, :content, :entry_id, :question_id, :_destroy])
end
end
And here is my entry form:
<%= simple_form_for([#contest, #entry]) do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<h3>Questions</h3>
<%= simple_fields_for :answers do |ff| %>
<% #contest.questions.each do |question| %>
<h4><%= question.content %></h4>
<%= ff.input :content, input_html: {class: 'form-control'} %>
<% end %>
<% end %>
</div>
<div class="form-actions">
<%= f.button :submit %>
</div>
<% end %>
I am still working on the logic but am perplexed as to why the entry form is giving me this error. Any help would be appreciated!
UPDATE
In the Rails Guide example they show the new action as:
def new
#person = Person.new
2.times { #person.addresses.build}
end
Do I need to build the answer objects in my new action? I'm not sure... I tried it but it didn't work. I feel like that can't be the problem though as the error is coming from the entry_params method
You should be adding this line to your new action.
#entry.answers.build
And change this line
<%= simple_fields_for :answers do |ff| %>
to
<%= f.simple_fields_for :answers do |ff| %>
I have a nested form question.erb that displays the pre written question (:poll) and then has text_fields (:response) for the answers. The problem that I am running into is that blank questions are being created in the database. I have done a fair bit of research on the issue and it appears that the answer would be adding
accepts_nested_attributes_for :questions, reject_if: proc { |attributes| attributes['poll'].blank? }
to the event model. The issue with this is of course is now I am preventing everything from being written. How would I go about only writing the answers to the database and ignoring (or not creating at all) the blank questions? Please let me know if any of the other files might be needed for the solution.
Thanks!
question.erb
<%= simple_form_for(#event) do |f| %>
<%= f.error_notification %>
<%= f.object.name %>
<%= f.simple_fields_for :questions, #event.questions do |q| %>
<%= q.object.poll%>
<%= q.simple_fields_for :answers, q.object.answers.build do |a|%>
<%= a.text_field :response %>
<% end %>
<% end %>
<%= f.button :submit%>
<% end %>
event.rb
class Event < ActiveRecord::Base
has_many :questions
accepts_nested_attributes_for :questions
end
events_controller.rb (selected portions)
def question
#event = Event.find(params[:id])
end
def update
#event = Event.find(params[:id])
if #event.update(event_params)
redirect_to events_path, notice: "Answers saved"
else
redirect_to events_question_path, notice: "Answers not saved"
end
end
def event_params
params.require(:event).permit(
questions_attributes: [:poll, answers_attributes: [:response]])
end
question.rb
class Question < ActiveRecord::Base
belongs_to :user
belongs_to :event
has_many :answers
accepts_nested_attributes_for :answers
end
question_controller.rb
class QuestionsController < ApplicationController
before_action :set_question, only: [:show, :edit, :update, :destroy]
def index
#questions = Question.all
end
def show
end
def new
#question = Question.new
end
def edit
end
def create
#question = Question.new(question_params)
#question.user = current_user
respond_to do |format|
if #question.save
format.html { redirect_to #question, notice: 'Question was successfully created.' }
format.json { render :show, status: :created, location: #question }
else
format.html { render :new }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
def update
#question.user = current_user.id
respond_to do |format|
if #question.update(question_params)
format.html { redirect_to #question, notice: 'Question was successfully updated.' }
format.json { render :show, status: :ok, location: #question }
else
format.html { render :edit }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
# DELETE /questions/1
# DELETE /questions/1.json
def destroy
#question.destroy
respond_to do |format|
format.html { redirect_to questions_url }
format.json { head :no_content }
end
end
private
def set_question
#question = Question.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def question_params
params.require(:question).permit(:poll, :event_id, answer_attributes: [:response, :question, :event, :user, :event_id, :question_id, :user_id])
end
end
Try this:
class Event < ActiveRecord::Base
has_many :questions
accepts_nested_attributes_for :questions, reject_if: :all_blank
end
You can read more about it on the official docs, but in a nutshell that will prevent any question that is completely blank from being saved, and let any populated question through.
class Event < ActiveRecord::Base
has_many :questions
accepts_nested_attributes_for :questions
validates_associated :questions
end
This should solve your problem, be careful though not to use this on the other side of your relation, as it will cause infinite loop.
I guess I should add that once you add the validates helper method to your model, you can do this:
e = Event.new
e.valid?
=>false
This may be of some help: http://edgeguides.rubyonrails.org/active_record_validations.html
Try:
#question = Question.create(question_params)
instead of:
#question = Question.new(question_params)
class Person < ActiveRecord::Base
validates :name, presence: true
end
>> p = Person.new
# => #<Person id: nil, name: nil>
>> p.errors.messages
# => {}
>> p.valid?
# => false
>> p.errors.messages
# => {name:["can't be blank"]}
>> p = Person.create
# => #<Person id: nil, name: nil>
>> p.errors.messages
# => {name:["can't be blank"]}
>> p.save
# => false
>> p.save!
# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
>> Person.create!
# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
I ended up just deleting a question with a blank :poll after the creation. question model is as follows
class Question < ActiveRecord::Base
belongs_to :user
belongs_to :event
has_many :answers
accepts_nested_attributes_for :answers
after_save { |question| question.destroy if question.poll.blank? }
end
I'm sure theres a better way and will gladly accept better answers. Thanks all for the help.
Yet another solution. A bit more explicit than the accepted answer IMHO.
accepts_nested_attributes_for : answers,
allow_destroy: true,
reject_if: -> { | question | [poll, foo, bar].any? &:blank? }
Having set up a has_many through relationship, I'm trying to iterate through associated B objects in the view of an object A. I.e. something like
<% for q in #survey.questions do %>
<%= q.name %> <br/>
<% end %>
yields nothing, while
<%= #survey.questions %>
yields
#<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Question:0x007f9859f221e8>
How could (should) I access these?
Here's the Controller
class SurveysController < ApplicationController
before_action :set_survey, only: [:show, :edit, :update, :destroy]
def index
#surveys = Survey.all
end
def show
end
def new
#survey = Survey.new
end
def edit
end
def create
#survey = Survey.new(survey_params)
respond_to do |format|
if #survey.save
format.html { redirect_to #survey, notice: 'Survey was successfully created.' }
format.json { render action: 'show', status: :created, location: #survey }
else
format.html { render action: 'new' }
format.json { render json: #survey.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if #survey.update(survey_params)
format.html { redirect_to #survey, notice: 'Survey was successfully updated.' }
format.json { head :no_content }
else
format.html { render action: 'edit' }
format.json { render json: #survey.errors, status: :unprocessable_entity }
end
end
end
def destroy
#survey.destroy
respond_to do |format|
format.html { redirect_to surveys_url }
format.json { head :no_content }
end
end
private
def set_survey
#survey = Survey.find(params[:id])
end
end
And here's the Models
class Survey < ActiveRecord::Base
has_many :assignments
has_many :questions, through: :assignments
end
.
class Question < ActiveRecord::Base
has_many :assignments
has_many :surveys, through: :assignments
end
.
class Assignment < ActiveRecord::Base
belongs_to :survey
belongs_to :question
end
As far as I can tell Powers answer should work. Try checking if the #survey actually has any questions. Add this in the page somewhere <%= #survey.questions.count %>
Are you sure the #survey in question really has assignments associated to it? Can you see them on the Rails console?
Try this:
<% #survey.questions.each do |q| %>
<%= q.name %> <br/>
<% end %>
The for loop should be avoided in Ruby. From this question, it looks like for loops operate on arrays, so something like this might also work (again, this is not the recommended solution):
<% for q in #survey.questions.to_a do %>
<%= q.name %> <br/>
<% end %>
Update
I think you need to make your associations singular in the Assignment model:
class Assignment < ActiveRecord::Base
belongs_to :survey
belongs_to :question
end
I created a quiz on many to many relationships that you might find helpful.