カテゴリー機能を作る(Rails6)
ブログ機能にカテゴリーをつけて記事を絞れるようにします
中間テーブルの作成
articleとcategoryは
article : 複数のcategoryを持つ
category : 複数のarticleに属する
ので多対多の関係にあります。
この時articleテーブルでcategoryを管理すると、categoryを増やすたびにカラムも増やさないといけなくなるため冗長なものとなります
そのためarticleと紐づくcategoryのidを管理する中間テーブル(article_category)を作成します
rails g model Article_Category article:references category:references
:referencesをつけることでそれぞれのモデルと紐づけることができ、自動的に外部キーが付与されます。
class CreateArticleCategories < ActiveRecord::Migration[6.1] def change create_table :article_categories do |t| t.references :article, null: false, foreign_key: true t.references :category, null: false, foreign_key: true t.timestamps end end end
migrateします
rails db:migrate
アソシエーションの記述
article_category.rb
class ArticleCategory < ApplicationRecord belongs_to :article belongs_to :category end
中間テーブルは1つのarticleと1つのcategoryに属しているのでbelongs_toを記述します
article.rb
class Article < ApplicationRecord has_many :article_categories has_many :categories, through: :article_categories end
category.rb
class Category < ApplicationRecord has_many :article_categories has_many :articles, through: :article_categories end
throughオプションをつけることで中間テーブル経由でモデルにアクセスできるようになります
viewの設定
今回はとりあえずカテゴリーは初期データとしてお知らせとブログの二つを投入しておきました
記事の編集や新規作成からカテゴリーを選択できるようにします
<%= form_with model: @article, local: true do |f| %> ... <div class="form-group"> <%= f.label :category %> <%= f.collection_check_boxes(:category_ids, Category.all, :id, :name) do |category| %> <%= category.label do %> <%= category.check_box %> <%= category.text %> <% end %> <% end %> </div> ... <% end %>
抜粋
f.collection_check_boxes(:category_ids, Category.all, :id, :name) do |category|
第一引数(:category_ids)
メソッド名。paramsで取り出せる
第二引数(Category.all)
コレクション。ここではカテゴリーのすべてを指定したいのでCategory.all
第三引数(:id)
inputタグのvalueに相当
第四引数(:name)
inputタグのテキストに相当
"article"=>{"title"=>"テスト記事", "content"=>"<div>これはお知らせです</div>", "category_ids"=>["", "1"]}, "commit"=>"記事の作成", "id"=>"3"}
送信するとこのように第一引数で指定したcategory_idsというキーで渡されます
controllerの設定
StrongParameterでcategory_idsを許可します
def article_params params.require(:article).permit(:title, :content, { category_ids: [] }) end
記事一覧ページではデフォルトではすべての記事を、サイドバーのカテゴリーリンクを押したときにはカテゴリーで記事を絞り込めるように以下のようにしました
def index @categories = Category.all if params[:category_id] @category = Category.find(params[:category_id]) @articles = @category.articles.recent.page(params[:page]) else @articles = Article.recent.page(params[:page]) end end
サイドバー部分
<h3 class="side-title">カテゴリー</h3> <ul> <% @categories.each do |category| %> <li> <%= link_to category.name, articles_path(category_id: category.id) %> </li> <% end %> </ul>
サイドバーのカテゴリーをクリックするとparams[:category_id]で該当カテゴリーを取得できます
params[:category_id]の有無で分岐させることで該当カテゴリーの記事のみを一覧画面に表示することできます
カテゴリーで記事を絞り込めるようになりました
参考
qiita.com
Rails6でページネーションの導入
kaminariのインストール
ページネーションを導入したのでメモ。
will_paginate gem などもありますが今回はkaminariを使います
gem 'kaminari'
の行を追加してbundle install
ページネーションの設定
def index @articles = Article.all.page(params[:page]).per(3) end
表示させたいデータに.page(params[:page])を記述
デフォルトでは25件が表示されるとのことですが、ページネーションを表示させるためにはそれ以上のデータを用意する必要があるので
.per(3)で1ページに表示するレコードを3件までとしました。
他にもモデルに直接記述したり
class Article < ApplicationRecord has_rich_text :content paginates_per 3 end
設定ファイルを作成して記述することも出来ます
rails g kaminari:config
# frozen_string_literal: true Kaminari.configure do |config| # デフォルトではコメントアウトされている config.default_per_page = 3 # config.max_per_page = nil # config.window = 4 # config.outer_window = 0 # config.left = 0 # config.right = 0 # config.page_method_name = :page # config.param_name = :page # config.max_pages = nil # config.params_on_first_page = false end
ページネーションの表示
表示する場合にはviewに<%= paginate @articles %>を記述します
このままだと質素なので用意されているbootstrapのパーシャルテンプレートを生成します
rails g kaminari:views bootstrap4
すると以下のようにbootstrapデザインが適用されます。また、ほかのテンプレートに関してはrails g kaminari:viewsで一覧が表示されます
ここでエラーが出てテンプレートを作成できない場合はgemfileを以下のように変更します
gem 'kaminari', git: 'https://github.com/kaminari/kaminari'
以下参考
qiita.com
qiita.com
github.com
Rails6でリッチテキストエディタを導入する
Rails6から導入されたActionTextを使ってリッチテキストコンテンツと編集機能を導入します
ActionTextを導入
rails action_text:install
を実行するとmigrationファイルが生成されるのでdb:migrateをします
モデルの設定
リッチテキストを既存のモデル(Article)に追加するには以下の記述をします
class Article < AplicationRecord has_rich_text :content end
コントローラーの設定
ストロングパラメーターで参照される属性を許可します
class ArticlesController < ApplicationController … … private def article_params params.require(:article).permit(:title, :content) end end
ビューの設定
フィールドをモデル内で参照します
<div class="form-group"> <%= f.label :content %> <%= f.rich_text_area :content, class: "form-control" %> </div>
また、ImageMagicなどのgemをインストールすることで画像を投稿することも出来ます。
参考サイト
railsguides.jp
ストップウォッチで経過時間を表示させるやつ
ドットインストールのストップウォッチを作るコードとほぼ一緒なので省略してます
let startTime; let timeoutId; let elapsedTime = 0; function countUp() { const d = new Date(Date.now() - startTime + elapsedTime); const m = String(d.getMinutes()).padStart(2, '0'); const s = String(d.getSeconds()).padStart(2, '0'); timer.textContent = `${m}:${s}`; timeoutId = setTimeout(() => { countUp(); }, 1000); }
なかなかいい感じ
今回は時分秒を測定したいのでこちらを追加
const h = String(d.getHours()).padStart(2, '0');
追加後
let startTime; let timeoutId; let elapsedTime = 0; function countUp() { const d = new Date(Date.now() - startTime + elapsedTime); const h = String(d.getHours()).padStart(2, '0'); const m = String(d.getMinutes()).padStart(2, '0'); const s = String(d.getSeconds()).padStart(2, '0'); timer.textContent = `${h}:${m}:${s}`; timeoutId = setTimeout(() => { countUp(); }, 1000); }
?????
本題
JavaScript の Date オブジェクトは、単一の瞬間の時刻をプラットフォームに依存しない形式で表します。 Date オブジェクトは協定世界時 (UTC) の1970年1月1日からの経過ミリ秒数を表現する Number の値を含んでいます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date
とのことなのでDate.now()で取得できる値は世界協定時(UTC)
一方でgetHours()を見てみると
Date.prototype.getHours()
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date
地方時に基づき、指定された日時の「時」 (0–23) を返します。
日本の地方時はUTC+9なのでgetHours()で取得すると最初から9時間差が出てしまっていたご様子
DataクラスにはUTCで取得できるgetUTCHours()があるので
let startTime; let timeoutId; let elapsedTime = 0; function countUp() { const d = new Date(Date.now() - startTime + elapsedTime); const h = String(d.getUTCHours()).padStart(2, '0'); const m = String(d.getMinutes()).padStart(2, '0'); const s = String(d.getSeconds()).padStart(2, '0'); timer.textContent = `${h}:${m}:${s}`; timeoutId = setTimeout(() => { countUp(); }, 1000); }
大丈夫そう
Webpack(er)メモ
Webpack(er)とは
Webpackerは、汎用的なwebpackビルドシステムのRailsラッパーであり、標準的なwebpackの設定と合理的なデフォルト設定を提供します。
https://railsguides.jp/webpacker.html
railsでwebpackをいい感じに設定して使えるようにしてくれるgem。rails6.0以降からデフォルトでインストールされている。
webpackは「JavaScript」「CSS」「画像やフォント」といった静的アセットを管理できます。webpackを使うと、「JavaScriptコードの記述」「アプリケーション内の他のコードの参照」「コードの変換(トランスパイル)や結合」をダウンロードしやすいpackにまとめられます。
https://railsguides.jp/webpacker.html
javascriptをwebpackerで扱う
webpackerではapp/javascript/packsにエントリーポイントを配置する。デフォルトではpacks/application.jsがエントリーポイントとなっているので
- app/views/layouts/application.html.slim
= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
エントリーポイントからコンパイルされたjsファイルパスを取得できる
CSSをwebpackerで扱う
メインのSCSSとしてapplication.scssを作成してimport
$ mkdir app/javascript/stylesheets $ touch app/javascript/stylesheets/application.scss
- app/javascript/packs/application.js
import "../stylesheets/application"
Webページで読み込むには同じようにビューコード内に記述
- app/views/layouts/application.html.slim
= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
ついでにbootstrap5を導入
- コンソール
$ yarn add bootstrap@next $ yarn add @popperjs/core
- app/javascript/stylesheets/application.scss
@import "bootstrap";
- app/javascript/packs/application.js
import "bootstrap" import "../stylesheets/application"
これで(たぶん)OK
UbuntuでHeroku CLI導入時のエラー
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY KEY <公開鍵>
公開鍵の有効期限が切れてたりするのが原因らしい
この場合公開鍵を登録しなおす必要がある
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys <公開鍵>
登録出来たら後は入れるだけ
curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
sudo heroku --version heroku/7.56.1 linux-x64 node-v12.21.0
SRPGの移動範囲的なアレを作る
これを作ります
TileTest.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; public class TileTest : MonoBehaviour { public Tilemap tilemap; public TileBase possibleTile; public Grid grid; public Camera camera; void ChackMoveRange(Vector3Int unitPos, int maxAmount) { SearchPossible(unitPos, maxAmount); } void SearchPossible(Vector3Int pos, int remainAmount) { if(remainAmount == 0) return; tilemap.SetTile(pos, possibleTile); remainAmount--; // 移動力を下げる // 再帰的に4方向を確認 SearchPossible(pos + Vector3Int.up, remainAmount); SearchPossible(pos + Vector3Int.right, remainAmount); SearchPossible(pos + Vector3Int.down, remainAmount); SearchPossible(pos + Vector3Int.left, remainAmount); } void Update() { if(Input.GetMouseButtonDown(0)) { Vector3 clickPos = Input.mousePosition; Vector3Int gridPos = grid.WorldToCell(camera.ScreenToWorldPoint(clickPos)); ChackMoveRange(gridPos, 3); } else if(Input.GetMouseButtonUp(0)) { tilemap.ClearAllTiles(); // タイルを全て外す } } }
GroundTilemapの上からPossibleTilemapで範囲だけ青の透明タイルで塗っています。
今回はユニットなど作っていないので移動力3として想定しています。そのうちタイルごとに必要移動力を変えたりしたいですね。