Ryota400’s blog

エンジニアを目指して書いてます。

記事ステータスの追加

実装したこと

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

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

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

コントローラー

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

参考文献

qiita.com

ログイン - はてな

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に追加する、というコードです。
コンテンツが存在すれば正常に処理され画面が表示されますが、コンテンツがないままだとエラーが発生する状態です

Image from Gyazo

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ページの上部箇所に表示されているケースが多い。

Image from Gyazo

パンくずリストの種類

位置型パンくずリスト

位置型パンくずリストとは自身が閲覧しているページが階層構造内のどこに位置しているかを示します。
そのページにたどり着く経路が異なる場合でも、同じページであれば表示されるリストも同じものになります。
ユーザ自身の現在位置を把握しやすいメリットがあり、階層構造に広がりのあるサイトに効果的です。

属性型パンくずリスト

位置型パンくずリストは、そのページにたどり着く経路が異なる場合でも、同じページであれば同じものが表示される「静的」なものでしたが、属性型パンくずリストは、ユーザの操作によって変化します。
サイト内構成の現在の位置を示すのではなく、閲覧しているページが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

参考文献

www.asobou.co.jp

qiita.com

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指定 → .

Image from Gyazo

参考文献

qiita.com

プルリクエスト

プルリクエストとは

分散バージョン管理システム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.最後にマージする

参考文献

backlog.com

youtu.be

モデルスペック

モデルスペックとは

モデルスペックとは、モデルに関するロジックのテストを指す。 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のメソッドを使用する際に、クラス名の指定を省略できるようになる。

参考文献

forest-valley17.hatenablog.com

www.code-magagine.com

qiita.com

qiita.com

RSpecのセットアップ

Rspecとは

RspecとはRubyRuby 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

参考文献

www.sejuku.net

agency-star.co.jp