個人的勉強メモ置き場

プログラミングど素人のメモ置き場

カテゴリー機能を作る(Rails6)

ブログ機能にカテゴリーをつけて記事を絞れるようにします

f:id:zykb:20211224025936p:plain

中間テーブルの作成

articleとcategoryは

article : 複数のcategoryを持つ
category : 複数のarticleに属する

ので多対多の関係にあります。

この時articleテーブルでcategoryを管理すると、categoryを増やすたびにカラムも増やさないといけなくなるため冗長なものとなります
f:id:zykb:20211223222731p:plain

そのため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タグのテキストに相当

f:id:zykb:20211224021958p:plain
こんなチェックボックスになります
"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]の有無で分岐させることで該当カテゴリーの記事のみを一覧画面に表示することできます

f:id:zykb:20211223220727g:plain

カテゴリーで記事を絞り込めるようになりました


参考
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 %>を記述します

f:id:zykb:20211207162151p:plain
pagination


このままだと質素なので用意されているbootstrapのパーシャルテンプレートを生成します

rails g kaminari:views bootstrap4

すると以下のようにbootstrapデザインが適用されます。また、ほかのテンプレートに関してはrails g kaminari:viewsで一覧が表示されます
f:id:zykb:20211207163156p:plain


ここでエラーが出てテンプレートを作成できない場合は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>

f:id:zykb:20211126013502p:plain


また、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);
  }

f:id:zykb:20210803205910g:plain


なかなかいい感じ
今回は時分秒を測定したいのでこちらを追加

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);
  }


f:id:zykb:20210803210646g:plain

?????

本題

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()
地方時に基づき、指定された日時の「時」 (0–23) を返します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date

日本の地方時は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);
  }

f:id:zykb:20210803212801g:plain

大丈夫そう

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
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
@import "bootstrap";
import "bootstrap"
import "../stylesheets/application"

これで(たぶん)OK

UbuntuでHeroku CLI導入時のエラー


ローカル環境のアプリをHerokuにデプロイするためWSL環境に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の移動範囲的なアレを作る

これを作ります

f:id:zykb:20210121220457g:plain

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(); // タイルを全て外す
        }
    }
}

f:id:zykb:20210121223044p:plain

GroundTilemapの上からPossibleTilemapで範囲だけ青の透明タイルで塗っています。

f:id:zykb:20210121221504p:plain
こんなん

今回はユニットなど作っていないので移動力3として想定しています。そのうちタイルごとに必要移動力を変えたりしたいですね。

参考

作るにあたって以下のサイト様を参考にしました。

itsukinamito.hatenablog.com


mynavi-agent.jp