- 投稿日:2020-03-24T22:40:28+09:00
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があまり理解できていなかったので、この機会復習もできました。
理解が間違っていましたらぜひ教えてください。
読んでくださりありがとうございました!!
- 投稿日:2020-03-24T22:31:16+09:00
商品出品画面を作る
メルカリの商品出品画面を参考にコピーを作る
参考画面メルカリ
使用する機能
- 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 /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 © Mercari, Inc.items_new.scssa { 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.rbdef 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 enditem.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階層以降がまだ未実装でした・・・
とりあえず、後から実装するとして、先ずは画像投稿から
- 投稿日:2020-03-24T22:19:00+09:00
7つのアクション
今回は、コントローラで使用する7つのアクションについて紹介します!
アクションとは、ルーティングがリクエストを受け取った時に動くものですアクション毎に行われる処理を分けて記述することによって、役割をわかりやすく分類化できます!
アクション名 どんなリクエストに対応して動く? index 一覧ページを表示する new 新規投稿ページを表示する create データの投稿を行う show 個別の詳細ページを表示する edit 投稿編集ページを表示する update データの編集を行う destroy データの削除を行う この7つのアクションは基本になる所なので、ぜひ覚えて活用できるようにしてみましょう^^!
- 投稿日:2020-03-24T21:58:42+09:00
【Rails】アクセス制限を記述する手順と、その思考手順をまとめてみました
はじめに
Railsを学習した直後のアウトプット時、いざアクセス制限機能を実装しようと試みた際に、何をどうしたらいいのか思い浮かばなかったのが悔しかったため
次に同じ轍を踏まないよう、自分用にアクセス制限の実装順序を整理してみました。
まず、アクセス制限って「何を」実装する機能なのん?
主にWebアプリケーションにおいてログイン機能を実装した後で
ユーザーがログインしている場合と、ログインしていない場合とで、使える機能を制限する機能。
それじゃあ、「どうすれば」実装できるのか?
例えば
・ユーザーがログインしている場合で一つ条件式を記述し、制限用の処理を記述する。
・ユーザーがログインしていない場合でもう一つ条件式を書き、制限用の処理を記述する。例
if #(ユーザーがログインしている場合の条件式) redirect_to("URL") #制限用の処理 end
コードは「どこに」記述するん?
簡単な掲示板アプリ作成を例として、「ユーザーがログインしていない場合」のアクセス制限機能を実装したいとします。
1.Railでは、主にルーティング→アクション→ビューの順でデータのやり取りが行われるので
2.例えば、ログインしていないユーザーに投稿機能のアクセス制限を実装したい場合は
3.投稿機能用のアクションにデータが渡されたタイミングで、ログインしていないユーザーを弾けば良いということになりますので
4.投稿用のアクション最上部に、「ユーザーがログインしていない場合」の条件式と処理を記述してあげます。
5.そうすれば、投稿用のアクションが実行される前に、アクセス制限の処理が実行されます。よってコードをどこに記述すればいいのかと言うと、制限を実装したい機能のアクション内の上部、ということになります。
例
posts_controller.rbdef #投稿用アクション if #(ユーザーがログインしていない場合の条件式) redirect_to("URL") #指定したURLに飛ばす end #投稿用の処理 endbefore_actionとは文字通り、actionのbeforeに実行される
ただ投稿制限の他にも、投稿を閲覧する機能や投稿を編集する機能なども制限したい場合もあるかと思います。
その際、制限したいアクション全てに対し条件式を書こうとするのは、同じ記述をすることになり好ましくない上に面倒臭いです……。そこで、全てのアクションに対して一括で処理を行う方法が存在します。
どのように全てのアクションに対し一括で処理を行うかというと、applicationコントローラに処理を記述してあげます。
applicationコントローラとは、全てのコントローラを管理している親のような存在です。
画像の一番上にあるのが、applicationコントローラで
その直下に他のコントローラが羅列され、applicationコントローラの処理を継承しています。
そのため、applicationコントローラで記述された処理は、別のコントローラで実行されることになります。ただし注意する点があり、「before_action :メソッド名」という処理をコントローラ内の上部に記述してあげる必要があります。
言葉だけでは伝わりにくいと思いますので、コード例を記述してみます。application_controller.rbbefore_action :#メソッド名 # ログインしていない場合のアクセス制限 def #メソッド名 if #(アクセスしたユーザーがログインしていない場合の条件式) redirect_to("URL") #指定したURLに飛ばす end endこのようにapplicationコントローラ内に「before_action :メソッド名」と記述してあげることで、メソッド内で定義した処理が全てのコントローラのアクションが実行される前に実行されることになります。
before_actiontに関しては文字通り、全てのアクションの前に実行される処理という風に覚えておけば、用途をしっかり理解して使えるようになるかと思います。
before_actionを特定のアクションにだけ適用させる方法があるだと!?
全てのアクションへ処理が適用されるとは言っても、特定のアクションにだけアクセス制限を適用させたいという場合もあるでしょう。
そう言った場合には、特定のアクションにだけアクセス制限の処理を適用させる方法が存在します。applicationコントローラ内で記述した「before_action :メソッド名」を
適用させたいアクションが記述されているコントローラの上部に記述した上で
メソッド名に続けて「, {only: [:アクション名, :アクション名,...]}」として
[ ]内に、各コントローラ内のアクセス制限を適用させたいアクション名を記述してあげることで、特定のアクションにだけアクセス制限を適用することができます。例
posts_controller.rbbefore_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そのもののデータやり取りの仕組みを理解することが重要だと思ったので、今回のように遠回りするような解説をさせて頂きました。
僕も慣れるまでは、この思考手順で学習を進めるつもりです。参考
- 投稿日:2020-03-24T21:58:42+09:00
【Rails】アクセス制限を記述する手順をまとめてみました!
はじめに
Railsを学習した直後のアウトプット時、いざアクセス制限機能を実装しようと試みた際に、何をどうしたらいいのか思い浮かばなかったのが悔しかったため
次に同じ轍を踏まないよう、自分用にアクセス制限の実装順序を整理してみました。
まず、アクセス制限って「何を」実装する機能なのん?
主にWebアプリケーションにおいてログイン機能を実装した後で
ユーザーがログインしている場合と、ログインしていない場合とで、使える機能を制限する機能。
それじゃあ、「どうすれば」実装できるのか?
例えば
・ユーザーがログインしている場合で一つ条件式を記述し、制限用の処理を記述する。
・ユーザーがログインしていない場合でもう一つ条件式を書き、制限用の処理を記述する。例
if #(ユーザーがログインしている場合の条件式) redirect_to("URL") #制限用の処理 end
コードは「どこに」記述するん?
簡単な掲示板アプリ作成を例として、「ユーザーがログインしていない場合」のアクセス制限機能を実装したいとします。
1.Railsのデータのやり取りは、基本的にルーティング→アクション→ビューの順で行われ
2.例えば、ログインしていないユーザーに投稿機能のアクセス制限を掛けたい場合は
3.投稿用のアクション最上部に、「ユーザーがログインしていない場合」の条件式と処理を記述してあげます。
4.そうすれば、投稿用のアクションが実行される前に、アクセス制限の処理が実行されます。なので、位置的に言えばアクション内の上部に書けばいいということなのですね。
例
posts_controller.rbdef #投稿用アクション if #(ユーザーがログインしていない場合の条件式) redirect_to("URL") #指定したURLに飛ばす end #投稿用の処理 endbefore_actionとは文字通り、actionのbeforeに実行される
ただ投稿制限の他にも、投稿を閲覧する機能や投稿を編集する機能なども制限したい場合もあるかと思います。
その際、制限したいアクション全てに対し条件式を書こうとするのは、同じ記述をすることになり好ましくない上に面倒臭いです……。そこで、全てのアクションに対して一括で処理を行う方法が存在します。
どのように全てのアクションに対し一括で処理を行うかというと、applicationコントローラに処理を記述してあげます。
applicationコントローラとは、全てのコントローラを管理している親のような存在です。
画像の一番上にあるのが、applicationコントローラで
その直下に他のコントローラが羅列され、applicationコントローラの処理を継承しています。
そのため、applicationコントローラで記述された処理は、別のコントローラで実行されることになります。ただし注意する点があり、「before_action :メソッド名」という処理をコントローラ内の上部に記述してあげる必要があります。
言葉だけでは伝わりにくいと思いますので、コード例を記述してみます。application_controller.rbbefore_action :#メソッド名 # ログインしていない場合のアクセス制限 def #メソッド名 if #(アクセスしたユーザーがログインしていない場合の条件式) redirect_to("URL") #指定したURLに飛ばす end endこのようにapplicationコントローラ内に「before_action :メソッド名」と記述してあげることで、メソッド内で定義した処理が全てのコントローラのアクションが実行される前に実行されることになります。
before_actiontに関しては文字通り、全てのアクションの前に実行される処理という風に覚えておけば、用途をしっかり理解して使えるようになるかと思います。
before_actionを特定のアクションにだけ適用させる方法があるだと!?
全てのアクションへ処理が適用されるとは言っても、特定のアクションにだけアクセス制限を適用させたいという場合もあるでしょう。
そう言った場合には、特定のアクションにだけアクセス制限の処理を適用させる方法が存在します。applicationコントローラ内で記述した「before_action :メソッド名」を
適用させたいアクションが記述されているコントローラの上部に記述した上で
メソッド名に続けて「, {only: [:アクション名, :アクション名,...]}」として
[ ]内に、各コントローラ内のアクセス制限を適用させたいアクション名を記述してあげることで、特定のアクションにだけアクセス制限を適用することができます。例
posts_controller.rbbefore_action :#メソッド名, {only: [:投稿用アクション名, 投稿表示アクション名,...]} def #投稿用アクション名 #処理 end def #投稿表示用アクション名 #処理 endこうすることにより、各々アクションが実行される前に、before_actionの処理が呼び出され、各アクションへのアクセス制限が可能となります。
まとめ
「アウトプットでアクセス制限を実装したい!」となった場合、考える順序として
1.まずアクセス制限とは、ログイン機能実装後に「ログインしているユーザーへの機能制限」や「ログインしていないユーザーへの機能制限」を行うこと。
2.「投稿を制限したい」「投稿の閲覧を制限したい」など、「どんなアクセス制限」を実装したいか思い浮かべる。
3.例えば、投稿を制限したいなら「投稿用のアクション」など、コードを記述すべき場所を押さえる。
4.該当アクション内に、「ユーザーがログインしていない場合は〜〜の処理を行う」などの「条件式」を記述する。
5.ただし複数個に渡って同じ記述をするのは面倒臭い……。
6.複数アクションに制限を掛けたい場合は、applicationコントローラを使えば、一つの記述で済むことを思い出す。
7.applicationコントローラ内に、条件式とアクセス制限の処理を記述する。
8.各コントローラ内のアクションが実行される前に、アクセス制限処理を実行しなければならないことを思い出す(before_action :メソッド名)。
(9.特定のアクションにアクセス制限を掛けたい場合の処理は、「 before_action :メソッド名, {only: [:アクション名, :アクション名,...]} 」を、各コントローラ内の上部に記述する。)
参考
- 投稿日:2020-03-24T20:43:05+09:00
[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
)のインデントの深さがdef
,class
,module
と同等か有効 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のミックスイン:include
,prepend
,extend
・定数定義
・クラス関係定義:belongs_to
,has_one
,has_many
・クラスマクロ:attr_accessor
,attr_writer
,attr_reader
・検証等その他マクロ:validates
,validate
・Publicクラスメソッド
・Initializer
・Publicインスタンスメソッド
・Protectedクラスマクロ:attr_accessor
,attr_writer
,attr_reader
・Protectedインスタンスメソッド
・Privateクラスマクロ:attr_accessor
,attr_writer
,attr_reader
・Privateインスタンスメソッド無効 Layout/ClosingHeredocIndentation ヒアドキュメントの識別子の揃いと、閉じ識別子が内部要素のより内側にあるか 有効 Layout/ClosingParenthesisIndentation 複数行のメソッド定義、メソッド呼出し、グループにおける閉じ括弧 )
が統一されたインデントがされているか有効 Layout/CommentIndentation コメント行が統一されたインデントがされているか 有効 Layout/ConditionPosition 条件文の判断条件が if
,while
,until
と同じ行に書かれているか有効 Layout/DefEndAlignment メソッド定義スコープ( def ~ end
)におけるend
が統一されたインデントがされているか
※ デフォルトはEnforcedStyleAlignWith: start_of_line
有効 Layout/DotPosition 複数行におけるレシーバーとメソッドの区切り .
が統一されたインデントがされているか
※ デフォルトはEnforcedStyle: leading
有効 Layout/ElseAlignment 条件文における else
がif
,while
,until
と揃っているか有効 Layout/EmptyComment 空のコメント行が統一されたインデントがされているか
※ デフォルトはAllowBorderComment: true
,AllowMarginComment: 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: true
,AllowBeforeTrailingComments: false
,ForceEqualSignAlignment: 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: key
,EnforcedColonStyle: 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 例外処理スコープ内の rescue
,ensure
にインデントがされているか有効 Layout/SpaceAfterColon コロン( :
)の直後に半角スペースが存在するか有効 Layout/SpaceAfterComma コンマ( ,
)の直後に半角スペースが存在するか有効 Layout/SpaceAfterMethodName メソッド名の直後に半角スペースが存在するか 有効 Layout/SpaceAfterNot 否定演算子( !
)の直後に半角スペースが存在しないか有効 Layout/SpaceAfterSemicolon セミコロン( ;
)の直後に半角スペースが存在するか有効 Layout/SpaceAroundBlockParameters ブロック引数前後に半角スペースが存在するか 有効 Layout/SpaceAroundEqualsInParameterDefault メソッド定義のデフォルト引数における =
演算子の前後に半角スペースが存在するか有効 Layout/SpaceAroundKeyword do
,if
等キーワード前後に半角スペースが存在するか有効 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
,while
,until
などの条件文において、判断条件の代入式の右辺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
,while
,until
などの条件文において、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. protected
,private
)が特異メソッドの定義に使われていないか
※ 特異メソッドの定義にはprivate_class_method
を使う有効 Lint/InheritException 継承する例外のスーパークラスが正しく指定されているか
※ デフォルトはEnforcedStyle: runtime_error
有効 Lint/InterpolationCheck シングルクオーテーション内で式展開が行われていないか 有効 Lint/LiteralAsCondition if
,while
,until
などの条件文において、&&
, || の中で判断条件や演算対象として使われているリテラルが正しく使われているか有効 Lint/LiteralInInterpolation 変数やメソッド呼出し以外で不要な式展開がされていないか 有効 Lint/Loop begin ~ end
,until
,while
等でループ処理が正しく書かれているか有効 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_i
,to_f
,to_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 モジュールのミックスインにおいて、プライベートメソッド呼出しの send
,public_send
,__send__
が使われていないか
※ Ruby2.0までinclude
,prepend
はプライベートメソッドであったが、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 Fixnum
,Bignum
等の非推奨の定数が使われていないか有効 Lint/UnreachableCode return
でメソッドを抜けた後に処理されないロジックが存在しないか有効 Lint/UnusedBlockArgument 利用されていないブロック引数が存在しないか
※ デフォルトはIgnoreEmptyBlocks: true
,AllowUnusedKeywordArguments: false
有効 Lint/UnusedMethodArgument 利用されていないメソッド引数が存在しないか
※ デフォルトはAllowUnusedKeywordArguments: false
,IgnoreEmptyMethods: true
,IgnoreNotImplementedMethods: true
有効 Lint/UriEscapeUnescape 非推奨のメソッドがユースケースに応じて推奨のメソッドに代替出来るか
・URI.escape
=>CGI.escape
,URI.encode_www_form
,URI.encode_www_form_component
・URI.unescape
=>CGI.unescape
,URI.decode_www_form
,URI.decode_www_form_component
有効 Lint/UriRegexp ・ URI.regexp
が使われていないか
・URI::DEFAULT_PARSER.make_regexp
が使われているか有効 Lint/UselessAccessModifier 無意味に public
,private
などのアクセサが使われていないか有効 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.open
,IO.popen
,URI#open
を使う方が安全である有効 Security/YAMLLoad 信頼していないソースコードを読み込んだ際に遠隔でのコード実行に繋がる潜在的なセキュリティ問題を孕む YAML
クラスメソッド が使われていないか有効 Style
作業中
- 投稿日:2020-03-24T18:23:56+09:00
Rails 投稿機能
Railsで投稿機能の作成です
部分的な備忘録ですので分かりづらいと思いますrailsアプリケーション作成
$ rails _5.2.3_ new sample_boad -d mysqlGem追加
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.rbclass 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 endrake db:migrateでマイグレーションファイルを実行
ルーティング
投稿一覧を表示するindexアクション 投稿画面を作成するためにnewアクション 投稿された内容を保存するためにcreateアクションを使います。
route.rbRails.application.routes.draw do resources :tweets only: [:index,:new,:create] endコントローラー
private以下はストロングパラメーターです。
ビューでフォームに入力された情報は、コントローラにキーと一緒にパラメーターとして送られます。
ストロングパラメーターは、指定したキーを持つパラメーターのみを受け取るようにするものです。
不正な情報を送信しようとしたときに、ストロングパラメーターを設定しておくと、不正な情報を受け取らずにすみます。$ rails g controller tweetstweets.controller.rbclass 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 endTweet.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>
- 投稿日:2020-03-24T18:16:04+09:00
RailsでRSpecを追加しようとした時に発生したエラー「Could not find diff-lcs-1.3 in any of the sources Run `bundle install` to install missing gems.」の対処法
Gemfilegroup :development, :test do gem 'rspec-rails' endGemfileに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 = 前もってロードしておく)
参考
- 投稿日:2020-03-24T16:11:23+09:00
rails 新規アプリ作成
はじめに
今まで学習した知識を使って、オリジナルアプリを作るカリキュラムの段階まできました。
自身のアウトプット、皆さんへの共有の目的で、これから個人アプリが完成するまでの流れを投稿していきます。Railsで新規アプリを作成する
ターミナルにて
$ rails _5.2.3_ new 〇〇 -d mysqlを、実行する。
railsのあとの『5.2.3』はrailsのバーション指定。
〇〇は作成したいアプリ名。
-dはデータベースの意味。
私の場合、データベースはmysqlを使用したかったので、-dで指定しました。作成するアプリのディレクトリへ入り、ターミナルにて
rails db:createを実行してデータベースを作る。
ここでひとまずアプリの大枠は完成です。
試しにターミナルにて
rails sでサーバーを立ち上げ、localhost:3000へアクセスすると
こちらの画像が表示され、正常にアプリが作られていることがわかります。
- 投稿日:2020-03-24T15:52:46+09:00
deviseでログイン機能を実装したものの、データベースへ書き込めない問題
起きた問題
deviseにてユーザーのログイン機能やユーザー編集機能などを実装したが、ユーザー編集機能のページedit.html.erbにて入力した内容がうまくデータベースに反映されない。
具体的にいうとnameのカラムだけなぜかdbに反映されなかったので、調べてみた。
schema.rbを見てもちゃんとカラムが追加されている。。。
解決した方法とソース
まず、解決した方法は簡単で、application_controller.rbに以下のコードを追加するだけ。
application_controller.rbclass 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 enddeviseではどうやらデフォルトでemail、password以外は許可しない設定になっているらしく、上記のようなコードを追加することで許可してあげないといけないようだった。
以下のURLはdeviseのGithub READMEから。
- 投稿日:2020-03-24T15:04:44+09:00
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参考資料
- 投稿日:2020-03-24T14:40:36+09:00
【Rspec・devise token auth】Rails APIアプリのRspecテストでユーザログイン・認証を実装する方法
Rspecテスト内で
devise token auth
をつかってユーザログイン・認証するのはどうするんだ!?
Devise
のやり方はたくさんあるのですがdevise token auth
は探すのに苦労したので、私と同じ状況に陥っている方のためにこの記事を残しておきます。やりたいこと
下記の様に、
BooksController
にcreate
メソッドがあり、create
メソッドを実行するにはユーザが認証が必須(authenticate_user!
)だとします。books_controller.rbclass 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 endRspecテストでユーザ認証を実装せずに書くと下記の様な感じになりますが・・・
books_spec.rbrequire '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でのテストの書き方を解説したいと思います。手順
- ヘルパーモジュールを作り
sign_in
メソッドを実装する- ヘルパーモジュールをRspecで読み込む
- テストでヘルパーモジュールを使って
sign_in
するヘルパーモジュールを作り
sign_in
メソッドを実装する
lib
ディレクトリ直下にauthorization_spec_helper.rb
という名前のファイルを作成します。下記のコードを貼り付けます。lib/authorization_spec_helper.rbmodule 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として返ってきたclient
、access-token
、uid
を返すヘルパーメソッドです。ヘルパーモジュールをRspecで読み込む
ヘルパーモジュールをRspecで使える様にするために下記の一文を追加します。
rails_helper.rbRSpec.configure do |config| # .... # .... # .... config.include AuthorizationSpecHelper, type: :request endテストでヘルパーモジュールを使って
sign_in
するこれでヘルパーモジュールを使う準備ができました。あとは
books_spec.rb
を修正していくだけです。books_spec.rbrequire '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調べるまでが大変でしたが、実装自体はとても簡単でした。お役に立てれば嬉しいです!
- 投稿日:2020-03-24T10:51:08+09:00
[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 1ActiveRecord Relationに関係するメソッドについて
データーベースに入っているデータの取り出しを行うものが多く、下記の様なメソッドを使用するとActiveRecord::Relationのインスタンス(後述)を返します。
例- order - where - limitActiveRecord::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クラスの存在が少しでも身近に感じていただけたら嬉しいです!
これからも頑張ります~?
- 投稿日:2020-03-24T09:27:04+09:00
コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.12 - TDDでPost機能をコーディング part1 -
はじめに
お待たせしました!第12回にしてやっとつぶやき機能を実装してまいります!ここではPost機能といいますね。
前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
どんなの実装するの?
最初に今回のゴールを見据えておきましょう。
画面遷移
新たにポストページを作ります。
ポストページはポストを投稿するフォームと、今までのユーザー全員のポストが投稿日時降順で表示される機能を具備しています。
ポストページはサインイン済のユーザーしかアクセスできません。
ポストの投稿ユーザーをクリックしたらそのポストのプロフィールページに遷移できます。
ユーザーのプロフィールページでは、そのユーザーの過去のポストが投稿日時降順で表示されています。ER図
テストシナリオ
さて、これを踏まえて今回のテストシナリオを考えてみましょう。
- 未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
- サインイン済のユーザーが、ポストページにアクセスしようとしたとき、ポストページにアクセスできること
- 未サインインのユーザーは、トップページでヘッダーにポストページへのリンクを見つけられないこと
- 未サインインのユーザーは、サインアップページでヘッダーにポストページへのリンクを見つけられないこと
- 未サインインのユーザーは、サインインページでヘッダーにポストページへのリンクを見つけられないこと
- 未サインインのユーザーは、ユーザー詳細ページでヘッダーにポストページへのリンクを見つけられないこと
- サインイン済のユーザーが、プロフィールページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
- サインイン済のユーザーが、ポストページでヘッダーのポストリンクをクリックしたとき、ポストページに遷移すること
- サインイン済のユーザーは、ポストページでポストを入力できること
- ポストページでポスト未入力のユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト未入力のエラーメッセージを確認できること
- ポストページでポストを141文字以上入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿は失敗しポスト文字数超過のエラーメッセージを確認できること
- ポストページでポストを正しく入力したユーザーが、「ポストする」ボタンをクリックしたとき、ポスト投稿が成功しポスト入力フィールドがクリアされ、ポスト一覧の最上部に投稿したポストを確認できること
- サインイン済のユーザーは、ポストページで全ユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ポストページでポストのユーザー名をクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- 未サインインのユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- 未サインインのユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、ユーザー詳細ページでそのユーザーのポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、ユーザー詳細ページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
- サインイン済のユーザーは、プロフィールページで自身のポストを投稿日時降順で閲覧できること
- サインイン済のユーザーが、プロフィールページでそのユーザーのポストのユーザー名をクリックしたとき、何も起こらないこと
こんな感じでしょう。
では元気にデベロップしてまいりましょう!今回はTDDで開発をしていくので、テストコードを書いて、アプリコードを書いて、を繰り返していきます。開発スタート
まずは、それぞれのテストシナリオをテストコードに落とし込みましょう。
開発はTDDで進めるので
- テストをコーディングする(Red)
- テストコードがパスするようにアプリケーションをコーディングする(Green)
- 非効率な記述があれば、Greenをキープしながらコーディングし直す(Refectoring)
です。
ではコンテナを起動しておきましょう!
$ docker-compose up -d $ docker-compose exec web ash未サインインのユーザーが、ポストページにアクセスしようとしたとき、トップページにリダイレクトされること
ポストページについては以下の仕様で実装していきます。ポストページのURLパスは
/posts
とします。まずはテストコードを書いていきます。今回のテストシナリオ用に新しいテストシナリオファイルを作りましょう。
# touch spec/system/07_posts_spec.rb07_posts_spec.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbRails.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.rbclass PostsController < ApplicationController def index # 未サインインの場合、トップページにリダイレクトする + redirect_to root_path unless signed_in? end end以前、サインイン済ならプロフィールページにリダイレクトさせる、という機能を作りましたね。今回はそれの条件が逆バージョンです。
今回使っているunless
はif
の逆の分岐です。つまり、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.rbfeature "ユーザーとして、ポストを投稿したい", 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_link
のid
を付与することにします。spec/system/07_posts_spec.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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 Smith
をcreate
したテストケースがありました。
これを簡易に使いまわせるように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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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を確認してみましょう。
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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbfeature "ユーザーとして、ポストを投稿したい", 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.rbclass 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.rbclass User < ApplicationRecord ... + has_many :posts ... end
ちなみにマイグレーションファイルの中身もみておきましょう。
db/migrate/YYYYMMDDhhmmss_create_posts.rbclass 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: true
がuser_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.rbclass 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の技術ですが、input
やtextarea
に何も入力がない時に限り、そのフォームの補助の役割でどういうものを入力すればいいかを表示してあげる機能です。
autofocus: true
はページが表示された時に自動的にフォーカスされるフィールドを指定できるHTML5の機能です。便利なのでつけときます。ここまでで再度テストを実行してみましょう。
# 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
- 投稿日:2020-03-24T01:29:00+09:00
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。
- 投稿日:2020-03-24T00:18:27+09:00
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:references2.各モデルの関連付け、フォロー機能のメゾット作成
作成されたマイグレーションファイルを確認します。
db/migrateclass 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.rbbelongs_to :follower, class_name: "User" belongs_to :following, class_name: "User" validates :follower_id, presence: true validates :following_id, presence: trueapp/model/user.rbhas_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 end3.コントローラーの作成
まずはコントローラーを作成します。
rails g controller follow_relationships:
続いてルーディングの設定です。フォロー/フォロワーの一覧ページ用のアクションはUserオブジェクト絡みの機能なのでコントローラー内に記載します。
そのために、resources: userにmemberを使用してルートを追加します。
memberを使用するとURL内にユーザーを識別するidが追加されます。
idは後ほどUserコントローラーの対応するアクションないで必要となるのでmemberでルートを追加しないといけません。(idを追加しないcollectionというメゾットもあります。)confing.routes.rbresources :users do member do get :following, :followers end end resources :follow_relationships, only: [:create, :destroy]一覧ページ用のアクションはこのようになります。(kaminariのページネーション機能を利用しています。)
※それぞれ対応するビューを作成してください。app/controllers/users.rbdef 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' endfollow_relationshipのコントローラーはこんな感じです。
followとunfollowが先ほどUser.rbに記載したメゾットになります。app/controllers/follow_relationships.rbdef 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 endrespond_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 end4.フォローボタン作成
コントローラーのアクションができたのフォローボタンを作成します。その前にフォロー一覧とフォロワー一覧ページを作成しておきます。今回は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)以上となります。
かなり長くなってしまいましたが、最後までお読みいただきありがとうございました。