20200403のRailsに関する記事は14件です。

初心者の気持ちがわかる!! Railsチュートリアル第1章(1.1 1.2)

初心者の気持ちがわかる!!Railsチュートリアル 1.1 1.2

投稿の目的

1、私自身(初心者)が躓いた部分の解決方法をできるだけわかりやすく記載(urlのみの場合あり)する事で、初心者の方が躓いてもリタイアする事なく、Railsチュートリアルを完走できることを目的とする。(読者)

2、1、2周目で学んだことをoutputする場とし、曖昧な部分を明確にし、理解を深める。(筆者)

筆者の現状

Railsチュートリアル1周目が終わりました。

とにかく手を動かし、理解は5割ぐらいというところでしょう、、、

そこで2周目は80~90%理解を目指して、進めていくつもりです。

1周目でわかりにくかった所、その際参考にしたサイトなど控えております。

それでは、早速、、、

Railsチュートリアル (1.1 1.2)

1.1 はじめに

1.1.1 前提知識

私は、Railsチュートリアルに取り掛かるまでに,
ProgateのHTML,CSS,コマンドラインコース,Gitコースをやっておきましょう。
完璧にする必要はありません。1、2周ぐらいで十分です。
(私は、上記に加えて、
Progate:Javascript(以後js),jQuery
ドットインストール:HTML,CSS,js,jQuery
を完了させていました。
余裕があればやってみてください。後々必要です。)

1.1.2 この本における取り決め

さらっと読んでおきましょう。

1.2 さっそく動かす

1.2.1 開発環境

本チュートリアルでは開発環境はIDE(統合開発環境)であるAWS Cloud9を使用します。

開発環境はローカル開発環境とIDE(統合開発環境)の二つに分けられます。
簡単にいうと、
ローカル開発環境:自分でカスタマイズしないといけない
→初心者には大変、、
IDE:ある程度元からカスタマイズされている。
→煩雑で時間のかかるカスタマイズをしなくてすむ。

AWS Cloud9のクラウドIDEを利用する手順はRailsチュートリアルの解説通り。

1.2.2 Railsをインストールする

最初に、Rubyドキュメントのインストールで無駄な時間を使わないための、コマンドを追加する。
(これはrailsチュートリアルに現段階では理解する必要がないとの記載のため追究せず、、)

$ printf "install: --no-document \nupdate:  --no-document\n" >> ~/.gemrc

今回は初めてなので以下に写真も載せておく、
ターミナルに記載していく。
(コピーしてくる時は「$」もコピーしてこないように、、
$は元からコンソールに記載済み)
スクリーンショット 2020-04-03 21.13.58.png

次に、gemコマンドを使ってrailsライブラリをインストールする。

$ gem install rails -v 5.1.6

上記にてrailsの5.1.6バージョンをインストールする。

vは(version)のこと、
-v以降にversion番号をいれる事でversionの指定ができる。

Q. ライブラリとは? gemとは? gemコマンドとは?

A.
ライブラリとは、rubyを簡単に速く、書くための便利グッズ的なものです。

rubyには便利なライブラリ(パッケージ)がたくさんあります。

今回はrailsというライブラリをインストールします。(今後他のライブラリもどんどんインストールしていきます。)

gemとは、rubyのライブラリ(パッケージ)事をいい、

gemコマンドとは、ライブラリ(パッケージ)を管理するものです。
つまり、gemコマンドでインストールやアンインストールができます。

筆者コメント

私自身初心者なので、間違いがあると思います。
間違いがあれば指摘していただけると幸いです。

また、
もう少しここを詳しく!!、イマイチわからなかった!!
等あれば、コメントいただけるとできる限り改善しようと思います。
私自身の成長にも繋がると思いますので、、、

最後まで、読んでいただきありがとうございました。

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

【Rails】Ajax通信でコメント機能を実装する

Qiita投稿の背景

Railsアプリケーションでコメント機能を実装する際に、非同期通信(Ajax通信)を活用しています。
しかし、非同期通信(Ajax通信)の全体の流れや、記述の方法をなかなか理解できないため、アウトプット兼自分の備忘録として投稿します。

この記事の対象者

  • RailsのMVCを理解している方
  • 非同期通信(Ajax通信)について、「なんとなく」知っている方

環境

  • ruby 2.5.1
  • Rails 5.2.4.1

事前準備

モデルとアソシエーション(事前準備①)

user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :buys
  has_many :comments
end
buy.rb
class Buy < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
end
comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :buy
end

コントローラー(事前準備②)

comments_controller.rb
class CommentsController < ApplicationController
  def create
    @comment = Comment.create(comment_params)
    respond_to do |format|
      format.html {redirect_to "/buys/#{@comment.buy.id}"}
      format.json
    end
  end

  private
  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, buy_id: params[:buy_id])
  end
end

Railsのコントローラーで利用できる respond_to というメソッドは、リクエストがHTMLのレスポンスを求めているのか、それともJSONのレスポンスを求めているのかを条件分岐してくれます。

コメント送信フォームとコメント一覧(事前準備③)

buys/show.html.haml
/ 省略

.buy-comment
  - if current_user
    .buy-comment__send
      = form_for [@buy, @comment], html: {class: "buy-comment__send-form", id: "buy-comment__send-form"} do |f|
        = f.text_area :text, class: "buy-comment__send-text", placeholder: "コメントをする"
        = f.submit "SEND", class: "buy-comment__send-submit"
  .buy-comment__title
    〈コメント一覧〉
    - if @comments
      .ajax
        - @comments.each do |comment|
          .buy-comment__list
            .buy-comment__list-writter
              = "#{comment.user.name}:"
            .buy-comment__list-comment
              = comment.text

コメント送信フォームとコメント一覧は、BUYモデルの詳細ページに表示することとします。

gem'jquery-rails'をインストール

Gemfile
gem'jquery-rails'

bundle installをします。

application.js
//= require jquery ←を追加
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require_tree .

//= require jquery は必ず //= require_tree . より上段に記述する必要があります。
各jsファイルを読み込むより先にjqueryを読み込まないと、各jsファイルでjqueryを使用できないからです。

jbuilderを作成する

views/comments/create.json.jbuilder
json.text @comment.text
json.user_id @comment.user.id
json.user_name @comment.user.name

rails newコマンドでアプリケーションを作成した際にgemfileにデフォルトで記述されている'jbuilder'を活用します。'jbuilder'とは、入力データをJSON形式で出力するテンプレートエンジンのことです。

(JSONとは、JavaScript Object Notationの略で、データ交換を行うデータフォーマットの一種です。)

