大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

Swiperをカスタマイズ

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

昨日出した記事ではSwiperの公式ドキュメントに合わせて、既存のscaffloldの雛形アプリケーションに追加して以下のようなスライダー画面を実装しました。

sakitadaiki.hatenablog.com

本日はSwiperの仕組みと、Swiperを使用して様々なスライダー画面を実装していきたいと思います!

Swiperの仕組み

昨日、公式のドキュメントをほぼ思考停止で実装していきました。しかし、完成した画面を見ると今まで再現したことのないスライダー画面をしっかりと作ることができましたね。この画面はどうやって作ることができたかわかりましたか?

ポイントはスライダーに与えたクラスとapplication.scss、application.jsの関係にあります。まず、現在スライダーを表示している画面は以下のファイルです。

 <!-- app/views/users/show.html.erb -->
<strong>Portrait:</strong>
<!-- Slider main container -->
<div class="swiper-container">
  <!-- Additional required wrapper -->
  <div class="swiper-wrapper">
    <!-- Slides -->
    <% if @user.portraits.present? %>
      <% @user.portraits.each do |portrait| %>
        <%= image_tag url_for(portrait), class: 'swiper-slide' %>
      <% end %>
    <% else %>
        <%= image_tag '/images/haikyu.jpg', class: 'swiper-slide' %>
    <% end %>
  </div>
  <!-- ページネーション -->
  <div class="swiper-pagination"></div>

  <!-- ナヴィゲーションボタン -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

  <!-- スクロールバー -->
  <div class="swiper-scrollbar"></div>
</div>

ここでスライダー画面を実際に表示しているHTMLは下記部分です。

  <!-- Additional required wrapper -->
  <div class="swiper-wrapper">
    <!-- Slides -->
    <% if @user.portraits.present? %>
      <% @user.portraits.each do |portrait| %>
        <%= image_tag url_for(portrait), class: 'swiper-slide' %>
      <% end %>
    <% else %>
        <%= image_tag '/images/haikyu.jpg', class: 'swiper-slide' %>
    <% end %>
  </div>

そして、スライダーにページネーションだったり、スクロールバーだったりを実装しているHTMLが下記部分です。

<!-- ページネーション -->
<div class="swiper-pagination"></div>

<!-- ナヴィゲーションボタン -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>

<!-- スクロールバー -->
<div class="swiper-scrollbar"></div>

つまり何が言いたいのかというと、「スライダー画面を実装しているHTML部分とその下のスライダーの機能を実装しているHTML部分は明確に役割が分かれている」、ということとです。

昨日説明しませんでしが、スライダー機能を実装しているHTMLのクラスにのみにapplication.jsで機能を与えています。paginationnavigationscrollbarの部分です。Optional parametersはスライダー全体の設定です。direction: 'vertical'は垂直方向に画像をスライドさせる設定をしています。loop: trueは1番最後に表示している画像から次の画像に移ると最初の画像に戻る設定をしています。

// app/assets/javascripts/application.js
const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'vertical',
  loop: true,

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },

  // Navigation arrows
  navigation: {
    nextEl: '.swiper-button-next',
    prevEl: '.swiper-button-prev',
  },

  // And if we need scrollbar
  scrollbar: {
    el: '.swiper-scrollbar',
  },
});

よって、スライダー画面で与えられているクラスであるswiper-wrapperswiper-slideにはこの遂ライダーの機能には一切関与していません。言い換えれば、スライダー画面で与えられているこれらのクラスは、現状ただクラスが与えられているだけで何の役割も果たしていないということです。

これらのクラスがなぜあるのかというとただレイアウトを整えることを想定しています。つまり、scssにそれらのクラスを用いてレイアウトを整えるということですね!

例えば、scssに適当にクラスにレイアウトを定義します。

// app/assets/stylesheets/application.scss
.swiper-container {
  .swiper-wrapper {
    width: 100%;
    height: 600px;
    .swiper-slide {
      object-fit: cover;
    }
  }
}

object-fit: cover;をつけることで現在表示している画像の縦横の比率を整えてくれます。width: 100%;とすることで画面いっぱいに画像を表示します。実際に見てみましょう。

https://i.gyazo.com/aca61c09f425a4a62501f6fa0bcca155.gif

当然ですが、scssが反映されました。こんな感じでswiper-wrapperswiper-slideは画像の表示幅だったりのレイアウトを整えるために使用されることが想定されています。

スライダーのカスタマイズ

ではpaginationnavigationscrollbarの部分を設定しているjsファイルを変更してみましょう。今回はAmazon Prime videoのような画面になるように編集していきます。

https://i.gyazo.com/0b05b7cec8266df792398cb13a0bb922.gif

まず画像がPrime Videoの方だと水平方向に流れているのがわかります。先ほどOptional parametersdirection: 'vertical'の箇所でスライダーが垂直方向に流れるように設定していました。これをverticalからhorizontalに変更しましょう。

const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'horizontal',
  loop: true,

画面を読み込み直すと表示が変わり、水平方向に画面が動くようになりました!

https://i.gyazo.com/8693f7a25926f99da10c284a5a10ee98.gif

あとPrime Videoと異なる部分として、数秒ごとに自動でスライドが切り替わるように設定されています。この自動で切り替わるようにする方法も実は簡単に導入することができます。autoplayという設定を加え、ms単位で切り替わる間隔を指定するだけです。

const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'horizontal',
  loop: true,
  autoplay: {
    delay: 2500,
  },

画面を再度読み込み直すと、

https://i.gyazo.com/2a15f45e08d583bb07ddafe0ab38bbc1.gif

切り替えボタンを押さずとも勝手に切り替わるようになりました。もっと早く自動でスライドさせたいなら、2500の秒数をより短い間隔(1000(1秒))などに設定するだけです。

Optional parametersの設定だけでもかなり自由にスライドの設定を変えることができたと思います。実際、他のスライド方法を調べてみるとそのほとんどがOptional parametersの設定を変えることで再現できています。例えば、下のボックスが回転するようなお洒落なスライダーもOptional parametersの設定を変更することで実装できます。

https://i.gyazo.com/a0d2c28a16e7bcef4ebeea39d900222d.gif

JSファイルを下記のように変更します。navigationscrollbarの部分は削除しましょう。

const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  effect: 'cube',
  grabCursor: true,
  cubeEffect: {
    shadow: true,
    slideShadows: true,
    shadowOffset: 20,
    shadowScale: 0.94,
  },

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },
});

app/views/users/show.html.erbでも同様に下記部分を削除しましょう。

<!-- ナヴィゲーションボタン -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>

<!-- スクロールバー -->
<div class="swiper-scrollbar"></div>

そしてscssで画像が正方形に表示されるように設定します。

.swiper-container {
  width: 300px;
  height: 300px;
  .swiper-wrapper {
    .swiper-slide {
      object-fit: cover;
    }
  }
}

これで画面を読み込み直すと、

https://i.gyazo.com/a504b7930d915b46cfd2458f49e553a3.gif

このように簡単に再現できてしまいました。RUNTEQでは9割りの学習がバックエンドでRailsしかほぼ学びません。しかし、ほんの少しのフロントエンドの知識があるだけでこんなふうにスライダーが実装できてしまいます!本当に便利ですよね。

加えていうと、paginationnavigationscrollbarの設定はもはやオプションと思ってください。そこにページネーションを付けるか、ボタンを付けるか、スクロールバーを付けるかの違いを作る、scssとは別で付ける装飾のようなものです。jsファイルにある設定でもほとんどいじることはありません。スクロールバーのデザインを変えるなどのオプションがあるだけです。

終わりに

3つの記事を通してswiperについて紹介しましたが、細かい仕組みの説明はしませんでしたが、正直仕組みを理解する必要はさほどない気がしています。結局調べてコピペするという作業で大体実装することができました。このほかにもおしゃんなスライダー画面を実装する方法はいくつもあるので皆さんも試してください!

以上、大ちゃんの駆け出し技術ブログでした!

Swiper

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

昨日の記事で複数の画像をアップロードできるように実装しました!

