Programming Journal

学習したことの整理用です。

【Rails】【 i18n】日本語化する

初期設定

config/application.rb モジュール内に以下を記入。 ※私は誤ってモジュール外に記入し、rails sエラー「undefined method 'config'」となって数十分溶かしました。

config.i18n.default_locale = :ja ##デフォルトの言語設定
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s] ##言語ファイルを階層ごとに設定するためのもの

i18nを導入する。

GitHub - svenfuchs/rails-i18n: Repository for collecting Locale data for Ruby on Rails I18n as well as other interesting, Rails related I18n stuff

Gemfileに以下を追記して、bundle install

gem 'rails-i18n'

yamlファイルに日本語訳を記入していく。

view用とactiverecord用で分ける。

ja:
  defaults: ##どこでも使えるもの
    login: 'ログイン'
    register: '登録'
    logout: 'ログアウト'
  users: ##usersコントローラーの
    new: ##newアクション(view)
      title: 'ユーザー登録' ## titleって箇所
      to_login_page: 'ログインページへ' ## to_login_pagesって箇所
  user_sessions:
    new:
      title: 'ログイン'    
      to_register_page: '登録ページへ'
      password_forget: 'パスワードをお忘れの方はこちら'
ja:
  activerecord:
    models:
      user: 'ユーザー'
    attributes:
      user:
        email: 'メールアドレス'
        password: 'パスワード'
        last_name: '姓'
        first_name: '名'
        password_confirmation: 'パスワード確認'

view側のファイルが翻訳を読み込めるようコードを書き換える。

※User登録用ページはモデルオブジェクトに紐付いているため、記載不要! ※ログインページはモデルと関連がないので(session)ラベルに対してi18nを記入していく。

      <h1><%= t('.title') %></h1> #ここ
      <%= form_with(url:login_path, local: true) do |f| %>
        <div class="form-group">
          <%= f.label :email, User.human_attribute_name(:email) %> #ここ
          <%= f.email_field :email, class: 'form-control' %>
        <div class="form-group">
          <%= f.label :password, User.human_attribute_name(:password) %> #ここ
          <%= f.password_field :password, class: 'form-control' %>
        <div class="actions">
          <%= f.submit t('defaults.login'), class: 'btn btn-primary' %> #ここ
        </div>
      <% end %>
      <div class='text-center'>
        <%= link_to t('.to_register_page'), new_user_path %> #ここ
        <a href="#"><%= t '.password_forget' %></a>
      </div>

こんな感じ。

以下のリンクに翻訳ファイルがある。これを適用してくれる

rails-i18n/ja.yml at master · svenfuchs/rails-i18n · GitHub

【Rails】【devise】deviseを使って認証機能を実装する

実装したいこと

ログイン機能・ユーザーの新規作成機能を実装したい。
sorceryは使ったことがあったのですが、試しにdeviseでも認証機能を作成したいと思います。
とりあえず、必要最低限な部分は結構簡単にできました。

実装の流れ

  • gem 'devise'のインストール
  • Model生成
  • View生成
  • Controller生成
  • Routing編集

gem 'devise' インストール

公式に沿ってインストールしていきます。
Starting with Rails?のところから
GitHub - heartcombo/devise: Flexible authentication solution for Rails with Warden.

gem 'devise'

ターミナルでbundle installします。

generatorを走らせます。

$ rails generate devise:install

Model

Userモデルを作ります。

$ rails generate devise User

$ rails db:migrate

View

ビューファイルも勝手に作られますが、とても簡素なので、カスタムしていくために生成します。

$ rails generate devise:views users

Controller

コントローラーもカスタムしたいので、生成します。

rails g devise:controllers users 

このコマンドを実行すると、一連のコントローラーが自動生成されます。(ログイン用、新規登録用などなど…

f:id:Study-Diary:20201013172308p:plain
rails g devise:controllers users

とりあえず、ログイン・ログアウト機能に必要な部分のコメントアウトを外してあげる。

# frozen_string_literal: true

module Users
  class SessionsController < Devise::SessionsController
    # before_action :configure_sign_in_params, only: [:create]

    def new
      super
    end

    def create
      super
    end

    def destroy
      super
    end

    # protected

    # If you have extra params to permit, append them to the sanitizer.
    # def configure_sign_in_params
    #   devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
    # end
  end
end

Routing

さっき、作成したコントローラーを使うことをルーティングファイルに知らせます。

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }

  devise_scope :user do
    get '/users/sign_in', to: 'users/sessions#new'
    get '/users/sign_out', to: 'users/sessions#destroy'
  end