拡張子を「.json.jbuilder」とするとそのビューファイル内でjbuilderが提供する記述を使うことができます。jbuilderは、viewと同じように該当するアクションと同じ名前にする必要があります。
commentのcreateアクションに対応するjbuilderのファイルにするので、作成するのはviews/comments/create.json.jbuilderになります。jbuilderファイルでは基本的に「json.KEY VALUE」という形で書くことができます。
これによって、jbuilderで定義したキーとバリューの形で、javascriptファイルにデータを返すことができます。

jsファイルを作成する

comment.jsというファイルをassets/javascripts以下に作成します。
記述は長くなりますが、最終的にはこのような形になります。

comment.js
$(function(){
  function buildHTML(comment){
    var html = `<div class = buy-comment__list>
                  <div class = buy-comment__list-writter>
                    ${comment.user_name}:
                  </div>
                  <div class = buy-comment__list-comment>
                    ${comment.text}
                  </div>
                </div>`
    return html;
  }
  $('#buy-comment__send-form').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      var html = buildHTML(data);
      $('.ajax').append(html);
      $('.buy-comment__send-text').val('');
      $('.buy-comment__send-submit').prop('disabled', false);
    })
    .fail(function(){
      alert('error');
    })
  })
});

記述内容を一つずつ説明していきます。

  • 2行目〜12行目

コメントを送信して保存後に、追加するHTMLの箇所を記述しています。このような記述で実現できるのは、テンプレートリテラル記法を使用しているからです。
(テンプレートリテラル記法とは、ダブルクオートやシングルクオートの代わりにバックティック文字で囲むことで、複数行文字列や文字列内挿入機能を使用できます。)
buildHTMLの引数として渡されたcommentはjbuilderのデータであるため、create.json.jbuilder内で定義したキーとバリューの形式で使用することができます。(詳細は後述)
また、「 ${} 」(プレースホルダー)を用いることでテンプレートリテラルの中で変数を使うことができるようになります。

  • 13行目

on メソッドを使用して、formのsubmitを押下するとイベントが着火するようにしています。
今回の非同期通信(Ajax通信)の起点となります。

  • 14行目

フォームが送信される時、デフォルトの状態だとフォームを送信するための通信が行われるため、preventDefault()を使用してデフォルトのイベントを止めます。非同期通信(Ajax通信)にてcreateアクションを実行するためです。

  • 15行目

new FormData(フォーム要素)とすることでFormDataを作成できます。

(FormDataとは、formタグ内の要素をjavascriptのオブジェクトにしたもので、input要素に入力された内容がキーバリュー形式でjavascriptのオブジェクトとして取得できます。)

  • 16行目

attr メソッドとは、要素が持つ指定属性の値を返します。要素が指定属性を持っていない場合、関数はundefinedを返します。
イベントが発生した要素のaction属性の値を取得しています。action属性にはフォームの送信先のurlの値が入っています。

  • 17行目〜24行目

非同期通信(Ajax通信)でcreateアクションを行うための記述です。 .ajax の部分がメソッドの呼び出しとなっています。この記述によって、画面が遷移することなく、formの内容が保存されるようになります。
ajaxメソッドには、キーバリュー形式で引数を渡します。JavaScriptでは、このような形式のオブジェクトはオブジェクト型と呼ばれます。
dataTypeに'json'を指定することによって、コントローラーからはJSON形式(create.json.jbuilder経由)でレスポンスが返ってきます。(同期通信の場合は、特に指定せずともHTML形式のデータを返すようRailsが動いてくれています。)

オプション 説明
type HTTP通信の種類を記述します。通信方法は、GETとPOSTの2種類があります。
url リクエストを送信する先のURLを記述します。
data サーバに送信する値を記述します。
dataType サーバから返されるデータの型を指定します。
processData デフォルトではtrueになっており、dataに指定したオブジェクトをクエリ文字列に変換する役割があります。
contentType サーバにデータのファイル形式を伝えるヘッダです。デフォルトでは「text/xml」でコンテンツタイプをXMLとして返してきます。

(ajaxのリクエストがFormDataのときは、processDataとcontentTypeについては値を適切な状態で送ることが可能なため、falseにすることで設定が上書きされることを防ぎます。FormDataをつかってフォームの情報を取得した時には必ずfalseにするという認識で構わないかと思います。)

  • 25行目と31行目

ajaxメソッドの後につけることで、非同期通信(Ajax通信)が成功した場合と失敗した場合に、条件分岐することができます。いずれも、ajaxメソッドとセットとなるメソッドです。
done は通信が成功したときに、fail は通信が失敗したときに動きます。

  • 25行目〜30行目

dataという変数には、ajaxメソッドによるリクエスト(JSON形式で返してねというリクエスト)によって返ってきたレスポンスが代入されます。この場合のレスポンスは、create.json.jbuilderのデータです。
非同期通信(Ajax通信)によるコメント登録が成功した後は、2行目に定義したHTMLを append メソッドで元のHTMLに追加しています。
さらに、val メソッドを使用して、フォームのコメント入力欄を空にしています。
最後に prop メソッドを使用して、disabled属性をfalseにしています。

(disabled属性とは、ボタンが押せなく属性のことです。disabled属性をfalseにするということは、ボタンを押せるようにするということです。)

メソッド 説明
append 対象の要素の末尾にテキストやHTML要素を追加できるメソッド
val HTMLに記述されているvalue属性を取得できるメソッド
prop HTMLに記述されているid・class・name…などの属性や、checked・selected…などのプロパティを取得できるメソッド
  • 31行目〜33行目

通信が失敗したときに、alertを出力するようにしています。

処理の流れを整理

実装はここまでで完了です。
最後に処理の流れを確認しておきましょう。

  1. フォームが送信されたら、javascriptのイベントが発火
  2. javascriptファイル内で非同期通信でコメントが保存される
  3. コメントが保存された後のレスポンスは、JSON形式にするように指示
  4. コントローラー内でrespond_toを使用してHTMLとJSONの場合で処理を分岐
  5. jbuilderを経由して、作成したメッセージをJSON形式でjavascriptに返す
  6. jbuilderから返ってきたJSONをdoneメソッドで受取り、HTMLを作成

最後に

非同期通信(Ajax通信)は、どの部分を実装しているのかが大変分かりにくかったです。
(個人的には、多くの記述が一つのjavascriptファイルに集約されているところが難解でした、、、。)

全体の流れをしっかり理解した上で、一つずつコードを記述していくという意識が大事だなと感じました。

エンジニア転職に向けて、日々学習中です、、、、、

twitterも繋がっていただけたら嬉しいです。

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

turbolinkのページごとの使用・不使用について

色々調べたものの、意外と時間を食ったので書きました。

ページごとに消すには、2つの方法がある。

  1. data-turbolinks="false"を、ページのトップの親要素に入れる.
  2. content_for?を使用する

1. data-turbolinks="false"を、ページのトップの親要素に入れる.