sakitadaiki.hatenablog.com

こちらは「パーフェクト Ruby on Rails」という本でActive Storageを実装した状態から改修するようにして実装しました。今回も現状の複数画像をアップロードできるようになった状態から改修するようにして実装を行いたいと思います!

今回は、複数アップロードした画像をスライダーで表示されるように実装します。スライダー表示とはなんぞやと思われるかもしれませんが、Amazon Prime Video の表示でおすすめの作品が順番にスライダーで表示されているかと思います。

https://i.gyazo.com/0b05b7cec8266df792398cb13a0bb922.gif

Swiper

この画像をスライド表示させるための方法として、RUNTEQではSwiperというjQueryプラグインを学習しました。

swiperjs.com

こちらの公式サイトでは以下のように説明されています。

Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior. It is intended to be used in mobile websites, mobile web apps, and mobile native/hybrid apps. Swiper is not compatible with all platforms, it is a modern touch slider which is focused only on modern apps/platforms to bring the best experience and simplicity.

[日本語訳] Swiperは、ハードウェアで加速されたトランジションと驚くべきネイティブ動作を持つ、最もモダンな無料のモバイルタッチスライダーです。モバイルウェブサイト、モバイルウェブアプリ、モバイルネイティブ/ハイブリッドアプリでの使用を想定しています。 Swiperはすべてのプラットフォームと互換性があるわけではなく、最高のUXとシンプルさを提供するために、モダンなアプリ/プラットフォームのみに焦点を当てたモダンなタッチスライダーです。

「最もモダンな無料のモバイルタッチスライダー」、「最高のUXとシンプルさを提供する」とあるように、インストール方法も簡単であり、かなりシンプルにスライダー機能の実装を行うことができました。

今回はバッグエンドではなくフロントエンドの内容となるため、フロントエンドに慣れていない方は難しいと思われるかもしれません。しかし、公式にインストール方法、実装方法が詳しく載っていたので心配しなくても大丈夫です!

インストール手順

今回は公式のインストール方法に従い実装をしていきたいと思います。

There are few options on how to include/import Swiper into your project

[日本語訳] スワイパーをプロジェクトにインクルード/インポートする方法には、いくつかのオプションがあります。

インストール方法には方法がいくつかあるようですね。今回はCDNからSwiperをインストールする方法を採用します。下記参考サイトではCDNを以下のように説明しています。(今回引用多くてごめんなさい、、、)

CDNとは「Content Delivery Network(コンテンツデリバリーネットワーク)」の略で、ウェブコンテンツを効率的かつスピーディーに配信できるように工夫されたネットワークのことです。

https://www.kagoya.jp/howto/network/cdn/

なるほど。プログインを含んだウェブコンテンツを素早く導入できるようにしたシステムのようですね。より詳しく知りたい方は参考記事を熟読してみてください!

CDNからSwiperを使用する

If you don't want to include Swiper files in your project, you may use it from CDN. The following files are available

[日本語訳] プロジェクトにSwiperファイルを入れたくない場合は、CDNから使用することができます。以下のファイルが利用可能です。

<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.css" />
<link rel="stylesheet" href="https://unpkg.com/swiper/swiper-bundle.min.css" />
<script src="https://unpkg.com/swiper/swiper-bundle.js"></script>
<script src="https://unpkg.com/swiper/swiper-bundle.min.js"></script>

上記のファイルをapp/views/layouts/application.html.erbに記述すればダウンロードできそうですね!しかし、erbファイルですので、erbの記法に則って記述しましょう!(試していないのですが、上記のHTMLのままでも大丈夫なはずです)

<%= stylesheet_link_tag 'https://unpkg.com/swiper/swiper-bundle.css' %>
<%= stylesheet_link_tag 'https://unpkg.com/swiper/swiper-bundle.min.css' %>

<%= javascript_include_tag 'https://unpkg.com/swiper/swiper-bundle.js' %>
<%= javascript_include_tag 'https://unpkg.com/swiper/swiper-bundle.min.js' %>

スワイパーのHTMLレイアウトを追加

Now, we need to add basic Swiper layout to our app

[日本語訳] ここで、アプリに基本的なSwiperのレイアウトを追加する必要があります。

<!-- Slider main container -->
<div class="swiper-container">
  <!-- Additional required wrapper -->
  <div class="swiper-wrapper">
    <!-- Slides -->
    <div class="swiper-slide">Slide 1</div>
    <div class="swiper-slide">Slide 2</div>
    <div class="swiper-slide">Slide 3</div>
    ...
  </div>
  <!-- If we need pagination -->
  <div class="swiper-pagination"></div>

  <!-- If we need navigation buttons -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

  <!-- If we need scrollbar -->
  <div class="swiper-scrollbar"></div>
</div>

こちらもHTMLを例にしています。画像がどこで表示するかというと<!-- Slides -->の部分ですね。実際の画像は指定していませんが、Slide 1、Slide 2、Slide 3というように、複数のスライドがここに表示されていることを想定しているかと思います。

また、スライド以降のHTMLはスライダーのオプションのようですね。

<!-- ページネーション -->
<div class="swiper-pagination"></div>

<!-- ナヴィゲーションボタン -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>

<!-- スクロールバー -->
<div class="swiper-scrollbar"></div>

それでは実際にファイルに反映していきましょう。画像を表示しているユーザー詳細ページのビューファイルを編集します。現在のファイルは以下のとおりです。

# app/views/users/show.html.erb
<strong>Portrait:</strong>
<% @user.portraits.each do |portrait| %>
  <%= image_tag portrait.variant(resize_to_limit: [100, 100]) %>
<% end %>

これを公式の指示通りに実装すると以下のようになります。

<strong>Portrait:</strong>
<!-- Slider main container -->
<div class="swiper-container">
  <!-- Additional required wrapper -->
  <div class="swiper-wrapper">
    <!-- Slides -->
    <% if @user.portraits.present? %>
      <% @user.portraits.each do |portrait| %>
        <%= image_tag url_for(portrait), class: 'swiper-slide' %>
      <% end %>
    <% else %>
        <%= image_tag '/images/haikyu.jpg', class: 'swiper-slide' %>
    <% end %>
  </div>
  <!-- ページネーション -->
  <div class="swiper-pagination"></div>

  <!-- ナヴィゲーションボタン -->
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>

  <!-- スクロールバー -->
  <div class="swiper-scrollbar"></div>
</div>

だいぶややこしいファイルになりましたが、公式のコメント部分をそのまま反映しています。画像がアップロードされていたら、画像の枚数だけimage_tagを使ってimgを生成します。逆にアップロードされている画像がなければ、一枚の画像を表示するようにしておきます。ですので、app/assets/images/配下に好きな画像を1枚格納しておいてください!

<% if @user.portraits.present? %>
  <% @user.portraits.each do |portrait| %>
    <%= image_tag url_for(portrait), class: 'swiper-slide' %>
  <% end %>
<% else %>
    <%= image_tag '/images/sample.jpg', class: 'swiper-slide' %>
<% end %>

Swiper CSS スタイル/サイズ

In addition to Swiper's CSS styles, we may need to add some custom styles to set Swiper size:

[日本語訳] SwiperのCSSスタイルに加えて、Swiperのサイズを設定するためにいくつかのカスタムスタイルを追加する必要があるかもしれません。

.swiper-container {
  width: 600px;
  height: 300px;
}

画像を表示する部分のレイアウトを整理するためにCSSを記載します。CSSについては特に自由にかけると思うので、今回はこちらで用意したCSSを記載してください。(application.cssapplication.scssにファイル名を変更してください!)

// app/assets/stylesheets/application.scss
.swiper-container {
  width: 600px;
  height: 300px;
}

スワイパーの実装

Finally, we need to initialize Swiper in JS:

[日本語訳] 最後に、SwiperをJSで実装する必要があります。

const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'vertical',
  loop: true,

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },

  // Navigation arrows
  navigation: {
    nextEl: '.swiper-button-next',
    prevEl: '.swiper-button-prev',
  },

  // And if we need scrollbar
  scrollbar: {
    el: '.swiper-scrollbar',
  },
});

