Rails ブックマークの実装part1

実装内容

  • 掲示板の☆ボタンを押すと、その掲示板をブックマーク/解除出来る機能を作成。
  • ブックマークすると★解除すると☆になるようにし、フラッシュメッセージも表示させる。
  • 同じユーザが同じ掲示板を複数回ブックマークできないようにする。
  • ブックマークの一覧ページを作成する。

実装の流れ

  • 中間モデルの定義
  • ルーティングを作成
  • bookmarkコントローラの実装
  • viewの実装

中間モデルの定義

userモデルとboardモデルの間にbookmarkモデルを作成する。
userモデル

id user
1 meo

bookmarkモデル←中間モデル

id user board
1 meo abcd

boardモデル

id board
1 abcd

user board bookmarkの関係

userが掲示板をブックマークするとその数だけ掲示板はブックマークを持っている。
つまり、userとboardはbookmarkを持っているので多対多の関係であると言える。

中間モデルについて

多対多である中間モデル(bookmark)には、外部キー制約(foreign_key)を付ける必要がある。
どのuserがどのboardをブックマークしたかuser_id,board_idが保存される。
bookmarkモデル

id user board
1 user_id board_id

bookmarkモデルを作成

外部キーをつけたいのでreference型を使い作成する。
紐づくモデル名+「_id」で外部キーと呼ぶ。
今回はuser,boardと紐付けるので以下のコマンドを実行する。

$ rails g model Bookmark user:references board:references
Running via Spring preloader in process 5265
      invoke  active_record
      create    db/migrate/20210603064703_create_bookmarks.rb
      create    app/models/bookmark.rb

同じユーザが同じ掲示板を重複してブックマークできないようにしたいので、 作成されたマイグレーションファイルへ以下の記述を追加する。
t.index [:user_id, :board_id], unique: true
unique:trueでテーブル内で重複するデータを禁止する一意性制約をつける。
マイグレーションを実行。

$ rails db:migrate
== 20210603064703 CreateBookmarks: migrating ==================================
-- create_table(:bookmarks)

各モデルのアソシエーション

  belongs_to :user
  belongs_to :board
  validates :user_id, uniqueness: { scope: :board_id }

データベース側に一意性制約をつけたが、モデル側にも同様に記述する。
uniquenessでオブジェクトが保存される前に属性の値が重複してないことを検証する。

  has_many :boards, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
  has_many :bookmark_boards, through: :bookmarks, source: :board

親(user)を削除したら、一緒に子(booksmark)も削除されるように
dependent: :destroyを追加。

has_many :bookmark_boards, through: :bookmarks, source: :board

について
boardsは既にあるのでbookmark_boardsを定義し、ユーザがブックマークしている掲示板を取得する。
through: :bookmarks, source: :boardでbookmarksを通してboardモデルから掲示板を取得している。

 has_many :bookmarks, dependent: :destroy
 belongs_to :user
同じく、親(board)を削除したら、一緒に子(bookmarks)も削除されるように
dependent: :destroyを追加。

userモデルにbookmarkしたか判定するメソッドを追加

コントローラにも記述できるが、モデルに書くのが一般的。

#ブックマーク追加
  def bookmark(board)
    bookmark_boards << board
  end
<<で引数で渡したboardレコードが先程has_many throughで定義したbookmark_boardsに保存される。

#ブックマーク外す
  def unbookmark(board)
    bookmark_boards.destroy(board)
  end

bookmark_boardsから引数のboardレコードを削除する。

#ブックマークしてるか判定するメソッド
  def bookmark?(board)
     bookmark_boards.include?(board)
  end
includes?メソッドで引数のboardレコードが含まれてたらtrue、なければfalseを返す。
part2へ

バグ修正メモ

System Versions

  • Ruby version
2.6.3

  • Rails version
5.2.3

以下のエラーを修正していく。

blog一覧ページにアクセスした場合 blogの一覧ページが表示されること

ルーティングが間違っていた。
blogではblogsコントローラのindexアクションを呼び出せないのでエラーになっている 一覧画面を表示させるルーティングを追加