end

私はこの設定を誤ってエラーを起こしました。

rails routesで全体のルーティングを確かめるとこんな感じです。

f:id:Study-Diary:20201013173354p:plain
rails routes

View諸々の編集

ログイン前と後でヘッダーの表示を変える設定
flash.noticeを表示させるところの設定など…

(略)
body
    -if user_signed_in? #signed_in?はdeviseで用意されてるメソッド
      = render 'shared/header'
    -else
      = render 'shared/before_login_header'
    -if flash.notice.present?
      .alert.alert-success= flash.notice
    = yield
    = render 'shared/footer'

ログイン前のヘッダー。ログイン用のリンクなど…

ログイン後のヘッダー。ログアウト用のリンクなど…

(略)
  #navbarSupportedContent.collapse.navbar-collapse
     ul.navbar-nav.ml-auto
      li.nav-item.active = link_to 'Logout', users_sign_out_path, method: :delete, class: 'nav-link'

これがデフォルトのログイン画面

f:id:Study-Diary:20201013174321p:plain
ログイン画面

Application_controller

ログインしていない場合は、ログインページにリダイレクトさせる。
このフィルターをかけたくないコントローラーには、skip_before_action :authenticate_user!を記入しておく。

class ApplicationController < ActionController::Base
  before_action :authenticate_user!
end

翻訳ファイル

GitHub - Junsuke/miscellaneous: convenient files for later use

参考

Rails Girls - Japanese

【Rails】deviseの使い方をマスターしてログイン認証機能を実装 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

Deviseの設定手順をまとめてみた。 その2 ViewとControllerのカスタマイズ編 - Qiita

【Rails】【Pundit】認可機能の追加

実装したいこと

  • 記事投稿アプリで、管理者以外は記事のCRUD機能を使用できないようにしたい。

  • 権限のないユーザーが該当のページにアクセスしたときは、403エラー画面を表示させる。

実装の流れ

  • Punditの導入
  • policyファイルの設定
  • Controller設定
  • View設定

  • エラー画面設定

前提

enumで、管理者adminと一般ユーザーwriterについて定義済み。
CRUD機能についても実装済

Pundit の導入

Punditを使えば、認可のシステムを簡単に実装することができます。
公式に沿って設定していきます。
GitHub - varvet/pundit: Minimal authorization through OO design and pure Ruby classes

Installation

gem "pundit"

ターミナルでbundle installします。

application controllerPunditincludeします。

class ApplicationController < ActionController::Base
  include Pundit
end

generatorを走らせると、policyファイルを生成してくれます。

rails g pundit:install

Policies

Policyファイルに認可を与えるユーザーについて設定していきます。
今回は、adminにだけ、認可を与えることにします。

class ArticlePolicy < ApplicationPolicy
  def index?
    user.admin? 
  end

  def create?
    user.admin?
  end

  def update?
    user.admin?
  end

  def destroy?
    user.admin? 
  end
end

クラスの継承について

クラスを継承している場合、継承元のPolicyに認可の設定をすれば、継承先でも適用されます。
私はそのことに気が付かずに、無駄に全ての継承先のPolicyファイルに同じ設定を記入してしまいました。。

Controller

def index
    authorize(Article)
end
(略)

View

権限の有無で、Viewファイルを表示させるかどうか判定

- if policy(Article).index?
              li
                = link_to articles_path do
                  i.fa.fa-folder-open
                  (略)  

エラー画面設定

publicディレクトリに403.htmlファイルをセットしておく。

<!DOCTYPE html>
<html>
<head>
  <title>Forbidden(403)</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>

<body>
<p>You are not allowed to visit this page.</p>
</body>
</html>

config/application.rb
config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbiddenを追記する。※公式にコードあり。

config/environments/development.rbconfig.consider_all_requests_local = trueの記載があれば、403のエラー画面が確認できます。

【Rails】【cron】1時間ごとにタスクを実行する

実装したいこと

記事公開アプリがあります。
記事の状態が「公開待ち」で公開日時を指定している記事を毎時間確認し、公開日時がきたら自動で公開したい。

新しい用語が色々出てきて混乱するのでまとめていきます。

実装の流れ

  • Rakeタスクに、『記事のステータスと公開日時を判定して、「公開」ステータスに変えるタスク』を用意する。
  • cronで、上記タスクを1時間毎に実行する

