4

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
B. Bulpett
  • 814
  • 12
  • 27
  • Can you test after removing the `validation` in `Post.rb`? – Richard Peck Jan 31 '16 at 14:01
  • @RichPeck removed the line `validates :content, presence: true`, restarted server (for good measure) and get the same error illustrated above. All that validation did was to ensure the **content** wasn't left blank. – B. Bulpett Jan 31 '16 at 14:05
  • I feel like the *routes* might be donked up? – B. Bulpett Jan 31 '16 at 14:12
  • Are there any other validations in `post.rb` that you haven't posted here? – Pavan Jan 31 '16 at 14:38
  • I thought the params looked unusual. Why are you using .merge in your .build? Maybe remove .merge and give it a test. – Elvn Jan 31 '16 at 14:43
  • @Pavan no that's all the validations I've got so far. Will add more (length, etc..) once I get this working. – B. Bulpett Jan 31 '16 at 14:44
  • @ValAsensio Thanks, but I removed it and there is no effect. It actually prints an additional error `User must exist` along with the other 3 above errors. The `merge` method is in there to pass the `user` id in the topic creation. I was actually considering "merging" the post parameters in there too, but I can't come up with a valid syntax. – B. Bulpett Jan 31 '16 at 14:50
  • @ValAsensio Interesting. I put that in there before adding the nested fields to the form. How to correct the ActiveRecord context then? Would `Post` need to "has_and_belongs_to" `User`? How is this normally done? A forum seems like a normal type of app. – B. Bulpett Jan 31 '16 at 15:00
  • Have you looked at the params hash prior to the create action? Maybe add a debug statement like --- flash[:info] = "Hash: #{params} User: #{current_user.id}" --- comment out the create action so you don't error out. Your goal is to see what is coming out of your form, and the state of your user prior to attempting to create records. – Elvn Jan 31 '16 at 15:17
  • 1
    Yes that's a good point about the routes – Richard Peck Jan 31 '16 at 16:04
  • @RichPeck Thanks. I know that shallow routes are preferable, but I really want the `posts` to be nested in `topics`, nested in `forums`. Am I to deduce that this is not possible in RoR? I have run a debug statement like the one above and it returns **nothing**, and the same when running byebug. I feel like the relationships are basically set up properly in the models. I also think the form partial makes sense. That's why I'm looking at the `topics_controller` and `routes.rb` for the solution. Most of the examples online are for old versions of rails, I guess because people are using Discourse. – B. Bulpett Jan 31 '16 at 16:15
  • 1
    I'm writing a post about that. It's not an answer in itself but may give some ideas – Richard Peck Jan 31 '16 at 16:15
  • 1
    Can you post your code on Github? – Richard Peck Jan 31 '16 at 16:20
  • 1
    Okay I found one potential issue. You're referencing `belongs_to :forum` in the `Post` model, yet are using `has_many :posts, through: :topics` in `Forum`. Remove `belongs_to :forum` -- if a `topic belong_to :forum` & `post belongs_to :topic`, `post` should `belong_to` forum – Richard Peck Jan 31 '16 at 16:31
  • @RichPeck Great! that helped get rid of the `Posts forum must exist` error, but the other 2 validation errors still appear... *Posts topic must exist* and *Posts user must exist*. Should I remove those as well? That doesn't really make sense since there is no `:through` method used. – B. Bulpett Jan 31 '16 at 16:38
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/102155/discussion-between-rich-peck-and-b-bulpett). – Richard Peck Jan 31 '16 at 16:40

1 Answers1

1

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] %>
Richard Peck
  • 76,116
  • 9
  • 93
  • 147