記事ステータスの追加
実装したこと
・記事を投稿するアプリの編集画面で、記事のステータスを「下書き」「公開」「公開待ち」に分類したい。
・ステータスと公開日時は編集時に選択可能。ただし、公開日時によって記事のステータスを「公開」「公開待ち」に自動で判定して変更する。
・ステータスを「下書き」に指定したときは、「下書き」のままにする。
コントローラー
artcles_controller.rb(記事更新用)
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メソッドで変更を受け取る #引数で指定したattributes(属性)を変更するメソッド
②現在時刻が公開日を過ぎているか? いないかでstateを公開、公開待ちと振り分ける。
③ saveメソッドで保存 #DBへの保存はされないので必要。
published_controller.rb(記事公開用)
def update @article.published_at = Time.current unless @article.published_at? @article.state = @article.publishable? ? :published : :publish_wait #上記の一文は三項演算子 if @article.valid? Article.transaction do @article.save! end 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.publishable?が真ならpublishedを 偽ならpublish_waitを@article.stateに代入
モデルへ切り分ける
①日時で公開可能か判定
②判定結果ごとのメッセージ分け
③draft?はstateがdraftかどうか判定する trueならreturnによりここで処理が終わる(nilが返る)
article.rb
def publishable? ① Time.current >= published_at end def message_on_published ② if published? '公開しました' elsif publish_wait? '公開待ちにしました。' end end def adjust_state ③ return if draft? self.state = if publishable? # stateがpublishedなら:published(公開)を返す :published else # それ以外(publish_waitしか残ってないのでpublish_wait)なら:publish_wait(公開待ち)を返す :publish_wait end end
公開日を1時間ごとにしか設定できないように変更する
admin.js
format: 'YYYY-MM-DD HH:00'
Rakeタスク
Rakeタスクとは
アプリケーションを起動せず、行いたい処理をCUI(コマンドプロンプトやターミナル)から実行できます。CSVデータのインポートなど、サーバーを起動せず任意の処理を実行する際にこの機能がよく利用されます。
今回でいうと、公開待ちの中で、公開日時が過去になっているものがあれば、ステータスを「公開」に変更されるようにする。
上記の処理をrakefileに記述し、後ほど紹介するwhenneverと組み合わせることにより、自由なタイミングで処理を走らせることができます。
前提
app/models/article.rb
enumで記事の状態を「下書き」「公開」「公開待ち」に分類しています。
今回のRakeタスクでは、「公開待ち」の記事について、公開日時を越えたら記事の状態を「公開」に変える処理を行います。
app/models/article.rb
# draft: 下書き, published: 公開, publish_wait: 公開待ち enum state: { draft: 0, published: 1, publish_wait: 2 }
Rakeファイルの作成
rails g task article_state # rails g task ファイル名
lib/tasks/article_state.rake
namespace :article_state do desc '公開日時が過去の日付の「公開待ち」記事があれば、ステータスを「公開」に変更する' # desc = description(説明) task update_article_state: :environment do # environmentはDBとのやりとりが必要な際に記述します Article.publish_wait.past_published.find_each(&:published!) end end
Articleから「公開待ち」の状態で公開日時が現在〜過去のものを取ってきてから、find_eachでループさせます。 必要なデータだけ先に抽出してから繰り返し処理をしています。 (&:メソッド名)でpublished!を実行
公開日時が現在〜過去の記事を取得するscopeを準備
app/models/article.rb
scope :past_published, ->{ where('published_at <= ?', Time.current) }
Rakeタスクの実行
bundle exec rake article_state:update_article_state
cronとは
UNIX系のOSには、cronと呼ばれる仕組みが標準で備わっています。
cronとは、多くのUNIX系OSで標準的に利用される常駐プログラム(デーモン)の一種で、利用者の設定したスケジュールに従って指定されたプログラムを定期的に起動してくれるもの。
そして、このcronに対して命令を行うには、crontabというコマンドを実行します。
crontab -l で現在設定されている定期実行タスクの一覧を表示させたり、
crontab -r でcronを削除できたりなど、様々な命令を行うことが可能となっております。
gem wheneverの導入
cronjobsを実行してくれるgem
gem 'whenever', require: false
require: falseとするのはこのGem自体がRailsアプリケーションに反映するものではなく、 ターミナル(言わばOS)に反映させるため
$ bundle exec wheneverize
config/schedule.rbファイルが生成
schedule.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
Crontabへの書き込み
$ bundle exec whenever --update-crontab
参考文献
TypeError - no implicit conversion of nil into String
エラー内容
記事投稿アプリの中で、記事の中身(文章)を記入せずに空のままプレビュー画面を見ようとしたらエラーが出ました。
TypeError - no implicit conversion of nil into String
エラー内容
nilからStringへの暗黙の変換がない。
string型に変換する必要がありそうです。
TypeErrorとは メソッドの引数に期待される型ではないオブジェクトや、期待される振る舞いを持たないオブジェクトが渡された時に発生します。
該当箇所確認
article.rb
def build_body(controller) result = '' article_blocks.each do |article_block| result << if article_block.sentence? sentence = article_block.blockable sentence.body elsif article_block.medium? medium = ActiveDecorator::Decorator.instance.decorate(article_block.blockable) controller.render_to_string("shared/_media_#{medium.media_type}", locals: { medium: medium }, layout: false) elsif article_block.embed? embed = ActiveDecorator::Decorator.instance.decorate(article_block.blockable) controller.render_to_string("shared/_embed_#{embed.embed_type}", locals: { embed: embed }, layout: false) end end result end
簡単に言うとコンテンツがあれば<<でresultに追加する、というコードです。
コンテンツが存在すれば正常に処理され画面が表示されますが、コンテンツがないままだとエラーが発生する状態です
sentence.body.classでsentence.bodyがnil型だと分かりました。
これをstring型にできれば解決すると思われる。
def build_body(controller) result = '' article_blocks.each do |article_block| result << if article_block.sentence? sentence = article_block.blockable sentence.body ||= ''#今回はここを変える! elsif article_block.medium? medium = ActiveDecorator::Decorator.instance.decorate(article_block.blockable) controller.render_to_string("shared/_media_#{medium.media_type}", locals: { medium: medium }, layout: false) elsif article_block.embed? embed = ActiveDecorator::Decorator.instance.decorate(article_block.blockable) controller.render_to_string("shared/_embed_#{embed.embed_type}", locals: { embed: embed }, layout: false) end end result end
元々、sentence.bodyだけだとnill型なので sentence.body ||= ''のようにする。
||= ''とは
||=は自己代入やnilガードと呼ばれるものです。
左辺がnilまたはfalseの場合は右辺が代入され、それ以外の場合は代入はされません。
パンくずリストを追加
パンくずリストとは
パンくずリストとは、Webサイトを訪れたユーザが今どこにいるかを視覚的にわかりやすくした誘導表示のことを言います。
基本的にWebページの階層順にリンクがリストアップされており、Webページの上部箇所に表示されているケースが多い。
パンくずリストの種類
位置型パンくずリスト
位置型パンくずリストとは自身が閲覧しているページが階層構造内のどこに位置しているかを示します。
そのページにたどり着く経路が異なる場合でも、同じページであれば表示されるリストも同じものになります。
ユーザ自身の現在位置を把握しやすいメリットがあり、階層構造に広がりのあるサイトに効果的です。
属性型パンくずリスト
位置型パンくずリストは、そのページにたどり着く経路が異なる場合でも、同じページであれば同じものが表示される「静的」なものでしたが、属性型パンくずリストは、ユーザの操作によって変化します。
サイト内構成の現在の位置を示すのではなく、閲覧しているページがWebサイト上のどのカテゴリー(属性)に分類されているのかを示します。
検索フィルタのような働きをし、ユーザが複数の選択肢を経て表示されるようなサイトに用いられる。
パス型パンくずリスト
現在閲覧しているページにどのようにしてたどり着いたのかを示します。ページへの経路が異なる場合、表示されるリストも変化する動的なパンくずリストです。
ブラウザの「戻る」ボタンや履歴機能と同じ役割であることから「履歴型のパンくずリスト」とも呼ばれています。また、同様の理由で機能が重複してしまうことから、最近のWebサイトではあまり見かけません。
パンくずリストのメリット
・ユーザビリティが高くなる
まずWebサイトにおけるユーザビリティとは「使いやすさ」「使い勝手のよさ」のことを指します。つまり使いやすいサイトほど、ユーザビリティが高いということになる。
ページ数の多いサイトになると構造が複雑になり、ユーザ自身が今サイト内のどこにいるか分かりにくくなることがあるが、パンくずリストを設置することで「自分が今サイト内のどこにいるのか」や「サイトの構造」を認識しやすくなり、結果として使いやすさを高めることができる。
・検索エンジンが効率的にクローリングできる
Google検索などの検索エンジンにヒットさせるには、まずはクローラーと呼ばれる情報収集ロボットに自身のサイトの情報を収集(クローリング)してもらう必要がある。
パンくずリストを設置することは、そのサイトが体系的に整理されることとなります。するとクローラーもパンくずリストによって効率的にそのサイト内のカテゴリーをたどることができるようになるため、サイトの全体像が把握しやすくなり、効率的なクローリングを期待することができる。
パンくずリストを導入
railsでパンくずを導入するには「gretel」というgemを使うと便利
Gemfile
gem 'gretel'
$ bundle install
その後起動コマンドを入力
$ rails generate gretel:install
このコマンドを入力すると、config配下にbreadcrumb.rbというファイルが生成される。
書き方の例
crumb crumsの名前を指定 do link '表示させたい文字', その文字のリンク先 parent 親の指定 end
今回の場合
breadcrumd.rb
#先頭ページ(先頭ページはparentは指定しない。) 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
views/layouts/admin.html.slim
main.content-wrapper section.content-header h1 = yield 'content-header' == breadcrumbs style: :ol, class: 'breadcrumb' #ここにパンくずリストが表示される
各ページでパンくずを出すところを指定していく。 ※各ページでは=は使わないです。ビューとして表示させません。
app/views/admin/tags/index.html.slim
= content_for 'content-header' do #記載する場所で迷ったのでこの文も追記しておく | タグ - breadcrumb :admin_tags
app/views/admin/tags/edit.html.slim
= content_for 'content-header' do | タグ編集 - breadcrumb :edit_admin_tag, @tag
参考文献
slim
slimとは
特徴
・< >や閉じタグなどを削り、最低限必要なものだけを残した、非常にシンプルなテンプレート言語
・軽量
といった特徴を持つ、Ruby製のテンプレートエンジンです。
つまり、このslimの記法を用いることによって、HTMLがより簡潔に記述できる。
Railsにはデフォルトでerbというテンプレートエンジンが導入されていますが、これをslimに変更したい場合、
Gemfile gem 'slim-rails'
とGemfileに追記してあげた上で、bundle intallを実行後、viewファイルの拡張子をhtml.slimとしてあげればOK
注意
・slim-rails導入後、ジェネレーター経由で生成されるviewファイルの拡張子は、.html.erbから.html.slimに変更される
・slimとerbの共存も可能ですが、一つのアプリケーション内に二つのテンプレートエンジンを使用すると記述がややこしくなってしまうので避けたほうが無難でしょう。 変更したい場合は、html2slimなどのgemを用いて記述を書き換えてしまうのが一般的です。
変更を行うとき、
gem install html2slim
これでhtml.erb → html.slim に変換させることができる!
slimの記法
例 erbの中でrubyのeach記法を使用したい場合
<% @articles.each do |a| %> <p><%= a.title %></p> <% end %>
上記をslimに変更すると
- @articles.each do |a| p = a.title
他の例
クラスやidの指定
クラスやidはそれぞれ、., #で表現することができる。
erb
<dev class="content"> <p class="title">タイトル</hoge> </dev> <dev id="content"> <p id="title">タイトル</hoge> </dev>
上記をslimに
dev.content p.title タイトル dev#content p#title タイトル
・<>がいらない
・<%= %> → =
・<% %> → -
・コメント → /
・id指定 → #
・class指定 → .
参考文献
プルリクエスト
プルリクエストとは
分散バージョン管理システム(VCS)の機能の一つで、コードなどを追加・修正した際、本体への反映を他の開発者に依頼する機能。「変更を本人以外がレビューしてから反映させる」という手順を容易に実現することができる。
・機能追加や改修など、作業内容をレビュー・マージ担当者やその他関係者に通知します。
・ソースコードの変更箇所をわかりやすく表示します。
・ソースコードに関するコミュニケーションの場を提供します。
プルリクエストのメリット
・ レビュー・マージ作業をタスク化して管理し、やりとりを記録できる
プルリクエスト作成者とレビュー担当者は、プルリクエスト上でコメントをやりとりして議論でき、必要があれば、対象のブランチに何度でも変更をコミット・プッシュできる。プッシュされたコミットは、自動的にプルリクエスト上に反映されます。
・ レビューを促進できる
プルリクエストは、ソースコードの変更部分を明確に表示できる。また、プルリクエストの作成者は、ソースコードの意図や補足事項をコメントとして伝えることができます。これらによって、レビュー担当者の負担を減らせる。
プルリクエストを使った開発プロセス
1. 共有ディレクトリのファイルをローカルに取り入れる。
$ git clone
2.作業用のbranchを作成し、移動する
$ git checkout -b 作業用のブランチ名
3.変更内容をローカルリポジトリに記録する
$ git add .
$ git commit -m "(変更内容を記載)"
※<span style="color: #d32f2f">変更内容は相手にわかりやすく、区切りがいいところでコミットしていくのがベスト!</span>
一気にコミットを行うと、相手にわかりずらい。
4.リモートリポジトリに変更内容をpush
$ git push リモート名 ブランチ名
※ リモート名は git remote で確認
5.プルリクセストを送る
※できるだけ修正をしたら新しいプルリクエストを作る
6.最後にマージする
参考文献
モデルスペック
モデルスペックとは
モデルスペックとは、モデルに関するロジックのテストを指す。 Rspecではテスト内容を記載したファイル(スペックファイル)をもとにモデルスペックを実行する。
モデルスペックの構造
モデルスペックには次を含める。
・有効な属性で初期化された場合は、モデルの状態が有効(valid)になっていること
・バリデーションを失敗させるデータであれば、モデルの状態が無効(invalid)であること
・クラスメソッド、インスタンスメソッド、スコープが期待通りに動作すること
・正常系テストと異常系テストの両方を書く(例えば、スコープで検索したが結果ゼロ、など)
モデルのテストのベストプラクティス
・期待する結果をまとめてdescribeし、モデルの仕様や振る舞いを示す。
・テスト1つ毎に結果を1つだけ期待し、問題箇所を素早く特定できる。
・どのexampleも説明文を付け、何をテストしているか明確にする。
モデルスペックを作成する
rails g rspec:model モデル名
spec/factories/tasks.rb
FactoryBot.define do factory :task do sequence(:title, "title_1") content { "content" } status { :todo } deadline { 1.week.from_now } association :user end end
spec/factories/users.rb
FactoryBot.define do factory :user do sequence(:email) { |n| "user_#{n}@example.com" } password { "password" } password_confirmation { "password" } end end
Taskモデルのスペックファイルにテストコード
spec/models/task_spec.rb
require 'rails_helper' RSpec.describe Task, type: :model do pending "add some examples to (or delete) #{__FILE__}" describe 'validation' do it 'is valid with all attributes' do task = build(:task) expect(task).to be_valid expect(task.errors).to be_empty end it 'is invalid without title' do task_without_title = build(:task, title: "") expect(task_without_title).to be_invalid expect(task_without_title.errors[:title]).to eq ["can't be blank"] end it 'is invalid without status' do task_without_status = build(:task, status: nil) expect(task_without_status).to be_invalid expect(task_without_status.errors[:status]).to eq ["can't be blank"] end it 'is invalid with a duplicate title' do task = create(:task) task_with_duplicated_title = build(:task, title: task.title) expect(task_with_duplicated_title).to be_invalid expect(task_with_duplicated_title.errors[:title]).to eq ["has already been taken"] end it 'is valid with another title' do task = create(:task) task_with_another_title = build(:task, title: 'another_title') expect(task_with_another_title).to be_valid expect(task_with_another_title.errors).to be_empty end end end
Factory_botの設定
RSpec.configure do |config| config.include FactoryBot::Syntax::Methods end
上述のコードを加えることで、以下のようにrspecのテストコード中でFactory_botのメソッドを使用する際に、クラス名の指定を省略できるようになる。
参考文献
RSpecのセットアップ
Rspecとは
RspecとはRubyやRuby on Railsで作ったクラスやメソッドをテストするためのドメイン特化言語 (DSL)を使ったフレームワーク
特徴
Ruby on Railsのテストサポート
モックやスタブ(擬似的なクラス)によるテスト支援
テストデータの作成支援
カバレッジツールとの連携(ソースコードが実行された割合を確認するツール)
テスト結果のHTML出力機能
テストの進め方
RSpecに限らずユニットテストの進め方には原則というものがあります。
1.プログラムを書く前にテストコードを書く
2.テスト実行して失敗することを確認する
3.テストが成功するようにプログラムを書く
4.初めに戻る
を繰り返し行います。
このように、コーディングする前にテストコードを書くことを、テストファーストという。
開発手法でいえばTDD(テスト駆動開発)とか、BDD(振る舞い駆動開発)に分類される開発方法
Rspecの環境構築
Gemfile.rb
group :development, :test do gem 'rspec-rails', '~> 4.0.0' #Rails 5.xでRspecを使用する場合、このようにバージョン指定する。 gem 'factory_bot_rails' end
bundle install
rails g rspec:install
このコマンドを実行することで、specディレクトリと設定ファイル(.rspec, spec_helper.rb, rails_helper.rb)が生成される
gitignore
/vender/bundle
vendor/bundle以下に取得したRubyのライブラリをリモートリポジトリにプッシュしたくないため、.gitignoreに/vender/bundleを書く
FactoryBot
FactoryBotは、テスト用のオブジェクトを生成するための、オブジェクトの「工場」をつくるような存在です。 フォルダーspecの下に、フォルダーfactoriesを作成する。
spec/factories/tasks.rb
FactoryBot.define do factory :task do end end
参考文献