このjavascriptファイルはこのまま記載してしまいましょう。

// app/assets/javascripts/application.js
const swiper = new Swiper('.swiper-container', {
  // Optional parameters
  direction: 'vertical',
  loop: true,

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },

  // Navigation arrows
  navigation: {
    nextEl: '.swiper-button-next',
    prevEl: '.swiper-button-prev',
  },

  // And if we need scrollbar
  scrollbar: {
    el: '.swiper-scrollbar',
  },
});

プリコンパイルするように実装

application.scssapplication.jsを読み込む設定を追加していないので、現状のままでは両方のファイルを読み込めていません。ですので読み込めるように設定します。

  • app/views/layouts/application.html.erb
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_include_tag 'application' %>
  • config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( application.css application.js )

現状確認

これで公式のインストール方法と簡易的な実装方法に合わせた実装は終わりです。試しに画像をアップロードしてみましょう。プライムビデオを真似てみると面白いかなと思いますので、各々好きな作品の画像を3枚ダウンロードしてみてください!自分はアニメが好きなのでアニメ3作品を用意します!笑

https://i.gyazo.com/1aa80fafa65d40d206795780aab148f5.gif

スライド機能が実際に導入されていますね!

思ったよりも簡単に実装できたのではないでしょうか!

終わりに

今回はSwiperについて紹介しましたが、インストールと簡単な実装方法のみを公式にしたがって実装しただけですね。こんな簡単な方法だけでなく、swiperにはスライド方法は細かく指定する方法があるようですね。

swiperjs.com

他のスライド指定方法も発信したいので、次回はプライムビデオのような見た目にする方法を発信したいと思います!

以上、大ちゃんの駆け出し技術ブログでした!

【Active Storage】単数画像→複数画像への移行方法

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

画像アップロード機能で少しだけ詰まったところがあるので、本記事ではそれを紹介します。

詰まった箇所は「Active Storageによる複数画像のアップロード方」です。画像アップロード方法として、自分は今までRUNTEQの基礎編課題でCarriWaveを学習していました。調べてみると、Active StorageはRails5.2よりgemではなく公式にファイルアップロード機能として追加されたようです。

今回使用するアプリはいわゆる「パルビ」と呼ばれる書籍のActive Stoageの項目を実装した状態から始めます!

パーフェクト Ruby on Rails 【増補改訂版】 (Perfect series)

現在のファイルの状況

アプリケーションはscaffoldを使用して作成されています。ファイルアップロードの機能がある箇所はユーザー作成ページです。

https://i.gyazo.com/8e35e80d762668e51e116d56e7454d4a.png

こちらに画像をアップしてユーザーを作成すると、ユーザー詳細ページにて画像が表示されます。

https://i.gyazo.com/3f40d2b691abdaa3b60dd08616a1d25b.png

詳しくコードを見ていきます。

まずUserモデルにはhas_one_attached :portraitとアソシエーションが指定されています。

# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :portrait
end

この:portraitは実際にはUserモデルのカラムにはありません。これはActive Storageがポリモーフィックなアソシエーションを採用しているからです。ポリモーフィックなアソシエーションは説明が大変複雑なので今後別の記事で説明する予定です。現状は下記記事を自分は参考にしているので、気になる方は下記を参照してください。

Railsのポリモーフィック関連とはなんなのか - Qiita

ここでの理解は、「ユーザーとは別のモデルであるActiveStorage::Blobが:portraitを持っており、ユーザーは:portraitと関連づけられている」という理解で大丈夫かなと思います。

ユーザー作成画面の名前入力とファイルアップロード箇所のerbの現状です。

<!-- app/views/users/_form.html.erb -->
<div class="field">
  <%= form.label :name %>
  <%= form.text_field :name %>
</div>

<div class="field">
  <%= form.label :portrait %>
  <%= form.file_field :portrait %>
</div>

こちらで入力されたnameとアップロードしたportraitがパラメーターとして渡されます。user_paramsでストロングパラメーターを介して値を取得しています。ユーザー作成であるcreateアクションでそれらの値を受け取って保存できればユーザー詳細ページをレンダリングします。

# app/controllers/users_controller.rb

def create
  @user = User.new(user_params)

  respond_to do |format|
    if @user.save
      format.html { redirect_to @user, notice: "User was successfully created." }
      format.json { render :show, status: :created, location: @user }
    else
      format.html { render :new, status: :unprocessable_entity }
      format.json { render json: @user.errors, status: :unprocessable_entity }
    end
  end
end
・
・
・
def user_params
  params.require(:user).permit(:name, :portrait)
end

ユーザー詳細ページです。image_tagで画像を表示していることがわかります。

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @user.name %>
</p>

<p>
  <strong>Portrait:</strong>
  <%= image_tag @user.portrait.variant(resize_to_limit: [100, 100]) %>
</p>

<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

variant(resize_to_limit: [100, 100])のメソッドはデフォルトでgemにコメントアウトされた状態で記載されているimage-processingをインストールすることで使用できます。これにより画像のサイズを指定することができます。

# Gemfile
gem 'image_processing', '~> 1.2'

以上ががざっくりとした現状のファイルの状態です。Active Storageのファイルアップロード機能は比較的簡単に実装することができます。しかし、現在のファイル状態では画像を一つしかアップロードすることができません。そこで、現在のファイル状態を変更し、複数の画像をアップロードできるように実装したいと思います。

複数の画像をアップロードできるように変更

モデルの編集

まず、複数のファイルを格納することができるように、モデルを変更します。Active Storageではファイルをそのモデルが所有する関係性をかなり直感的に表しています。has_one_attached :portraitとあるように、Userモデルがportrait(1つの画像)をアタッチメントとして所有していることがわかります。

では、複数の画像をアップロードする場合は?

実は、has_many_attached :portraitsと変更をするだけです。

# app/models/user.rb
class User < ApplicationRecord
  # has_one_attached :portrait
  has_many_attached :portraits
end

これにより、「Userモデルがportraits(複数の画像)をアタッチメントとして所有している」ことを示しています。

アップロード画面を編集

次に、パラメーターを渡すビューを変更します。

これも直感的で、portraitをportraitsに変更します。

<div class="field">
  <%= form.label :portrait %>
  <!-- <%= form.file_field :portrait %> -->
  <%= form.file_field :portraits %>
</div>

ただ、これだけでは複数のファイルをパラメーターとして渡すことはできるようにしたけど、複数のファイルをアップできるようにHTMLが対応されていません。対応するためにはオプションにmultiple: trueを追記します。

<div class="field">
  <!--  <%= form.label :portrait %> -->
  <%= form.label :portraits %>
  <!-- <%= form.file_field :portrait %> -->
  <%= form.file_field :portraits, multiple: true %>
</div>

ストロングパラメーターを編集

次はストロングパラメーターです。

もうどこを変更すればいいかわかると思います。「portraitをportraitsに変更」ですね。

def user_params
    # params.require(:user).permit(:name, :portrait)
  params.require(:user).permit(:name, :portraits)
end

と私も最初は思ったのですが、実は違います!!!!

正しい実装は以下のとおりです。

def user_params
    # params.require(:user).permit(:name, :portrait)
    # params.require(:user).permit(:name, :portraits)
    params.require(:user).permit(:name, portraits: [])
end

これはアップロードされるファイルが複数であることに対応するためです。空の配列を指定しないと複数のファイルを格納する器がないということです。

詳細ページを編集

最後にユーザー詳細ページです。現状image_tagは1つしかないので複数の画像を表示できません。複数画像を表示するために、アップロードされたportraitsの数だけimage_tagが生成されるように繰り返し処理をします。

<p>
  <strong>Portrait:</strong>
  <% @user.portraits.each do |portrait| %>
    <%= image_tag portrait.variant(resize_to_limit: [100, 100]) %>
  <% end %>
</p>

これで複数の画像が表示するように実装することができました!!

https://i.gyazo.com/53ab15b583c1ac5ff7afff3ae70aac6f.gif

終わりに