1ページだけなど、使用しないページが少ない場合は、1番だけで済む。
ポイントは、最上位の親要素にいれること。
他の記事で、親要素に入れると書いてあるが、該当リンクの親要素では効かない。
ページ全体の親要素にいれること。

app/views/
<article data-turbolinks="false">
  <div>
    <a>
  </div>
</article>

こんな感じ。


2. content_for?を使用する

具体的な使用法はこちらに書いてあったので、ご紹介。
基本的には、キーを決めて、そのキーの有無でページの処理を決めるイメージ。

https://qiita.com/Cheekyfunkymonkey/items/216bf7426493e6213927


ほとんど1番の方で済みそうですよね。

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

nginx + unicorn を使用してみた (Mac & Heroku)

nginx + Unicorn + rails の導入方法(ローカル環境)

unicorn

Gemfileにunicornのgemを追加

Gemfile.rb
#Gemfileに追加
gem 'unicorn'
gem 'unicorn-rails'

bundle installを実行する

bundle install

unicornの設定ファイルを作成

unicorn.rb
# config/unicorn.rb に下記内容を記載
# ※パスは変更する。
rails_root = File.expand_path('../../', __FILE__)

worker_processes 2
working_directory rails_root

listen "#{rails_root}/tmp/unicorn.sock"
pid "#{rails_root}/tmp/unicorn.pid"

stderr_path "#{rails_root}/log/unicorn_error.log"
stdout_path "#{rails_root}/log/unicorn.log"

デーモンとして起動

# 対象のrailsアプリケーションのディレクトリ内にいること
unicorn_rails -c config/unicorn.rb -E development -D

停止

# 対象のrailsアプリケーションのディレクトリ内にいること
kill -QUIT `cat tmp/unicorn.pid`

nginx

brewコマンドでインストール

brew install nginx

起動確認

sudo nginx

設定ファイル編集

バックアップをしてからnginx.conf.defaultをコピーする。

cd /usr/local/etc/nginx
cp nginx.conf nginx.conf.YYYYMMDD
cp nginx.conf.default nginx.conf