Rails.application.routes.draw do
  root to: 'blogs#index'
  resources :blogs do
    resources :comments, only: [:create, :destroy]
  end
end

blog→blogsへ変更することで blog#index→blogs#indexになるのでindexが呼び出せる。

新規作成ページにアクセスした場合 blogの新規作成ページが正しく表示されていること

これはすぐに分かった。missing templateなので
_new.html.erbnew.html.erbフォルダ名が違った。

Blogs blog新規作成ページにアクセスした場合 blogの新規作成ができること

これはbinding.irbで処理を確認した。

    19:   def create
    20:     @blog = Blog.new(blog_params)
 => 21:     binding.irb
    22:     if @blog.save
    23:       redirect_to @blog, notice: 'Blog was successfully created.'
    24:     else
    25:       render :new
    26:     end

#入力内容がサーバーへ送られてるか確認
irb(#<BlogsController:0x00007fd818aa88e8>):001:0> params
=> <ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"o7JUcI/7WunHKid9VYaN74qxOioy57jTpGlH954E5kUnM4IQS/8C1gf9YaEHO4WJ54EOO4StgWLdyZb2d/gBeA==", "blog"=><ActionController::Parameters {"title"=>"ssssss", "content"=>"ssssssss"} permitted: false>, "commit"=>"Create Blog", "controller"=>"blogs", "action"=>"create"} permitted: false>