単数から複数の画像をアップロードすること自体は難しくありません!単数形を複数形に変更するみたいな作業ですからね。

ただ、大きなアプリケーションともなると、様々なファイルで画像のカラムが使われていたら大変です。例えば、画像のバリデーションをするデコレイターがあったとすると、それが単数ようのファイルで定義されていた場合は、複数のファイルを持つ配列ではエラーが起きてしまいます。なのでアプリケーションを作成する際は、なるべく単数か複数かを明確な理由を持って決めておくと、後々変更するという手間を減らすことができるでしょう。

以上、大ちゃんの駆け出し技術ブログでした!

Kaminari

ページネーションとは

pagination ・・・ ページ付け、ページを示す数字 weblioより引用

ページ付けとあるように、"ページを持たせる"という認識でOKです。

例えば、多くの人が使う投稿アプリで一覧表示で全ての投稿を表示するとしましょう。 ページネーションなしだと、全ての投稿が1ページの中に表示されてしまいます。 投稿数が10個程だと問題ありませんが、複数人が使うようなアプリで全ての投稿を一度に表示すると、1ページに100, 1000と表示されてしまい、非常に見づらくなります。カオス状態です。。。

しかし、ページネーションを使えば、非常に多くの投稿があっても、1ページに表示する投稿数が決まっています。 よって、カオス状態とならず、数ページにわたって投稿を表示することができます。

kaminari

Railsのgemの1つで簡単にページネーション機能を実装することができます。 なお、今回は掲示板一覧表示機能にページネーション機能を実装します。

英語ですが、Github上で仕様を確認することができますので、こちらも参照してくださいね。 https://github.com/kaminari/kaminari

kaminari導入

インストール方法

まずがkaminariをinstallするためにGemfileに記載

gem 'kaminari'

そしてお決まりbundle install

$ bundle install

コントローラの編集

一覧表示機能なので、indexアクションを編集していきます。 まずは編集前の状態を確認します。

  def index
    @boards = Board.all.includes(:user).order(created_at: :desc)
  end

Board掲示板モデルです。 Board.allでDBに保存されている全ての投稿を参照し取得します。 そしてincludes(:user)で、BoardモデルのユーザーID(user_id)をBoardモデル参照時に同時に参照します。 order(created_at: :desc)で作成された日時が新しい順番で投稿を取得します。

なぜそのようなことをするのかですが、詳しくは自分の別の記事で紹介しているので、よかったらそちらを見ていただけると幸いです。 N + 1問題 - Railsで解説!

上のファイルの編集後がこちら

  def index
    @boards = Board.all.includes(:user).order(created_at: :desc).page(params[:page])
  end

page(params[:page])が出てきました。これがkaminariをインストールすることで使えるメソッドです。 実は、kaminariを導入したことで、パラメータのキーが一つ増えています。

params
=> <ActionController::Parameters {"page"=>"2",,,,

なるほど。どうやらpageキーでどのページかを判別しているようです。 このキーの増加でpage(params[:page])で何ページ目の投稿であるかを取得することが可能となっています。

ビューにpaginateを追加

ページごとの投稿は取得できるようになったので、ページを移動するための実装をしましょう。 見た目は以下のようになります。

image.png

それではビューに以下の文を追加します。

<div class='mb-3'>
  <%= paginate @boards %>
</div>

paginateによって次のページや前のページに移動することができるようになります。

おそらく見た目が画像と違うのでは? 以下のコマンドで表示が同じになると思いますので、ターミナルに打ち込んでみてください。

$ bin/rails g kaminari:views bootstrap4

これで主なページネーションの実装は終了です。 とっても簡単ですよね!

###ページに表示する数を指定

実は、各ページに表示する投稿数は今の時点では25個となっています。 25個はデフォルトで決まっています。 下記コマンドを実行してkaminariのデフォルトを設定をみることができます。

$ rails g kaminari:config

作成されたファイルを見てみると

Kaminari.configure do |config|
  # config.default_per_page = 25
  # 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

いろいろな設定が表示されていると思います。 今回はデフォルトのページ数を25から10に変えてみようと思います。

Kaminari.configure do |config|
  # 25から10に変更
  # config.default_per_page = 10
  # 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

これでページごとに表示される投稿数が10となりました。

しかし、先ほども言いましたように、こちらのファイルはkaminariのデフォルト値を設定するファイルです。 つまり、全体で共通の設定となります。 投稿一覧機能にのみページネーションするのであれば問題ありません。 しかし、投稿に対しコメント機能を実装し、そのコメントに対しページネーションをすると、そのコメントはページごとに10個のコメントを表示することになります。

これを避ける方法があります。 各モデルに対し個別でページごとの表示数を設定するのです。

もしページごとの投稿の表示数を10とする場合、直接モデルに対しその設定を加えます。

class Board < ApplicationRecord
  # specify default per_page value per each board
  paginates_per 20

paginate_perでデフォルト値を上書きすることができます。 コメント機能にページネーションを実装したい場合、コメントのモデルに対し同じようにpaginate_perを書けばいいのです。

Notionは最強!!

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今日は技術系ではなく、普段自分が愛用しているアプリ「Notion」を紹介したいと思います!

このアプリは自分がブログを書く時、プログラミング学習をする時はずっと開いています。それぐらい使える便利なアプリですので、是非プログラミング学習初学者の方々に使っていただきたいです!

今回の記事の構成は以下の3項目になります。

  • Notionとは
  • プログラミング勉強としてのNotion
  • ☆私のNotionの使い方☆

Notionとは

Notionのキャッチフレーズは以下のとおりです。

All-in-one workspace
One tool for your whole team. 
Write, plan, and get organized.

日本語に訳すと、

オールインワンのワークスペース
チーム全体のための1つのツール。
書く、計画する、整理する。

です。

つまり、チーム全体のドキュメントを1つのツールで管理するために使われるアプリであり、Googleカレンダースプレッドシートをチームで共有しているイメージで合っています。

https://i.gyazo.com/c0d358e1e84e7836ecf42c6f805da710.png

画像のように項目ごとに分別し、それをチーム全体で共有することができるので、チームで仕事をする全員がこのファイルを閲覧することができ、情報を書き込むことができます。これにより、チームのドキュメントが統一化され、そしてドキュメントの共有により共通認識が用意になります。しかも、ドキュメントの保存はファイルが編集されると自動で行ってくれるので、保存し忘れなどの心配は入りません!とても便利ですよね!

プログラミング勉強としてのNotion

「でもこれってチームのためのアプリであって個人の勉強には関係ないじゃん」

こう思った方もいるとおもいますが、このNotionはプログラマーのメモ帳として最適だと自分は思っています。理由は多々ありますが、個人的に1番使い勝手がいいと思っているNotionの機能はマークダウン記法が簡単にできることです。

マークダウン記法はQiitaブログやはてなブログでアウトプットする際によく使用する記法です。コードや大文字に装飾するために文字の前後にマーク(例: *, ```, など)を配置する方法です。例えば以下のように記載したとしましょう。

https://i.gyazo.com/c1f56e4bf73fcb71792d465ca3ccb6f0.png

それが実際のプレビューでは次のように表示されています。

https://i.gyazo.com/8fb1527f2182a0621d3c1d71c90cd56b.png

皆さんもQiita、はてなブログといった技術ブログを書いたことがあるなら共感していただけると思うのですが、、このマークをブログ全体に書くのは手間ではないでしょうか。いちいちコードのある部分をマークで記載し、大文字の部分は**を使用して囲まなければいけません。そして、自分のマークダウンが反映されているかを確認しなければならないので、一度プレビューのページに移る必要があります。つまり、編集ページでマーク → プレビューで確認 → 編集ページに戻るの3つの工程が確認する度に必要になります。 https://i.gyazo.com/10e9fe9382cfe56bde9de3ee328289ab.png

この手間をNotionでマークダウンで書くことで解決できます。Notionでは上述したように簡易的にマークダウン記法を行うことができます。方法は簡単で、文字を選択したら表示される装飾方法を選択するだけです。

https://i.gyazo.com/ad9c304c795b56c085c29307ded40e0d.png

例えば、「Text」のプルダウンは見出し、リスト形式などが選択できます。

https://i.gyazo.com/34f2610d0a789a5629e12061cd5f504b.png

A」のプルダウンはテキストカラーや背景色を選択できます。しかも色も豊富です!

https://i.gyazo.com/5f4ba83bcdc33ff1c060762d5bb12c33.png

個人的にテキスト装飾で最も助かっているのはコードの記述です。技術ブログだとプログラムのコードを表現するためにバッククオーテーションを使う方法が2つあると思います。

  • 文章の中にあるメソッド名など単語コード(文字をバッククオーテーションで囲む)

例)

sample_method

  • ファイル全体やメソッドの中身など文章コード(バッククオーテーション3つ(```)で囲む)