Rakeとは

Rakeとは…Rubyのタスクランナー。
タスク…例えばテストを実行する・データベースを定期的に更新する…などのタスクを実行してくれる。
Rakeなしでこのような小さなタスクを実行しようと思うと、違うファイルを行き来するし、コードが散らかってしまう。

https://www.rubyguides.com/2019/02/ruby-rake/

Rails P20にも説明あり。

cronとは

The software utility cron also known as cron job is a time-based job scheduler in Unix-like computer operating systems. Users that set up and maintain software environments use cron to schedule jobs[3] (commands or shell scripts) to run periodically at fixed times, dates, or intervals. cron - Wikipedia

定時や定期的にjobを実行してくれる。DBの管理などで使う。

Rakeタスクの実装

前提

enumで記事の状態を「下書き」「公開」「公開待ち」に分類しています。
今回のRakeタスクでは、「公開待ち」の記事について、公開日時を越えたら記事の状態を「公開」に変える処理を行います。

enum state: { draft: 0, published: 1, publish_wait: 2 }

lib/tasks以下ディレクトリにrakeファイルを作ります。

$ rails g task ファイル名

私のNGコード

namespace :classify_article_status do
  desc '公開待ちの中で、公開日時が過去のもの:ステータスを「公開」に変更する'
  task :manage_status => :environment do
    Article.where(state: :publish_wait).find_each do |article| #publish_waitの状態のものを取得してeachで回している。
      if article.published_at <= Time.current #公開日時が現時刻より過去だったら
        article.published! #enumで定義しているので、このような書き方が可能
      end
    end
  end
end

descは、タスクの説明文です。タスク一覧を出力すると、表示されます。
task :タスク名
:environmentでモデルへアクセスします。必要ないときは省略可能。
do以下に実行したい処理を書いていきます。
find_each はレコードを1,000件単位で分割で取得してブロック内の処理をループします。
大量のレコードがあったとき、一度に処理を行おうとするとメモリの大量確保が必要となり、サーバの負担となってしまいます。そのため、find_eachで処理を分割して行います。

NG部分

  • 不要なデータもeachで回してしまっている
  • each文の中で公開日時の判定をしてしまっている
  • ファイル名とTask名が分かりにくい。今回は、updateというニュアンスが必要。

OKコード

公開日時が現在〜過去の記事を取得するscopeを準備します。

scope :past_published, ->{ where('published_at <= ?', Time.current) }

ファイル名・タスク名も分かりやすいものにする。

namespace :article_state do
  desc '公開待ちの中で、公開日時が過去のもの:ステータスを「公開」に変更する'
  task update_article_state: :environment do
    Article.publish_wait.past_published.find_each(&:published!) #ここ
  end
end

Articleから「公開待ち」の状態で公開日時が現在〜過去のものを取ってきてから、find_eachでループさせます。
必要なデータだけ先に抽出してから繰り返し処理をしています。
(&:メソッド名)published!を実行しています。
Rubyチェリー本P98に詳細の説明あり

Rakeタスクの実行

Rakeタスクの一覧表示
descで記入した説明文が表示されています。descriptionの略。

$ bundle exec rake --tasks

f:id:Study-Diary:20201009085044p:plain
タスク一覧

Rakeタスクの実行

$ bundle exec rake article_state:update_article_state

gem wheneverの導入

cronjobsを実行してくれるgem
GitHub - javan/whenever: Cron jobs in Ruby

公式に沿って導入していきます。

gem 'whenever', require: false
$ bundle install

$ bundle exec wheneverize
# config/schedule.rbファイルが生成される。

suchedule.rb

# Rails.rootを使用する
require File.expand_path(File.dirname(__FILE__) + "/environment")

# cronを実行する環境変数(RAILS_ENVが指定されていないときはdevelopmentを使用)
rails_env = ENV['RAILS_ENV'] || :development

# cronの実行環境を指定(上記で作成した変数を指定)
set :environment, rails_env

# cronのログファイルの出力先指定
set :output, "#{Rails.root}/log/cron.log"

#一時間毎に実行する&タスク名の指定
every 1.hours do
  rake 'article_state:update_article_state'
end

gemのwheneverを導入してみた - まっしろけっけ

Railsで定期的にバッチ回す「Whenever」 - Qiita

Crontabへの書き込み

$ bundle exec whenever --update-crontab

