Hello I am making a Forum application in Rails 4. It can have numerous forums, each with numerous topics. Each topic can have many posts. When creating a new topic, one must also create the initial post, much like Stack Overflow itself. Therefore, I have a text area in the "New Topic" form that allows this with a fields_for
method. The Problem is, when you click the "Create Topic" button after filling out the form (including the "post" field), the transaction is rolled back. The following validation error appears:
3 errors prohibited this topic from being saved:
- Posts forum must exist
- Posts topic must exist
- Posts user must exist
This is my form: app/views/topics/_form.html.erb
<%= form_for([ @forum, topic ]) do |f| %>
<% if topic.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(topic.errors.count, "error") %> prohibited this topic from being saved:</h2>
<ul>
<% topic.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :title %><br>
<%= f.text_field :title %>
</div>
<div class="field">
<%= f.fields_for :posts do |builder| %>
<%= builder.label :content %><br>
<%= builder.cktext_area :content, class: 'ckeditor' %>
<% end %>
</div>
<div class="actions">
<%= f.submit 'Create Topic', class: "btn btn-l btn-success" %>
</div>
<% end %>
Models: forum.rb
class Forum < ApplicationRecord
has_many :topics, dependent: :destroy
has_many :posts, through: :topics
def most_recent_post
topic = Topic.last
return topic
end
end
topic.rb
class Topic < ApplicationRecord
belongs_to :forum
belongs_to :user
has_many :posts, dependent: :destroy
accepts_nested_attributes_for :posts
end
post.rb
class Post < ApplicationRecord
belongs_to :forum
belongs_to :topic
belongs_to :user
validates :content, presence: true
end
The controller for topics, app/controllers/topics_controller.rb
class TopicsController < ApplicationController
before_action :get_forum
before_action :set_topic, only: [:show, :edit, :update, :destroy]
# GET /topics
# GET /topics.json
def index
@topics = @forum.topics
end
# GET /topics/1
# GET /topics/1.json
def show
end
# GET /topics/new
def new
@topic = @forum.topics.build
@topic.posts.build
end
# GET /topics/1/edit
def edit
# @topic.posts.build
end
# POST /topics
# POST /topics.json
def create
@topic = @forum.topics.build(topic_params.merge(user_id: current_user.id))
@topic.last_poster_id = @topic.user_id
respond_to do |format|
if @topic.save
format.html { redirect_to forum_topic_path(@forum, @topic), notice: 'Topic was successfully created.' }
format.json { render :show, status: :created, location: @topic }
else
format.html { render :new }
format.json { render json: @topic.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /topics/1
# PATCH/PUT /topics/1.json
def update
respond_to do |format|
if @topic.update(topic_params)
format.html { redirect_to forum_topic_path(@forum, @topic), notice: 'Topic was successfully updated.' }
format.json { render :show, status: :ok, location: @topic }
else
format.html { render :edit }
format.json { render json: @topic.errors, status: :unprocessable_entity }
end
end
end
# DELETE /topics/1
# DELETE /topics/1.json
def destroy
@topic.destroy
respond_to do |format|
format.html { redirect_to forum_path(@forum), notice: 'Topic was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def get_forum
@forum = Forum.find(params[:forum_id])
end
def set_topic
@topic = Topic.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def topic_params
params.require(:topic).permit(:title, :last_poster_id, :last_post_at, :tags, :forum_id, :user_id, posts_attributes: [:id, :content])
end
end
As you see I've added the posts_attributes
to the strong parameters for topic. These are the only fields that posts have besides the foreign key references (:forum_id, :topic_id, :user_id
). And I've tried putting those attributes in, but I get the same error.
Finally, this is my routes.rb
Rails.application.routes.draw do
resources :forums do
resources :topics do
resources :posts
end
end
resources :sessions
resources :users
mount Ckeditor::Engine => '/ckeditor'
end
I should also mention that I have tried adding hidden_fields
inside of fields_for
, with the id
criteria for @forum, @topic, and current_user. That throws the same validation error.
What am I missing? I feel like it's something in the controller. Like I'm not saving it properly. Every tutorial I've seen has it this way. Except for the Rails <=3 versions, which are way different because of no strong_params.
Any ideas? Thanks for the help!
EDIT Here is the log output when I try to submit a topic entitled "I am a title" and the content "I am some content"...
Started POST "/forums/1/topics" for 127.0.0.1 at 2016-01-31 09:03:33 -0500
Processing by TopicsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"pYt842XQHiOKqNjPHBO8lNP2z92gHF7Lpt24CppbuvHR/cFHky3FVCpBs77p7WFRKmYBHgeZQjx0sE+DI+Q+sQ==", "topic"=>{"title"=>"I am a title", "posts_attributes"=>{"0"=>{"content"=>"<p>I am some content</p>\r\n"}}}, "commit"=>"Create Topic", "forum_id"=>"1"}
Forum Load (0.6ms) SELECT "forums".* FROM "forums" WHERE "forums"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
(0.3ms) BEGIN
CACHE (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
(0.4ms) ROLLBACK
This is not a direct answer; too long for comment.
One of the issues you have with your routes
is that you're nesting too many resources:
Resources should never be nested more than 1 level deep...
resources :x do resources :y end
--
Although you can do what you're doing, it would perhaps be better to use a scope
:
#config/routes.rb
scope ':forum' do
resources :topics do
resources :posts
end
end
The issue you're facing is that things can get very complicated, very quickly. Although the
This way, you could make the forums
CRUD accessible in its own set of functionality:
#config/routes.rb
resources :forums #-> only accessible to admins?
scope ...
Either way, you'd still need to define your routes with the forum
present:
<%= link_to "Test", [@forum, @topic, @post] %>