Search code examples
ruby-on-railsmany-to-manyblogscategories

How can I set my controller and views when I have two Many-to-Many association?


My aim is to organize uploaded posts by categories. These categories are shown on the nav bar and if you click one of the category's names, you will see the posts assigned with the category. When uploading a post, you can assign it with multiple categories too. So, I think this is like many posts can have many categories and many categories can have many posts.

This is how I want my posts organized by categories

However, I cannot set things right in my posts_controller.rb, posts/index.html.erb, posts/show.html.erb, and _navigation.html.erb

post.rb

class Post < ActiveRecord::Base
  #This validates presence of title, and makes sure that the length is not more than 140 words
  validates :title, presence: true, length: {maximum: 140}
  #This validates presence of body
  validates :body, presence: true
    has_many :categorizations
    has_many :categories, :through => :categorizations 
end

category.rb

class Category < ApplicationRecord
    has_many :categorizations
    has_many :posts, :through => :categorizations
end

categorization.rb

class Categorization < ApplicationRecord
    belongs_to :post
    belongs_to :category
end

Then, here are these controller and views I am confused with:

posts_controller.rb

class PostsController < ApplicationController
  before_action :find_post, only: [:edit, :update, :show, :delete]
  before_action :authenticate_admin!, except: [:index, :show]
  # Index action to render all posts
  def index
    if params.has_key?(:category)
    @category = Category.find_by_name(params[:category])
    @posts = Post.where(category: @category)
    else
    @posts = Post.all
    end
  end

  # New action for creating post
  def new
    @post = Post.new
  end

  # Create action saves the post into database
  def create
    @post = Post.new(post_params)
    if @post.save
      flash[:notice] = "Successfully created post!"
      redirect_to post_path(@post)
    else
      flash[:alert] = "Error creating new post!"
      render :new
    end
  end

  # Edit action retrives the post and renders the edit page
  def edit
  end

  # Update action updates the post with the new information
  def update
    @post = Post.find(params[:id])
    if @post.update_attributes(post_params)
      flash[:notice] = "Successfully updated post!"
      redirect_to posts_path(@posts)
    else
      flash[:alert] = "Error updating post!"
      render :edit
    end
  end

  # The show action renders the individual post after retrieving the the id
  def show 
  end


  # The destroy action removes the post permanently from the database
  def destroy
    @post = Post.find(params[:id])
    if @post.present?
       @post.destroy
      flash[:notice] = "Successfully deleted post!"
      redirect_to posts_path
    else
      flash[:alert] = "Error updating post!"
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, category_ids: [])
  end

  def find_post
    @post = Post.find(params[:id])
  end
end

index.html.erb

<div class="container">
  <div class="col-sm-10 col-sm-offset-1 col-xs-12">
    <% @posts.each do |post| %>
    <div class="col-xs-12 text-center">
      <div class="text-center">
        <h2><%= post.title %></h2>
        <h6><%= post.created_at.strftime('%b %d, %Y') %></h6>
      </div>
      <div>
        <%= raw post.body.truncate(358) %>
      </div>
      <div class="text-center">
        <%= link_to "READ MORE", post_path(post) %>
      </div>
      <% if admin_signed_in? %>
        <%= link_to "Show", post_path(post), class: "btn btn-primary" %>
        <%= link_to "Edit", edit_post_path(post), class: "btn btn-default" %>
        <%= link_to "Delete", post_path(post), class: "btn btn-danger", data: {:confirm => "Are you sure?"}, method: :delete %>
      <% end %>
      <hr />
    </div>
    <% end %>
  </div>
</div>

show.html.erb

<div class="col-sm-11 col-xs-12 blog-content">
  <h2 class="text-center"><%= @post.title %></h2>
  <h1 class="text-center"><%= @category.name %></h1>
  <h5 class="text-center"><%= @post.created_at.strftime('%b %d, %Y') %></h5>
  <div class="text-center"><%= raw @post.body %></div>
</div>

_navigation.html.erb(part of it)

   <ul class="nav navbar-nav navbar-left">
    <% Category.all.each do |cat|  %>
    <li class="text-center"><%= link_to cat.name, posts_path(category: cat.name) %></li>
    <% end %>
   </ul>

Just in case, schema.rb

ActiveRecord::Schema.define(version: 2018_11_07_082317) do

  create_table "admins", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "avatar"
    t.index ["email"], name: "index_admins_on_email", unique: true
    t.index ["reset_password_token"], name: "index_admins_on_reset_password_token", unique: true
  end

  create_table "categories", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "categories_posts", id: false, force: :cascade do |t|
    t.integer "category_id"
    t.integer "post_id"
  end

  create_table "categorizations", force: :cascade do |t|
    t.integer "post_id"
    t.integer "category_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "ckeditor_assets", force: :cascade do |t|
    t.string "data_file_name", null: false
    t.string "data_content_type"
    t.integer "data_file_size"
    t.string "type", limit: 30
    t.integer "width"
    t.integer "height"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["type"], name: "index_ckeditor_assets_on_type"
  end

  create_table "posts", force: :cascade do |t|
    t.string "title"
    t.text "body"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

Solution

  • In show view you have

    <h1 class="text-center"><%= @category.name %></h1>
    

    But you don't define @category in show action. If you want to list categories, it should be

    <h1 class="text-center"><%= @post.categories.pluck(:name).join(', ') %></h1>
    

    Btw, looks like you have useless table categories_posts in the schema.rb

    Update:

    About index action - you should change query for @posts, since the post doesn't have a category column, but he has categories association:

    def index
      if params.has_key?(:category)
        # you can remove @category defining if you don't need it somewhere in view
        @category = Category.find_by_name(params[:category])
        @posts = Post.joins(:categories).where(categories: { name: params[:category] } )
      else
        @posts = Post.all
      end
    end
    

    Note, it is better to use id for the query, not name, searching by id is faster. You need to change link in navbar to link_to cat.name, posts_path(category: cat.id) for it and replace name with id in the query. And it is better to move the whole query to named scope in the Post model.