def sample_method
 puts 'sample_method'
end

コードを記事の中で沢山表示させようとすると、バッククォーテーションを連打しなければならずとても手間ですよね。特に単語コードは文章中にコードが出るたびにマークしなければならないため、マーク忘れなどが頻発してしまうかと思います。

ですが、Notionを使えば簡単にコードのマークを行うことができます。

  • 単語コードに関して

装飾範囲を選択して、「<>」のボタンを押すだけです。ショートカットで「⌘ + E」でコード装飾も行えます(ショートカットの方が楽です!)。

  • 文章コードをに関して

Text」のプルダウンから「Code」を選択します。文章の先頭でバッククオーテーションを3回入力することで文章コードを作成できます。

加えて、Notionの文章コードのさらに凄いところは、左上のプルダウンから言語ごとに文字の色の装飾を指定できることです。

https://i.gyazo.com/6dfc635634c52cbe548c7f6de35038dd.png

さらにさらに!

Notionで書いたマークダウンはコピペすると、マークダウン記法を反映してペースとされます!

つまり、Notionの便利なマークダウン機能でブログの内容を書けば、あとはコピペするだけでブログの投稿準備が完了するのです!いちいちマークダウンが反映されているかを確認しながら装飾していく面倒な方法から完全におさらばできます!

私自身も最近はNotionにブログの内容を書いてそれをブログにコピペする方法を採用していて、ブログ投稿のスピードがかなり上がりました!本当にNotionには感謝しかないです!

☆私のNotionの使い方☆

最後にNotionを愛用し始めた私のプログラミング学習時のNotionおすすめ使用方法を紹介します!

※ 私はRUNTEQに入校しているので、RUNTEQの課題を日々行っています。そのため、スクールに通っていない人、つまり独学の人とは少し状況が違いますが、独学の人は独学の人なりの方法で参考にしてください。

  • 課題と新しい知識でグループを分ける

Notionはファイルを管理する際、タグで分けるように設定できます。

https://i.gyazo.com/3e3bed857864c85469684baf4270928d.png

画像のように、左には課題全般のメモをし、右には課題で出てきた知らないメソッドや項目をタイトルにしファイルを作成しておきます。ファイル内にはテキトーに公式で調べた内容や自分で調べたことなどをメモしておきましょう。

https://i.gyazo.com/c6a2ba1a3dbc9d558a4ba07f78c5fa7a.png

  • ブログの管理ページでアウトプットしたい内容を管理

そしてファイルから自分がアウトプットしたい項目をピックアップし、Blog専用のページにファイルを移します。

https://i.gyazo.com/b2f002272a9647d0d042210fdf8e7f31.png

Not started」はブログ記事未着手のファイル,「In progress」はブログ着手中だけどまだ投稿していないファイル、「Completed」はブログ投稿済みのファイルです。お好みでタグの名前は簡単に変えられるので変えたいという方は変えてください!

終わりに

基本的には課題と新しい知識でグループを分け、アウトプットしたい内容をブログの管理ページで管理するという極めてシンプルな方法をとっています!しかし、Notionは他にも機能が盛り沢山です。個人的に好きなの機能は、ページによってアイコンを変えられるところとかですかね。かわいい(笑)

https://i.gyazo.com/8bebc1edd9aa878598530e9662c79094.png

是非Notionを使い倒してみてください!勉強が効率化し幅が広がります!

以上、大ちゃんの駆け出し技術ブログでした!

YouTube動画を表示

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回はYouTube動画とツイートを表示させる方法について書きたいと思います。これはRUNTEQの応用編で習うものなのです。 RUNTEQで紹介している技術なので現場でも使うのかなと思いまして今回記事にしてみることにしました。

今回はRUNTEQのYouTube動画は以下の動画です。

youtu.be

本記事での使用アプリ

今回はいわゆる現場Railsと言われるRUNTEQで推奨している書籍のアプリを使用します。

www.amazon.co.jp

いつも自分はrails newを使ってscaffoldを作り1から実装したり、みなさんが使用していないアプリを使っていますが、みなさんが手元で同じアプリを使用して実装する方が遥かに理解ができるかなと思いました。

現場RailsのChapter7まで終えていればアプリの状態は同じです。今回の実装ではTaskモデルに対して、新たにカラム「media_url」を実装して、それをタスク詳細画面のビューに表示するように実装します。

現状のタスク詳細画面は以下のようになっています。

Image from Gyazo

ここにYouTube動画の埋め込みをできるように実装します。

カラムを追加

media_urlカラムをタスクテーブルに追加します。

$ rails g migration AddYoutubeUrlToTasks media_url:string
Running via Spring preloader in process 14590
      invoke  active_record
      create    db/migrate/20210210071748_add_youtube_url_to_tasks.rb

マイグレーションファイルは以下のような感じになっています。

class AddYoutubeUrlToTasks < ActiveRecord::Migration[5.2]
  def change
    add_column :tasks, :media_url, :string
  end
end

このままDBにカラムを反映させましょう。

$ rails db:migrate

無事カラムが追加されました。

# db/schema.rb
create_table "tasks", force: :cascade do |t|
  t.string "name", null: false
  t.text "description"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.bigint "user_id"
  t.string "media_url"
  t.index ["user_id"], name: "index_tasks_on_user_id"
end

パラメーター取得の設定

次にapp/views/tasks/_form.html.slimに新しく作成したカラムを入力できるようにします。

# app/views/tasks/_form.html.slim
.form_group
  = f.label :image
  = f.file_field :image, class: 'form-control'
# 追加↓↓↓↓↓↓
.form_group
  = f.label :media_url
  = f.text_field :media_url, class: 'form-control'

Image from Gyazo

コントローラのパラメーターの取得にmedia_urlを追加します。

# app/controllers/tasks_controller.rb
def task_params
  params.require(:task).permit(:name, :description, :image, :media_url)
end

URLハッシュ値取得用のメソッドを定義

また、独自メソッドとしてsplit_id_from_youtube_urlを定義します。

# app/models/task.rb
def split_id_from_youtube_url
  media_url.split('/').last if media_url.present?
end

これはURLの後ろの部分を取得するためです。例えば、YouTubeの共有URLを見てみましょう。

https://youtu.be/nB3ceSVmHeY

YouTubeのURLはその動画を表すハッシュ値がyoutu.be/以降に存在します。split('/')によってURLを'/'ごとに分割した配列に変更します。

task.media_url.split('/')
=> ['https:', 'youtu.be', 'nB3ceSVmHeY']

そしてlastメソッドにより最後のハッシュ値のみを取得しています。

Task.media_url.split('/').last
=> 'nB3ceSVmHeY'

これによりその動画固有のハッシュ値が取得できるようになります。

詳細ページを編集

最後にタスク詳細ページにビューを追加します。src:オプションで先ほど定義したsplit_id_from_youtube_urlメソッドを使用しています。

tr
  th= Task.human_attribute_name(:image)
  td= image_tag @task.image if @task.image.attached?
# 追加↓↓↓↓↓↓
tr
  th= Task.human_attribute_name(:media_url)
  td= content_tag 'iframe', nil, src: "https://www.youtube.com/embed/#{@task.split_id_from_youtube_url}", \
      frameborder: 0, gesture: 'media', allow: 'encrypted-media', allowfullscreen: true

