20200324のRailsに関する記事は17件です。

Railsフリマアプリ payjpを使ってクレジットカードを登録・削除

payjp とは・・・?

クレジットカードを登録、変更、購入を行ってくれる便利なAPIです。
みんさんもフリマアプリ、ECサイトでクレジットカードを使って商品の購入すると思います。その時登録したクレジットカード情報はどこに保存されているでしょうか?私は運営するサイトで暗号化されて管理されているのだと思っていましたが、違うようです。たしかにサイトのデータベースにクレジットカード情報を登録するのはセキュリティ上よろしくないですよね。
このとき登場するのがpayjpになります。

payjpの仕組み

まずpayjpとフリマアプリではどのような流れでクレジットカードの処理が行われているのかざっくりとイメージしましょう!
私はここを飛ばしてすぐコードを書き初めてため概念の理解に時間がかかりました。わかれば非常に単純でした。笑
以下の記事がとてもわかりやすく参考になります。
Pay.jpの仕組みについて!
セキュリティに加えて、payjpで発行されるトークンと顧客IDが紐つけられ永続化することで、ユーザーがスムーズに決済できることがポイントかと思います。

実装内容

参考記事

実装においては以下の記事を参考としました。事情にわかりやすく説明されていています。
Payjpでクレジットカード登録と削除機能を実装する(Rails)

基本的な実装な上記記事を見れば理解できると思います。今回は実装中につまずいたポイントや復習した内容についてまとめます。

JavaScript(jQuery)を使ってトークンを発行

ここまでの学習でJSについて理解が足りていなかったので、カリキュラムを復習、新しい知識をインプットしながらトークンの発行の仕方を理解しました。payjpに関して記事は結構あるのですが、コードがボーンって書いてあるだけだったので詳しく見ていきましょう!!

payjp.js

document.addEventListener(
  "DOMContentLoaded", e => {
    Payjp.setPublicKey("自分の公開鍵を記述する");
    var btn = document.getElementById('token_submit');
    btn.addEventListener("click", (e) => {
      e.preventDefault();

      var card = {
        number: $("#number").val(),
        cvc: $("#cvc").val(),
        exp_month: $("#exp_month").val(),
        exp_year: $("#exp_year").val()
      }; 


      Payjp.createToken(card, (status, response) => {
        if (status === 200) {
          $("#number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name"); 
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          document.inputForm.submit();
          alert("登録が完了しました"); 
        } else {
          alert("カード情報が正しくありません。"); 
        }
      });
    });
  },
  false
);

まずpayjpとに入力データを渡すために、公開鍵を記述します。
秘密鍵と公開鍵がありますので間違わないように注意しましょう。秘密鍵はcontrollerに記述します。

Payjp.setPublicKey("自分の公開鍵を記述する");