#@blogが作れてるか確認するとcontentがnilになってる
irb(#<BlogsController:0x00007fd818aa88e8>):002:0> @blog
=> #<Blog id: nil, title: "ssssss", content: nil, created_at: nil, updated_at: nil>

irb(#<BlogsController:0x00007fd818aa88e8>):003:0> @blog = Blog.new(blog_params)
Unpermitted parameter: :content
=> #<Blog id: nil, title: "ssssss", content: nil, created_at: nil, updated_at: nil>

Unpermitted parameter: :contentからストロングパラメータにcontentが抜けていることがわかる

blog一覧から詳細ページにアクセスした場合 blogの詳細ページが表示されること

Did you mean? で言われてるように
comments→commentと直せばいいが,アソシエーションをかえたほうがいい
has_many :comment→has_many :commentsと直せばcommentsというメソッドが使えるようになる。

blog詳細ページで編集画面へのリンクをクリックした場合 blogの編集ページが表示されること

<%= link_to 'Edit', '/blogs/#{blog.id}/edit' %>
“”’’になっていた。blog →@blogへ変更。

blog編集ページにアクセスした場合 blogの編集用フォームが表示されること

<% render 'form', blog: @blog %>のイコールがぬけていたのでレンダーされていなかった。
@blogをローカル変数blogへ渡しフォームへ渡している

blog編集ページにアクセスした場合 内容を編集できる

編集ページにアクセスしたらブログの投稿フォームが表示されてしまう
サーバーログを確認するとcreateアクションが実行されている

_form.html.erbをみてみるとform_with (model: Brog.new, local: true) do |f|となっていて
引数に新規作成newを渡していることが原因でcreateアクションが実行されている。
blog: @blogで定義したローカル変数blogにかえる。

blog新規作成ページにアクセスした場合 blogの新規作成でcontentも正しく作成できること

blogを新規作成した時にcontentだけ0になる。
データベースのテーブルの定義を確認すればいい。
マイグレーションファイルのcontentがinteger型で定義されていたので文字列を入れても0になっていた
rails db:rollbackで1つずつもどしてrails db:migrateすればおk

blog詳細ページでコメントした場合 blogの詳細ページにコメントが表示されること

@blog.comments.count.to_iでintegerとして認識されてしまっている。
文字列として表示させたいのでto_sにする。

動的タイトル表示の実装

ヘルパーを使ってタイトルを動的に表示させる。

ここでいうタイトルとはブラウザのタブに表示される文字のこと↓



実装内容

今回はヘルパーを使い、以下の各ページごとに動的にタイトルを表示させる。
content_forを使い、「ページごとのタイトル名 | タイトル名」のように表示させる。
トップページは「タイトル名」のみ表示。

  • トップページ
  • ログインページ
  • ユーザー登録ページ
  • 掲示板作成ページ
  • 掲示板一覧ページ
  • 掲示板詳細ページ

実装手順

  • app/helpers/application_helper.rbにメソッドを作成する。
  • 各ページのviewファイルへ設定を読み込ませる。

ヘルパーの作成。

empty?メソッドを使い、真偽判定をする。
変数の値が""(文字列の場合)や値が空白の場合、真となる。 条件演算子(?:)
条件式 ? 真の時の値 : 偽の時の値で記述。
page_titleの引数に''を入れて例外が発生するのを防ぐ。

module ApplicationHelper
  def page_title(page_title = '')
    base_title = 'SAMPLE APP'
    page_title.empty? ? base_title : page_title + ' | ' + base_title
  end 
end

webサイト全体のviewへ適応する。

i18n翻訳ファイルの編集。

ja:
  defaults:
    login: 'ログイン'
    register: '登録'
    title: 'ユーザー登録'
    to_login_page: 'ログインページへ'
  user_sessions:
     new:
       Back: '戻る'
       title: 'ログイン'
       email: 'メールアドレス'
       password: 'パスワード'
       to_register_page: '登録ページへ'
  users:
     new:
       title: 'ユーザー登録'  
  boards:
     title: '掲示板作成'
     index: '掲示板一覧'

ヘルパーで作成したメソッドを読み込む。
yieldメソッド
ビューを挿入すべき場所を指定。

〜
<title><%= page_title(yield(:title)) %></title>

これでトップページにbase_titleが適応される。

各viewファイルに読み込ませていく

content_forメソッド
コンテンツを名前付きのyieldとしてレンダリング

<%= content_for(:title, t('.title')) %>
<%= content_for(:title, t('boards.title')) %>
<%= content_for(:title, t('boards.index')) %>

掲示板詳細画面は閲覧中の掲示板名を表示させる。

<% content_for(:title, @board.title) %>

と記述。

Rails 掲示板にコメント機能を追加 part2

part1からの続き。
掲示板の詳細画面を実装したので、そこにコメント機能を実装していく。

投稿詳細画面へコメントの入力フォーム、エリアを追加する。
# Controllerへ追記。

boards_controllerにインスタンス変数@commentと@commentsを定義し、view側で呼び出す。

class BoardsController < ApplicationController
  skip_before_action :require_login, only: %i[index create show]
  def show
    if logged_in?
      @board = Board.find(params[:id])
      @comment = Comment.new
      @comments = @board.comments.includes(:user).order(created_at: :desc)
    else
      redirect_back_or_to login_path, danger: 'ログインしてください'
    end
  end

logged_in?メソッドを使い、ログインしていない場合 ログインページにリダイレクトされるようにした。
ここは単純にskip_before_action :require_login, only: %i[index create]で良かった。
コメントを習得する際はN+1問題に注意し、includesメソッドを使用しユーザーを呼び出す。
created_at: :descで作成された順に降順で表示。

コメントフォームのview

パーシャルで作成する。
ルーティングをネスト(board/board_id/comment)している場合はurlを2つ指定する。

<!-- コメントフォーム -->
<div class="row mb-3">
  <div class="col-lg-8 offset-lg-2">
    <%= form_with model: comment, url: [board, comment], local: true do |f| %>
      <%= render 'shared/error_messages', object: f.object %>    
      <%= f.label :body %>
      <%= f.text_area :body, class: 'form-control mb-3', rows: 4,  placeholder: 'コメント' %>
      <%= f.submit class: 'btn btn-primary', value: '投稿' %>
    <% end %>
  </div>
</div>   

コメントエリアのview

こちらもパーシャルで作成。
コメントを作成したユーザーにだけアイコンを表示。

<!-- コメントエリア -->
<%= comment.user.decorate.full_name %>
<%= simple_format(comment.body) %>
#入力値の改行に対応する
〜
 <% if current_user.own?(comment) %>  
#自分のコメントだけ編集、削除アイコンを表示する
<td class="action">
  <ul class="list-inline justify-content-center" style="float: right;">
    <li class="list-inline-item">
      <a href="#" class='js-edit-comment-button' data-comment-id="<%= comment.id %>">
        <%= icon 'fa', 'pen' %>
      </a>
    </li>
    <li class="list-inline-item">
      <a href="#" class='js-delete-comment-button' data-comment-id="<%= comment.id %>">
        <%= icon 'fas', 'trash' %>
      </a>
    </li>
   </ul>
 </td>
<% end %>
  • コメントエリア_comment.html.erbにbootstrapを適応。
    render commentsでよしなに_comment.html.erbを呼び出している。
    render @commnetsでも同様。
<div class="row">
  <div class="col-lg-8 offset-lg-2">
    <table id="js-table-comment" class="table">
      <%= render comments %>
    </table>
  </div>
</div>

掲示板一覧画面と掲示板詳細画面において、コメントを作成したユーザーにのみ編集、削除ボタンを表示させる。

  • モデル側にユーザーのコメントであるか判定するメソッドを定義。
  def own?(object)
    id == object.user_id
  end
  • 削除、編集ボタンのパーシャルを作成。
    掲示板の編集と削除のボタンは、掲示板の一覧と詳細ページで同じものを表示する。
<ul class='crud-menu-btn list-inline float-right'>
  <li class="list-inline-item">
      <%= link_to '#', id: "button-edit-#{board.id}" do %>
      <%= icon 'fa', 'pen' %>
    <% end %>
  </li>
  <li class="list-inline-item">
    <%= link_to '#', id: "button-delete-#{board.id}" do %>
      <%= icon 'fas', 'trash' %>
    <% end %>
  </li>
</ul>

掲示板詳細画面show.html.erb

  • 編集削除ボタン
  • コメントフォーム
  • コメントエリア
    を読み込む。
    フォームは、comments/_form.html.erbをレンダーし、@boardと@commentを引数として渡している。
    エリアは、comments /_comments.html.erbをレンダーし、@commentsをcommentsという変数で使えるようにしている。
<!-- 編集、削除ボタン-->
<%= render 'crud_menus', board: @board %>
<!-- コメントフォーム -->
<%= render 'comments/form',{board: @board, comment: @comment} %> 

<!-- コメントエリア -->
<%= render "comments/comments", {comments: @comments} %>
  • 掲示板一覧画面に編集、削除ボタンを読み込む。
<%= render 'crud_menus', board: board %>

comments_controllerの作成。

$ rails g controller comments
userとcommentでアソシエーションが定義されてるのでその点に注意する。

class CommentsController < ApplicationController
  def create
    comment = current_user.comments.build(comment_params)
    if comment.save
      redirect_to board_path(comment.board), success: 'コメントを作成しました'
    else
      redirect_to board_path(comment.board), danger: 'コメントを作成できませんでした'
    end
  end
end

ログインユーザーに関連したコメントを表示させる。
current_userメソッドでログイン中のユーザーを返す。
アソシエーションで定義されたオブジェクトを初期化する際はnewではなくbuildを使う。

 comment = current_user.comments.build(comment_params)

ストロングパラメータの追加。

コメントをデータベースへ保存するにはuser_idとboard_idが必要でcommentの値(body)とboard_idが保存される。
(board_id: params[:board_id])はURLであるboards/:board_id/commentsからboard_idをとってきている。
mergeはハッシュを合体させるメソッド。

  private

  def comment_params
    params.require(:comment).permit(:body).merge(board_id: params[:board_id])
  end

Rails 掲示板にコメント機能を追加 part1

概要

  • 掲示板詳細画面を追加し、その中でコメントをできるようにする。
  • 掲示板一覧画面から掲示板のタイトル名を押すと掲示板詳細画面へ行けるようにする。
  • 書き込んだコメントが一番上に表示されるようにコメントの並び順を指定
  • コメントした本人のみに削除・編集ボタンを表示させる
  • コメントを投稿したユーザーが削除されると、ユーザーが投稿したコメントも削除される
  • コメントが投稿された掲示板が削除されると、掲示板に投稿されたコメントも削除される

モデルの作成

Commentモデルを作成し、User、Boardモデルとアソシエーションを定義する。
Commentモデルに外部キー(user_id, board_id)を設定。
Commentモデルにバリデーションを設定。
今回はreferencesでモデルと同時に作成したのでCommentモデル側のアソシエーションは自動で定義される。
(CommentをUserとBoardに関連付けたいので、User、Boardにhas_many、Commentにbelong_toを定義)

$ rails g model Comment body:text user:references board:references
      invoke  active_record
      create    db/migrate/20210522131533_create_comments.rb
      create    app/models/comment.rb

空のデータは登録できないようにNOTNULL制約を追加。
外部キーが記述されていることを確認する。

class CreateComments < ActiveRecord::Migration[5.2]
  def change
    create_table :comments do |t|
      t.text :body, null: false
      t.references :user, foreign_key: true
      t.references :board, foreign_key: true

      t.timestamps
    end
  end
end

ここまで確認できたらrails db:migrateを実行。

== 20210522131533 CreateComments: migrating ===================================
-- create_table(:comments)
   -> 0.0036s
== 20210522131533 CreateComments: migrated (0.0040s) ==========================

Commnetモデルにバリデーションの追加。
空の投稿を防ぐ為、presence: trueをつける。

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :board

  validates :body, presence: true, length: { maximum: 10_000 }
end

User、Boardモデル側に以下のアソシエーションを定義
has_many :comments, dependent: :destroy
dependent: :destroyオプションを定義しないと、親モデル(user board)を削除した時に、その親モデルに紐づく子モデル(comment)の内容が残ってしまうので注意する。

掲示板詳細画面の作成

ルーティングの作成

今回は、BoardモデルとCommentモデルが紐付いてるので、ルーティングをネスト(入れ子)する必要がある。
ネストする場合はshallowオプションを使用する。
shallowオプションを使用するとルーティングが簡潔になる。

  resources :boards, only: %i[index new create show] do
    resources :comments, only: %i[create destroy], shallow: true
  end

使用前
comments#createはcommentのidではなくboard_idを指定する。

        Prefix Verb   URI Pattern                              Controller#Action
board_comments POST   /boards/:board_id/comments(.:format)     comments#create
 board_comment DELETE /boards/:board_id/comments/:id(.:format) comments#destroy

使用後
comments#destroyはcommentのidを指定するのでboard_idは指定しない。

        Prefix Verb   URI Pattern                          Controller#Action
board_comments POST   /boards/:board_id/comments(.:format) comments#create
       comment DELETE /comments/:id(.:format)              comments#destroy

controllerの作成

@board = Board.find(params[:id]) でリクエストパラメータ(params)経由でidを受け取りBoardオブジェクトをデータベースから取得。
findはidによってそのモデルに対応するレコードをデータベースから検索する。
params[:id]にはURL “localhost:3000/boards/[id]”の[id]の部分が格納されてる。

class BoardsController < ApplicationController
  skip_before_action :require_login, only: %i[index create show]
#...
  def show
    @board = Board.find(params[:id])
#...

viewの作成

boards/show.html.erbを作成。

#...
<%= image_tag @board.board_image.url, class: 'card-img-top img-fluid', size: '300x200' %>
<%= @board.title %>
#掲示板名の表示
<%= @board.user.decorate.full_name %>
#user_decorator.rbのfull_nameメソッドを呼び出し、ユーザー名を表示する。
<%= l @board.created_at, format: :long %>
#...

lメソッドについて

lメソッドはi18nにおいて日付や時刻を表す際に使用する。
config/locales/ja.ymlに書かれた内容を読み込む。
formatオプションを使うことで複数の書式を使い分けることができる。

ja:
  time:
    formats:
      default: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
      # 2021年05月23日(日) 02時10分39秒 +0900
      long: "%Y/%m/%d %H:%M"
      # 2021/05/23 02:10
      short: "%m/%d %H:%M"
      # 05/23 02:10

掲示板一覧画面から掲示板のタイトル名を押すと掲示板詳細画面へ行けるようにする。

掲示板一覧画面に詳細画面へのリンクを追加。
今回のboard_pathはidが指定されているのでlink_to prefix_path(渡すid)と記述。

   <%= link_to board.title, board_path(board.id) %>

part2へ続く。

掲示板に画像アップロード機能を実装

実装内容

  • 掲示板作成画面から画像を投稿する。
  • gem CarrierWaveとMiniMagickを利用。
  • サムネイルというラベルを追加し、画像の選択をする。
  • 画像を選択せずに投稿した場合、デフォルトの画像を表示。
  • アップロードできるファイルはjpg, jpeg, png, gifのみに制限。
  • 画像投稿時にプレビューとして表示。

    導入

    CarrierWaveRailsアプリケーションに画像アップロード機能をつけることができる。
    MiniMagickはアップロードされた画像のリサイズなどができる。
    MiniMagickを使用するには、ImageMagickが必要なので事前にインストールする。

$ brew install imagemagick
gem 'carrierwave'
gem 'mini_magick'

Boardモデルのboardsテーブルにboard_imageカラムを追加する。

board_imageカラムには画像情報(ファイルそのものではない)が格納される。

$ rails g migration AddBoardImageToBoards board_image:string
Running via Spring preloader in process 10390
      invoke  active_record
      create    db/migrate/20210520113137_add_board_image_to_boards.rb

画像を選択せずに掲示板を作成できるようにしたいので、マイグレーションファイルはそのままでNOTNULL制約はつけない

class AddBoardImageToBoards < ActiveRecord::Migration[5.2]
  def change
    add_column :boards, :board_image, :string
  end
end
$ rails db:migrate
== 20210520113137 AddBoardImageToBoards: migrating ============================
-- add_column(:boards, :board_image, :string)
   -> 0.0013s
== 20210520113137 AddBoardImageToBoards: migrated (0.0016s) ===================

画像uploaderの作成

ジェネレータでuploaderを生成することから始める。

$ rails g uploader image(アップローダー名)

これにより次のファイルが生成される。
ここには画像のリサイズなど、設定を記述する。

 create  app/uploaders/image_uploader.rb

uploaderとモデルを紐付ける。

今回はBoardモデルと紐付ける。

class Board < ApplicationRecord
  mount_uploader :board_image, ImageUploader

image_uploader.rbの編集

このファイルはデフォルトでコードが記述されており、コメントアウトを解除して使用する。 MiniMagickでリサイズするので記述。

class ImageUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

デフォルトの設定

  • storage :file
    public/uploads/に画像が保存される。

  • def store_dir
    画像が保存される先のpathを指定。

  storage :file

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

追加設定

  • 画像を選択せずに投稿した場合、デフォルトとして使う画像を指定する。
  • アップロードできるファイルはjpg, jpeg, png, gifのみに制限。
  def default_url
    'board_placeholder.png'
 # app/assets/images/board_placeholder.png
  end
 
  def extension_allowlist
    %w(jpg jpeg gif png)
  end

viewの作成

掲示板作成フォームを編集する。
画像投稿時にプレビューとして表示するには、画像はまだデータベースへ登録されていないので、選択した画像はjavascriptを使って表示させる必要がある。

#...
 <div class="form-group">
 <%= f.label :board_image %>
 <%= f.file_field :board_image, onchange: 'previewFile(preview)', class: 'form-control mb-3', accept: 'image/' %>
 <%= f.hidden_field :board_image_cache %>
 </div>
 <div class='mt-3 mb-3'>
 <%= image_tag board.board_image.url, id: 'preview', size: '300x200' %>
 </div>
#...
onchangeイベントハンドラでjavascriptを指定
フォームの入力値、選択が変更されたときに処理を行う。
acceptでファイルを指定する。image/
は画像ファイル、video/*は動画ファイル。

<%= f.hidden_field :board_image_cache %>
これは、掲示板作成できなかった場合、アップロードした画像を消えないようにする処理。

javascriptでプレビューを表示させる。

jsファイルの作成。
application.jsは個別のjavascriptを設定するファイルなので、ここには記述してはいけない。

function previewFileWithId(id) {
    const target = this.event.target;
    const file = target.files[0];
    const reader  = new FileReader();
    reader.onloadend = function () {
        preview.src = reader.result;
    }
    if (file) {
        reader.readAsDataURL(file);
    } else {
        preview.src = '';
    }
  }

掲示板一覧画面へアップロードした画像を表示させる。

board.rbのboard_imageからurlをとってくる。

<%= image_tag board.board_image.url, class: 'card-img-top', size: '300x200' %>

ストロングパラメータに画像のフィールドを追加。

  private

def board_params params.require(:board).permit(:title, :body, :board_image, :board_image_cache) end end

ローカルでアップロードした画像をリモートへアップロードしないように設定。

.gitignoreに画像のアップロード先を追加する。

# Ignore vendor
/public/uploads
既にpushしてしまった場合
rmコマンドでリモートリポジトリから削除する。
 $ git rm -r --cached public/uploads
# public/uploadsディレクトリ下のファイルをすべて削除

ログイン中のユーザーで掲示板の新規作成をする

UserモデルとBoardモデルにアソシエーションを設定

Userと関連付いたboardを作成するので、has_manyをUserモデルに定義する。

class User < ApplicationRecord
  authenticates_with_sorcery!
  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
#...

  has_many :boards, dependent: :destroy
end

controller

UserモデルとBoardモデルにアソシエーションが定義されている場合の記述。
以下のように記述した場合、登録に成功しない。

class BoardsController < ApplicationController
  skip_before_action :require_login, only: %i[index create]
  def create
    @board = Board.new(board_params) ←ここ
    if @board.save
      redirect_back_or_to boards_path, info: '掲示板を作成しました'
    else
      flash.now[:danger] = '掲示板を作成できませんでした'
      render 'new'
    end
  end

  private

  def board_params
    params.require(:board).permit(:title, :body)
  end
end

原因

user_idカラム作成時にNOT NULL制約をつけているので、上記コードだとuser_idがnilになってしまう。

ログインユーザーに関連した掲示板のオブジェクトを作成するには

@board = current_user.boards.build(board_params)

current_user.boards.newとすることで、user_idを登録したboardオブジェクトを初期化できる。引数としてパラメータ(board_params)を渡すことでフォームに入力されたオブジェクトを作成することができる。アソシエーションで定義されたオブジェクトを初期化する際はnewではなくbuildを使う。(どちらを記述しても挙動は変わらない)
また、以下のようにmergeを使ってuser_idを用意することもできるが、

Board.new(board_params.merge(user_id: current_user.id))

アソシエーション関連であるオブジェクトは強調したほうが良いので、モデルのアソシエーションに関連性のない情報を登録する時にmergeは使うと良い。

view

ビューはパーシャルを使い表示させる。

      <%= form_with model:board, local: true do |f| %>
        <%= render 'shared/error_messages', object: f.object %> 
        <div class="form-group">
          <%= f.label :title %>
          <%= f.text_field :title, class: "form-control" %>
        </div>
        <div class="form-group">
          <%=f.label :body %>
          <%=f.text_area :body, class: "form-control", low: "10", style: "height: 200px" %>
        </div>
        <%= f.submit "登録する", class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
</div>

パーシャルをnew.htmlで読み込む。

<div class="container">
  <div class="row">
    <div class="col-lg-8 offset-lg-2">
      <h1>掲示板作成</h1>
      <%= render 'form', { board: @board } %>
    </div>
  </div>
</div>