実装完了

これで必要なファイルへの記載は全て終わりました。

では実際にYouTube動画を表示させて意味ましょう。 先ほどの例で紹介した共有URL(https://youtu.be/nB3ceSVmHeY)を使用します。

編集画面で共有URLを入力しUpdate Taskを押します。(国際化されていませんね、申し訳ないです。)

そして更新したタスクの詳細画面に遷移すると、、、

Image from Gyazo

動画が表示されたかと思います。

終わりに

YouTubeのURLを入力して、その動画が表示されるサイトはたくさんあると思います。 例えば、はてなブログもそうですね。 したがって、はてなブログも今回実装したような方法を使用していると言えるかなと!

以上!大ちゃんの駆け出し技術ブログでした!

Pundit

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

最近は朝投稿が多いのでわりと朝活成功できています。

今日はRUNTEQの応用編で学習したgemの「Pundit」について発信しようと思います!

認可とは

Punditは"認可"の設定を効率よく行うためのgemです。

ん?認可とは何ぞや?

あまり聴き慣れない言葉ですよね。

認可というのは とある特定の条件に対して、リソースアクセスの権限を与えること

引用: https://dev.classmethod.jp/articles/authentication-and-authorization/

認可されることを具体例として管理者ページへのアクセスを使って説明すると

とある条件が「ユーザーが管理者ユーザーか」とであるとき、管理者ユーザーであれば管理者ページ(リソース)にアクセスする権限が与えられること」となります。

逆に一般ユーザーは管理者ページへのアクセスができないです。これが認可されないということになります。

gyazo.com

Punditの導入

Gemfileに追記してbundle install

# Gemfile
gem "pundit"
$ bundle
・
・
Fetching pundit 2.1.0
Installing pundit 2.1.0

アプリケーションコントローラに Punditincludeします。

class ApplicationController < ActionController::Base
  include Pundit
end

次に下記コマンドを実行します。

$ rails g pundit:install
Running via Spring preloader in process 47291
      create  app/policies/application_policy.rb

すると行数の多いファイルが作成されました。

# app/policies/application_policy.rb
class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end
  end
end

app/policies配下にファイルが置かれたように、Punditのファイルは全てこのディレクトリ配下に格納されます。applicationとあるように、こちらのファイルにはPunditのファイル全てに共通する内容を書きます。application_controller.rbなどと同じですね。

しかし、今回はよりPunditを理解するために固有の認可システムを作成します。例えば、このアプリケーションにBoard(掲示板)というモデルがあるとします。

このBoardを操作するにあたり、board_controller.rbupdateアクションに管理者ユーザーのみが行えるようにするとします。実装するためにapp/policies配下にboard_policy.rbを作成します。

# app/policies/board_policy.rb
class BoardPolicy
  attr_reader :user, :board

  def initialize(user, post)
    @user = user
    @post = post
  end

  def update?
    user.admin?
  end
end

次にboard_controller.rbupdateアクションにauthorizeメソッドをインスタンス変数を引数に使用します。

# app/controllers/boards_controller.rb
def update
  authorize @board
  if @board.update(board_params)
    redirect_to @board, success: t('defaults.message.updated', item: Board.model_name.human)
  else
    flash.now['danger'] = t('defaults.message.not_updated', item: Board.model_name.human)
    render :edit
  end
end

ここで何が行われるのかというと、updateアクションが実行時(掲示板を更新した時)、@boardからauthorizeメソッドはboard_policy.rbがあることを推測します。そして、現在実行されているアクション名に?が追加されたメソッドを探します。今回でいうと、board_policy.rbupdate?メソッドです。

def update?
  user.admin?
end

そしてuser.admin?tureを返さなければ、例外が発生します。これで管理者ユーザーのみがupdateアクションを実行することができ、一般ユーザーはupdateアクションを実行できなくなりました。これが認可を定義するPunditの力です!

終わりに

今回は簡単にざっくりとPunditを紹介しました!

1からアプリを実装しようとすると、ログイン機能を実装したりしなければならないので、今回は既存のアプリで試しました。みなさんもお手元の簡単なアプリで是非Punditを実装してみてください!

以上、大ちゃんの駆け出し技術ブログでした!

RUNTEQに入校して2ヶ月

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

先月にRUNTEQに入学して1ヶ月という記事を投稿しました。

sakitadaiki.hatenablog.com

この記事は私のはてなブログ初投稿の記事でしたが、1番読まれた記事です。

なぜかというと、

RUNTEQの社長さんがリツイートしてくれたから!ww

自分のような駆け出しエンジニアがやっているブログなんて、検索自体にかかることがないですし、そこまで見られることがないのは承知の上です。

それに、自分が覚えたことをさらに記憶に定着させる目的もありますし。この一週間毎日欠かさず記事を1本投稿していますし。

この1ヶ月でまた新たにRUNTEQの良い部分を見つけましたが、悪い部分(ネタ)も見つけられました! ぜひ、プログラミングスクールを探している人などに読んで欲しいと思います。

基礎編→応用編へ

「ある一定の期間を天気で例えて」という質問が研修や業務として人事の人やメンターから聞かれることがあると思います。先月のこの頃はRUNTEQの課題で言うところの基礎編の最後あたりでした。そして、その後に応用編(間にRSpecの課題を30時間ほど挟みますが)になることを知っていたので、さらに大変になるいう予想をしていました。しかし、上記質問を雨、晴、曇りとかから答えるとすると、徐々に太陽が見えてきたかなーとみたいな感じです。

「何で?むしろ簡単になったの?」

なんて思われれるかもしれませんが全然そんなことないですwwむずいよ本当にもう、、、

応用編の内容としては既にほとんど完成されられているアプリに対して、バグを修正したり、機能を追加したりします。

ただ、基礎編で学習していないメソッドとかライブラリとかが多く使われており、アプリ構成も基礎編で学習したものとは比べ物にならないくらい複雑です。応用編を開始して10日目ですが、いまだに全体の構成の半分程度も理解しているか怪しいです、、、

しかしながら、毎回出される課題はずっとわからないことだらけで、とりあえず調べてみることを繰り返した結果、わからないことに慣れてしまいました笑。わからない→調べる・質問する→解決するを繰り返し行ったので、わからないことに対して感じるストレスが減ったんだと思います。

それに、現場ではもっと複雑な既に完成させられたアプリに対して改修を行うことになります。ですので、本当に現場レベルのことをRUNTEQは学ばせてくれるのだなと改めて感じました。応用編自体も実際半分程度はもう終わっており、ペースも順調と個人的には感じています。

次の一ヶ月で応用編も終わって、いよいよPF作成に入っていくと思うので今から楽しみです!!

RUNTEQ生のレベルの高さには慣れない、、、

RUNTEQの課題には慣れてきましたが、他に慣れないものがあります。

それはRUNTEQ生です。

これは最近RUNTEQ生のコミュニティに馴染んできたのが原因かなと思うのですが、RUNTEQに入る人の意識が本当に高いです。

毎日勉強をするマインド、その人の頑張りやひたむきさ、それが見てわかります。

前の記事に書いたのですが、RUNTEQには生徒一人一人のslackチャンネルを開設します。そこで生徒は学んだことをアウトプットしたり、勉強の進捗、成果を報告したりするのですが、本当にみんな毎日チャンネルを更新しているので感心しています。

特に、最近「RUNTEQ生凄すぎ!!!」と感じたのは、editchというプログラミングスクールの合同PFコンテストです。

editch.org

このイベントでは内定をもらっていないプログラミングスクールの生徒が作成したPFを審査形式で発表するイベントなのですが、これが本当にレベル高いんです、、、。

まず何がすごいかて審査員の方々なんですよね。

安川 要平さん、西 武史さん、井上 慎也さん、兼城 駿一郎さんと、全員がIT企業の代表取締役なんですよね。審査員レベル高すぎて自分なら緊張して声が出るだろうかわかりません、、、

そして、このコンテストでRUNTEQからは3人の方が出場しました。そして、3人ともクオリティの高いPFをプレゼンし、素晴らしい評価をいただいていました。

自分が出場した3人のようにレベルの高いPFを作成することは想像できません。ぶっちゃけた話、周りがレベル高いと良い環境になると思いますが、高すぎるとどうしようもない劣等感に襲われますねww

ですが、editchは出場すると本当に就活に有利になると思いますし、自分のPFもこれぐらいハイレベルにして就活したいと考えています。

次のeditchは4月下旬です。あと2ヶ月程度しか時間がありませんが、出場できるように膝下入りと学習を進めていきたいと思います。

高まるコミュニティ力

RUNTEQは本当にコミュニティ内でのgive & giveが活発です! 例えば、エラーに詰まったりすると、卒業生が講師に代わって助けてくれたりアドバイスをくれたり。

RUNTEQはdiscordというチャットツールも導入しているため、話したいときに話にいくこともできるということで、最近コロナのせいで減った雑談もすることができています!

discord.com

受講生同士がお互いに励ましあったり笑いあったりできるのは本当にRUNTEQの暖かいコミュニティのおかげです。 ここに卒業しても入れるなんて本当にすごいことだと思います!無料のオンラインサロンになってます。

さらにRUNTEQ自体も自明度が上がっているのか最近は入学する生徒が昔に比べてとても多いようです。自分より半年に入学した人はわずか5人程度なのですが、現在RUNTEQ生は月に約30名は入ってくるのでslackチャンネルは人で溢れています。

自分はRUNTEQに入って2ヶ月なのでだいぶコミュニティにも馴染みはじめようやく受講生の顔と名前が一致してきました。 自分も覚えてもらえてるみたいで、自分のslackチャンネルにはコメントやスタンプもよくもらえるようになったのは嬉しいです。

しかし、これからまたどんどん人が増えていくと、大量のgive & giveがまた生まれてくると思います! 自分もgiveしてコミュニティを盛り上げていければと思います!

RUNTEQに入る = 別れる!?

これは冒頭で触れたRUNTEQの悪い部分(ネタ)にあたります。

どういうことかというと、「RUNTEQの学習で忙しくパートナーとの時間が取れなくなるせいで別れる人が続出している」という、ただの自分の仮説ですww

ですが、この仮説は正しいと思っています。実際、RUNTEQに入校してから2ヶ月経ちましたが、別れたという報告をした人は3人ほどいました。自分も最近彼女と会う頻度が減ったことに不満を言われたので、フラグが立ってしまいました、、、ww

RUNTEQのカリキュラムは本当に難しいので、それ相応の時間を取らないといけません。実際、RUNTEQのYouTubeチャンネルであるエンジニア転職チャンネルでは、RUNTEQのカリキュラムは1000時間の学習時間を想定していると説明しています。

www.youtube.com

確かにRUNTEQ入学前、説明会では週にできれば20時間以上と言っていました。でも、体感的に20時間じゃ全然足りないと思います。絶対最低30時間は必要です。

そして、自分は働きながらRUNTEQを受講している状態です。したがって、自分に残された自由時間は以下の通りになります。

1週間(168時間) -
 ( 通勤時間を含めた1週間の就業時間(50時間) + 
RUNTEQのカリキュラム時間(30時間) + 
睡眠時間(50時間) + 
食事や掃除等の一人暮らしの家事時間(15時間) = 
自由時間(23時間)

「23時間もあるなら週一で遊べるじゃん」

とか思われるかもしれませんが、RUNTEQのカリキュラム時間とは別に個人で学習する時間も取らなくてはいけません。例えば、ブログや市販テキスト学習です。これらも1日2~3時間以上は当然かかります。23時間の自由時間があるのではなく23時間の自由"学習"時間があるんですねww

といっても捻出すれば、全然彼女とも会うことができるのですが、それでも会う頻度は激減しました。

なので、RUNTEQに入る際はパートナーに会う頻度が激減することをしっかりとお伝えし、了承を得ておきましょう(本当に重要です)。もしくは、RUNTEQさんがパートナー保証を制度として設けてくれることを期待しましょうww

終わりに

今回の記事は個人的な体験だけを書いたので、RUNTEQのことについてちゃんと紹介しなくて申し訳ないです。 また1ヶ月後に報告として、「RUNTEQに入校して3ヶ月」も書くかと思いますが、次はしっかりとRUNTEQをお勧めできるような記事ないようにします!

以上、大ちゃんの駆け出し技術ブログでした!

RSpecでページのステータスコードを取得する方法

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回は超短いアウトプットです!!

いつも6000字は超えるようにしてるんですけど、今回は3000字ぐらいになります。

というのも紹介することがシンプルなんですよね。

タイトルにあるとおり、今回は「RSpecでページのステータスコードを取得する方法」を紹介します。

Capybaraではテストでステータスコードを取得できない

RSpecのテスト方法にはCapybaraを使ったテスト方法が主流かなと思っています。

テスト時に実際にブラウザを起動させて行うテストで、エラーがあったときにはエラー時のスクリーンショットを取ってくれる便利なやつです。

設定ファイルは以下のようになります。

# spec/support/capybara.rb
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium, using: :chrome, screen_size: [1920, 800]
  end
end

spec/supportディレクトリを生成し、spec_helper.rbとは違うファイルでテストの設定を行えるようにしています。

一見完璧に見えるCapybaraなのですが、意外と欠点があるんですよね。

それが「ページのステータスコードを取得できない」です。

例えば、権限がないページにユーザーがアクセスしたときに、403エラー(:forbidden)が発生します。そして、エラーが発生した時のステータスコードは403となるはずです。ですので、ステータスコードを検証したいのですが、それがCapybaraだとできないんです。

driven_by(:rack_test)

ステータスコードを取得するテストはどうすれば良いのかというと、実は簡単でCapybaraではなく、テストの設定をdriven_by(:rack_test)に変えるだけです。

この設定は実はシステムスペックファイルを作成するコマンド実行時にデフォルトで生成されたシステムスペックファイルに記述されています。

$ bin/rails g rspec:system sample
      create  spec/system/samples_spec.rb

作成されたシステムスペックファイル

# spec/system/samples_spec.rb
require 'rails_helper'

RSpec.describe "Samples", type: :system do
  before do
    driven_by(:rack_test) # ここ!
  end

  pending "add some scenarios (or delete) #{__FILE__}"
end

これでステータスコードを取得できるんですね。

実際にテストコードに書いてみます。

RSpec.describe "Pundit", type: :system do
  let(:writer) { create(:user, :writer) }
  before do
    driven_by(:rack_test) # ここに記載
  end

  describe 'Writer' do
    before { login_as(writer) }
    describe 'Not authorized pages' do
      context 'When writer accesses to admin_categories_path' do
        before { visit admin_categories_path }
        fit 'Current_page is 403 error page' do
          expect(page).to have_content('You are not authorized to perform this action.'), '403エラーページが表示されていません'
          expect(page.status_code).to eq(403), 'ステータスコードが403ではありません'
        end
      end
    end
  end
end

今回はadmin_categories_pathに権限を持たないwriterがアクセスするとステータスコードが403になるかを検証します。driven_by(:rack_test)を使うとステータスコードを取得するpage.status_codeが使用できるようになります。実際に使ってみると403が返ってきます。

[1] pry(#<RSpec:・・・省略)> page.status_code
=> 403

しかし、driven_by(:rack_test)を記述すると、Capybaraのテスト設定がオーバーライドされてしまい、スクリーンショットをとることができなくなるのでご注意を。

じゃあCapybaraでpage.status_codeを実行すればいいのでは?と思う方もいると思いますが、残念ながらCapybaraのテストでpage.status_codeを実行するとエラーが返ってきます。

[1] pry(#<RSpec:・・・省略)> page.status_code
Capybara::NotSupportedByDriverError: Capybara::Driver::Base#status_code

なのでステータスコードを取得することを検証するシステムスペックファイルにのみ、driven_by(:rack_test)を記載しましょう。

終わりに

これで連続投稿8日目となりました!

どこまで行けるかわかりませんが、少なくとも今月中は毎日アウトプットを繰り返してみようと思います。

以上、大ちゃんの駆け出し技術ブログでした!

transientとafter(:build)

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

RSpecで少し詰まった箇所があったので、今回はそちらを記事にします。今回の記事は試験的に少し雑なアウトプットをさせていただきます。いつもの記事も雑かもしれませんが、、、

理由は下記2つです。

  • そもそも読者想定は初学者ですが、ある程度勉強を進めているので1からアプリを用意するということはもうすでに必要ないことなのかなと思った。
  • 2月は毎日投稿を目指していますが、仕事しながらだとRUNTEQの課題や転職用のポートフォリオのために使う時間が撮れないと感じた。

2つ目の理由についてはネガティブな理由ではあるものの、やはり投資時間を分散させるために試験的に行ってみたいと思います。

トピックは記事のタイトル通り、transientafter(:build)です。

RSpecを書く上でとっても役に立つと思ったので是非みなさんも使ってみてください。

テストの概要

早速本題に入らせていください。

RUNTEQの応用編に現在取り組んでいますが、その課題でRSpecを作成する課題があります。

どのようなページで検証するのかと言うと、記事(Article)について、著者がいる記事Aと著者がいない記事Bが記事一覧ページにて検証します。

Image from Gyazo

検証する内容は下記の通りです。

記事Aの著者で検索をすると記事Aだけが表示される。

このページには検索窓がありそれが下記の画像のような感じ。

https://i.gyazo.com/7c0d3f4ea882d8489b2fd64347ee9acc.png

この著者のセレクトボックスで著者Aを選択肢検索すると記事Aだけが表示されるということです。

テストのシステムスペックファイル

よってシステムスペックのシナリオは以下の通りになります。

# spec/system/admin_article_searches_spec.rb
describe '著者検索' do
    let(:admin_user) { create(:user, :admin) }
        let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
      let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }
    before do
      article_with_author_A
      article_with_author_B
      login_as(admin_user) # ポイント1
      visit admin_articles_path # ポイント2
    end
    context "著者Aで検索される時" do
      before do
        within('.form-inline') do
          select article_with_author_A.author.name, from: 'q[author_id]' # ポイント3
          click_button '検索'
        end
      end
      it '著者Aの記事のみが表示される' do # ポイント3
        expect(page).to have_content(article_with_author_A.title), '著者Aの記事が表示されていません'
        expect(page).not_to have_content(article_with_author_B.title), '著者A以外の記事が表示されています'
      end
    end
  end

letの部分の説明は今回の記事の肝ですので、後で説明します。最初にシナリオの流れだけざっくり説明します。

[ポイント1] login_as(admin_user)でまずは管理者画面にログインします。login_asメソッドはモジュールで独自に定義してあります。

# spec/support/login_support.rb
module LoginSupport
  def login_as(user)
    visit admin_login_identifier_path
    fill_in 'user_name', with: user.name
    click_button '次へ'
    expect(current_path).to eq admin_login_password_path
    fill_in 'user_password', with: 'password'
    click_button 'ログイン'
  end
end

[ポイント2] Article(記事)モデルとAuthor(著者)モデルの関係性は多:1です。Authorが複数の記事を持つということです。

ですので、セレクトボックスから著者の名前を取り出す方法はarticle_with_author_A.author.nameとなります。

[ポイント3] そして、検証部分では著者Aの記事だけが表示され、著者Bの記事は表示されないことを想定しています。

expect(page).to have_content(article_with_author_A.title), '著者Aの記事が表示されていません'
expect(page).not_to have_content(article_with_author_B.title), '著者A以外の記事が表示されています'

問題なのはletの部分です。

admin_userに関しては簡単です。createの第二引数でtraitを指定し、管理者ユーザーが作成されるように指定しています。UserモデルのFactoryBotの中身を見てみると、trait:adminの記述があるのが分かります。

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
・
・
    trait :admin do
      sequence(:name) { |n| "writer-#{n}" }
      role { :admin }
    end
・
・
  end
end

一方でarticle_with_author_Aarticle_with_author_Bはどうでしょうか。

let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }

createの第三引数にauthor_nameの指定がありました。自分はRSpecにおけるcreateの第三引数自体が初見だったので少し混乱しました。

ArticleのFactoryBotでtraitが:with_authorの部分を見てみると見慣れない記述があります。

FactoryBot.define do
  factory :article do

    trait :with_author do
      transient do
        sequence(:author_name) { |n| "test_author_name_#{n}" }
        sequence(:author_slug) { |n| "test_author_slug_#{n}" }
      end
  
      after(:build) do |article, evaluator|
        article.author = build(:author, name: evaluator.author_name, slug: evaluator.author_slug)
      end
    end

これが自分にとって初となるtransientafter(:build)の出会いでしたww

なんだこの記述ってすごく思ったんですけど、理解するのは意外と簡単なんです!

transient

まずtransientから説明していきます!

lettraitを指定すると、同時にtraitのブロック内に存在するtransient内のの値が取得できます。例えば、下記のようにcreateに第三引数を渡さずにFactoryBotを呼び出す記述を書くとします。

let(:article_with_test_author) { create(:article, :with_author) }

このFactoryBotが呼び出されるとtransient内の中の値、つまりauthor_nameauthor_slugも呼び出されます。

試しに、binding.pryで止めて値を確かめてみます。

transient do
  sequence(:author_name) { |n| "test_author_name_#{n}" }
  sequence(:author_slug) { |n| "test_author_slug_#{n}" }
  binding.pry # ここで止める!
end

するとそれぞれの値は以下のようになっていました。

[1] pry(#<FactoryBot::Declaration::Implicit>)> author_name
NameError: undefined local variable or method `author_name' for #<FactoryBot::Declaration::Implicit:0x00007f852fc39ba8>
Did you mean?  Pathname
from (pry):5:in `__pry__'
[2] pry(#<FactoryBot::Declaration::Implicit>)> author_slug
NameError: undefined local variable or method `author_slug' for #<FactoryBot::Declaration::Implicit:0x00007f852fc39ba8>
from (pry):6:in `__pry__'

ありゃりゃ、取得できていませんね。

ご安心ください。値自体はafter(:build)の部分で確認することができます!

補足しておくと、letcreateの第三引数であった:author_name"test_author_name_#{n}"の値ではなく別の値を格納したい時に指定します。今回の場合は"著者A"、"著者B"といった具合ですね。

after(:build)

続いてafter(:build)の中を覗いてみましょう。

after(:build) do |article, evaluator|
  article.author = build(:author, name: evaluator.author_name, slug: evaluator.author_slug)
end

evaluatorとは何なんでしょうか??

実はさっきのtrasient内の値はこの中に格納されています。

[2] pry(#<FactoryBot::SyntaxRunner>)> evaluator.author_name
=> "test_author_name_1"
[3] pry(#<FactoryBot::SyntaxRunner>)> evaluator.author_slug
=> "test_author_slug_1"

そしてこの値がarticle.authorというAuthorモデルの作成時に渡されています。

ブロック内の部分はとても単純ですよね。AuthorモデルのFactoryBotを作成しています。

build(:author, name: evaluator.author_name, slug: evaluator.author_slug)

だんだん見えて理解できてきましたでしょうか。

本来であれば、アソシエーション関係のあるFactoryBotの呼び出しをletのみで行う場合、letを2回使う必要がありました。今回の場合、記事が1つとその記事の著者が1つです。

しかし、transientを使用した場合、let1回の呼び出しで2つのFactoryBotを作成しています!

let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }

これによりletをシステムスペックファイルに記述する量を減らしているのです。今回の場合、著者Aの記事だけでなく著者Bの記事も必要だったため、letのみでFactoryBotを作成しようとすると、合計で4回も呼び出さなければなりません。すごい冗長ですよね、、、。

終わりに

今回の説明でtransientafter(:build)の概要は何となく理解できたかなと思います。

たくさんのletの使用は冗長化につながりますので、1回のletの使用でアソシエーション関係にあるFactoryBotの作成は是非とも使ってみてください!

以上、以上、大ちゃんの駆け出し技術ブログでした!