viewでカード情報を入力して、登録ボタン(#token_submit)をクリックした時、データを送信するデフォルトの機能を停止し、

var btn = document.getElementById('token_submit');
    btn.addEventListener("click", (e) => {
      e.preventDefault();

クレジットカード情報の、number,cvc,exp_month,exp_yearを.val()で属性の値を取得します。

var card = {
        number: $("#number").val(),
        cvc: $("#cvc").val(),
        exp_month: $("#exp_month").val(),
        exp_year: $("#exp_year").val()
      }; 

上記で取得した値はPayjp.createToken()の引数に渡し管理されます。
status === 200はリクエストが成功している状況です。
ここでカード情報はサーバーには保存しないため、removeAttrによって属性を削除します。そしてresponse.idを取得しトークンをデータベースに送るため隠しタグを生成します。

Payjp.createToken(card, (status, response) => {
        if (status === 200) {
          $("#number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name"); 
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          document.inputForm.submit();
          alert("登録が完了しました"); 

最後に!

便利なAPIを使うと簡単に便利な機能を実装することができますが(結構時間かかったけど)、何でこの記述をしているのか考えるのもいい勉強になりました。特にJSがあまり理解できていなかったので、この機会復習もできました。
理解が間違っていましたらぜひ教えてください。
読んでくださりありがとうございました!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

商品出品画面を作る

メルカリの商品出品画面を参考にコピーを作る

参考画面メルカリ

使用する機能

  • ActiveStorage(画像投稿)リンク
  • ancestry(多階層カテゴリー)
  • active_hash(静的データ作成)

とりあえず出来たコード

new.html.haml
.sell
  %header.sell-header
    = link_to root_path do
      = image_tag 'mercari_top_logo.svg', alt: 'mercari', height: '49', width: '185'
  -#メイン部分
  %main
    %section.sell-container
      = form_with model: @item do |f|

        -# 画像部分
        .sell-container__content
          .sell-title
            %h3.sell-title__text
              出品画像
              %span.sell-title__require
                必須
          .sell-container__content__max-sheet 最大10枚までアップロードできます
          .sell-container__content__upload
            .sell-container__content__upload__items
              .sell-container__content__upload__items__box
                %ul#output-box
                  %div#image-input{tabindex:"0"}
                    = f.label :images, for: "item_images0", class: 'sell-container__content__upload__items__box__label', data: {label_id: 0 } do 
                      = f.file_field :images, multiple: true, class: "sell-container__content__upload__items__box__input", id: "item_images0", style: 'display: none;'
                      %pre
                        %i.fas.fa-camera.fa-lg
                        ドラッグアンドドロップ
                        またはクリックしてファイルをアップロード
          .error-messages#error-image

        -#商品名部分
        .sell-container__content
          .sell-title
            %h3.sell-title__text
              商品名
              %span.sell-title__require
                必須
          = f.text_field :name, {class:'sell-container__content__name', required: "required", placeholder: '商品名(必須 40文字まで)'}
          .error-messages#error-name

          .sell-title
            %h3.sell-title__text
              商品の説明
              %span.sell-title__require
                必須
          = f.text_area :text,{class: 'sell-container__content__description', required: "required", rows: '7', maxlength: '1000', placeholder: text_placeholder}
          -# placeholderでtems_helperを呼び出す
          .sell-container__content__word-count
            %span#word-count
              0
            &#47;1000
          .error-messages#error-text

        -# 詳細部分
        .sell-container__content
          %h3.sell-sub-head 商品の詳細
          .sell-container__content__details
            .sell-title
              %h3.sell-title__text
                カテゴリー
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :category_id, {class: 'sell-collection_select__label'} do
                = f.collection_select :category_id, @category_parent, :id, :name, {prompt: "選択して下さい"},{ class: 'sell-collection_select__input', id: 'category-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-category

            .sell-title
              %h3.sell-title__text
                商品の状態
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :condition_id, {class: 'sell-collection_select__label'} do
                = f.collection_select :condition_id, Condition.all, :id, :condition, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'condition-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-condition

        -# 配送部分
        .sell-container__content
          %h3.sell-sub-head
            %p 配送について
            = link_to '/delivery',target: '_blank',class: 'sell-sub-head__guides-link' do
              %i.far.fa-question-circle
          .sell-container__content__delivery
            .sell-title
              %h3.sell-title__text
                配送料の負担
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :deliverycost_id, {class: 'sell-collection_select__label'} do
                = f.collection_select :deliverycost_id, Deliverycost.all, :id, :payer, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'deliverycost-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-deliverycost

            .sell-title
              %h3.sell-title__text
                発送元の地域
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :pref_id, class: 'sell-collection_select__label' do
                = f.collection_select :pref_id, Pref.all, :id, :name, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'pref-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-pref

            .sell-title
              %h3.sell-title__text
                発送までの日数
                %span.sell-title__require
                  必須
            .sell-collection_select
              = f.label :delivery_days_id, class: 'sell-collection_select__label' do
                = f.collection_select :delivery_days_id, DeliveryDays.all, :id, :days, {prompt: '選択して下さい'},{ class: 'sell-collection_select__input', id: 'delivery_days-select', required: "required"}
                %i.fas.fa-chevron-down
            .error-messages#error-delivery_days

        -# 価格部分
        .sell-container__content
          %h3.sell-sub-head
            %p 販売価格(300〜9,999,999)
            = link_to '/price',target: '_blank', class: 'sell-sub-head__guides-link' do
              %i.far.fa-question-circle
          .sell-container__content__price
            .sell-title
              %h3.sell-title__text
                販売価格
                %span.sell-title__require
                  必須
            .sell-container__content__price__form
              = f.label :price, class: 'sell-container__content__price__form__label' do
                ¥
                = f.number_field :price, {placeholder: '0', value: '', autocomplete:"off", class: 'sell-container__content__price__form__box', required: "required"}
          .error-messages#error-price

          .sell-container__content__commission
            .sell-container__content__commission__left
              販売手数料 (10%)
            .sell-container__content__commission__right.sell-container__content__profit
            .sell-container__content__profit__left
              販売利益
            .sell-container__content__profit__right.submit-btn
            = f.submit '出品する', class: 'submit-btn__sell-btn'
            = link_to 'もどる', root_path, class: 'submit-btn__return-btn'
          .attention-box
            %p
              禁止されている
              = link_to '行為', '/prohibited_conduct', target: '_blank'
              および
              = link_to '出品物', '/prohibited_item', target: '_blank'
              を必ずご確認ください。
              = link_to '偽ブランド品', '/counterfeit_goods', target: '_blank'= link_to '盗品物', '/stolen_goods', target: '_blank'
              などの販売は犯罪であり、法律により処罰される可能性があります。また、出品をもちまして
              = link_to '加盟店規約', '/seller_terms', target: '_blank'
              に同意したことになります。

  %footer.sell-footer
    %nav
      %ul.clearfix
        %li
          = link_to '#' do
            プライバシーポリシー
        %li
          = link_to '#' do
            メルカリ利用規約
        %li
          = link_to '#' do
            特定商取引に関する表記
    = link_to root_path, class: 'footer__logo' do
      = image_tag 'logo-gray.svg', alt: 'mercari', height: '65', width: '80'
    %p
      %small
        &copy; Mercari, Inc.
items_new.scss
a {
  color: inherit;
  text-decoration: none;
  box-sizing: border-box;
}
img {
  vertical-align: middle;
  box-sizing: border-box;
}
.error-messages{
  color: #ff0211;
  font-size: 14px;
  line-height: 1.4em;
  margin: 16px 0;
  box-sizing: border-box;
}

.sell-title{
  align-items: center;
  margin: 0!important;
  box-sizing: border-box;
  &__text{
    font-size: 14px;
    font-weight: 600;
    line-height: 1.4em;
  }
  &__require{
    margin-left: 8px;
    font-size: 12px;
    padding: 0 4px;
    background-color: #ff0211;
    color: #fff;
    border-radius: 2px;
    display: inline-block;
    font-style: normal;
    font-weight: 600;
    line-height: 1.4em;
    margin: 0;
  }
}

.sell-sub-head{
  box-sizing: border-box;
  color: rgb(136, 136, 136);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.4em;
  margin-bottom: 24px;
  display: flex;
  &__guides-link{
    color: rgb(0, 149, 238);
    margin-left: 4px;
  }
}

.sell-collection_select{
  box-sizing: border-box;
  margin-top: 16px;
  &__label{
    display: inline-block;
    position: relative;
    width: 100%;
    .fas.fa-chevron-down{
      box-sizing: border-box;
      pointer-events: none;
      position: absolute;
      right: 16px;
      top: 40%;
      color: rgb(136, 136, 136);
      height: 48px;
    }
  }
  &__input{
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background-color: #fff;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  color: #222;
  font-size: 16px;
  height: 48px;
  line-height: 1;
  margin: 0;
  outline: none;
  padding: 0 56px 0 16px;
  width: 100%;
  }
}
// ここより上は繰り返し使用するパーツ

.sell {
  box-sizing: border-box;
  position: relative;
  color: rgb(51, 51, 51);
  background-color: rgb(245, 245, 245);
  font-family: Arial, 游ゴシック体, YuGothic, メイリオ, Meiryo, sans-serif;
  font-size: 14px;
  line-height: 1;
  box-sizing: border-box;
  .sell-header{
    box-sizing: border-box;
    height: 128px;
    align-items: center;
    display: flex;
    justify-content: center;
  }
  .sell-container{
    box-sizing: border-box;
    max-width: 700px;
    width: 100%;
    margin: 0px auto;
    background-color: rgb(255, 255, 255);

    // 画像部分
    &__content{
      height: auto;
      padding: 40px;
      border-bottom: 1px;
      border-bottom-color: #efefef;
      border-bottom-style: solid;
      &__max-sheet{
        margin-top: 16px;
      }
      &__upload{
        margin-top: 16px;
        display: flex;
        flex-wrap: wrap;
        &__items{
          height: auto;
          width: 100%;

          &__box{
            height: auto;
            align-content: center;
            align-items: center;
            cursor: pointer;
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            position: relative;
            border-width: 1px;

            #output-box{
              box-sizing: border-box;
              display: flex;
              flex-wrap: wrap;
              width: 100%;
              height: auto;

              .preview-image{
                box-sizing: border-box;
                height: 150px;
                width: 20%;
                padding: 0px 4px;
                margin-top: 8px;

                &__figure{
                  margin:0 auto;
                  height: 118px;
                  background-color: rgb(245, 245, 245);
                  img{
                    box-sizing: border-box;
                    width: 100%;
                    height: 100%;
                    object-fit: contain;
                  }
                }
                &__button{
                  border-top-width: 1px;
                  border-top-color: rgb(204, 204, 204);
                  border-top-style: solid;
                  background-color: rgb(245, 245, 245);
                  justify-content: space-around;
                  display: flex;
                  align-items: center;
                  height: 32px;
                  color: rgb(0, 149, 238);
                }
              }

              #image-input{
                box-sizing: border-box;
                height: 100%;
                -webkit-flex: 1;
                flex: 1;
                margin-top: 8px;

                .sell-container__content__upload__items__box__label{
                  box-sizing: border-box;
                  background-color: rgb(245, 245, 245);
                  height: 150px;
                  border-width: 1px;
                  border-style: dashed;
                  border-color: rgb(204, 204, 204);
                  text-align: center;
                  display: flex;
                  align-items: center;
                  justify-content: center;
                  i{
                    box-sizing: border-box;
                    margin-bottom: 8px;
                  }
                }
              }
            }
          }
        }
      }
    }

    // 商品名部分
    &__content{
      &__name{
        margin-top: 16px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        height: 48px;
        padding: 0 16px;
        width: 100%;
      }
      &__description{
        margin-top: 16px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        padding: 16px;
        width: 100%;
        font-size: 16px;
        display: block;
      }
      &__word-count{
        text-align: right;
        color: #888;
        font-size: 12px;
        line-height: 1.4em;
      }
    }
    // 詳細は共通パーツのみ
    // 配送は共通パーツのみ
    // 価格部分
    &__content{
      &__price{
        -webkit-box-align: center;
        align-items: center;
        box-sizing: content-box;
        display: flex;
        height: 46px;
        justify-content:space-between;
        &form__label{
          font-size: 14px;
        }
        &__form__box{
          border: 1px solid #ccc;
          border-radius: 4px;
          height: 48px;
          margin-left: 8px;
          padding: 0 16px;
          width: 300px;
          align-items: center;
          display: inline-flex;
          text-align: right;
        }
      }
      #error-price{
        box-sizing: border-box;
        text-align: right;
      }
      &__commission{
        display: flex;
        justify-content:space-between;
        height: 70px;
        padding: 12px 0px;
        align-items: center;
        border-bottom: 1px;
        border-bottom-color: #efefef;
        border-bottom-style: solid;
      }
      &__profit{
        display: flex;
        justify-content:space-between;
        height: 70px;
        padding: 12px 0px;
        align-items: center;
      }
    }
    .submit-btn{
      box-sizing: border-box;
      margin: 0 auto;
      width: 360px;
      margin-bottom: 32px;
      &__sell-btn{
        background-color: #ea352d;
        color: #fff;
        margin-bottom: 24px;
        width: 100%;
        font-size: 17px;
        height: 48px;
        font-weight: 600;
        border-radius: 4px;
      }
      &__return-btn{
        background-color: #ccc;
        color: #222;
        width: 100%;
        font-size: 17px;
        height: 48px;
        font-weight: 600;
        padding: 14px 0;
        border-radius: 4px;
        display: inline-block;
        text-align: center;
      }
    }
    .attention-box{
      box-sizing: border-box;
      font-size: 12px;
      line-height: 1.4em;
      word-break: keep-all;
      a{
        box-sizing: border-box;
        color: #0095ee;
      }
    }
  }
  .sell-footer{
    box-sizing: border-box;
    padding: 40px 0px;
    text-align: center;
    nav{
      box-sizing: border-box;
      .clearfix{
        box-sizing: border-box;
        display: inline-block;
        font-size: 12px;
        display: flex;
        justify-content: center;
        margin-bottom: 40px;
        li{
          box-sizing: border-box;
          margin: 0px 8px;
        }
      }
    }
  }
}
items_new.js
$(document).on('turbolinks:load', function(){
  // 画像が選択された時プレビュー表示、inputの親要素のulをイベント元に指定
  $('#image-input').on('change', function(e){

    //ファイルオブジェクトを取得する
    let files = e.target.files;
    $.each(files, function(index, file) {
      let reader = new FileReader();

      //画像でない場合は処理終了
      if(file.type.indexOf("image") < 0){
        alert("画像ファイルを指定してください。");
        return false;
      }
      //アップロードした画像を設定する
      reader.onload = (function(file){
        return function(e){
          let imageLength = $('#output-box').children('li').length;
          // 表示されているプレビューの数を数える

          let labelLength = $("#image-input>label").eq(-1).data('label-id');
          // #image-inputの子要素labelの中から最後の要素のカスタムデータidを取得

          // プレビュー表示
          $('#image-input').before(`<li class="preview-image" id="upload-image${labelLength}" data-image-id="${labelLength}">
                                      <figure class="preview-image__figure">
                                        <img src='${e.target.result}' title='${file.name}' >
                                      </figure>
                                      <div class="preview-image__button">
                                        <a class="preview-image__button__edit" href="">編集</a>
                                        <a class="preview-image__button__delete" data-image-id="${labelLength}">削除</a>
                                      </div>
                                    </li>`);
          $("#image-input>label").eq(-1).css('display','none');
          // 入力されたlabelを見えなくする

          if (imageLength < 9) {
            // 表示されているプレビューが9以下なら、新たにinputを生成する
            $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                        <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                        <i class="fas fa-camera fa-lg"></i>
                                      </label>`);
          };
        };
      })(file);
      reader.readAsDataURL(file);
    });
  });

  //削除ボタンが押された時
  $(document).on('click', '.preview-image__button__delete', function(){
    let targetImageId = $(this).data('image-id');
    // イベント元のカスタムデータ属性の値を取得
    $(`#upload-image${targetImageId}`).remove();
    //プレビューを削除
    $(`[for=item_images${targetImageId}]`).remove();
    //削除したプレビューに関連したinputを削除

    let imageLength = $('#output-box').children('li').length;
    // 表示されているプレビューの数を数える
    if (imageLength ==9) {
      let labelLength = $("#image-input>label").eq(-1).data('label-id');
      // 表示されているプレビューが9なら,#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得
      $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                  <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                  <i class="fas fa-camera fa-lg"></i>
                                </label>`);
    };
  });

  // f.text_areaの文字数カウント
  $("textarea").keyup(function(){
    let txtcount = $(this).val().length;
    $("#word-count").text(txtcount);
  });

  //販売価格入力時の手数料計算
  $('#item_price').keyup(function(){
    let price= $(this).val();
    if (price >= 300 && price <= 9999999){
      let fee = Math.floor(price * 0.1);
      // 小数点以下切り捨て
      let profit = (price - fee);
      $('.sell-container__content__commission__right').text('¥'+fee.toLocaleString());
      // 対象要素の文字列書き換える
      $('.sell-container__content__profit__right').text('¥'+profit.toLocaleString());
    } else{
      $('.sell-container__content__commission__right').html('');
      $('.sell-container__content__profit__right').html('');
    }
  });

  // 各フォームの入力チェック
  $(function(){
    //画像
    $('#image-input').on('focus',function(){
      $('#error-image').text('');
      $('#image-input').on('blur',function(){
        $('#error-image').text('');
        let imageLength = $('#output-box').children('li').length;
        if(imageLength ==''){
          $('#error-image').text('画像がありません');
        }else if(imageLength >10){
          $('#error-image').text('画像を10枚以下にして下さい');
        }else{
          $('#error-image').text('');
        }
      });
    });

    //送信しようとした時
    $('form').on('submit',function(){
      let imageLength = $('#output-box').children('li').length;
      if(imageLength ==''){
        $('body, html').animate({ scrollTop: 0 }, 500);
        $('#error-image').text('画像がありません');
      }else if(imageLength >10){
        $('body, html').animate({ scrollTop: 0 }, 500);
        $('#error-image').text('画像を10枚以下にして下さい');
      }else{
        return false;
      }
    });

     //画像を削除した時
    $(document).on('click','.preview-image__button__delete',function(){
      let imageLength = $('#output-box').children('li').length;
      if(imageLength ==''){
        $('#error-image').text('画像がありません');
      }else if(imageLength >10){
        $('#error-image').text('画像を10枚以下にして下さい');
      }else{
        $('#error-image').text('');
      }
    });

    //商品名
    $('.sell-container__content__name').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-name').text('入力してください');
        $(this).css('border-color','red');
      }else{
        $('#error-name').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //商品説明
    $('.sell-container__content__description').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-text').text('入力してください');
        $(this).css('border-color','red');
      }else{
        $('#error-text').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //カテゴリー
    $('#category-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-category').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-category').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //状態
    $('#condition-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-condition').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-condition').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //送料負担
    $('#deliverycost-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-deliverycost').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-deliverycost').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //発送元
    $('#pref-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-pref').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-pref').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //発送までの日数
    $('#delivery_days-select').on('blur',function(){
      let value = $(this).val();
      if(value == ""){
        $('#error-delivery_days').text('選択して下さい');
        $(this).css('border-color','red');
      }else{
        $('#error-delivery_days').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

    //価格
    $('.sell-container__content__price__form__box').on('blur',function(){
      let value = $(this).val();
      if(value < 300 || value > 9999999){
        $('#error-price').text('300以上9999999以下で入力してください');
        $(this).css('border-color','red');
      }else{
        $('#error-price').text('');
        $(this).css('border-color','rgb(204, 204, 204)');
      }
    });

  });
});
items.controller.rb
def new
    @item = Item.new
    @category_parent =  Category.where("ancestry is null")
  end

  def create
    @item = Item.new(item_params)
    if @item.save
      redirect_to root_path
    else
      render :new
    end
  end



  private

  def item_params
    params.require(:item).permit(:name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :price, images: []).merge(user_id: current_user.id, boughtflg_id:"1")
  end
end
item.rb(モデル)
class Item < ApplicationRecord

  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to_active_hash :condition
  belongs_to_active_hash :pref
  belongs_to_active_hash :deliverycost
  belongs_to_active_hash :delivery_days
  belongs_to_active_hash :boughtflg
  # 上記active_hashのアソシエーション
  validate :images_presence
  validates :name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :boughtflg_id, presence: true
  validates :price, presence: true, inclusion: 300..9999999

  has_many_attached :images
  belongs_to :user, foreign_key: 'user_id'
  # optional: true後で消す belongs_toのnotnull制約解放のため使用している
  belongs_to :category

  #imageのバリデーション
  def images_presence
    if images.attached?
      # inputに保持されているimagesがあるかを確認
      if images.length > 10
        errors.add(:image, '10枚まで投稿できます')
      end
    else
      errors.add(:image, '画像がありません')
    end
  end
end

カテゴリーの2階層以降がまだ未実装でした・・・

とりあえず、後から実装するとして、先ずは画像投稿から

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

7つのアクション

今回は、コントローラで使用する7つのアクションについて紹介します!
アクションとは、ルーティングがリクエストを受け取った時に動くものです

アクション毎に行われる処理を分けて記述することによって、役割をわかりやすく分類化できます!

アクション名 どんなリクエストに対応して動く?
index 一覧ページを表示する
new 新規投稿ページを表示する
create データの投稿を行う
show 個別の詳細ページを表示する
edit 投稿編集ページを表示する
update データの編集を行う
destroy データの削除を行う

この7つのアクションは基本になる所なので、ぜひ覚えて活用できるようにしてみましょう^^!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】アクセス制限を記述する手順と、その思考手順をまとめてみました

はじめに

Railsを学習した直後のアウトプット時、いざアクセス制限機能を実装しようと試みた際に、何をどうしたらいいのか思い浮かばなかったのが悔しかったため
次に同じ轍を踏まないよう、自分用にアクセス制限の実装順序を整理してみました。

まず、アクセス制限って「何を」実装する機能なのん?

主にWebアプリケーションにおいてログイン機能を実装した後
ユーザーがログインしている場合と、ログインしていない場合とで、使える機能を制限する機能。

それじゃあ、「どうすれば」実装できるのか?

例えば
ユーザーがログインしている場合で一つ条件式を記述し、制限用の処理を記述する。
ユーザーがログインしていない場合でもう一つ条件式を書き、制限用の処理を記述する。

 if #(ユーザーがログインしている場合の条件式)
   redirect_to("URL") #制限用の処理
 end

 

コードは「どこに」記述するん?

簡単な掲示板アプリ作成を例として、「ユーザーがログインしていない場合」のアクセス制限機能を実装したいとします。

1.Railでは、主にルーティング→アクション→ビューの順でデータのやり取りが行われるので
2.例えば、ログインしていないユーザー投稿機能のアクセス制限を実装したい場合は
3.投稿機能用のアクションにデータが渡されたタイミングで、ログインしていないユーザーを弾けば良いということになりますので
4.投稿用のアクション最上部に、「ユーザーがログインしていない場合」の条件式と処理を記述してあげます。
5.そうすれば、投稿用のアクションが実行される前に、アクセス制限の処理が実行されます。

よってコードをどこに記述すればいいのかと言うと、制限を実装したい機能のアクション内の上部、ということになります。

posts_controller.rb
 def #投稿用アクション
   if #(ユーザーがログインしていない場合の条件式)
     redirect_to("URL") #指定したURLに飛ばす
   end

   #投稿用の処理

 end

before_actionとは文字通り、actionのbeforeに実行される

ただ投稿制限の他にも、投稿を閲覧する機能や投稿を編集する機能なども制限したい場合もあるかと思います。
その際、制限したいアクション全てに対し条件式を書こうとするのは、同じ記述をすることになり好ましくない上に面倒臭いです……。

そこで、全てのアクションに対して一括で処理を行う方法が存在します。

どのように全てのアクションに対し一括で処理を行うかというと、applicationコントローラに処理を記述してあげます。
applicationコントローラとは、全てのコントローラを管理している親のような存在です。
スクリーンショット 2020-03-24 19.52.54.png
画像の一番上にあるのが、applicationコントローラで
その直下に他のコントローラが羅列され、applicationコントローラの処理を継承しています。
そのため、applicationコントローラで記述された処理は、別のコントローラで実行されることになります。

ただし注意する点があり、「before_action :メソッド名」という処理をコントローラ内の上部に記述してあげる必要があります。
言葉だけでは伝わりにくいと思いますので、コード例を記述してみます。

application_controller.rb
before_action :#メソッド名

# ログインしていない場合のアクセス制限
def #メソッド名
  if  #(アクセスしたユーザーがログインしていない場合の条件式)
    redirect_to("URL") #指定したURLに飛ばす
  end
end

このようにapplicationコントローラ内に「before_action :メソッド名」と記述してあげることで、メソッド内で定義した処理が全てのコントローラのアクションが実行される前に実行されることになります。

before_actiontに関しては文字通り、全てのアクションの前に実行される処理という風に覚えておけば、用途をしっかり理解して使えるようになるかと思います。

before_actionを特定のアクションにだけ適用させる方法があるだと!?

全てのアクションへ処理が適用されるとは言っても、特定のアクションにだけアクセス制限を適用させたいという場合もあるでしょう。
そう言った場合には、特定のアクションにだけアクセス制限の処理を適用させる方法が存在します。

applicationコントローラ内で記述した「before_action :メソッド名」を
適用させたいアクションが記述されているコントローラの上部に記述した上で
メソッド名に続けて「, {only: [:アクション名, :アクション名,...]}」として
[ ]内に、各コントローラ内のアクセス制限を適用させたいアクション名を記述してあげることで、特定のアクションにだけアクセス制限を適用することができます。

posts_controller.rb
 before_action :#メソッド名, {only: [:投稿用アクション名, 投稿表示アクション名,...]}

 def #投稿用アクション名
   #処理
 end

 def #投稿表示用アクション名
   #処理
 end

こうすることにより、各々アクションが実行される前に、before_actionの処理が呼び出され、各アクションへのアクセス制限が可能となります。

まとめ

「アウトプットでアクセス制限を実装したい!」となった場合、考える順序として

1.まずアクセス制限とは、ログイン機能実装後に「ログインしているユーザーへの機能制限」や「ログインしていないユーザーへの機能制限」を行うこと。
2.「投稿を制限したい」「投稿の閲覧を制限したい」など、「どんなアクセス制限」を実装したいか思い浮かべる。
3.もし投稿機能を制限したいなら、「投稿用のアクション」など、コードを記述すべき場所を思い浮かべる。
4.該当アクション内の上部に、「ユーザーがログインしていない場合は〜〜の処理を行う」などの「条件式」を記述する。
5.他の機能も制限したいけれど、複数個に渡って同じ記述をするのは面倒臭いな……。
6.複数アクションに制限を掛けたい場合は、applicationコントローラを使えば、一つの記述で済むことを思い出す。
7.applicationコントローラ内に、条件式とアクセス制限の処理を記述する。
8.各コントローラ内のアクションが実行される前に、アクセス制限処理を実行しなければならないことを思い出す(before_action :メソッド名)。
(9.特定のアクションにアクセス制限を掛けたい場合の処理は、「 before_action :メソッド名, {only: [:アクション名, :アクション名,...]} 」を、各コントローラ内の上部に記述する。)


最後に

周りくどい説明をしてきましたが、このあたりは基礎の基礎ですし
始めはRailsそのもののデータやり取りの仕組みを理解することが重要だと思ったので、今回のように遠回りするような解説をさせて頂きました。
僕も慣れるまでは、この思考手順で学習を進めるつもりです。

参考

Progate

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】アクセス制限を記述する手順をまとめてみました!

はじめに

Railsを学習した直後のアウトプット時、いざアクセス制限機能を実装しようと試みた際に、何をどうしたらいいのか思い浮かばなかったのが悔しかったため
次に同じ轍を踏まないよう、自分用にアクセス制限の実装順序を整理してみました。

まず、アクセス制限って「何を」実装する機能なのん?

主にWebアプリケーションにおいてログイン機能を実装した後
ユーザーがログインしている場合と、ログインしていない場合とで、使える機能を制限する機能。

それじゃあ、「どうすれば」実装できるのか?

例えば
ユーザーがログインしている場合で一つ条件式を記述し、制限用の処理を記述する。
ユーザーがログインしていない場合でもう一つ条件式を書き、制限用の処理を記述する。

 if #(ユーザーがログインしている場合の条件式)
   redirect_to("URL") #制限用の処理
 end

 

コードは「どこに」記述するん?

簡単な掲示板アプリ作成を例として、「ユーザーがログインしていない場合」のアクセス制限機能を実装したいとします。

1.Railsのデータのやり取りは、基本的にルーティング→アクション→ビューの順で行われ
2.例えば、ログインしていないユーザー投稿機能のアクセス制限を掛けたい場合は
3.投稿用のアクション最上部に、「ユーザーがログインしていない場合」の条件式と処理を記述してあげます。
4.そうすれば、投稿用のアクションが実行される前に、アクセス制限の処理が実行されます。

なので、位置的に言えばアクション内の上部に書けばいいということなのですね。

posts_controller.rb
 def #投稿用アクション
   if #(ユーザーがログインしていない場合の条件式)
     redirect_to("URL") #指定したURLに飛ばす
   end

   #投稿用の処理

 end

before_actionとは文字通り、actionのbeforeに実行される

ただ投稿制限の他にも、投稿を閲覧する機能や投稿を編集する機能なども制限したい場合もあるかと思います。
その際、制限したいアクション全てに対し条件式を書こうとするのは、同じ記述をすることになり好ましくない上に面倒臭いです……。

そこで、全てのアクションに対して一括で処理を行う方法が存在します。

どのように全てのアクションに対し一括で処理を行うかというと、applicationコントローラに処理を記述してあげます。
applicationコントローラとは、全てのコントローラを管理している親のような存在です。
スクリーンショット 2020-03-24 19.52.54.png
画像の一番上にあるのが、applicationコントローラで
その直下に他のコントローラが羅列され、applicationコントローラの処理を継承しています。
そのため、applicationコントローラで記述された処理は、別のコントローラで実行されることになります。

ただし注意する点があり、「before_action :メソッド名」という処理をコントローラ内の上部に記述してあげる必要があります。
言葉だけでは伝わりにくいと思いますので、コード例を記述してみます。

application_controller.rb
before_action :#メソッド名

# ログインしていない場合のアクセス制限
def #メソッド名
  if  #(アクセスしたユーザーがログインしていない場合の条件式)
    redirect_to("URL") #指定したURLに飛ばす
  end
end

このようにapplicationコントローラ内に「before_action :メソッド名」と記述してあげることで、メソッド内で定義した処理が全てのコントローラのアクションが実行される前に実行されることになります。

before_actiontに関しては文字通り、全てのアクションの前に実行される処理という風に覚えておけば、用途をしっかり理解して使えるようになるかと思います。

before_actionを特定のアクションにだけ適用させる方法があるだと!?

全てのアクションへ処理が適用されるとは言っても、特定のアクションにだけアクセス制限を適用させたいという場合もあるでしょう。
そう言った場合には、特定のアクションにだけアクセス制限の処理を適用させる方法が存在します。

applicationコントローラ内で記述した「before_action :メソッド名」を
適用させたいアクションが記述されているコントローラの上部に記述した上で
メソッド名に続けて「, {only: [:アクション名, :アクション名,...]}」として
[ ]内に、各コントローラ内のアクセス制限を適用させたいアクション名を記述してあげることで、特定のアクションにだけアクセス制限を適用することができます。

posts_controller.rb
 before_action :#メソッド名, {only: [:投稿用アクション名, 投稿表示アクション名,...]}

 def #投稿用アクション名
   #処理
 end

 def #投稿表示用アクション名
   #処理
 end

こうすることにより、各々アクションが実行される前に、before_actionの処理が呼び出され、各アクションへのアクセス制限が可能となります。

まとめ

「アウトプットでアクセス制限を実装したい!」となった場合、考える順序として

1.まずアクセス制限とは、ログイン機能実装後に「ログインしているユーザーへの機能制限」や「ログインしていないユーザーへの機能制限」を行うこと。
2.「投稿を制限したい」「投稿の閲覧を制限したい」など、「どんなアクセス制限」を実装したいか思い浮かべる。
3.例えば、投稿を制限したいなら「投稿用のアクション」など、コードを記述すべき場所を押さえる。
4.該当アクション内に、「ユーザーがログインしていない場合は〜〜の処理を行う」などの「条件式」を記述する。
5.ただし複数個に渡って同じ記述をするのは面倒臭い……。
6.複数アクションに制限を掛けたい場合は、applicationコントローラを使えば、一つの記述で済むことを思い出す。
7.applicationコントローラ内に、条件式とアクセス制限の処理を記述する。
8.各コントローラ内のアクションが実行される前に、アクセス制限処理を実行しなければならないことを思い出す(before_action :メソッド名)。
(9.特定のアクションにアクセス制限を掛けたい場合の処理は、「 before_action :メソッド名, {only: [:アクション名, :アクション名,...]} 」を、各コントローラ内の上部に記述する。)


参考

Progate

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[WIP]RuboCop規約一覧表

一覧

区分 規約 Cop数
Bundler Bundlerファイル 4
Gemspec gemspecファイル 4
Layout インデント、行端揃え、スペースの一貫性 88
Lint 曖昧性と潜在エラー 82
Metrics プロパティ 9
Migration rubocop.yml内のCop名 1
Naming 命名規則 16
Security メソッド呼び出し、構造に潜むセキュリティホール 5
Style Ruby Style Guideに準拠したスタイルの一貫性 172

Bundler

Cop名 解析対象 デフォルト
Bundler/DuplicatedGem Gemfile内のライブラリが重複していないか 有効
Bundler/GemComment Gemfile内のライブラリ用途を説明するコメントがあるか 無効
Bundler/InsecureProtocolSource ・シンボル引数の:gemcutter:rubygems:rubyforge が使われていないか
・ソースURLに https://rubygems.org が指定されているか
有効
Bundler/OrderedGems Gemfile内のライブラリがアルファベット昇順でソートされているか 有効

Gemspec

Cop名 解析対象 デフォルト
Gemspec/DuplicatedAssignment gemspec内の属性定義メソッド呼出しが重複していないか 有効
Gemspec/OrderedDependencies gemspec内の依存関係がアルファベット昇順でソートされているか 有効
Gemspec/RequiredRubyVersion 以下ファイル内の指定Rubyバージョンが互いに一致しているか
・gemspec: required_ruby_version
・.rubocop.yml: TargetRubyVersion
有効
Gemspec/RubyVersionGlobalsUsage gemspec内で定数 RUBY_VERSION が使われていないか 有効

Layout

Cop名 解析対象 デフォルト
Layout/AccessModifierIndentation 単体のアクセス修飾子(e.g. private)のインデントの深さが defclassmodule と同等か 有効
Layout/ArgumentAlignment 複数行メソッド呼出しにおける引数が統一されたインデントがされているか 有効
Layout/ArrayAlignment 複数行配列の要素が統一されたインデントがされているか 有効
Layout/AssignmentIndentation 代入式における複数行右辺の一行目が統一されたインデントがされているか 有効
Layout/BlockAlignment 複数行ブロックの do ~ end が統一されたインデントがされているか 有効
Layout/BlockEndNewline 複数行ブロックの do ~ end{ ~ } が独立した行に書かれているか 有効
Layout/CaseIndentation ・case文における case ~ when ~ end が統一されたインデントがされているか
・代入式における右辺のcase文の揃い: デフォルトは EnforcedStyle: case
有効
Layout/ClassStructure クラススコープ(class ~ end)内の要素が以下の構造に準拠しているか
・moduleのミックスイン: includeprependextend
・定数定義
・クラス関係定義: belongs_tohas_onehas_many
・クラスマクロ: attr_accessorattr_writerattr_reader
・検証等その他マクロ: validatesvalidate
・Publicクラスメソッド
・Initializer
・Publicインスタンスメソッド
・Protectedクラスマクロ: attr_accessorattr_writerattr_reader
・Protectedインスタンスメソッド
・Privateクラスマクロ: attr_accessorattr_writerattr_reader
・Privateインスタンスメソッド
無効
Layout/ClosingHeredocIndentation ヒアドキュメントの識別子の揃いと、閉じ識別子が内部要素のより内側にあるか 有効
Layout/ClosingParenthesisIndentation 複数行のメソッド定義、メソッド呼出し、グループにおける閉じ括弧 ) が統一されたインデントがされているか 有効
Layout/CommentIndentation コメント行が統一されたインデントがされているか 有効
Layout/ConditionPosition 条件文の判断条件が if,  whileuntil と同じ行に書かれているか 有効
Layout/DefEndAlignment メソッド定義スコープ(def ~ end)における end が統一されたインデントがされているか
※ デフォルトは EnforcedStyleAlignWith: start_of_line
有効
Layout/DotPosition 複数行におけるレシーバーとメソッドの区切り . が統一されたインデントがされているか
※ デフォルトは EnforcedStyle: leading
有効
Layout/ElseAlignment 条件文における else が if, whileuntil と揃っているか 有効
Layout/EmptyComment 空のコメント行が統一されたインデントがされているか
※ デフォルトは AllowBorderComment: trueAllowMarginComment: true
有効
Layout/EmptyLineAfterGuardClause ガード構文の直後に空行が存在するか 有効
Layout/EmptyLineAfterMagicComment マジックコメント最終行の直後に空行が存在するか 有効
Layout/EmptyLineBetweenDefs メソッド定義スコープ(def ~ end)間に空行が存在しないか 有効
Layout/EmptyLines 二行以上連続した空行が存在しないか 有効
Layout/EmptyLinesAroundAccessModifier アクセス修飾子(e.g. private)前後に空行が存在するか 有効
Layout/EmptyLinesAroundArguments 複数行メソッド呼出しの引数に空行が存在しないか 有効
Layout/EmptyLinesAroundBeginBody begin ~ end スコープ内に空行が存在しないか 有効
Layout/EmptyLinesAroundBlockBody ブロック内に空行が存在しないか 有効
Layout/EmptyLinesAroundClassBody クラススコープ(class ~ end)内に空行が存在しないか
※ デフォルトは EnforcedStyle: no_empty_lines
有効
Layout/EmptyLinesAroundExceptionHandlingKeywords 例外処理スコープ(begin ~ rescue ~ else ~ ensure ~ end)内に空行が存在しないか 有効
Layout/EmptyLinesAroundMethodBody メソッド定義スコープ(def ~ end)内に空行が存在しないか 有効
Layout/EmptyLinesAroundModuleBody モジュールスコープ(module ~ end)内に空行が存在しないか
※ デフォルトは EnforcedStyle: no_empty_lines
有効
Layout/EndAlignment スコープの end が統一されたインデントがされているか
※ デフォルトは EnforcedStyleAlignWith: keyword
有効
Layout/EndOfLine WindowsOSにおける改行コード
※ デフォルトは EnforcedStyle: native(WindowsOS: CR+LF / その他OS: LF)
有効
Layout/ExtraSpacing 無意味なスペースが存在しないか
※ デフォルトは AllowForAlignment: trueAllowBeforeTrailingComments: falseForceEqualSignAlignment: false
有効
Layout/FirstArgumentIndentation メソッド呼出しの第一引数にインデントがされているか
※ デフォルトは EnforcedStyle: special_for_inner_method_call_in_parentheses
有効
Layout/FirstArrayElementIndentation 複数行配列において、[ と第一要素が別々の行にある場合の第一要素にインデントがされているか
※ デフォルトは EnforcedStyle: special_inside_parentheses
有効
Layout/FirstArrayElementLineBreak 複数行配列における第一要素の前に改行がされているか 無効
Layout/FirstHashElementIndentation 複数行ハッシュにおける最初のキーにインデントがされているか
※ デフォルトは EnforcedStyle: special_inside_parentheses
有効
Layout/FirstHashElementLineBreak 複数行ハッシュにおける最初のキーの前に改行がされているか 無効
Layout/FirstMethodArgumentLineBreak メソッド呼出しの第一引数の前に改行がされているか 無効
Layout/FirstMethodParameterLineBreak メソッド定義の最初のパラメーターの前に改行がされているか 無効
Layout/FirstParameterIndentation メソッド定義の最初のパラメーターにインデントがされているか
※ デフォルトは EnforcedStyle: consistent
有効
Layout/HashAlignment 複数行ハッシュのキー、セパレーター、値にインデントがされているか
・ハッシュロケット / コンマ: デフォルトは EnforcedHashRocketStyle: keyEnforcedColonStyle: key
・メソッド呼出しの最後の引数としての扱い: デフォルトは EnforcedLastArgumentHashStyle: always_inspect
有効
Layout/HeredocArgumentClosingParenthesis メソッド呼出しの引数としてのヒアドキュメントの閉じ識別子にインデントがされているか 無効
Layout/HeredocIndentation ヒアドキュメント内部にインデントがされているか
※ デフォルトは EnforcedStyle: squiggly
有効
Layout/IndentationConsistency インデントに一貫性があるか
※ デフォルトは EnforcedStyle: normal
有効
Layout/IndentationWidth インデントにおける半角スペースの数が正しいか
※ デフォルトは半角スペース2つ
有効
Layout/InitialIndentation ファイル内の非ブロック、非コメントの最初の行にインデントがされているか 有効
Layout/LeadingCommentSpace コメントの # 識別子の直後に半角スペースが存在するか 有効
Layout/LeadingEmptyLines ファイル一行目に空行が存在しないか 有効
Layout/LineLength 一行あたりの文字数は多すぎないか 有効
Layout/MultilineArrayBraceLayout 複数行の括弧における (]} にインデントがされているか
※ デフォルトは EnforcedStyle: symmetrical
有効
Layout/MultilineArrayLineBreaks 複数行配列のそれぞれの要素が改行によって独立しているか 有効
Layout/MultilineAssignmentLayout 複数行の右辺を持つ代入式における、演算子の後に改行がされているか 無効
Layout/MultilineBlockLayout do ~ end{ ~ } ブロックのブロック引数と改行
・ブロック引数: ブロックの最初の行の do または { の後
・改行: ブロックの最初の行の do{ またはブロック引数の後
有効
Layout/MultilineHashBraceLayout 複数行ハッシュの {} にインデントがされているか
※ デフォルトは EnforcedStyle: symmetrical
有効
Layout/MultilineHashKeyLineBreaks 複数行ハッシュのキー: 値がそれぞれ改行によって独立しているか 無効
Layout/MultilineMethodArgumentLineBreaks メソッド呼出しの複数行の引数がそれぞれ改行によって独立しているか 無効
Layout/MultilineMethodCallBraceLayout 複数行メソッドの呼出しの閉じ括弧 ) にインデントがされているか
※ デフォルトは EnforcedStyle: symmetrical
有効
Layout/MultilineMethodCallIndentation .メソッド名 にインデントがされているか
※ デフォルトは EnforcedStyle: aligned
有効
Layout/MultilineMethodDefinitionBraceLayout メソッド定義の複数行のパラメーターにおける閉じ括弧 ) にインデントがされているか
※ デフォルトは EnforcedStyle: symmetrical
有効
Layout/MultilineOperationIndentation 複数行の演算式にインデントがされているか
※ デフォルトは EnforcedStyle: aligned
有効
Layout/ParameterAlignment 複数行のメソッド定義、メソッド呼出しにインデントがされているか
※ デフォルトは EnforcedStyle: with_first_parameter
有効
Layout/RescueEnsureAlignment 例外処理スコープ内の rescueensure にインデントがされているか 有効
Layout/SpaceAfterColon コロン(:)の直後に半角スペースが存在するか 有効
Layout/SpaceAfterComma コンマ(,)の直後に半角スペースが存在するか 有効
Layout/SpaceAfterMethodName メソッド名の直後に半角スペースが存在するか 有効
Layout/SpaceAfterNot 否定演算子(!)の直後に半角スペースが存在しないか 有効
Layout/SpaceAfterSemicolon セミコロン(;)の直後に半角スペースが存在するか 有効
Layout/SpaceAroundBlockParameters ブロック引数前後に半角スペースが存在するか 有効
Layout/SpaceAroundEqualsInParameterDefault メソッド定義のデフォルト引数における = 演算子の前後に半角スペースが存在するか 有効
Layout/SpaceAroundKeyword doif 等キーワード前後に半角スペースが存在するか 有効
Layout/SpaceAroundOperators 演算子前後に半角スペースが存在するか
※ デフォルトは AllowForAlignment: true
有効
Layout/SpaceBeforeBlockBraces ブロックの始まりの { 直前に半角スペースが存在するか
※ デフォルトは EnforcedStyle: space
有効
Layout/SpaceBeforeComma コンマ(,)の直前に半角スペースが存在しないか 有効
Layout/SpaceBeforeComment コメントの直線に半角スペースが存在するか 有効
Layout/SpaceBeforeFirstArg () のないメソッド呼出しで、メソッド名と引数の間に連続して半角スペースが複数存在しないか 有効
Layout/SpaceBeforeSemicolon セミコロン(;)の直前に半角スペースが存在しないか 有効
Layout/SpaceInLambdaLiteral lambdaの引数の ( の直後に半角スペースが存在しないか
※ デフォルトは EnforcedStyle: require_no_space
有効
Layout/SpaceInsideArrayLiteralBrackets 配列の [ の直後と ] の直前に半角スペースが存在しないか
※ デフォルトは EnforcedStyle: no_space
有効
Layout/SpaceInsideArrayPercentLiteral %記法の () 内に連続して半角スペースが複数存在しないか 有効
Layout/SpaceInsideBlockBraces ブロックの { の直後と } の直前に半角スペースが存在するか
※ デフォルトは EnforcedStyle: space
有効
Layout/SpaceInsideHashLiteralBraces ハッシュの { の直後と } の直前に半角スペースが存在するか
※ デフォルトは EnforcedStyle: space
有効
Layout/SpaceInsideParens ( の直後と ) の直前に半角スペースが存在しないか
※ デフォルトは EnforcedStyle: no_space
有効
Layout/SpaceInsidePercentLiteralDelimiters %記法の ( の直後と ) の直前に半角スペースが存在しないか 有効
Layout/SpaceInsideRangeLiteral Rangeの ..... の前後に半角スペースが存在しないか 有効
Layout/SpaceInsideReferenceBrackets ハッシュのキー取得における [ の直後と ] の直前に半角スペースが存在しないか
※ デフォルトは EnforcedStyle: no_space
有効
Layout/SpaceInsideStringInterpolation 式展開の { の直後と } の直前に半角スペースが存在しないか
※ デフォルトは EnforcedStyle: no_space
有効
Layout/Tab インデントにタブが使われていないか 有効
Layout/TrailingEmptyLines ファイルの最後に空行が存在するか 有効
Layout/TrailingWhitespace 無意味な半角スペースが存在しないか 有効

Lint

Cop名 解析対象 デフォルト
Lint/AmbiguousBlockAssociation メソッド呼び出しの引数に () がないことによって、ブロックの結合度が不明確になっていないか 有効
Lint/AmbiguousOperator メソッド呼び出しの第一引数に () がないことによって、演算子の意味が不明確になっていないか 有効
Lint/AmbiguousRegexpLiteral メソッド呼び出しの第一引数に () がないことによって、正規表現のエスケープシーケンスの意味が不明確になっていないか 有効
Lint/AssignmentInCondition if,  whileuntil などの条件文において、判断条件の代入式の右辺 true を代入するときに () が使われているか
※ デフォルトは AllowSafeAssignment: true
有効
Lint/BigDecimalNew 非推奨の BigDecimal.new() が使われていないか 有効
Lint/BooleanSymbol boolean型がシンボルで使われていないか(e.g. :true:false) 有効
Lint/CircularArgumentReference 調査中 有効
Lint/Debugger 開発環境以外(テスト環境、本番環境)でデバッグ用のメソッド(pry, byebug)が使われていないか 有効
Lint/DeprecatedClassMethods 非推奨のクラスメソッド(File.exists?Dir.exists?iterator?)が使われていないか 有効
Lint/DisjunctiveAssignmentInConstructor initializerで、インスタンス変数の代入に `\ =` 演算子が使われていないか
Lint/DuplicateCaseCondition case文において、when句の判断条件が重複していないか 有効
Lint/DuplicateHashKey ハッシュのキーが重複していないか 有効
Lint/DuplicateMethods メソッド定義が重複していないか 有効
Lint/EachWithObjectArgument Enumerable#each_with_object において、引数にイミュータブルな値が使われていないか
※ Enumerable#each_with_object の引数は、当該メソッドが可算集合対して行うループ処理に基づいてあるデータを作るために与えられたブロックから呼び出されるオブジェクトであるので、イミュータブルな引数は意味をなさない上、これは明らかなバグである。
有効
Lint/ElseLayout if,  whileuntil などの条件文において、else 句に条件判断が書かれていないか 有効
Lint/EmptyEnsure 例外処理において、ensure 句に処理が書かれているか 有効
Lint/EmptyExpression 変数や条件判断に空値が使われていないか 有効
Lint/EmptyInterpolation 空の式展開が使われていないか 有効
Lint/EmptyWhen case文の when句に処理が書かれているか 有効
Lint/EndInMethod 調査中 有効
Lint/EnsureReturn 例外処理において、ensure 句で return が使われていないか
※ return を明記するとコントロールフローが変わる。理由は以下の2つ。
raise された例外より優先して return が実行されるため
rescue されたが如く、キャッチした例外は通知なしに放棄される
有効
Lint/ErbNewArguments 調査中 有効
Lint/FlipFlop flip-flop演算子が含まれていないか
※ Ruby 2.6.0 ~ 非推奨
有効
Lint/FloatOutOfRange floatの数値が大きすぎないか 有効
Lint/FormatParameterMismatch Kernel#format メソッド呼出しで、渡される引数の数とデータ型が一致しているか 有効
Lint/HeredocMethodCallPosition ヒアドキュメントのメソッド呼出しにおいて、レシーバーが呼び出される順序づけが正しいか 有効
Lint/ImplicitStringConcatenation 同じ行の文字列結合が正しく行われているか 有効
Lint/IneffectiveAccessModifier アクセス修飾子(e.g. protectedprivate)が特異メソッドの定義に使われていないか
※ 特異メソッドの定義には private_class_method を使う
有効
Lint/InheritException 継承する例外のスーパークラスが正しく指定されているか
※ デフォルトは EnforcedStyle: runtime_error
有効
Lint/InterpolationCheck シングルクオーテーション内で式展開が行われていないか 有効
Lint/LiteralAsCondition if,  whileuntil などの条件文において、&&, || の中で判断条件や演算対象として使われているリテラルが正しく使われているか 有効
Lint/LiteralInInterpolation 変数やメソッド呼出し以外で不要な式展開がされていないか 有効
Lint/Loop begin ~ enduntilwhile 等でループ処理が正しく書かれているか 有効
Lint/MissingCopEnableDirective copを一部無効にする際、# rubocop:disable ... の後に # rubocop:enable ... が書かれて有効な状態に戻されているか 有効
Lint/MultipleComparison x < y < z のような、3つ以上の比較演算を行っている式が存在しないか
※ Rubyでは使用不可
有効
Lint/NestedMethodDefinition メソッド定義がネストされていないか 有効
Lint/NestedPercentLiteral %記法がネストされていないか 有効
Lint/NextWithoutAccumulator reduce ブロック内である条件でループ処理をスキップする際、累算器が next メソッド呼出しの引数に取られているか 有効
Lint/NonDeterministicRequireOrder Dir[...]Dir.glob(...) で取得したファイルを格納した配列がソートされているか
※ 上記メソッドで取得したファイルの順番はOSやファイルシステムに依存するため、アルファベット昇順とは限らないため
有効
Lint/NonLocalExitFromIterator 調査中 有効
Lint/NumberConversion 安全でない数値変換が行われていないか
to_ito_fto_c メソッドが使われていないか
Integer()Float()Complex() が使われているか
無効
Lint/OrderedMagicComments マジックコメントが正しい順序で書かれており、#! がマジックコメントの先頭に書かれているか 有効
Lint/ParenthesesAsGroupedExpression 引数ありのメソッド呼出しと ( の間に半角スペースが存在しないか 有効
Lint/PercentStringArray 文字列配列の %w 記法において、クオーテーションやコンマが使われていないか 有効
Lint/PercentSymbolArray シンボル配列の %i 記法において、コロン(:)やコンマが使われていないか 有効
Lint/RandOne rand(1) が呼び出されていないか 有効
Lint/RedundantCopDisableDirective 省略できる rubocop:disable コメントが存在しないか 有効
Lint/RedundantCopEnableDirective 省略できる rubocop:enable コメントが存在しないか 有効
Lint/RedundantRequireStatement 無意味な require が存在しないか 有効
Lint/RedundantSplatExpansion 配列展開の splat(*)が無意味に使われていないか 有効
Lint/RedundantStringCoercion 式展開で文字列変換(to_s)がされていないか 有効
Lint/RedundantWithIndex 無意味な with_index メソッドが使われていないか 有効
Lint/RedundantWithObject 無意味な with_object メソッドが使われていないか 有効
Lint/RegexpAsCondition 条件文の判断条件で match-current-line として正規表現が使われているか 有効
Lint/RequireParentheses 以下の条件に合致する場合に引数が () で囲まれているか
・少なくとも一つ以上の引数があるメソッド呼出しが並列されている
・並列されているメソッド呼出しの引数がいずれも () で囲まれていない
・並列されている最後のメソッド呼出しの前に &&, || がある
有効
Lint/RescueException 例外処理における rescue のブロックの中で、最上位の Exception クラスを指定していないか 有効
Lint/RescueType 例外が発生した時に rescue の引数が TypeError を発生させるものではないか 有効
Lint/ReturnInVoidContext return メソッドの引数の値に関して、それが無視される文脈(initializer やセッター)で引数に取られていないか 有効
Lint/SafeNavigationChain NoMethodError の例外が発生しないようにnilガードが適切にチェーンされているか 有効
Lint/SafeNavigationConsistency &&, || などの論理演算子でnilガードを使う場合において、両辺で同じオブジェクトを返す書き方がされているか
※ デフォルトは AllowedMethods: [present?, blank?, presence, try, try!]
有効
Lint/SafeNavigationWithEmpty 条件式の中でnilガードの後に empty? メソッドがチェーンされていないか
nil.empty? => NoMethodError
nil&.empty? => nil
有効
Lint/ScriptPermission #! から始まるマジックコメントが一行目に書かれているファイルが実行権限を有しているか 有効
Lint/SendWithMixinArgument モジュールのミックスインにおいて、プライベートメソッド呼出しの sendpublic_send__send__ が使われていないか
※ Ruby2.0まで includeprepend はプライベートメソッドであったが、Ruby2.1以降はパブリックメソッドである。extend は元々パブリックメソッドである。
有効
Lint/ShadowedArgument 引数のシャドーイングがなされていないか
※ デフォルトは IgnoreImplicitReferences: false
有効
Lint/ShadowedException 例外処理において、抽象度の高い例外クラスが具体性の高い例外クラスに先んじて例外を補足していないか 有効
Lint/ShadowingOuterLocalVariable ブロック引数がスコープ外のローカル変数と同名であることによってシャドーイングがなされていないか 有効
Lint/SuppressedException rescue ブロック内に処理が書かれているか
※ デフォルトは AllowComments: false
有効
Lint/Syntax 厳密にはcopではなく、診断情報やエラーをRubocopの規約違反に組み入れ直すメソッド群を提供する 有効
Lint/ToJSON to_json メソッドがオプション引数を取っているか 有効
Lint/UnderscorePrefixedVariableName アンダースコア付ブロック変数が内部処理に使われていないか
※ デフォルトは AllowKeywordBlockArguments: false
有効
Lint/UnifiedInteger FixnumBignum 等の非推奨の定数が使われていないか 有効
Lint/UnreachableCode return でメソッドを抜けた後に処理されないロジックが存在しないか 有効
Lint/UnusedBlockArgument 利用されていないブロック引数が存在しないか
※ デフォルトは IgnoreEmptyBlocks: trueAllowUnusedKeywordArguments: false
有効
Lint/UnusedMethodArgument 利用されていないメソッド引数が存在しないか
※ デフォルトは AllowUnusedKeywordArguments: falseIgnoreEmptyMethods: trueIgnoreNotImplementedMethods: true
有効
Lint/UriEscapeUnescape 非推奨のメソッドがユースケースに応じて推奨のメソッドに代替出来るか
URI.escape => CGI.escapeURI.encode_www_formURI.encode_www_form_component
URI.unescape => CGI.unescapeURI.decode_www_formURI.decode_www_form_component
有効
Lint/UriRegexp URI.regexp が使われていないか
URI::DEFAULT_PARSER.make_regexp が使われているか
有効
Lint/UselessAccessModifier 無意味に publicprivate などのアクセサが使われていないか 有効
Lint/UselessAssignment 使われていないローカル変数が存在しないか 有効
Lint/UselessComparison 同一のオブジェクトが比較演算子の両辺で使われていないか 有効
Lint/UselessElseWithoutRescue 例外処理において、begin ~ end の中で rescue 句なしで else が使われていないか 有効
Lint/UselessSetterCall セッターメソッドの中で、ローカル変数の値が最終行で返されているか
※ ローカルスコープ外からアクセス出来る値をローカル変数が参照するエッジケースが存在する。copには検出されず、誤判定が生じる可能性がある。
有効
Lint/Void 調査中
※ デフォルトは CheckForMethodsWithNoSideEffects: false
有効

Metrics

Cop名 解析対象 デフォルト
Metrics/AbcSize 算出されたABC(代入式、メソッド呼出し、条件式)の大きさが最大値を超えていないか
※ デフォルトは Max: 15
有効
Metrics/BlockLength ブロックの行数が最大値を超えていないか
※ デフォルトは以下の通り
CountComments: false
Max: 25
ExcludedMethods: [refine]
Exclude: [**/*.gemspec]
有効
Metrics/BlockNesting 条件式や繰返し処理等のブロック内の入れ子の数が最大値を超えていないか
※ デフォルトは以下の通り
CountBlocks: false
Max: 3
有効
Metrics/ClassLength クラスあたりの行数が最大値を超えていないか
※ デフォルトは以下の通り
CountComments: false
Max: 100
有効
Metrics/CyclomaticComplexity 循環的複雑度の値が最大値を超えていないか
※ デフォルトは Max: 6
有効
Metrics/MethodLength メソッド定義の行数が最大値を超えていないか
※ デフォルトは以下の通り
CountComments: false
Max: 10
ExcludedMethods: []
有効
Metrics/ModuleLength モジュールあたりの行数が最大値を超えていないか
※ デフォルトは以下の通り
CountComments: false
Max: 100
有効
Metrics/ParameterLists メソッド定義のパラメーターの数が最大値を超えていないか
※ デフォルトは以下の通り
Max: 5
CountKeywordArgs: true
有効
Metrics/PerceivedComplexity コードの複雑度が最大値を超えていないか
※ デフォルトは Max: 7
有効

Migration

Cop名 解析対象 デフォルト
Migration/DepartmentName rubocop:disable内のコメントでcop名が区分名を伴っているか 有効

Naming

Cop名 解析対象 デフォルト
Naming/AccessorMethodName アクセサのメソッド名が適切に命名されているか 有効
Naming/AsciiIdentifiers メソッド名や変数名などの識別名に非ASCII文字が使われていないか 有効
Naming/BinaryOperatorParameterName 二項演算子メソッド定義のパラメーターの引数名が other になっているか 有効
Naming/BlockParameterName ブロックパラメーターは意味のある命名がされているか
※ デフォルトは以下の通り
MinNameLength: 1
AllowNamesEndingInNumbers: true
AllowedNames: []
ForbiddenNames: []
有効
Naming/ClassAndModuleCamelCase クラス名とモジュール名がキャメルケースで統一されているか 有効
Naming/ConstantName 定数はスネークケースかつ大文字のみで構成されているか 有効
Naming/FileName ファイル名はスネークケースで統一されているか
※ デフォルトは以下の通り
Exclude: []
ExpectMatchingDefinition: false
Regex: <none>
IgnoreExecutableScripts: true
AllowedAcronyms: [CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS]
有効
Naming/HeredocDelimiterCase ヒアドキュメントの識別子は大文字のみで構成されているか
※ デフォルトは EnforcedStyle: uppercase
有効
Naming/HeredocDelimiterNaming ヒアドキュメントの識別子は意味のある命名がされているか
※ デフォルトは ForbiddenDelimiters:(?-mix:(^ 。また、デフォルトで END と EO*` の使用は不許可。
有効
Naming/MemoizedInstanceVariableName メモ化インスタンスメソッド名とスコープ内のインスタンス変数名が一致しているか
※ デフォルトは EnforcedStyleForLeadingUnderscores: disallowed
有効
Naming/MethodName メソッド名が決められた形式(スネークケース/キャメルケース)で命名されているか
※ デフォルトは EnforcedStyle: snake_case
有効
Naming/MethodParameterName メソッド定義のパラメーターは意味のある命名がされているか
※ デフォルトは以下の通り
MinNameLength: 3
AllowNamesEndingInNumbers: true
AllowedNames: [io, id, to, by, on, in, at, ip, db, os, pp]
ForbiddenNames: []
有効
Naming/PredicateName true/false を返す叙述メソッドが適切な命名をされているか
※ デフォルトは以下の通り
NamePrefix: [is_, has_, have_]
ForbiddenPrefixes: [is_, has_, have_]
AllowedMethods: [is_a?]
MethodDefinitionMacros: [define_method, define_singleton_method]
Exclude: [spec/**/*]
有効
Naming/RescuedExceptionsVariableName 例外処理において、例外を代入する変数が適切な命名がされているか
※ デフォルトは PreferredName: e
有効
Naming/VariableName 変数名が決められた形式(スネークケース/キャメルケース)で命名されているか
※ デフォルトは EnforcedStyle: snake_case
有効
Naming/VariableNumber アラビア数字を含む変数名における書式が正しいか
※ デフォルトは EnforcedStyle: normalcase
有効

Security

Cop名 検査対象 デフォルト
Security/Eval 非推奨の Kernel#eval や Binding#eval が使われていないか 有効
Security/JSONLoad 潜在的なセキュリティ問題を孕む JSON クラスメソッド が使われていないか 有効
Security/MarshalLoad 信頼していないソースコードを読み込んだ際に遠隔でのコード実行に繋がる潜在的なセキュリティ問題を孕む Marshal クラスメソッド が使われていないか 有効
Security/Open Kernel#open が危険な使われ方をされていないか
※ Kernel#open はファイルアクセスだけでなく、パイプシンボル(|)を前に置くことでプロセス呼出しも可能にする。よって、引数に取る変数によってな重大なセキュリティリスクを孕む
※ File.openIO.popenURI#open を使う方が安全である
有効
Security/YAMLLoad 信頼していないソースコードを読み込んだ際に遠隔でのコード実行に繋がる潜在的なセキュリティ問題を孕む YAML クラスメソッド が使われていないか 有効

Style

作業中

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 投稿機能

Railsで投稿機能の作成です:point_up:
部分的な備忘録ですので分かりづらいと思います

railsアプリケーション作成

$ rails _5.2.3_ new sample_boad -d mysql

Gem追加

bundle installとbundle updateを実行

gem 'pry-rails'
gem 'compass-rails', '3.1.0'
gem 'sprockets', '3.7.2'

データベース作成

$ rake db:create

モデル作成

$ rails g model tweet

テーブル作成

マイグレーションファイルはテーブルの設計図のようなものです。
t.の後に続いている記述は、どんなデータが入るのかを示す型です。

srting 文字(少なめ)
text 文字(多め)
integer 数字
などがあります。

timespasはcreated_at(作成時間) や updated_at(更新時間) を定義する時に使用します。
:nameや:textはカラム名です。

この場合、nameカラム(ユーザー名)をstring型(ユーザー名は文字少なめ)、textカラム(投稿された文章)をtext型(文章だから文字多め)、imageカラム(画像)をstring型としています。

2020XXXXXXXXXXXX_create_tweets.rb
class CreateTweets < ActiveRecord::Migration[5.2]
  def change
    create_table :tweets do |t|
      t.string :name
      t.text   :text
      t.string   :image
      t.timestamps null: true
    end
  end
end

rake db:migrateでマイグレーションファイルを実行

ルーティング

投稿一覧を表示するindexアクション 投稿画面を作成するためにnewアクション 投稿された内容を保存するためにcreateアクションを使います。

route.rb
Rails.application.routes.draw do
  resources :tweets only: [:index,:new,:create]
end

コントローラー

private以下はストロングパラメーターです。
ビューでフォームに入力された情報は、コントローラにキーと一緒にパラメーターとして送られます。
ストロングパラメーターは、指定したキーを持つパラメーターのみを受け取るようにするものです。
不正な情報を送信しようとしたときに、ストロングパラメーターを設定しておくと、不正な情報を受け取らずにすみます。

$ rails g controller tweets
tweets.controller.rb
class TweetsController < ApplicationController

  def index
    @tweets = Tweet.order("created_at DESC").page(params[:page]).per(5)
  end

  def new
  end

  def create
    Tweet.create(tweet_params)
  end

  private
  def tweet_params
    params.permit(:name,:image,:text)
  end
end

Tweet.order("created_at DESC")は、投稿された内容を降順に並び替えて表示させています。
つまり、新しい投稿ほど上に表示されます。
page(params[:page]).per(5)はページネーションです。説明は省略します。Gemはkaminariを使用しています。

ビュー作成

投稿一覧
index.html
<html>
<head>
  <title> Sample boad</title>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>

<body>
<header class="header">
  <h1>掲示板</h1>
  <%= link_to "投稿する", "/tweets/new" ,class: "send_btn"%>
</header>
<div class="comment">
  <% @tweets.each do |tweet|%>
  <div class="comment-list">
    <div class="title">
      <%= tweet.title%>
    </div>
    <div class="comments">
      <%= tweet.text %>
    </div>
  </div>
   <% end %>
</div>
<%= paginate(@tweets)%>
</div>
</body>
</html>
投稿完了画面
create.html
<div class="contents row">
  <div class="succes">
    <h3>
    投稿が完了しました
    </h3>
    <a class="btn" href="/tweets">投稿一覧へ戻る</a>
  </div>
</div>
投稿画面

7〜9行目に:name :image :textの記述があります。
これが、コントローラーに送信されるキーを表しています。ユーザーに入力された情報はキーと一緒にパラメーターとして送られます。
コントローラーでストロングパラメーターを使っているので、:userなど指定されていないキーは受け取られません。

new.html
<div class="contents row">
  <div class="container">
    <%= form_with(model: @tweet, local: true) do |form| %>
      <h3>
        投稿する
      </h3>
      <%= form.text_field :name, placeholder: "Nickname" %>
      <%= form.text_field :image, placeholder: "Image Url" %>
      <%= form.text_area :text, placeholder: "text", rows: "10" %>
      <%= form.submit "SEND" %>
    <% end %>
  </div>
</div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでRSpecを追加しようとした時に発生したエラー「Could not find diff-lcs-1.3 in any of the sources Run `bundle install` to install missing gems.」の対処法

Gemfile
group :development, :test do
  gem 'rspec-rails'
end

GemfileにRSpecを追加して、以下のコマンドを実行したらエラーが発生した。

エラーメッセージ

$ bundle install
・・・
$ rails generate rspec:install
Could not find diff-lcs-1.3 in any of the sources
Run `bundle install` to install missing gems.

解決方法

$ ps axu | grep spring
maiamea              53324   0.1  0.1  4336604   9524   ??  Ss    4:41PM   0:00.92 spring app    | api-project | started 5 mins ago | development mode      
maiamea              36768   0.0  0.0  4366272    748 s002  S+   月04PM   0:00.58 spring server | api-project | started 24 hours ago   
maiamea              53389   0.0  0.0  4286728    716 s003  S+    4:46PM   0:00.00 grep spring
$ spring stop
Spring stopped.
$ rails generate rspec:install
Running via Spring preloader in process 53437
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

原因

rails server を高速で起動させるSpringが悪さをしていた。
Springが起動していたことによりGemfileの変更が反映されなかった。

Gemfileに変更を加えた場合は、まず rails server を止め、
次にSpringをstopさせる。
その後 rails server を起動させる必要があった。

Springとは?

Railsアプリケーションのプリローダーのgem (Rubyのライブラリ)
Railsアプリケーションをバックグラウンドで走らせたままにしておくことにより、2回目以降のコマンド実行時の待ち時間が減り開発効率が上がる。

プリローダー:preloader (pre + load = 前もってロードしておく)

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails 新規アプリ作成

はじめに

今まで学習した知識を使って、オリジナルアプリを作るカリキュラムの段階まできました。
自身のアウトプット、皆さんへの共有の目的で、これから個人アプリが完成するまでの流れを投稿していきます。

Railsで新規アプリを作成する

ターミナルにて

$ rails _5.2.3_ new 〇〇 -d mysql

を、実行する。

railsのあとの『5.2.3』はrailsのバーション指定。
〇〇は作成したいアプリ名。
-dはデータベースの意味。
私の場合、データベースはmysqlを使用したかったので、-dで指定しました。

作成するアプリのディレクトリへ入り、ターミナルにて

rails db:create

を実行してデータベースを作る。

ここでひとまずアプリの大枠は完成です。
試しに

ターミナルにて

rails s

でサーバーを立ち上げ、localhost:3000へアクセスすると

rails_welcome.png

こちらの画像が表示され、正常にアプリが作られていることがわかります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseでログイン機能を実装したものの、データベースへ書き込めない問題

起きた問題

deviseにてユーザーのログイン機能やユーザー編集機能などを実装したが、ユーザー編集機能のページedit.html.erbにて入力した内容がうまくデータベースに反映されない。

具体的にいうとnameのカラムだけなぜかdbに反映されなかったので、調べてみた。

schema.rbを見てもちゃんとカラムが追加されている。。。

解決した方法とソース

まず、解決した方法は簡単で、application_controller.rbに以下のコードを追加するだけ。

application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    # 「登録時(sign_up)」に許可するパラメータを追加
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])

    # 「更新時(account_update)」に許可するパラメータを追加
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end
end

deviseではどうやらデフォルトでemail、password以外は許可しない設定になっているらしく、上記のようなコードを追加することで許可してあげないといけないようだった。

以下のURLはdeviseのGithub READMEから。

https://github.com/heartcombo/devise#strong-parameters

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Heroku環境下のテーブル変更作業メモ

前提条件と結論

migrateしてから立ち上げないとテーブルの変更を認識しないので気をつけようといいたいだけのメモです。
リスタートするだけでいいし、なんらかの理由でHerokuでデプロイと同時にrake db:migrateを実行することを選択しない場合の備忘録なので、Release Phaseが設定できている場合は読む必要はありません。

  • Ruby on Rails
  • Heroku Postgres
  • --app yourappsは複数環境を持っている場合のお約束

実際のコマンド

# 現在のheroku側のmigrate状況を確認、すべてupになっている
heroku rake db:migrate:status --app yourapps

# herokuにpushした状態で、再度確認。追加分だけdownになっている
heroku rake db:migrate:status --app yourapps

# migration実行
heroku rake db:migrate --app yourapps

# アプリをリスタートしないとテーブルの変更を認識しないので注意
heroku restart --app yourapps

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rspec・devise token auth】Rails APIアプリのRspecテストでユーザログイン・認証を実装する方法

Rspecテスト内でdevise token authをつかってユーザログイン・認証するのはどうするんだ!?
Deviseのやり方はたくさんあるのですがdevise token authは探すのに苦労したので、私と同じ状況に陥っている方のためにこの記事を残しておきます。

やりたいこと

下記の様に、BooksControllercreateメソッドがあり、createメソッドを実行するにはユーザが認証が必須(authenticate_user!)だとします。

books_controller.rb
class BooksController < ApplicationController
  before_action :authenticate_user!

  def create
    book = Book.new(book_params)

    if book.save
     render json: { status: 'SUCCESS', data: book }
    else
     render json: { status: 'ERROR', data: book.errors }
    end
  end

  def book_params
   params.require(:book).permit(:name, :category, :author, :price)
  end
end

Rspecテストでユーザ認証を実装せずに書くと下記の様な感じになりますが・・・

books_spec.rb
require 'rails_helper'

RSpec.describe 'BooksAPI', type: :request do
  describe 'POST /books/create' do
    it '本を新規登録する' do
      params = {
        book: {
          name: "Rspecがよくわかる本",
          author: "豊田桃子",
          category: "プログラミング",
          price: 1400  
        }
      }

      post api_v1_gommit_books_path, params: params
      expect(response).to have_http_status :ok
    end
  end
end

もちろんauthenticate_user!が通らないので、expected the response to have status code :ok (200) but it was :unauthorized (401)というエラーが返ってきます。

BooksAPI
  POST books/create
    本を新規登録する (FAILED - 1)

Failures:

  1) BooksAPI POST books/create 本を新規登録する
     Failure/Error: expect(response).to have_http_status :ok
       expected the response to have status code :ok (200) but it was :unauthorized (401)
     # ./spec/requests/api/v1/gommit/books_spec.rb:17:in `block (3 levels) in <top (required)>'

Finished in 0.14515 seconds (files took 3.38 seconds to load)
1 example, 1 failure

ということでdevise token authを使ってtokenベースで認証を行っている場合のRspecでのテストの書き方を解説したいと思います。

手順

  1. ヘルパーモジュールを作りsign_inメソッドを実装する
  2. ヘルパーモジュールをRspecで読み込む
  3. テストでヘルパーモジュールを使ってsign_inする

ヘルパーモジュールを作りsign_inメソッドを実装する

libディレクトリ直下にauthorization_spec_helper.rbという名前のファイルを作成します。下記のコードを貼り付けます。

lib/authorization_spec_helper.rb
module AuthorizationSpecHelper
  def sign_in(user)
    post "/auth/sign_in/",
      params: { email: user[:email], password: user[:password] },
      as: :json

    response.headers.slice('client', 'access-token', 'uid')
  end
end

userを使ってdevise_token_authのPOSTメソッドにてサインイン後、responseとして返ってきたclientaccess-tokenuidを返すヘルパーメソッドです。

ヘルパーモジュールをRspecで読み込む

ヘルパーモジュールをRspecで使える様にするために下記の一文を追加します。

rails_helper.rb
RSpec.configure do |config|
  # ....
  # ....
  # ....
  config.include AuthorizationSpecHelper, type: :request
end

テストでヘルパーモジュールを使ってsign_inする

これでヘルパーモジュールを使う準備ができました。あとはbooks_spec.rbを修正していくだけです。

books_spec.rb
require 'rails_helper'

RSpec.describe 'BooksAPI', type: :request do
  describe 'POST books/create' do
    it '本を新規登録する' do
      # 下記の2行を追加
      # DBに下記user情報のuserが存在している前提です
      user = { email: "momoko@test.com", password: "123456" }
      auth_tokens = sign_in(user)

      params = {
        book: {
          name: "Rspecがよくわかる本",
          author: "豊田桃子",
          category: "プログラミング",
          price: 1400  
        }
      }

      # headersを追加
      post api_v1_gommit_books_path, params: params, headers: auth_tokens

      expect(response).to have_http_status :ok
    end
  end
end

これで再度テストを実行してみると・・・・通った!!!

BooksAPI
  POST books/create
    本を新規登録する

Finished in 0.7509 seconds (files took 4.08 seconds to load)
1 example, 0 failures

これでもOK

require 'rails_helper'

RSpec.describe 'BooksAPI', type: :request do
  describe 'POST books/create' do
    let(:user) { { email: "momoko@test.com", password: "123456" } }
    let(:auth_tokens) { sign_in(user) }

    it '本を新規登録する' do
      params = {
        book: {
          name: "Rspecがよくわかる本",
          author: "豊田桃子",
          category: "プログラミング",
          price: 1400  
        }
      }

      post api_v1_gommit_books_path, params: params, headers: auth_tokens

      expect(response).to have_http_status :ok
    end
  end
end

調べるまでが大変でしたが、実装自体はとても簡単でした。お役に立てれば嬉しいです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ああああ

Carraca_lila_(Coracias_caudata),_parque_nacional_Kruger,_Sudáfrica,_2018-07-26,_DD_17.jpg

ああああ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]ActiveRecord Relationについて調べたことざっくりまとめた[初心者向け]

はじめに

Railsを何気なくつかっていたのであまり意識していなかったですが、とても大事な役割をになっているActiveRecord Relationのことをふと思い出したのでまとめてみました??

ActiveRecord Relationクラスとは

クエリ(DBに指示する命令文)を生成するための条件を持っていて、必要に応じて適切なSQLクエリを生成・発行してくれるクラス。

ざっくりいうと、データーベースとRailsのやりとりを担ってくれているクラス。

データーベースに命令を出す時はデーターベースとのやりとりをするための言葉(SQL)が必要ですが、Railsで自分でSQLを記述することはないかなと思います。

しかし、データーベースからデータを取得したり保存したりできている、、、!
これはまさにActiveRecord Relationというクラスが存在しているからなのです、、、!

SQL文の例
User Load (28.6ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 2 ORDER BY `users`.`id` ASC LIMIT 1

ActiveRecord Relationに関係するメソッドについて

データーベースに入っているデータの取り出しを行うものが多く、下記の様なメソッドを使用するとActiveRecord::Relationのインスタンス(後述)を返します。

- order
- where
- limit

メソッドについての参考記事

ActiveRecord::Relationのインスタンスとは

ActiveRecord Relationに関係するメソッド(上記)を使用し、データーベースから取得したデータ。
このデータは配列のような形でデータが格納されている。

whereメソッドの例
# idが4未満のtweetsテーブルのインスタンスを取得

Tweet.where('id < 4')
  #=> [#<Tweet id: 1, name: "藤井" , text: "おはよう!" ...>,#<Tweet id: 2, name: "山本", text: "眠いよ~"...>,#<Tweet id: 3, name: "田中", text: "良い天気!!"...>]

ActiveRecord Relationのインスタンスの特徴

①配列のような性質を持つ。配列に対し使用するメソッド(each/map/shuffleなど)が使用可能。

eachの使用例
# ActiveRecord Relationのインスタンスをtweetに代入する
tweet = Tweet.where('id < 4')

tweet.each do |t|
  p t.text
end
 #=> "おはよう!" , "眠いよ~" , "良い天気!!"

②ActiveRecord系のメソッド(find_by/find/averageなど)を実行することができる。

findの使用例
# ActiveRecord Relationのインスタンスをtweetに代入する
tweet = Tweet.where('id < 4')

tweet.find(2)
 #=>#<Tweet id: 2, name: "山本", text: "眠いよ~"...>

さいごに

最後まで見てくださりありがとうございます!
ActiveRecord Relationクラスの存在が少しでも身近に感じていただけたら嬉しいです!
これからも頑張ります~?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.12 - TDDでPost機能をコーディング part1 -

はじめに

お待たせしました!第12回にしてやっとつぶやき機能を実装してまいります!ここではPost機能といいますね。

前回のソースコード

前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。

どんなの実装するの?

最初に今回のゴールを見据えておきましょう。

画面遷移

screen_transition_diagram.png
新たにポストページを作ります。
ポストページはポストを投稿するフォームと、今までのユーザー全員のポストが投稿日時降順で表示される機能を具備しています。
ポストページはサインイン済のユーザーしかアクセスできません。
ポストの投稿ユーザーをクリックしたらそのポストのプロフィールページに遷移できます。
ユーザーのプロフィールページでは、そのユーザーの過去のポストが投稿日時降順で表示されています。

ER図

entity_relation_diagram.png
user:post = 1:nの関係です。

テストシナリオ

さて、これを踏まえて今回のテストシナリオを考えてみましょう。

  1. 未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
  2. サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること
  3. 未サインインのユーザーは、トップページでヘッダーにポストページへのリンクを見つけられないこと
  4. 未サインインのユーザーは、サインアップページでヘッダーにポストページへのリンクを見つけられないこと
  5. 未サインインのユーザーは、サインインページでヘッダーにポストページへのリンクを見つけられないこと
  6. 未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと
  7. サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
  8. サインイン済のユーザーが、ユーザー詳細ページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
  9. サインイン済のユーザーが、ポストページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
  10. サインイン済のユーザーは、ポストページでポストを入力できること
  11. ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
  12. ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
  13. ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
  14. サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
  15. サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
  16. 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  17. 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  18. サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
  19. サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
  20. サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
  21. サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと

こんな感じでしょう。
では元気にデベロップしてまいりましょう!今回はTDDで開発をしていくので、テストコードを書いて、アプリコードを書いて、を繰り返していきます。

開発スタート

まずは、それぞれのテストシナリオをテストコードに落とし込みましょう。
開発はTDDで進めるので

  1. テストをコーディングする(Red)
  2. テストコードがパスするようにアプリケーションをコーディングする(Green)
  3. 非効率な記述があれば、Greenをキープしながらコーディングし直す(Refectoring)

です。

ではコンテナを起動しておきましょう!

$ docker-compose up -d
$ docker-compose exec web ash

未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること

ポストページについては以下の仕様で実装していきます。ポストページのURLパスは/postsとします。

まずはテストコードを書いていきます。今回のテストシナリオ用に新しいテストシナリオファイルを作りましょう。

# touch spec/system/07_posts_spec.rb
07_posts_spec.rb
feature "ユーザーとして、ポストを投稿したい", type: :system do
  scenario "未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること" do
    # ポストページにアクセスする
    visit posts_path

    # 現在のパスがトップページのパスであることを検証する
    expect(current_path).to eq root_path
  end
end

ポストページへのルーティングの名前付きルートをposts_pathとしています。
今までも何度か書いてきましたが、posts_pathにアクセスしようとしたのに現在のパスはroot_pathです、というリダイレクトの検証です。

この時点ではアプリケーションのコーディングを行っていないのでテストは失敗します。

# rspec spec/system/07_posts_spec.rb
...
Failures:

  1) ユーザーとして、ポストを投稿したい 未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
     Failure/Error: visit posts_path

     NameError:
       undefined local variable or method `posts_path' for #<RSpec::ExampleGroups::Nested:0x0000555f60b1ccf8>

     # ./spec/system/07_posts_spec.rb:3:in `block (2 levels) in <main>'

Finished in 2.41 seconds (files took 8.4 seconds to load)
1 example, 1 failure
...

RSpecではこのような形でテストの失敗の理由が示されるので、それが解消されるようにアプリケーションをコーディングしていきましょう。
今回はposts_pathという変数もしくはメソッドがアプリケーションに定義されていないことがテスト失敗の理由として示されているので、まずはルーティングの設定をする必要がありそうです。

rails g controllerコマンドでポストページに必要な設定・ファイルを揃えましょう。

# rails g controller posts index

今回はポストページのためのアクションとしてposts#indexを用意することにしました。
ルーティングを定義します。

config/routes.rb
  Rails.application.routes.draw do
-   get 'posts/index'
    root 'static_pages#home'

    get   '/sign_up', to: 'users#new',    as: :sign_up
    post  '/sign_up', to: 'users#create', as: :create_user
    resources :users, only: [:show]

    get     '/sign_in',   to: 'sessions#new',     as: :sign_in
    post    '/sign_in',   to: 'sessions#create',  as: :create_session
    delete  '/sign_out',  to: 'sessions#destroy', as: :sign_out
+
+   get '/posts', to: 'posts#index', as: :posts
  end

これで名前付きルートposts_pathが定義されたのでテスト結果が変わるはずです。もう一度テストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい 未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
     Failure/Error: expect(current_path).to eq root_path

       expected: "/"
            got: "/posts"

       (compared using ==)



     # ./spec/system/07_posts_spec.rb:5:in `block (2 levels) in <main>'

Finished in 7.94 seconds (files took 16.58 seconds to load)
1 example, 1 failure

以前テストは失敗していますが、エラー理由が変わっていますね。
今回は/にリダイレクトされることが期待されていたけど/postsにアクセスできてしまっていることがテスト失敗の理由のようです。
今コントローラーで何も制御をしていないので誰でもポストページにアクセスできてしまいますね。
では、未サインインのユーザーがposts#indexにルーティングされた場合、root_pathにリダイレクトするようにアプリをコーディングしていきましょう!

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    # 未サインインの場合、トップページにリダイレクトする
+   redirect_to root_path unless signed_in?
  end
end

以前、サインイン済ならプロフィールページにリダイレクトさせる、という機能を作りましたね。今回はそれの条件が逆バージョンです。
今回使っているunlessifの逆の分岐です。つまり、trueの場合は何もなし、falseの場合に実行する、という挙動をとります。

またテストを実行してみましょう!

# rspec spec/system/07_posts_spec.rb

Finished in 4.35 seconds (files took 7.28 seconds to load)
1 example, 0 failures

テストがパスしました!
では次のテストシナリオの実装に移りましょう!

サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること

まずはテストシナリオをコーディングします。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること" do
      # テストシナリオ用のユーザーを作成
+     user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
+
      # サインインページにアクセスする
+     visit sign_in_path
      # サインインページで作成したユーザーのメールアドレスを入力する
+     fill_in :user_email, with: user.email
      # サインインページで作成したユーザーのパスワードを入力する
+     fill_in :user_password, with: user.password
      # サインインボタンをクリックする(サインインする)
+     click_on :sign_in_button
+     
      # ポストページにアクセスする
+     visit posts_path
+ 
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 6.5 seconds (files took 13.64 seconds to load)
2 examples, 0 failures

テストがパスしていますね。ちゃんとサインイン前後でリダイレクト機能の出しわけができているようです。

未サインインのユーザーは、トップページでヘッダーにポストページへのリンクを見つけられないこと

こちらもテストから書き始めます。ヘッダーのポストページへのリンクはheader_posts_linkidを付与することにします。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、トップページでヘッダーにポストページへのリンクを見つけられないこと" do
      # トップページにアクセスする
+     visit root_path
+     
      # ページ内に"header_posts_link"をid属性に持つ要素がないことを検証する
+     expect(page).not_to have_selector "#header_posts_link"
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 7.61 seconds (files took 6.77 seconds to load)
3 examples, 0 failures

まだリンクをコーディングしていないので当然見つからないですね。テストをパスできています。

未サインインのユーザーは、サインアップページでヘッダーにポストページへのリンクを見つけられないこと

一つ前とほぼ同じテストですね。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、サインアップページでヘッダーにポストページへのリンクを見つけられないこと" do
      # サインアップページにアクセスする
+     visit sign_up_path
+     
      # ページ内に"header_posts_link"をid属性に持つ要素がないことを検証する    
+     expect(page).not_to have_selector "#header_posts_link"
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 6.83 seconds (files took 6.03 seconds to load)
4 examples, 0 failures

未サインインのユーザーは、サインインページでヘッダーにポストページへのリンクを見つけられないこと

また、ほぼ同じです。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、サインインページでヘッダーにポストページへのリンクを見つけられないこと" do
      # サインインページにアクセスする
+     visit sign_in_path
+ 
      # ページ内に"header_posts_link"をid属性に持つ要素がないことを検証する    
+     expect(page).not_to have_selector "#header_posts_link"
+   end    
  end
# rspec spec/system/07_posts_spec.rb

Finished in 9.04 seconds (files took 6.57 seconds to load)
5 examples, 0 failures

どんどん進みましょう。

未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと

ほぼ同じテストケース最後です。ユーザー詳細ページなので、Userモデルが1つ必要なのですが、前のテストでもJohn Smithcreateしたテストケースがありました。
これを簡易に使いまわせるようにJohn Smithを作成するコードをメソッド化してみましょう。メソッド化はとてもシンプルなRubyコードでdefを使うだけです。メソッド外で変数を使うことになるのでインスタンス変数を使う必要があります。

spec/system/07_posts_spec.rb
  # ユーザー「John Smith」をDBに作成する
+ def create_john
+   User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
+ end

  feature "ユーザーとして、ポストを投稿したい", type: :system do
  ...
    scenario "サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること" do
      # テストシナリオ用のユーザーを作成
-     user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
+     user = create_john
      ...
    end
  end

これでテストがパスするか一度確認しておきましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 7.8 seconds (files took 6.24 seconds to load)
5 examples, 0 failures

これでJohn Smithのユーザー作成を単純なメソッド
呼び出しで必要なテストシナリオからのみ呼び出すことができるようになりました!
では今回のテストシナリオを追加していきます。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと" do
      # テスト用のユーザーを作成する
+     user = create_john
+
      # テストユーザーのユーザー詳細ページにアクセスする
+     visit user_path(user)
+
      # ページ内に"header_posts_link"をid属性に持つ要素がないことを検証する
+     expect(page).not_to have_selector "#header_posts_link"
+   end
  end
# rspec spec/system/07_posts_spec.rb

Finished in 8.91 seconds (files took 6.91 seconds to load)
6 examples, 0 failures

今回もテストをパスできていることがわかります。メソッド
の呼び出しもうまくいっていますね。

サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること

次はサインイン後にヘッダーにポストページへのリンクが表示されており、クリックするとポストページに行けるようになる機能を作っていきます。今までと同様に、テストからコーディングしていきましょう。
サインイン済のユーザーでテストをするシナリオは前にもあったので、まずはサインイン済の状態にする操作をメソッド化してみます。

spec/system/07_posts_spec.rb
  ...
  # 与えられたユーザーでサインインする
+ def sign_in(user)
    # サインインページにアクセスする
+   visit sign_in_path
    # サインインページで作成したユーザーのメールアドレスを入力する
+   fill_in :user_email, with: user.email
    # サインインページで作成したユーザーのパスワードを入力する
+   fill_in :user_password, with: user.password
    # サインインボタンをクリックする(サインインする)
+   click_on :sign_in_button
+ end
  ...
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
    scenario "サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること" do
      user = create_john
-     visit sign_in_path
-     fill_in :user_email, with: user.email
-     fill_in :user_password, with: user.password
-     click_on :sign_in_button
+     sign_in(user)
      ...
    end
    ...
  end

再度、メソッド化してもテストがパスするかを確認します。

# rspec spec/system/07_posts_spec.rb

Finished in 8.96 seconds (files took 8 seconds to load)
6 examples, 0 failures

メソッド化成功です!
ではこのメソッドを使って、今回のテストコードを記述してみます。

spec/system/07_posts_spec.rb
feature "ユーザーとして、ポストを投稿したい", type: :system do
  ...
+   scenario "サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること" do
      # テスト用のユーザーを作成する
+     user = create_john
      # テスト用のユーザーでサインインする
+     sign_in(user)
+
      # プロフィールページにアクセスする
+     visit user_path(user)
      # "header_posts_link"をid属性に持つ要素(=ヘッダーのポストリンク)をクリックする
+     click_on :header_posts_link
+
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+   end
  end

はい。これでテストを回してみましょう。まだヘッダーにポストリンクを作っていないのでRedになるはずです。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
     Failure/Error: click_on :header_posts_link

     Capybara::ElementNotFound:
       Unable to find link or button :header_posts_link

Finished in 11.92 seconds (files took 6.78 seconds to load)
7 examples, 1 failure

header_posts_linkをクリックしようとしたけどそんな要素見つからなかった、という失敗理由が表示されていますね。
サインイン後のリンクにポストリンクを追加してあげましょう。

app/views/layouts/application.html.erb
  <% if signed_in? %>
+   <li class="nav-item"><%= link_to "Posts", posts_path, class: "nav-link", id: :header_posts_link %></li>
    <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link", id: :header_profile_link %></li>
    <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link", id: :header_sign_out_link %></li>
  <% else %>

1行、ポストページへのリンクを追加しました。

# rspec spec/system/07_posts_spec.rb

Finished in 10.38 seconds (files took 6.17 seconds to load)
7 examples, 0 failures

今回はテストがパスしています。今まで実装もしていなかったのでパスしてた、未サインインユーザーにはポストページへのリンクがヘッダーに表示されないテストもパスしているのでデグレなく機能実装ができましたね。

ちょっとViewを確認してみましょう。
image.png
Viewを確認してみても、Postsリンクが追加されたことがわかりますね。

サインイン済のユーザーが、ユーザー詳細ページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること

サインインしているユーザーとは別のユーザーを作成し、そのユーザーのユーザー詳細ページからのポストページへの遷移をテストしてみましょう。
別のユーザーも他のテストシナリオでも使えるようにメソッド化しておきたいところです。create_johnメソッドを少し改良して、引数によって異なるユーザーをDB作成できるように改良して使ってみましょう。

spec/system/07_posts_spec.rb
- def create_john
-   User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
- end
  # user_typeに応じて、ユーザーをDBに作成する
  # 1: John Smith
  # 2: Taro Tanaka
+ def create_user(user_type = 1)
+   case user_type
+   when 1
+     User.create(name: "John Smith", email: "john@sample.com", password: "john1234")
+   when 2
+     User.create(name: "Taro Tanaka", email: "taro@sample.com", password: "taro1234")
+   end
+ end
  ...
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ... 
    scenario "サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること" do
-     user = create_john
+     user = create_user(1)
      ...
    end
    ...
    scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと" do
-     user = create_john
+     user = create_user(1)
      ...
    end
    ...
    scenario "サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること" do
-     user = create_john
+     user = create_user(1)
      ...
    end
  end

create_userメソッドではcase文を使ってみました。case文はある変数の値に応じて動作を変える分岐を作ることができます。

case target
when value1
  # target == value1 の場合のコード
when value2
  # target == value2 の場合のコード
...
else
  # どれにも当てはまらなかった場合のコード
end

では、メソッドがうまく置き換われたかを確認してみます。

# rspec spec/system/07_posts_spec.rb

Finished in 9.62 seconds (files took 6.84 seconds to load)
7 examples, 0 failures

ちゃんとテストがパスしていますので、新しいメソッドは機能しています。
ではこのメソッドを使って二人のユーザーを作成して実行するテストコードを記述してみましょう。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること" do
      # テスト用のユーザーを2人作成する
+     user1 = create_user(1)
+     user2 = create_user(2)
      # user1でサインインする
+     sign_in(user1)
+
      # user2のユーザー詳細ページにアクセスする
+     visit user_path(user2)
      # "header_posts_link"をid属性に持つ要素(=ヘッダーのポストリンク)をクリックする
+     click_on :header_posts_link
+    
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+   end
    ...
  end

テストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 12.04 seconds (files took 5.41 seconds to load)
8 examples, 0 failures

問題なくGreenです。

サインイン済のユーザーが、ポストページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること

これも一つ前と同じようなケースです。

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーが、ポストページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること" do
      # テスト用のユーザーを1人作成する
+     user = create_user(1)
      # userでサインインする
+     sign_in(user)
+
      # ポストページにアクセスする
+     visit posts_path
      # "header_posts_link"をid属性に持つ要素(=ヘッダーのポストリンク)をクリックする    
+     click_on :header_posts_link
+
      # 現在のページがポストページであることを検証する
+     expect(current_path).to eq posts_path
+   end
    ...
  end
# rspec spec/system/07_posts_spec.rb

Finished in 13.63 seconds (files took 6.56 seconds to load)
9 examples, 0 failures

遷移系はここまでですね。全てGreenをキープしています。

サインイン済のユーザーは、ポストページでポストを入力できること

いよいよポストページの機能に入っていきます。
でもやり方は変わりません。まずはテストをコーディングしましょう!

spec/system/07_posts_spec.rb
  feature "ユーザーとして、ポストを投稿したい", type: :system do
    ...
+   scenario "サインイン済のユーザーは、ポストページでポストを入力できること" do
      # テスト用のユーザーを作成する
+     user = create_user(1)
      # このテストシナリオで使うポスト内容を定義する
+     content = "Hello world."
      # userでサインインする
+     sign_in(user)
+ 
      # ポストページにアクセスする
+     visit posts_path
      # ポスト入力欄(#post_content)にcontentを入力する
+     fill_in :post_content, with: content 
+ 
      # ポスト入力欄(#post_content)にcontentが入力されていることを検証する
+     expect(find("#post_content").value).to eq content
+   end
    ...
  end

前回のハンズオンでも出てきた入力してちゃんと入力できているかを確認するテストコードですね。
今回はポストを投稿するための入力エリアであるpost_contentに「Hello world.」を入力できるかをチェックしています。

# rspec spec/system/07_posts_spec.rb

Failures:

  1) ユーザーとして、ポストを投稿したい After sign in サインイン済のユーザーは、ポストページでポストを入力できること
     Failure/Error: fill_in :post_content, with: content

     Capybara::ElementNotFound:
       Unable to find field :post_content that is not disabled

Finished in 20.46 seconds (files took 7.82 seconds to load)
10 examples, 1 failure

このテストは失敗します。なぜならまだポストを投稿するための入力エリアであるpost_contentを作っていないからです。

post_contentはPostモデルオブジェクトを作るためのフォームです。
なのでまずはPostモデルを作成しましょう。

# rails g model post content:string user:references
# rm -rf spec/models

ここで見かけたことのないreferences型が出てきました。
これは外部キーを定義するための型です。今回のようにuser:referencesとするとuser_idという項目が定義され、ここにはUserモデルの主キーであるidが入るようになります。

Postモデルファイルをみてみましょう。

app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

今まではclass定義しかされていませんでしたが、今回はbelongs_to :userというコードがデフォルトで記述されています。
これはモデルの関連付けです。(参考: Active Record の関連付け - Railsガイド

belongs_toは指定するモデルを1つに特定できることを表します。つまり、あるPostから見ると紐づくUserが一意に決まることを示しています。

逆にUserは複数のPostを行います。これを表す関連付けがhas_manyです。
UserモデルにはまだPostモデルとの関連付けがコーディングされていないので、自分で記述しておきましょう。

app/models/user.rb
  class User < ApplicationRecord
    ...
+   has_many :posts
    ...
  end

ちなみにマイグレーションファイルの中身もみておきましょう。

db/migrate/YYYYMMDDhhmmss_create_posts.rb
class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :content
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

t.references :user, null: false, foreign_key: trueuser_idを外部キーとして利用できるようにDBにSQLを発行してくれます。

では、マイグレーションファイルを適用します。

# rails db:migrate

== YYYYMMDDhhmmss CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.1530s
== YYYYMMDDhhmmss CreatePosts: migrated (0.1536s) =============================```

Postモデルの準備ができたので、今までと同じようにPostモデルを作成するためのフォームを作っていきましょう。
Userモデルの時と同じように、コントローラーで空のPostモデルオブジェクトを作成し、Viewでform_withヘルパーを使ってフォームを作ってみます。

app/controllers/posts_controller.rb
  class PostsController < ApplicationController
    def index
      redirect_to root_path unless signed_in?
+     @post = Post.new
    end
  end
app/views/posts/index.html.erb
- <h1>Posts#index</h1>
- <p>Find me in app/views/posts/index.html.erb</p> %>
+ <div class="container my-5">
+   <%= form_with model: @post, url: nil, local: true do |form| %>
+     <div class="form-group">
+       <%= form.text_area :content, class: "form-control", placeholder: "いまどうしてる?", autofocus: true %>
+     </div>
+   <% end %>
+ </div>

まだリクエスト先のルーティングを決めていないのでurlにはnilを定義しています。
また、今までと違う点としてはform.text_areaを使っています。text_areaヘルパーは<textarea>タグを生成するヘルパーです。
ポストは今までのように1行の短い文字列ではなく、改行などを含んだ140文字の文章になるのでそちらを選択してます。
さらに、placeholderを使っています。これはHTML5の技術ですが、inputtextareaに何も入力がない時に限り、そのフォームの補助の役割でどういうものを入力すればいいかを表示してあげる機能です。
autofocus: trueはページが表示された時に自動的にフォーカスされるフィールドを指定できるHTML5の機能です。便利なのでつけときます。

今回の場合は、以下のようなフォームができあがっています。
image.png

ここまでで再度テストを実行してみましょう。

# rspec spec/system/07_posts_spec.rb

Finished in 16.43 seconds (files took 6.9 seconds to load)
10 examples, 0 failures

これでテストをパスすることができました!

今回はこの辺りで時間切れなので、残りのテスト&コーディングは次回に回したいと思います!

まとめ

今回はポスト機能をTDD/BDDでコーディングしてきました。なんかいよいよコーディングしている感じが高まってきましたね。
次回は残りのユーザー詳細ページ側でそのユーザーの投稿に絞ってポストを確認できる機能をコーディングしていきます。
次回以降もBDDで実装を進めていくので、是非ともテストコードも振り返っておいてくださいね。

後片付け

いつものようにコンテナを落としておきます。

# exit
$ docker-compose down

本日のソースコード

Other Hands-on Links

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

git pull時に 【Your local changes to the following files would be overwritten by merge】とエラー

エラー内容

$ git pull origin masterをすると

error: Your local changes to the following files would be overwritten by merge:
config/routes.rb
Please commit your changes or stash them before you merge.

が出てくる。

原因

pullした内容と自分の編集した箇所(ここではconfig/routes.rb)が被っている。

解決策

pullした内容の箇所が自分も編集している所の為、mergeする前にcommitするかstashしてと言われる。

なので、今回はコミットを選択。

$ git add *
$ git commit -m "コミット名"
$ git push origin 自分の作業ブランチ

でpushし、Github上で
コンフリクトが起きなければ再度、

$ git pull origin master

とpullすればOK。
コンフリクトの場合はコンフリクト
内容を修正してから再度、pull。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails form_withでフォロー機能作成

はじめに

今回はフォロー機能を作成します。railsチュートリアルではform_forを利用していましたが、今回はform_withを利用して作成します。
(題材は自分のポートフォリオです。Userモデルはある前提で進めます。)

対象読者

railsチュートリアル終わったレベルくらいの人

作成の流れ

1.relationshipモデルの作成
2.各モデルの関連付け、フォロー機能のメゾット作成
3.対応するコントローラーの作成
4.form_withを利用したフォローボタン作成
5.jsファイルの作成
6.フォロー機能の流れ

今回のAjaxの流れ

①fomr_withで作成したフォロー/アンフォローボタンを押すとPost/deleteリクエストが送られる。

②対応するコントローラのcreate/destroyアクションを実行

③js.erbファイルをレンダリング

④Ajax対象範囲のフォロー/アンフォローボタンを非同期で切り替え

1.relationshipモデルの作成

relationshipモデルの中身はこんな感じです。中間テーブルを作成しますが別にフォローしているユーザーの情報を記録するようなテーブルは作りません。実際のフォローする際のテーブルの流れは
Userモデル→Follow_relationship→Userモデルと帰ってくる流れになります。
follower,followingと名前がついていますがどちらもUserのidが入ります。

カラム
id integer
follower_id integer
following_id integer

モデルを作成します。
それぞれのカラムに外部キー制約とindexをこの後つけるのでreferencesを付けて生成します。(referencesをつけると自動で〇〇_idの形にしてくれます。)

rails g model Follow_Relationship follower:references following:references

2.各モデルの関連付け、フォロー機能のメゾット作成

作成されたマイグレーションファイルを確認します。

db/migrate
class CreateFollowRelationships < ActiveRecord::Migration[5.1]
  def change
    create_table :follow_relationships do |t|
      t.references :follower, foreign_key: { to_table: :users }
      t.references :following, foreign_key: { to_table: :users }

      t.timestamps
    end
    add_index :follow_relationships, [:follower_id, :following_id], unique: true
  end
end

ここでforegin_key(外部キー)を設定しています。よく外部キーを設定する時に使う「foreign_key: true」としてしまうと存在しないfollowersデーブルを参照してしまうので「to_table: :users」として参照はusersテーブルのidであることを指定してます。
外部キーを設定することによりindexの追加と存在するuserのidのみをdbに保存するようになります。
また、unique: trueとすることにより、同じ組み合わせでデータを保存するのを防ぐようにしているので同じユーザーを2回フォローできなくしています。

各モデルの関連付けはこのような形になります。

app/model/follow_relationship.rb
belongs_to :follower, class_name: "User"
belongs_to :following, class_name: "User"

validates :follower_id, presence: true
validates :following_id, presence: true
app/model/user.rb
has_many :following_relationships,foreign_key: "follower_id", class_name: "FollowRelationship",  dependent: :destroy
has_many :followings, through: :following_relationships
has_many :follower_relationships,foreign_key: "following_id",class_name: "FollowRelationship", dependent: :destroy
has_many :followers, through: :follower_relationships

それぞれのオプションの意味は
foreign_key: "follower_id"  外部キーの名前を直接参照します。
※マイグレーションで設定した物はあくまでもUserのidがこのカラムに入りますと指定しただけなのでフォローしているユーザーを検索等するときはfollowingカラムを参照するよと指定しないといけません。
class_name:関連名(following_relationships)としていますがそのようなテーブルは存在しないので実際に参照するテーブルであるFollowRelationshipを指定してあげます。
dependent: :destroy Userが削除された時にrelationshipのuseridが削除されるようにしています。

has_many :followings, through: :following_relationships
とすると先ほど指定したfollowing_relationshipsを通してフォローしているユーザーのidを取得できるようになります。

フォロー関連のメゾットを作成します。

app/model/user.rb
#すでにフォロー済みであればture返す
  def following?(other_user)
    self.followings.include?(other_user)
  end

  #ユーザーをフォローする
  def follow(other_user)
    self.following_relationships.create(following_id: other_user.id)
  end

  #ユーザーのフォローを解除する
  def unfollow(other_user)
    self.following_relationships.find_by(following_id: other_user.id).destroy
  end

3.コントローラーの作成

まずはコントローラーを作成します。

rails g controller follow_relationships

:
続いてルーディングの設定です。フォロー/フォロワーの一覧ページ用のアクションはUserオブジェクト絡みの機能なのでコントローラー内に記載します。
そのために、resources: userにmemberを使用してルートを追加します。
memberを使用するとURL内にユーザーを識別するidが追加されます。
idは後ほどUserコントローラーの対応するアクションないで必要となるのでmemberでルートを追加しないといけません。(idを追加しないcollectionというメゾットもあります。)

confing.routes.rb
resources :users do
      member do
        get :following, :followers
      end
    end
resources :follow_relationships, only: [:create, :destroy]

一覧ページ用のアクションはこのようになります。(kaminariのページネーション機能を利用しています。)
※それぞれ対応するビューを作成してください。

app/controllers/users.rb
def followings
    @user =User.find(params[:id])
    @users =@user.followings.page(params[:page]).per(5)
    render 'show_followings'
  end

  def followers
    @user =User.find(params[:id])
    @users =@user.followers.page(params[:page]).per(5)
    render 'show_followers'
  end

follow_relationshipのコントローラーはこんな感じです。
followとunfollowが先ほどUser.rbに記載したメゾットになります。

app/controllers/follow_relationships.rb
def create
    @user =User.find(params[:follow_relationship][:following_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html {redirect_back(fallback_location: root_url)}
      format.js
    end
  end

  def destroy
    @user = User.find(params[:follow_relationship][:following_id])
    current_user.unfollow(@user)
    respond_to do |format|
      format.html {redirect_back(fallback_location: root_url)}
      format.js
    end
  end
end

respond_to do |format|はリクエストの種類によってフォロー/アンフォローした際にレンダリングするビューを指定しています。クライアント側の設定でjsが無効になっている場合にhtml側の処理を書いておかないとエラーが出るので注意してください。
[format.html]ではredirect_backメゾットで直前のページを表示、表示できなければroot_urlに戻しています。※redirect_toを使用してflashを表示するパターンも設定できます。
jsでリクエストがくれば[format.js]を通って、follow_relationshipのcreate.js.erbへ処理が向かいます。format.jsの後にrender先を指定しない場合は自動的にアクションに対応するjsファイルを読み込みます。この場合はこの後記載するcreate.js.erbが該当するファイルになります。

app/controllers/follow_relationships.rb
flashバージョン
def create
    @user =User.find(params[:follow_relationship][:following_id])
    current_user.follow(@user)
    respond_to do |format|
      format.html { redirect_to @user, flash: {success: 'フォローしました!'} }
      format.js
    end
  end

4.フォローボタン作成

コントローラーのアクションができたのフォローボタンを作成します。その前にフォロー一覧とフォロワー一覧ページを作成しておきます。今回はUserのshowページにリンクを用意してます。※フォロー機能には直接の関係はありません。

app/views/users/show_html
<%= link_to "フォロー(#{@user.followings.count})", followings_user_path(@user), class: "nav-link" %>

<%= link_to "フォロワー(#{@user.followers.count})", followers_user_path(@user), class: "nav-link" %>

フォローボタンも同じくshowページに配置しますがコードがグチャグチャにならないようにボタンのフォームはrenderしてます。

app/views/users/show_html
<% if logged_in? && @user != current_user%>
  <div id="follow_form">
    <% if current_user.following?(@user) %>
      <%= render "unfollow" %>
    <% else %>
      <%= render "follow" %>
    <% end %>
  </div>
<% end %>

if logged_in? && @user != current_user
→ログイン済みであることと、このshowページが自分以外のユーザーのページであることを確認しています。&&はAND条件を表すので全ての条件を満たさないとボタンが
表示されません。!=は等しくないときtureとなります。
if current_user.following?(@user)
→ログイン中のユーザーがすでにフォロー済みであればunfollowをまだフォローしていなければfollowボタンを表示するようにしています。
次は実際のボタンの中身です。

app/views/users/follow_html
<%= form_with(model: current_user.following_relationships.build) do |f| %>
  <%= f.hidden_field :following_id, value: @user.id %>
  <%= f.submit "フォローする", class: "btn btn-outline-secondary" %>
<% end %>

from_withはデフォルトでAjax通信をするようになっています。
modelにはモデルクラスのインスタンス(@userとか)を渡す必要があるので、空のfollow_relationshipインスタンスを作成してpostリクエストを動作させ、follow_relationshipのコントローラーのcreateアクションに渡します。(userを作成するときにコントローラーのアクションでUser.newをする意味と同じです。多分)
f.hidden_fieldでfollowing_idにユーザーidを入れるようにしています。
※クライアント側には見えずに、ボタンを押した時にパラメーターに入ります。

app/views/users/unfollow_html
<%= form_with(model: current_user.following_relationships.find_by(following_id: @user.id),method: :delete) do |f| %>
  <%= f.hidden_field :following_id %>  
  <%= f.submit "フォロー", class: "btn btn-outline-secondary" %>
<% end %>

フォロー解除ボタンはフォローボタンとは違ってdeleteリクエストを指定してdestroyアクションを動作させています。

5.jsファイルの作成

フォームからのPostリクエストをcreateアクションで処理したあとはcreate.js.erbにたどり着きます。そこでAjaxでレンダリングする部分(今回の場合はボタン)を指定します。create.js.erbにたどり着くのはcreateアクション後なのでレンダリングするのはフォロー解除ボタンになります。

app/views/follow_relationships/create.js.erb
$("#follow_form").html("<%= j(render("users/unfollow")) %>");
app/views/follow_relationships/destroy.js.erb
$("#follow_form").html("<%= j(render("users/follow")) %>");

それぞれ対応するフォームを読み込みます。follow_formはshowページに記載したcssのidです。
以上でフォロー機能が動作するようになりました。
最後にフォロー機能の流れだけまとめで記載しておきます。

6.フォロー機能の流れ
実際にフォローボタンを押すとこのような動きをサーバーのログから確認できます。
※流れを追うだけなので一部省略してます。

Started POST "/follow_relationships"→フォローボタンが押される
Processing by FollowRelationshipsController#create as JS
Parameters:"follow_relationship"=>{"following_id"=>"3"}→パラメータにfollow_relationshipをキーとしたフォロー対象のユーザーidが入ります。
コントローラーのアクション完了!
Rendering follow_relationships/create.js.erb→アクション後にjsのファイル読み込み。
Rendered users/_unfollow.html.erb (2.1ms)
Rendered follow_relationships/create.js.erb (3.2ms)

以上となります。
かなり長くなってしまいましたが、最後までお読みいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む