nginx.confの編集を行う
※パスは変更する

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       8080;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
    include servers/*;
    upstream unicorn {
        # nginxとunicornの連携
        # unicorn.rbで設定したunicorn.sockを指定
        server unix:/Users/yotanaka/diveintocode/nginx_rails_app/tmp/unicorn.sock;
    }

server {
        listen 8081;
        server_name localhost;

        root /Users/yotanaka/diveintocode/nginx_rails_app/public;

        access_log /usr/local/var/log/nginx/nginx_app_access.log;
        error_log /usr/local/var/log/nginx/nginx_app_access_error.log;

        client_max_body_size 100m;
        error_page 500 502 503 504 /500.html;

        try_files $uri/index.html $uri @unicorn;

        location @unicorn {
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $http_host;
                proxy_pass http://unicorn;
        }
    }   
}

nginx再起動

sudo nginx -s reload

起動確認

下記アドレスにアクセスしRailsの画面が表示されることを確認する。

http://localhost:8081

nginx + Unicorn + rails の導入方法(HEROKU環境)

Herokuのアプリケーションを作成する

対象のアプリケーションフォルダ内で下記コマンドを実行する。

rails assets:precompile RAILS_ENV=production
git add .
git commit -m 'コミットメッセージ'
git push heroku master
heroku run rails db:migrate

buildpacksを導入

下記コマンドを対象のRailsアプリケーションフォルダ内で実行

heroku buildpacks:add heroku-community/nginx

設定ファイル修正

アプリケーションフォルダの直下にProcfileを作成する

作成後下記内容を記述

※ vim Procfile

web: bin/start-nginx bundle exec unicorn -c config/unicorn.rb

unicorn.rbを修正する。

※ vim config/unicorn.rb

unicorn.rb
require 'fileutils'
listen '/tmp/nginx.socket'
before_fork do |server,worker|
    FileUtils.touch('/tmp/app-initialized')
end

修正内容をコミットする

git add .
git commit -m 'コミットメッセージ '

Herokuにデプロイする

git push heroku master

参考

Nginx+UnicornでRailsを動かすon Mac

heroku-buildpack-nginx
Rails と Rack

なぜrailsの本番環境ではUnicorn,Nginxを使うのか?  ~ Rack,Unicorn,Nginxの連携について ~【Ruby On Railsでwebサービス運営】

Rails開発におけるwebサーバーとアプリケーションサーバーの違い(翻訳)

なぜRailsのWebアプリにunicornが必要なのか

Herokuに必要なProcfileの書き方についてまとめておく

unicornの起動と終了

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

[Rails]httpからhttpsにリダイレクトする方法

はじめに

オリジナルサービスを作成した際に、httpで作成しておりパスワードを入れる時に
「このサイトは危険です!」みたいな警告が流れてきました。
これは。と思い、URLを見るとhttpのままになっていました。パスワード打つ時にこんな警告が流れるともうそのサイト使いたくならないですよね笑

結論

単純ですが、 config/environments/production.rb にこの一文を書き足す・または修正するだけでhttps化できました。

config/environments/production.rb
config.force_ssl = true

まとめ

railsってなんでもありますね。ほんとすごい。
他にも設定することがあるのかもしれませんが、特に現状は問題なく経過しています。

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

nullを条件にしたクエリでデータを取ってくる方法 in ruby and firebase

一覧表示させるためにwhereを使う

hogehoge.rb
    def self.all
        list = collection.where("format", "=", null).order(:created_at).get
    end

上記のような形でformatというフィールドにnullを持ったドキュメントを全て持ってくるようなクエリを作るようなコードを書いているつもりなのですが、null部分がundefinedのエラーが出てきてしまい、取得することができなさそうなんですね。

ここの記事には、

Firestore select where a field is null [duplicate]

it is not possible to index on a field that is NOT in the document

と書いてあるし、無理なのかとも思ったんですが、更に下の方には、

However there is a workaround which consists in querying documents that contain properties with a null value data type

とあるので、いけそうでもある。

解決策

rubyではnullってnilで表すんだわ
そう、これですね。ruy上ではnullを扱うにはnilにするのでした。

hogehoge.rb
    def self.all
        list = collection.where("format", "=", nil).order(:created_at).get
    end

とすれば、firebaseからnullを条件にしてドキュメントを取ってくることができました。

おまけ

データを取得してくる時にコレクションに対して、インデックスがないから作れという形でエラーが出ることがあります。

9: The query requires an index. You can create it here: 長ったらしいURL

こんな感じのエラーが出るので、URLをコピペすると、firebaseコンソールに飛び、インデックスを作成するためのダイアログが開くので、ポチッと押してインデックスがビルドされるのを待ってください。ビルドには数分かかります。しばらく経ってまだビルド中みたいになっていてもコンソール自体に更新かけると終わってたりします。

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

【rspec】confirmダイアログのテスト

開発環境

ruby '2.6.3'
rails '6.0.2'

railsでconfirmダイアログのテストをする。

systemテスト
  context "削除する" do
    it "投稿消去" do
      click_button "消去する"
      expect{
        expect(page.accept_confirm).to eq "本当に削除しますか?"
        expect(page).to have_content "レビューを消去しました。"
        }. to change(@user.posts, :count).by(-1)
    end
  end

注意点

  • expectのブロック内にひとつ以上のexpectもしくはfindを入れないと、ダイアログが表示されてacceptされる前に次へ進んでしまうので注意が必要です。 つまり
systemテスト
  context "削除する" do
    it "投稿消去" do
      click_button "消去する"
      expect{
        expect(page.accept_confirm).to eq "本当に削除しますか?"
        expect(page).to have_content "レビューを消去しました。"
     # ↑この一文かsleepが必要です。
        }. to change(@user.posts, :count).by(-1)
    end
  end

その他

slim
= button_to "消去する",post_path(@post), method: :delete, data: {confirm: "本当に削除しますか?"}
コントローラー
  def destroy
    @post = current_user.posts.find(params[:id])
    flash[:notice] = "レビューを消去しました。"
    @post.destroy
    redirect_to user_path(current_user.id)
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Remember機能を実装する~Railsチュートリアル9章~

第9章も終了しました。
9章では主にRemember機能の実装方法について学びました。
ここはあまり時間をかけずに少し足早に通りすぎましたね。記憶トークンやRemember me機能についてざっくりとだけ理解した感じです。というのも今回はログイン機能の発展型、応用であるためまだまだ覚えることがたくさんある初学者の自分としては現段階では優先度が低い内容なのかなと感じました。

Remember me機能

ユーザーのログイン状態をブラウザを閉じた後でも有効にする機能を実装する。
またこの機能を使うかをユーザーに決めてもらうためチェックボックスをフォームに追加する。
Sessionの永続化を行うために、記憶トークン(要するにパスワードのようなもの)を生成しcookiesメソッドによる永続cookiesの作成や、安全性の高い記憶ダイジェストによるトークン認証に記憶トークンを活用する。

パスワードとトークンの違い

パスワードはユーザーが作成・管理する情報
トークンはコンピュータが作成・管理する情報

sessionメソッドで保存した情報は自動的に安全が保たれるが、cookiesメソッドに保存する情報はそうはなっていない。特に、cookiesを永続化するとセッションハイジャックという攻撃を受ける可能性があります。(記憶トークンを奪って、特定のユーザーになりすましてログインする)

cookiesを盗み出す有名な方法

(1) 管理の甘いネットワークを通過するネットワークパケットからパケットスニッファという特殊なソフトウェアで直接cookieを取り出す。

対策...Secure Sockets Layer (SSL) をサイト全体に適用して、ネットワークデータを暗号化で保護し、パケットスニッファから読み取られないようにしている。

(2) データベースから記憶トークンを取り出す。

記憶トークンをそのままデータベースに保存するのではなく、記憶トークンのハッシュ値を保存するようにする。

(3) クロスサイトスクリプティング (XSS) を使う。

Railsによって自動的に対策が行われます。具体的には、ビューのテンプレートで入力した内容をすべて自動的にエスケープする。

(4) ユーザーがログインしているパソコンやスマホを直接操作してアクセスを奪い取る。

ログイン中のコンピュータへの物理アクセスによる攻撃については、さすがにシステム側での根本的な防衛手段を講じることは不可能。二次被害を最小限に留めることは可能である。具体的には、ユーザーが (別端末などで) ログアウトしたときにトークンを必ず変更するようにし、セキュリティ上重要になる可能性のある情報を表示するときはデジタル署名 (digital signature) を行うようにする。

記憶トークンと暗号化

まずデータベースに種類=stringのremember_digest属性をApplicationに追加する。

記憶ダイジェスト用に生成したマイグレーション

class AddRememberDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :remember_digest, :string
  end
end

これをデータベースに追加。次は記憶トークンの作成を行う。新しいトークンを作成するためにnew_tokenメソッドを作成する。このダイジェストメソッドではユーザーオブジェクトが不要となるためUserモデルのクラスメソッドとして作成する。

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end
end

次にuser.rememberメソッドを作成する。これは
記憶トークンをユーザーと関連付け、トークンに対応する記憶ダイジェストをデータベースに保存する役割を担う。
またremember_token属性を設定しなければならない。
(要するにパスワードを生成するフェーズ)
attr_accessorを使って「仮想の」属性を作成する。

class User < ApplicationRecord
  attr_accessor :remember_token


  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

selfキーワードを与えると、この代入によってユーザーのremember_token属性が期待どおりに設定される。
update_attributeメソッドを使って記憶ダイジェストを更新することでこのメソッドはバリデーションを素通りさせる。

ログイン状態の保持

記憶トークンをユーザーと関連付け、トークンに対応する記憶ダイジェストをデータベースに保存する役割が完成。
次にユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する準備ができました。これを実際に行うにはcookiesメソッドを使います。
個別のcookiesは、1つのvalue (値) と、オプションのexpires (有効期限) からできている。

ユーザーIDとcookiesに保存するにはcookies[:user_id] = user.idを使用するがこれではIDがそのままcookiesに保存されてしまう。
そのため署名付きcookiesを使用する。

cookies.signed[:user_id] = user.id

さらに有効期限20年cookiesを設定できるpermanentメソッドを追加するとこうなる。

cookies.permanent.signed[:user_id] = user.id

cookiesを設定すると以後のビューでこのようにcookiesからユーザーを取り出せる。

User.find_by(id: cookies.signed[:user_id])

cookies.signed[:user_id]では自動的にユーザーIDのcookiesの暗号が解除され、元に戻る。
続いて、bcryptを使ってcookies[:remember_token]がremember_digestと一致することを確認します

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

引数がremember_tokenとなっているが、これはattr_accessorで定義したremember_tokenとは無関係のただの引数であることに注意。ここにはログイン時にremember(user)メソッドで設定したcookieに保存されたremember_tokenが代入される。
つまり、remember_tokenを使って設定されたcookieとremember_digestカラムの値が一致するかを検証しており、一致すればtrueを返す。

cookiesメソッドでユーザーIDと記憶トークンの永続cookiesを作成する。

  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

永続セッションの場合は、session[:user_id]が存在すれば一時セッションからユーザーを取り出し、それ以外の場合はcookies[:user_id]からユーザーを取り出して、対応する永続セッションにログインする必要があります

if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
`

さらにcurrent_userヘルパーを定義する

# 記憶トークンcookieに対応するユーザーを返す

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

新しくログインしたユーザーは正しく記憶される。

ユーザーを忘れる

ユーザーがログアウトできるようにするために、ユーザーを記憶するためのメソッドと同様の方法で、ユーザーを忘れるためのメソッドを定義する。このuser.forgetメソッドによって、user.rememberが取り消さる。

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end

永続Sessionからログアウトする

  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end

2つの目立たないバグ

実は小さなバグが2つ残っている。
1つ目のバグ
ユーザーは場合によっては、同じサイトを複数のタブ (あるいはウィンドウ) で開いていることもあり、ログアウト用リンクはログイン中のみ表示される。
今のcurrent_userの使い方では、ユーザーが1つのタブでログアウトし、もう1つのタブで再度ログアウトしようとするとエラーになってしまう。これは、もう1つのタブでログアウトし、current_userがnilとなるため、log_outメソッド内のforget(current_user)が失敗してしまうからである。
→この問題を回避するためには、ユーザーがログイン中の場合にのみログアウトさせる必要がある。

2番目の問題は、ユーザーが複数のブラウザ (FirefoxやChromeなど) でログインしていたときに起こります。例えば、Firefoxでログアウトし、Chromeではログアウトせずにブラウザを終了させ、再度Chromeで同じページを開くと起こる。例えばユーザーがFirefoxからログアウトすると、user.forgetメソッドによってremember_digestがnilになります。この時点では、Firefoxでまだアプリケーションが正常に動作しているがlog_outメソッドによってユーザーIDが削除されるため、ハイライトされている2つの条件はfalseになります。

記憶トークンcookieに対応するユーザーを返す

def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end

結果として、current_userメソッドの最終的な評価結果は、期待どおりnilになる。

一方、Chromeを閉じたとき、session[:user_id]はnilになります (これはブラウザが閉じたときに、全てのセッション変数の有効期限が切れるためです)。しかし、cookiesはブラウザの中に残り続けているため、Chromeを再起動してサンプルアプリケーションにアクセスすると、データベースからそのユーザーを見つけることができてしまいます。

# 記憶トークンcookieに対応するユーザーを返す
def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end

結果として、次のif文の条件式が評価される。

user && user.authenticated?(cookies[:remember_token])

このとき、userがnilであれば1番目の条件式で評価は終了するのですが、実際にはnilではないので2番目の条件式まで評価が進み、そのときにエラーが発生します。原因は、Firefoxでログアウトしたときにユーザーのremember_digestが削除してしまっているにもかかわらず、Chromeでアプリケーションにアクセスしたときに次の文を実行してしまうからです。

BCrypt::Password.new(remember_digest).is_password?(remember_token)

上のremember_digestがnilになるので、bcryptライブラリ内部で例外が発生します。この問題を解決するには、remember_digestが存在しないときはfalseを返す処理をauthenticated?に追加する必要があります。

テスト駆動開発は、この種の地味なバグ修正にはうってつけです。そこで、2つのエラーをキャッチするテストから書いていくことにしましょう。まずはリスト 8.31の統合テストを元に、redになるテストを作成する。

リスト 9.14: ユーザーログアウトのテスト red
test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    # 2番目のウィンドウでログアウトをクリックするユーザーをシミュレートする
    delete logout_path
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end

current_userがないために2回目のdelete logout_pathの呼び出しでエラーが発生し、テストスイートは redになる。

次にこのテストを成功させます。具体的にはリスト 9.16のコードで、logged_in?がtrueの場合に限ってlog_outを呼び出すように変更します。

ログイン中の場合のみログアウトする green

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

2番目の問題は、統合テストで2種類のブラウザをシミュレートは困難である。その代わり、同じ問題をUserモデルで直接テストするだけなら簡単に行えます。記憶ダイジェストを持たないユーザーを用意し (setupメソッドで定義した@userインスタンス変数ではtrueになります)、続いてauthenticated?を呼び出す。この中で、記憶トークンを空欄のままにしていることにご注目ください。記憶トークンが使われる前にエラーが発生するので、記憶トークンの値は何でも構わないのです。

ダイジェストが存在しない場合のauthenticated?のテスト red

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end

上のコードではBCrypt::Password.new(nil)でエラーが発生するため、テストスイートは redになる。
このテストを greenにするためには、記憶ダイジェストがnilの場合にfalseを返すようにすれば良い

authenticated?を更新して、ダイジェストが存在しない場合に対応 green

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

# ユーザーのログイン情報を破棄する

  def forget
    update_attribute(:remember_digest, nil)
  end
end

ここでは、記憶ダイジェストがnilの場合にはreturnキーワードで即座にメソッドを終了している。処理を中途で終了する場合によく使われるテクニックである。

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

herokuでデプロイしたDBにSequelProで接続してみる

個人アプリをherokuでデプロイした際、DBにSequelProから接続するための備忘録。

状況

フレームワーク rails 5.2
PaaS heroku
DB clearDB

やったこと

heroku configのコマンドで接続情報が出てくるのでそれをバラして入力。

CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true

スクリーンショット 2020-04-03 10.34.01.png

・ポートは空欄で大丈夫でした。
・標準を選択しました。

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

Active Storageでファイルアップロード時に403エラー

現象

Active StorageでS3にファイルアップロードした際に以下のエラーダイアログを表示し、エラー。
3回に1回くらいの割合で失敗しているため、CORSやパーミッションのエラーではない。

  Error storing "ファイル名.pdf". Status: 403

Chrome Developer ToolsのNetworkタブ

  Request URL: 割愛(ActiveStorageのurl_forヘルパー押下により作成されたリンク)
  Request Method: PUT
  Status Code: 403 Forbidden
  Remote Address: XXX.XXX.XXX.XXX:443
  Referrer Policy: strict-origin-when-cross-origin

環境

  • Ruby 2.7.0
  • Rails 6.0.2.1
  • Active Storage 6.0.2.1
  • S3

原因

セキュリティ対策として、S3の署名付きURLの有効期限を1秒にしていたことが原因と思われる。

active_storage.rb
  Rails.application.config.active_storage.service_urls_expire_in = 1.second

対応

有効期限を適切に(今回は10秒)変更した。

active_storage.rb
  Rails.application.config.active_storage.service_urls_expire_in = 10.second
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ユーザーページのURLをユーザーネームにする(twitter.com/hanamichi10みたいにする)

【結論】

ルーティング

routes.rb
get '/mypage' => 'users#mypage'
get '/:username' => 'users#show'
get '/:username/edit' => 'users#edit'
patch '/:username' => 'users#update'

リンクの作り方

<div><%= link_to "#{user.username}のユーザーページ", "/#{user.username}" %></div>

<div><%= link_to 'マイページ', "/#{current_user.username}" %></div>

<div><%= link_to "プロフィール編集", "/#{current_user.username}/edit" %></div>

こんな感じ。
現状どうで、どんなことしたくて、どうやったか
の順番に述べます。誰かの役に立てることを願って。

【現状】

【Rails】Deviseを使ってユーザーページ/マイページを作る。プロフィール編集機能も実装する。
を参照してください。

テーブル

  • Userテーブル

カラム: email, password, username

ルーティング

routes.rb
get '/mypage' => 'users#mypage'
resources :users, only: [:show, :edit, :update]

コントローラー

users_controller.rb
before_action :authenticate_user!, only: [:mypage, :edit, :update]
before_action :set_user, only: [:show, :edit, :update]

def mypage
  redirect_to user_path(current_user)
end

def show
end

def edit
  unless @user == current_user
    redirect_to user_path(@user)
  end
end

def update
  if current_user.update(user_params)
    redirect_to user_path(current_user)
  else
    redirect_to edit_user_path(current_user)
  end
end

private

  def set_user
    @user = User.find([:id])
  end

  def user_params
    params.fetch(:user, {}).permit(:username)
  end
users/show.html.erb
<% if user_signed_in? && @user == current_user %>
  <h1>マイページ</h1>
<% else %>
  <h1>ユーザーページ</h1>
<% end %>

<h1><%= @user.username %></h1>

<% if user_signed_in? && @user == current_user %>
  <%= link_to "プロフィール編集", edit_user_path(current_user) %>
<% else %>

【】やりたいこと

ユーザーページを表示するには、現状appname.com/users/1にアクセスする仕組みになっている。
これを、appname.com/hanamichi10のように、appname.com/ユーザーネームにアクセスして表示できるようにしたい。
twitter.comとかinstagram.comみたいに.

【手順】

ルーティング

routes.rb
get '/mypage' => 'users#mypage'
get '/:username' => 'users#show'
get '/:username/edit' => 'users#edit'
patch '/:username' => 'users#update'

ここが重要。
まずはルーティングを変更する。
urlのparamsを:usernameで受け取る仕様にした。

自分のアプリケーションに追加で実装する場合は、書く位置にも注意。
これらをこの順番で一番下に書くことをおすすめする。
一番下に書かないと、他の全てのurlはget '/:username'として処理されてしまう。

コントローラー

ルーティングの変更に伴ってパスが変わる。

users_controller.rb
def mypage
-   redirect_to user_path(current_user)
+   redirect_to "/#{current_user.username}"
end

def show
end

def edit
  unless @user == current_user
-     redirect_to user_path(@user)
+     redirect_to "/#{@user.username}"
  end
end

def update
  if current_user.update(user_params)
-     redirect_to user_path(current_user)
+     redirect_to "/#{current_user.username}"
  else
-     redirect_to edit_user_path(current_user)
+     redirect_to "/#{current_user.username}/edit"
  end
end

private

  def set_user
-     @user = User.find([:id])
+     @user = User.find_by(username: params[:username])
  end

  def user_params
    params.fetch(:user, {}).permit(:username)
  end

ビュー

  • マイページへのリンク
<div><%= link_to 'マイページ', "/#{current_user.username}" %></div>
  • 他人のユーザーページへのリンク
<div><%= link_to "#{user.username}のユーザーページ", "/#{user.username}" %></div>
  • プロフィール編集
users/show.html.erb
<div><%= link_to "プロフィール編集", "/#{current_user.username}/edit" %></div>
  • プロフィール編集ページ
users/edit.html.erb
<h1>プロフィール編集</h1>

<%= form_with(model: @user, url: "/#{@user.username}") do |form| %>
  <div>username<%= form.text_field :username %></div>
  <div><%= form.submit "保存" %></div>
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Deviseを使ってユーザーページ/マイページを作る。プロフィール編集機能も実装する。

Deviseを使ってユーザーページを作る

deviseをインストール

Gemfile
gem 'devise'
$ bundle install
$ rails g devise:install

deviseでUserモデルを作成(usernameカラムを持つ)

$ rails g devise User username
$ rails db:migrate

ユーザーページのために、deviseと別でUsersコントローラーを作成

$ rails g controller Users show

ユーザーページのルーティングを編集

routes.rb
- get 'users/show' => 'users#show'
+ resources :users, only: [:show]

Usersコントローラーを編集

user_controller.rb
before_action :set_user, only: [:show]

def show
end

private

  def set_user
    @user = User.find([:id])
  end

ユーザーページを編集

users/show.html.erb
<h1><%= @user.username %>のユーザーページ</h1>

動作確認

アカウントを2人作成

email password username
a@a aaaaaa hanamichi10
b@b bbbbbb kaede11
$ rails c
> User.create(email: "a@a", password: "aaaaaa", usename: "hanamichi10")
> User.create(email: "b@b", password: "bbbbbb", username: "kaede11")

表示

$ rails sして、localhost:3000/users/1'にアクセスすれば、

hanamichi10のユーザーページ

localhost:3000/users/2'にアクセスすれば、

kaede11のユーザーページ

と表示されるはず。

【+1】ユーザーページのビューを使ってマイページを作ろう

マイページがユーザーページと違うのは、

  • 表示するにはログインが必要
  • プロフィール編集機能がある

大きくはこの2点だろう。
順番に実装していく。

マイページを表示する前にログインを要求する

mypageアクションを追加

deviseのauthenticate_userを使いたいが、
ユーザーページはログインなしでも表示できるようにしたいので、Usersコントローラーのshowアクションには付けられない。

よって、仕方なく、アクションをもう一つ作る。

users_controller.rb
def mypage
end

ブサイクだけど我慢。

ルーティング

ルーティングは、

routes.rb
get '/mypage' => 'users#mypage'

とする。

コントローラー編集

mypageアクションにauthentivcate_userを付ける。

users_controller.rb
before_action :authenticate_user!, only: [:mypage]

def mypage
end

そして、mypageアクションからshowアクションにリダイレクトする。
全体はこうなる.

users_controller.rb
before_action :authenticate_user!, only: [:mypage]
before_action :set_user, only: [:show]

def mypage
  redirect_to user_path(current_user)
end

def show
end

private

  def set_user
    @user = User.find([:id])
  end

ビューも少しいじる

users/show.html.erb
<% if user_signed_in? && @user == current_user %>
  <h1>マイページ</h1>
<% else %>
  <h1>ユーザーページ</h1>
<% end %>

<h1><%= @user.username %></h1>

動作確認

これで、ただユーザーページを表示したい時は、ログイン不要。
マイページを表示したい時は、ログインを要求する仕組みになったはず。

$ rails sして、ログアウトしていることを確認してから、
localhost:3000/mypageにアクセスする.
ログイン画面を経由してマイページが表示されればOK
ログアウトするには、ログアウトボタンを画面に用意しておくのが便利。(次項)

【番外編】application.html.erbに追記しておくと便利な代物

application.html.erb
<div><%= link_to 'マイページ', mypage_path %></div>
<% if user_signed_in? %>
  <div><%= link_to 'ログアウト', destroy_user_session_path, method: :delete %></div>
<% else %>
  <div><%= link_to 'ログイン', new_user_session_path %></div>
  <div><%= link_to 'アカウント作成', new_user_registration_path %></div>
<% end %>

プロフィール編集機能を実装する

マイページが完成したので、プロフィール編集機能を実装する。

editアクションとupdateアクションを追加する

users_controller.rb
def edit
end

def update
end

ルーティング

routes.rb
- resources :users, only: [:show]
+ resources :users, only: [:show, :edit, :update]

コントローラーを編集

users_controller.rb
- before_action :authenticate_user!, only: [:mypage]
+ before_action :authenticate_user!, only: [:mypage, :edit, :update]
- before_action :set_user, only: [:show]
+ before_action :set_user, only: [:show, :edit, :update]

def mypage
  redirect_to user_path(current_user)
end

def show
end

def edit
  # 編集するユーザーが本人じゃない場合はユーザーページにリダイレクトする
  # これをしないと、ログインさえしていれば、"/users/5/edit"みたいな適当なurlにアクセスすると、他の人のプロフィール編集画面を表示できてしまう
+   unless @user == current_user
+     redirect_to user_path(@user)
+   end
end

def update
    # アップデートがうまくいけば、マイページに戻利、
    # うまくいかなければ、もう一度編集ページを表示する
+   if current_user.update(user_params)
+     redirect_to user_path(current_user)
+   else
+     redirect_to edit_user_path(current_user)
+   end
end

private

  def set_user
    @user = User.find([:id])
  end

+   def user_params
+     params.fetch(:user, {}).permit(:username)
+   end

ビュー編集・追加

編集

users/show.html.erb
<% if user_signed_in? && @user == current_user %>
  <h1>マイページ</h1>
<% else %>
  <h1>ユーザーページ</h1>
<% end %>

<h1><%= @user.username %></h1>

+ <% if user_signed_in? && @user == current_user %>
+   <%= link_to "プロフィール編集", edit_user_path(current_user) %>
+ <% else %>

追加

users/edit.html.erb
<h1>プロフィール編集</h1>

<%= form_with model: @user do |form| %>
  <div>username<%= form.text_field :username %></div>
  <div><%= form.submit "保存" %></div>
<% end %>

動作確認

マイページにアクセス > プロフィール編集ボタン > フォーム入力、保存ボタン >
マイページが表示されて、ユーザーネームが更新されていればOK

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

【Rails】Deviseを使ってユーザーページ/マイページを作成・プロフィール編集機能も実装(usernameカラムを編集)

Deviseを使ってユーザーページを作る

deviseをインストール

Gemfile
gem 'devise'
$ bundle install
$ rails g devise:install

deviseでUserモデルを作成(usernameカラムを持つ)

$ rails g devise User username
$ rails db:migrate

ユーザーページのために、deviseと別でUsersコントローラーを作成

$ rails g controller Users show

ユーザーページのルーティングを編集

routes.rb
- get 'users/show' => 'users#show'
+ resources :users, only: [:show]

Usersコントローラーを編集

user_controller.rb
before_action :set_user, only: [:show]

def show
end

private

  def set_user
    @user = User.find([:id])
  end

ユーザーページを編集

users/show.html.erb
<h1><%= @user.username %>のユーザーページ</h1>

動作確認

アカウントを2人作成

email password username
a@a aaaaaa hanamichi10
b@b bbbbbb kaede11
$ rails c
> User.create(email: "a@a", password: "aaaaaa", usename: "hanamichi10")
> User.create(email: "b@b", password: "bbbbbb", username: "kaede11")

表示

$ rails sして、localhost:3000/users/1'にアクセスすれば、

hanamichi10のユーザーページ

localhost:3000/users/2'にアクセスすれば、

kaede11のユーザーページ

と表示されるはず。

【+1】ユーザーページのビューを使ってマイページを作ろう

マイページがユーザーページと違うのは、

  • 表示するにはログインが必要
  • プロフィール編集機能がある

大きくはこの2点だろう。
順番に実装していく。

マイページを表示する前にログインを要求する

mypageアクションを追加

deviseのauthenticate_userを使いたいが、
ユーザーページはログインなしでも表示できるようにしたいので、Usersコントローラーのshowアクションには付けられない。

よって、仕方なく、アクションをもう一つ作る。

users_controller.rb
def mypage
end

ブサイクだけど我慢。

ルーティング

ルーティングは、

routes.rb
get '/mypage' => 'users#mypage'

とする。

コントローラー編集

mypageアクションにauthentivcate_userを付ける。

users_controller.rb
before_action :authenticate_user!, only: [:mypage]

def mypage
end

そして、mypageアクションからshowアクションにリダイレクトする。
全体はこうなる.

users_controller.rb
before_action :authenticate_user!, only: [:mypage]
before_action :set_user, only: [:show]

def mypage
  redirect_to user_path(current_user)
end

def show
end

private

  def set_user
    @user = User.find([:id])
  end

ビューも少しいじる

users/show.html.erb
<% if user_signed_in? && @user == current_user %>
  <h1>マイページ</h1>
<% else %>
  <h1>ユーザーページ</h1>
<% end %>

<h1><%= @user.username %></h1>

動作確認

これで、ただユーザーページを表示したい時は、ログイン不要。
マイページを表示したい時は、ログインを要求する仕組みになったはず。

$ rails sして、ログアウトしていることを確認してから、
localhost:3000/mypageにアクセスする.
ログイン画面を経由してマイページが表示されればOK
ログアウトするには、ログアウトボタンを画面に用意しておくのが便利。(次項)

【番外編】application.html.erbに追記しておくと便利な代物

application.html.erb
<div><%= link_to 'マイページ', mypage_path %></div>
<% if user_signed_in? %>
  <div><%= link_to 'ログアウト', destroy_user_session_path, method: :delete %></div>
<% else %>
  <div><%= link_to 'ログイン', new_user_session_path %></div>
  <div><%= link_to 'アカウント作成', new_user_registration_path %></div>
<% end %>

プロフィール編集機能を実装する

マイページが完成したので、プロフィール編集機能を実装する。

editアクションとupdateアクションを追加する

users_controller.rb
def edit
end

def update
end

ルーティング

routes.rb
- resources :users, only: [:show]
+ resources :users, only: [:show, :edit, :update]

コントローラーを編集

users_controller.rb
- before_action :authenticate_user!, only: [:mypage]
+ before_action :authenticate_user!, only: [:mypage, :edit, :update]
- before_action :set_user, only: [:show]
+ before_action :set_user, only: [:show, :edit, :update]

def mypage
  redirect_to user_path(current_user)
end

def show
end

def edit
  # 編集するユーザーが本人じゃない場合はユーザーページにリダイレクトする
  # これをしないと、ログインさえしていれば、"/users/5/edit"みたいな適当なurlにアクセスすると、他の人のプロフィール編集画面を表示できてしまう
+   unless @user == current_user
+     redirect_to user_path(@user)
+   end
end

def update
    # アップデートがうまくいけば、マイページに戻利、
    # うまくいかなければ、もう一度編集ページを表示する
+   if current_user.update(user_params)
+     redirect_to user_path(current_user)
+   else
+     redirect_to edit_user_path(current_user)
+   end
end

private

  def set_user
    @user = User.find([:id])
  end

+   def user_params
+     params.fetch(:user, {}).permit(:username)
+   end

ビュー編集・追加

編集

users/show.html.erb
<% if user_signed_in? && @user == current_user %>
  <h1>マイページ</h1>
<% else %>
  <h1>ユーザーページ</h1>
<% end %>

<h1><%= @user.username %></h1>

+ <% if user_signed_in? && @user == current_user %>
+   <%= link_to "プロフィール編集", edit_user_path(current_user) %>
+ <% else %>

追加

users/edit.html.erb
<h1>プロフィール編集</h1>

<%= form_with model: @user do |form| %>
  <div>username<%= form.text_field :username %></div>
  <div><%= form.submit "保存" %></div>
<% end %>

動作確認

マイページにアクセス > プロフィール編集ボタン > フォーム入力、保存ボタン >
マイページが表示されて、ユーザーネームが更新されていればOK

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

[refile][heroku]refileを使った本番環境での画像投稿機能[AWS][s3]

はじめに

refileを使って、画像投稿機能を作って、herokuにデプロイして、オリジナルサービス完成!
と思ってたら、思わぬことに。
なぜか投稿した画像が一定時間過ぎると見れなくなる。 とか
パソコンでは見れるのに、iPhoneで画像が見れない。
という問題に直面しました。ので、その状況に陥ったときの対処方法を今回は書いていきます。

そもそも

herokuにアップした画像は一定時間しか保存してくれないとのこと(どっかの記事で見ました)。
なので、どこかに保存する場所を作ってあげないといけない。そこで見つけたのが、AWSのs3という機能(?)。
s3のバケットというところに保存すればこの問題は解決しました。
ちょっと長くなりますが、方法をまとめていきたいと思います。

どうするか

細かく伝えいきますので、順にやっていってください。

gemの追加

refileには、AWSのs3と連携してくれる便利なgemがあります。それがこれ↓です。(refile/github)

Gemfile
gem "refile-s3"

これをGemfileに記述した後、 $ bundle install します。

ファイルの追加

config/initializersの中に refile.rbというファイルを作成します。
その後

config/initializers/refile.rb
require "refile/s3"

aws = {
  access_key_id: "AWS_ACCESS_KEY_ID",
  secret_access_key: "AWS_SECRET_ACCESS_KEY",
  region: "リージョン",
  bucket: "バケット名",
}
Refile.cache = Refile::S3.new(prefix: "cache", **aws)
Refile.store = Refile::S3.new(prefix: "store", **aws)

これをまるっと書きます。
記載してある"AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY",リージョン,バケット名はまだ置いといていいです。

AWSのs3にバケットを作る

このバケットというところに画像を保存していくのですが、まずはAWSのアカウントを作らなくてなりません。
参考になった記事がこちら → Railsでcarrierwaveを使ってAWS S3に画像をアップロードする手順を画像付きで説明する

この記事のAWS/IAM ~ S3バケット作成まで進めてください。かなり分かりやすく、僕でも楽々できました!

!注意!
作成途中で、アクセスキーとシークレットアクセスキーは後で使うので、どこか自分しかわからないところに控えておいてください。

バケットができたら次です。

config/initializers/refile.rb

ここに戻ってきます。
先ほど作成したrefile.rbの中にあった AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY ですが、
控えておいたアクセスキーとシークレットアクセスキーを入れることになります。
ただ、ここに直接は書かず、別のファイルに書いていきます。(書いてもいいのかもしれないけど。)
ひとまず、先ほどバケットを作った際にきめた リージョン と バケット名 を記述してください。
ちなみにリージョンを東京にした場合は ap-northeast-1 と記述すればいいです。

config/initializers/refile.rb
require "refile/s3"

aws = {
  access_key_id: "AWS_ACCESS_KEY_ID",
  secret_access_key: "AWS_SECRET_ACCESS_KEY",
  region: "リージョン",   ←ここと
  bucket: "バケット名",  ←ここ
}
Refile.cache = Refile::S3.new(prefix: "cache", **aws)
Refile.store = Refile::S3.new(prefix: "store", **aws)

gemの追加

次に環境変数というものを使うために 

gem 'dotenv-rails'

を追加し、 $ bundle install します。
環境変数とはパスワードを公開しないための変数とのこと。(あまりわかっていません。)

そのあとホームディレクトリ直下(gemfileとかがあるところ)に、 .env というファイル名でファイルを作成します。
dotenvはこの .envというファイルの中に書いてあるデータを参照します。
なのでここで環境変数を定義していきます。 さきほど控えておいたキーをここに記述します。

.env
AWS_ACCESS_KEY_ID = アクセスキー
AWS_SECRET_ACCESS_KEY = シークレットアクセスキー

記述が終わったら、 .gitignoreに先ほどの .envを記述します。
これはgithubにデプロイしたときに誰にも見られたくない環境変数を隠すためのファイルです。

.gitignore
/.env

heroku

次にherokuに移動してください。作成したアプリを選択して、 settings → Config Varsにある Reveal Config Vars を押すと
KEY と VALUE を追加する画面が出てきます。
ここで
KEYには AWS_ACCESS_KEY_ID, VALUEには アクセスキー
KEYには AWS_SECRET_ACCESS_KEY, VALUEには シークレットアクセスキー
の2つを記述してください。

最後に

$ git add .
$ git commit -m ""
$ git push heroku master
$ heroku run rails db:migrate
$ heroku open

をして、デプロイできればOKです!
そのあと、画像投稿をしてみて先ほど作成したバケットを確認すると、投稿した画像がそこに入っていると思います!
お疲れ様でした!

まとめ

ほんと、これをするためだけに丸2日くらいかかりました…。センスないのかなってめっちゃ思いました。
知らない言葉とか多いけど、やっていくうちに慣れると思うので、今はひたすらにいろんな事を吸収します。

とりあえずこの手順でやれば本番環境での画像投稿機能はつけられると思います!

参考記事

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