【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を導入する。
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】【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
このコマンドを実行すると、一連のコントローラーが自動生成されます。(ログイン用、新規登録用などなど…
とりあえず、ログイン・ログアウト機能に必要な部分のコメントアウトを外してあげる。
# 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
で全体のルーティングを確かめるとこんな感じです。
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 'Posts', posts_path, class: 'nav-link' li.nav-item.active = link_to 'Login', users_sign_in_path, class: 'nav-link' li.nav-item.active = link_to 'Sign Up', new_user_registration_path, class: 'nav-link'
ログイン後のヘッダー。ログアウト用のリンクなど…
(略) #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'
これがデフォルトのログイン画面
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】deviseの使い方をマスターしてログイン認証機能を実装 | Pikawaka - ピカ1わかりやすいプログラミング用語サイト
【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 controller
にPundit
をinclude
します。
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.rb
にconfig.consider_all_requests_local = true
の記載があれば、403のエラー画面が確認できます。
【Rails】【cron】1時間ごとにタスクを実行する
実装したいこと
記事公開アプリがあります。
記事の状態が「公開待ち」で公開日時を指定している記事を毎時間確認し、公開日時がきたら自動で公開したい。
新しい用語が色々出てきて混乱するのでまとめていきます。
実装の流れ
- Rakeタスクに、『記事のステータスと公開日時を判定して、「公開」ステータスに変えるタスク』を用意する。
- cronで、上記タスクを1時間毎に実行する
Rakeとは
Rake
とは…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
Rakeタスクの実行
$ bundle exec rake article_state:update_article_state
gem wheneverの導入
cron
jobsを実行してくれる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
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.current
とpublished_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:
英単語の意味
implicit
suggested or understood without being stated directly.
conversion
when you change something from one form to a different one.
要するに、nil
をstring
にchangeできてないってこと。
nil
をstring型
に変換する必要があります。
問題のコードに.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.body
がnil
やfalse
だったら、''
を返す。それ以外だったら、sentence.body
をそのまま使うことを表しています。
article_blocks.each do |article_block| result << if article_block.sentence? sentence = article_block.blockable sentence.body ||= '' #ここ
参考
『プロを目指す人のためのRuby入門』P174
【Rails】パンくずリストの実装
実装したいこと
ウェブページの上部にパンくずリストを表示したいです。 パンくずリストとは… 自分が今どのページにいるか分かるリスト。 リスト内の文字をクリックすると、リンク先へ遷移されます。
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 (略)