crontabコマンドの使い方: UNIX/Linuxの部屋

crontab -lで現在設定されているタスクの一覧が表示される。

【Rails】FatControllerをスッキリさせる

実装したいこと

  • 記事を投稿するアプリの編集画面で、記事のステータスを「下書き」「公開」「公開待ち」に分類したい。

  • ステータスと公開日時は編集時に選択可能。ただし、公開日時によって記事のステータスを「公開」「公開待ち」に自動で判定して変更する。

  • ステータスを「下書き」に指定したときは、「下書き」のままにする。

前提

enumで記事のステータスを定義しています。

enum state: { draft: 0, published: 1, publish_wait: 2 }

コントローラーは2つに分けています。
articles_controller.rb 記事全般の挙動を持つコントローラ
publishes_controller.rb 記事を公開するためのコントローラ

FatController

私が誤ったコード 反省と復習用にみていきます。

  def update
    authorize(@article) 

    if @article.update(article_params)
      if @article.state == 'draft' #下書きのときはそのまま
        @article.draft!
      elsif Time.current >= @article.published_at #公開日時が現在〜過去のとき
        @article.published! #公開
      elsif Time.current < @article.published_at #公開日時が未来のとき
        @article.publish_wait! #公開待ち
      end
      flash[:notice] = '更新しました'
      redirect_to edit_article_path(@article.uuid)
    else
      render :edit
    end
  end

Time.currentpublished_at(公開日時)を比較し、公開日時が現在〜過去のときは、articleのステータスをpublishedに更新しています。
@article.draft!のように直接属性を繋げて書けるのは、enumで定義しているためです。

 def update
    @article.published_at = Time.current unless @article.published_at? 
    
    if @article.valid?
      if Time.current >= @article.published_at
        @article.published!
        flash[:notice] = '記事を公開しました'
      elsif Time.current < @article.published_at
        @article.publish_wait!
        flash[:notice] = '記事を公開待ちにしました'
      end
      redirect_to edit_article_path(@article.uuid)
    else
      flash.now[:alert] = 'エラーがあります。確認してください。'
      @article.state = @article.state_was if @article.state_changed?
      render 'admin/articles/edit'
    end
  end

こちらも、選択した公開日時を現在日時と比較して、「公開」or 「公開待ち」のステータスに変更しています。

何がダメなのか

  • ステータスの判定でenum用のメソッドを使っていない。 (例)@article.draft?
  • コントローラで判定の処理をしていて、FatControllerになっている。

改善

日時で判定してステータスを変更する処理はmodelに切り出していきます。

諸々をモデルへ切り分ける

コントローラで判定していたものを以下のメソッドに移していきます。

  • 日時で公開可能か判定 #1
  • 判定結果ごとのメッセージ分け #2
  • ステータス(state)の調整 #3
  def publishable? #1
    Time.current >= published_at
  end

  def message_on_published #2
    if published?
      '公開しました'
    elsif publish_wait?
      '公開待ちにしました。'
    end
  end

  def adjust_state #3
    return if draft? 

    self.state = if publishable?
                    :published
                 else
                    :publish_wait
                 end
  end

#3のメソッドについて詳しくみていきます。
return if draft? draftだった場合はreturnメソッドで式を抜けます。

self.state = 〜 selfをつけないと、stateに代入することになってしまうので、省略不可です。
article.stateのように使われます。
if~else文の戻り値をself.stateに代入しています。

修正したコントローラ

  def update
    authorize(@article)

    @article.assign_attributes(article_params)
    @article.adjust_state #3でステータスを調整
    if @article.save
      flash[:notice] = '更新しました'
      redirect_to edit_article_path(@article.uuid)
    else
      render :edit
    end
  end

assign_attributesで送られたパラメーターの属性をまとめて上書きする。DBは更新されないので、saveは必要。

ActiveModel::AttributeAssignment
まとめてオブジェクトの属性を変更したい時に便利!assign_attributesメソッド - その辺にいるWebエンジニアの備忘録


  def update
    @article.published_at = Time.current unless @article.published_at?
    @article.state = @article.publishable? ? :published : :publish_wait

    if @article.valid?
      flash[:notice] = @article.message_on_published
      redirect_to edit_admin_article_path(@article.uuid)
    else
      flash.now[:alert] = 'エラーがあります。確認してください。'
      @article.state = @article.state_was if @article.state_changed?
      render 'admin/articles/edit'
    end
  end

@article.state = @article.publishable? ? :published : :publish_wait 三項演算子です。
@article.publishable?が真ならpublishedを、偽ならpublish_wait@article.stateに代入します。

どちらのコントローラもまず、ステータス判定処理をしてから、保存処理をしています。

最初に実装したコードはif文をネストさせており、かなり読みにくかったです。 コードを読むことはできても、いざ自分で実装!となると汚くなってしまいます。。
英語の勉強と同じで、反復あるのみだと思うのでがんばります。

【Rails】【エラー】TypeError - no implicit conversion of nil into String:

エラーを解消したい

記事投稿アプリの中で、記事の中身(文章)を記入せずに空のままプレビュー画面を見ようとしたらエラーがでました。

TypeError - no implicit conversion of nil into String:

f:id:Study-Diary:20201004140239p:plain
エラー画面

英単語の意味
implicit suggested or understood without being stated directly.
conversion when you change something from one form to a different one.

要するに、nilstringにchangeできてないってこと。
nilstring型に変換する必要があります。

問題のコードに.to_sを追加したらOKでした。

(略)
article_blocks.each do |article_block|
      result << if article_block.sentence? #<<で式の戻り値をresultに追加している。
                  sentence = article_block.blockable
                  sentence.body.to_s #追加
(略)

悪くはないけど、以下のコードのほうがよいみたいです。
空文字を代入してることを明示しています。
このコード、現場railsでもチェリー本でも勉強したのに、自分で実装しようと思うとぱっとでてこない…
sentence.body ||= '' は、
sentence.bodynilfalseだったら、''を返す。それ以外だったら、sentence.bodyをそのまま使うことを表しています。

article_blocks.each do |article_block|
      result << if article_block.sentence?
                  sentence = article_block.blockable
                  sentence.body ||= '' #ここ

参考

『プロを目指す人のためのRuby入門』P174

【Rails】パンくずリストの実装

実装したいこと

ウェブページの上部にパンくずリストを表示したいです。 パンくずリストとは…

f:id:Study-Diary:20201004090314p:plain
パンくずリスト
自分が今どのページにいるか分かるリスト。 リスト内の文字をクリックすると、リンク先へ遷移されます。

gem " gretel"の導入

英語でもそのまんま、breadcrumbs(パンくず)といいます。
breadcrumbsを生成するgemが、童話『ヘンゼルとグレーテル』から、gretelというのは遊び心があります。

(TL;DR) Gretel is a Ruby on Rails plugin that makes it easy yet flexible to create breadcrumbs.

GitHub - kzkn/gretel: Flexible Ruby on Rails breadcrumbs plugin.

公式に沿って実装していきます。

gem "gretel"

記入したら、bundle installします。

Generating breadcrumbs configuration file

設定ファイルを生成します。

$ rails generate gretel:install

config/breadcrumbs.rbが生成されます。
今回は、Home > タグ > タグ編集と表示されるように設定していきます。

#先頭ページ
crumb :admin_dashboard do
  link '<i class="fa fa-dashboard"></i> Home'.html_safe, admin_dashboard_path
end

#タグ一覧ページ
crumb :admin_tags do
  link 'タグ', admin_tags_path
  parent :admin_dashboard
end

#タグ編集個別ページ
crumb :edit_admin_tag do |tag|
  link 'タグ編集', edit_admin_tag_path(tag)
  parent :admin_tags
end

link(表示名), (パス名)です。
parentで一つ上の階層を指定していきます。先頭ページはparentは指定しません。

View

      main.content-wrapper
        section.content-header
          h1
            = yield 'content-header'
          == breadcrumbs style: :ol, class: 'breadcrumb'
          #ここにパンくずリストが表示される

各ページでパンくずを出すところを指定していく。
※各ページでは=は使わないです。ビューとして表示させません。

- breadcrumb :admin_tags
- breadcrumb :edit_admin_tag, @tag

RSpec

それぞれのタグが表示されていること&リンク先に遷移することをテストします。
かなり省略していますが、こんな感じで、within('.breadcrumb')とクラス名を指定するとうまくいきます。

(略)
it 'タグのパンくずリンクが機能すること' do
      visit edit_admin_tag_path(tag)
      within('.breadcrumb') do
        click_link 'タグ'
      end
      expect(current_path).to eq(admin_tags_path),'パンくずのタグを押した時にダッシュボードに遷移していません'
    end
(略)