20191129のRubyに関する記事は30件です。

Twitterでのコード投稿の見栄えどうにかならんのと思った話

たぶん、長い投稿

きっかけ(こんな呟きを見かけた

出来たもの

キャプチャ.JPG

作成の過程で収穫物

  • Rails5.2での追加分(Active Record Storage含む
  • Twitter Login方法と仕組み、そのたTwitterあれこれ
  • JSの基礎(getElementByIdやsetAttribute、文字カウントなど
  • AWS S3の使い方
  • XSS対策

未だ残る改修すべき箇所

  • 検索結果画面のリダイレクトエラー(多分route.rbの書き順番由来
  • js辺りのエラー(動いてるけど、consoleではjs/mapのルーティングが何とか
  • スマホで
    タグ等を打つの面倒なので、なにか投稿補助ボタンでも
  • 本来の目的をよく考えたら、マークダウンの方は不要なのでは。
  • 作成要件

    Untitled.png

    • マークダウン投稿、シンタックスハイライト
      • gem: redcarpet, rouge(結局syntax-hightlightだけは反映されないまま
    • 投稿から画像生成
    • AWS S3にog:image用の画像を保存

    作成の流れ:予定

    1. rails new codr, git init, heroku create、Active Storage
    2. AWS S3あれこれ
    3. twitter登録、ログイン機能作成

    開発環境

    • vm : Linux Ubuntu (virtualbox + vagrant)
      • Ruby 2.5.1p57
      • Rails 5.2.3
      • Postgresql

    実作業: アプリ作成、諸準備

    rails new codr -d postgresql
    # DB設定等は割愛
    

    Gem

    今回は公開にまで至る予定なので、railsやdeviseの日本語化等も。が、想定ユーザはエンジニアだしと思い、殆ど英語になった。

    Gemfile
    gem 'mini_racer'   # uncomment
    gem 'rails-i18n'   # japanize
    
    # authetication
    gem 'devise' # login
    gem 'omniauth' # SNS login
    gem 'omniauth-twitter' # twitter login
    gem 'devise-i18n'  # japanize devise
    gem 'devise-i18n-views'
    
    gem 'redcarpet'   # for markdown
    gem 'rouge' # for syntax highlight
    
    gem 'meta-tags'
    
    gem 'aws-sdk-s3' # for aws s3
    

    kpumuk/meta-tags:Search Engine Optimization (SEO) for Ruby on Rails applications.は割愛。

    gitignore => rails.credentials.yml

    当初は.gitignoregem 'dotenv'等を使っていた。が、作成途中でRails5.2からのrails.credentials.ymlを知り、利用した。rails.credentials.ymlは暗号化されており、なお、復号化には/config/master.key`を利用。

    irb
    # editor setting
     EDITOR="vi" bin/rails credentials:edit
    # edit credentials.yml
    rails credentials:edit
    # show credential.yml
    rails credentials.yml:show
    
    # herokuにmaster.keyを環境変数として指定
    heroku config:set ENV_VAR="環境変数" --app "アプリ名"
    
    # 追加した変数を使用するには
    Rails.application.credentials.dig(:twitter, :API_Key)
    

    rails gあれこれ

    # devise
    # install devise
    rails g devise:install
    rails g devise User name:String
    
    # Add Admin column to User
    rails g migration AddAdminToUsers
    # add setting at /db/migrate/20191103141531_add_admin_to_users.rb
    add_column :users, :admin, :boolean, default: false
    
    # add views and controllers to modify devise
    rails g devise:controllers users
    rails g devise:views users
    
    # japanize
    # add at /config/application.rb
    config.i18n.default_locale = :ja
    => create /config/locale/devise.view.ja.yml
    
    # scaffold post
    rails g scaffold Post user:references name:string content:text date:datetime
    

    Active Record Associations関連付け

    /app/model/user.rb
    has_many :posts
    
    /app/model/post.rb
    belongs_to :user
    

    投稿関連

    マークダウン投稿

    基本:Redcarpet::Markdown.new(renderer, extensions = {}).render(@post.content)
    オプションやXSS対策等を追加したく、helperメソッドを作成した。

    app/helpers/posts_helper.rb
    Module PostsHelper
      require 'rouge/plugins/redcarpet'
      class RougeRedcarpetRenderer < Redcarpet::Render::HTML
        include Rouge::Plugins::Redcarpet
    
        def header(text, level)  # #や##等がh2、h3となるようにした。
          level += 1
          "<h#{level}>#{text}</h#{level}>"
        end
      end
    
      def markdown(text)
        render_options = {
          filter_html: true,  # do not allow any user-inputted HTML in the output.
          hard_wrap: true,
        }
    
        extensions = {
          autolink: true, # <>で囲まれていない時は、リンクとして認識しない
          fenced_code_blocks: true,   #  ```\n ```内をコード部分と見做す
          lax_spacing: true, 
          no_intra_emphasis: true,
          strikethrough: true,
          superscript: true,
          tables: false,  # テーブルを認識しない
          highlight: true,
          disable_indented_code_blocks: true,
          space_after_headers: false # #の後にスペースが無くても、h1等とする。
        }
        renderer = RougeRedcarpetRenderer.new(render_options)
        Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
      end
    end
    

    html_safe => sanitize

    html_safeではXSS対策としては駄目と知った。名前詐欺である。
    sanitizeヘルパーを使用した。ホワイトリスト方式。要参照

    app/views/posts/index.html.erb
    # sanitize(html, options = {})
     <div id="capture" class="content">
        <%= sanitize(markdown(@post.content), tags: %w(div img h1 h2 h3 h4 h5 strong em a p pre code ), attributes: %w(class href)) %>
    </div>
    

    投稿内容のデータ化、AWSへの画像保存

    最初はTwitterAPIを利用して、投稿から作成、DBに直接保存した画像でTwitter投稿しようとした。だが、Herokuでは画像が保持されない事、TwitterAPIの変更などいろいろ面倒なことが発生したので、最終的には画像をAWS S3に保存し、og:imageに添付する形を取った。

    1. Webアプリ内で通常投稿
    2. showページ表示(同時にhtml2canvasでBase64としてデータ取得、hidden_fieldに格納
    3. Tweetボタン押す(Postされ、postモデル内でbase64をデコード
    4. Active Storageを通して、AWS S3に保存

    Active Storage

    Rail5.2からの機能で、今までのcarrievaveやpaperclip等を使わずに、クラウドストレージ等へのアップロードが容易になる。今回はAWS S3を使った。

    irb
    # set up
    rails active_storage:install
    # 今回は画像が紐づくPostテーブルが既にあるので、不要
    # rails g resource comment content:text
    rails db:migrate
    
    app/models/post.rb
    class Post < ApplicationRecord
    # 今回は1つの投稿につき、1枚の画像なので。複数なら => has_many_attached :prtscs
      has_one_attached :prtsc
    end
    
    app/config/enviroments/
    # ファイル保存先変更
    # development.rb
    config.active_storage.service = :local
    # production.rb
    config.active_storage.service = :amazon
    

    rails credentials:editでAWSアクセスキーとシークレットキーを追加。

    config/credentials.yml.enc
    aws:
      access_key_id: 
      secret_access_key: 
    
    config/storage.yml
    test:
      service: Disk
      root: <%= Rails.root.join("tmp/storage") %>
    
    local:
      service: Disk
      root: <%= Rails.root.join("storage") %>
    
    amazon:
      service: S3
      access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
      secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
      region: ap-northeast-1
      bucket: codr0
    
    Gemfile
    # gemが必要
    gem 'aws-sdk-s3', require: false
    # 今回は不要だったので、入れず。
    gem 'mini_magick'
    

    html2canvas

    参考:htmlを画像化する方法(html2canvasの使い方)
    jsはProgateレベルだったので、DOM操作は初めてで、なんか楽しかったぞ。

    1. Tweetボタン押下時に、画像をPostするためのフォーム、hidden_fieldを用意
    2. html2canvas.jsapp/assets/javascriptsディレクトリ配下に保存。
    3. html上に置くscriptコードを改修
    app/views/posts/show.html.erb
    <%= form_with(model: @post, local: true) do |form| %>
      <%= form.hidden_field :id, value: @post.id %>
      <%= form.hidden_field :prtsc, value: "" %> # idはpost_prtscになる。
      <%= form.submit "Post", class:"btn btn-outline-dark", id:"tweet", value:"tweet" %>
    <% end %>
    
    app/views/layouts/application.html.erb
    <script type="text/javascript">
      html2canvas(document.querySelector("#capture"),{scale:1, width:600}).then(canvas => {
        var base64 = canvas.toDataURL('image/jpeg', 1.0);
        document.getElementById('post_prtsc').setAttribute('value', base64);
     });
    </script>
    

    Base64デコード

    大学で画像処理していたとはいえ、 Base64とは?Blobとは?となり、良い機会だった。

    app/models/post.rb
    attr_accessor :img
    
    def parse_base64(img)
      if img.present?
        # data:image/jpeg;base64,/9j/4AAQSkZJRgABA・・・から/9j/4AA以降を選択取得
        content = img.split(',')[1]
        # 今回は、ユーザによる画像アップロード投稿ではなく、拡張子が決まっている
        filename = Time.zone.now.to_s + '.jpg'
        decoded_data = Base64.decode64(content)
        # String.IO.newにより、アプリ内に一時ファイルを作成しなくて済む
        prtsc.attach(io: StringIO.new(decoded_data), filename: filename)
      end
    end
    

    あとはposts_controllerで、paramsから受け取ったBase64データを上のparse_base64(img)で変換し、保存すれば完了。

    AWS S3

    AWS上での登録、設定、バケット作成等は割愛。

    Tweet button

    公式で生成されるTweetボタンのURLを利用し、押下時にwindow.openでTweet投稿ページを開くようにした。rubyonrailsで用意した変数をjsに渡すgem 'gon'も考えたが、見送った。

    app/views/layouts/application.html.erb
    <script>
      var base = 'https://twitter.com/intent/tweet?url=';
      var pageUrl = 'https://codr0.herokuapp.com/posts/' + document.getElementById('post_id').value;
      var option = '&button_hashtag=Codr0&ref_src=twsrc%5Etfw';
      var href = base + pageUrl + option;
      var twit = document.getElementById('tweet');
      twit.addEventListener('click', function() {
            window.open( href );
          });
    </script>
    

    og:imageに画像添付

    なお、headのmeta情報セットには、gem 'meta-tags'を使用。参照 : kpumuk/meta-tags

    service_url()とurl_for()

    基本的にはどちらも、ActiveStorageに保存したデータのUrlを取得するメソッドの様だ。
    どちらもセキュリティの為にリンクの有効期限が短いみたいだが、違いが分からなかった。今回はTweetボタン押下し、Tweetした際にog:imageとして表示されればいい。

    app/views/posts/show.html.erb
    # 画像がActive StorageでAWS S3に保存されて入れば
    <% if @post.prtsc.attached? %>
      <% set_meta_tags og:{image: @post.prtsc.service_url} %>
    <% end %>
    

    Twitterログイン

    TwitterDeveloperAccountが必要。割愛。

    なお、omniauthは脆弱性が見つかっており、githubの方でもアラートが来るのだが、パッチが無いのだが。クックパッドの人が対処してくれたので、感謝したい。

    app/models/user.rb
    # 参考ページと同じ基礎的な所は割愛する。
    class User < ApplicationRecord
      def self.from_omniauth(auth)
        find_or_create_by!(provider: auth['provider'], uid: auth['uid']) do |user|
          # 一部割愛
          user.username = auth['info']['nickname']
          # SNS登録時は、ダミーメールを登録
          user.email = User.dummy_email(auth)
        end
       end
    
      # SNS登録(providerが存在する)時は、パスワード要求をしない
      def password_required?
        super && provider.blank?
      end
    
      def self.new_with_session(params, session)
        if session['devise.user_attributes']
          new(session['devise.user_attributes']) do |user|
            user.attributes = params
          end
        else
          super
        end
      end
    
      private
    
      def self.dummy_email(auth)
        "#{auth.uid}-#{auth.provider}@example.com"
      end
    end
    

    Twitterのニックネームが取得できるようになったので、元からあるUserのnameテーブルは削除した。

    css

    今回はBootstrapを部分的に使用した。cssの優先順位など収穫があった。

    改修(加筆

    メディアクエリ

    想定ユーザは殆どスマホなのに、PCで作成し、CSSをPCの見た目でやってた。折角SCSSでやってるので、変数を利用した。

    app/assets/stylesheets/scaffold.scss
    # ディスプレイサイズが680pxまでなら。
    $tab: 680px; 
    @mixin tab {
      @media (max-width: ($tab)) {
        @content;
      }
    }
    
    // .box {
    //   @include tab {
    //     background-color: blue;
    //   };
    // }
    

    最後に

    gist等がコードスクショをog:imageで表示してくれたら全て済むんじゃと思った。
    因みにもう1段階先のWebアプリを考えてあるけど、たぶんjsの知識が足りないので、今は無理。
    転職活動中の無職です。働きたい。。。。

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

ABC085C - Otoshidama

問題

スクリーンショット 2019-11-29 21.59.08.png

回答

N,Y = gets.chomp.split.map(&:to_i)

x = -1
y = -1
z = -1

N.step( 0, -1 ) do |i|
  if 10000 * i > Y
    next
  else
    ( N - i ).step( 0, -1 ) do |j|
      if 10000 * i + 5000 * j + 1000 * ( N - i - j ) == Y
        x = i
        y = j
        z = N - i - j
        break
      end
    end
    if x != -1 || y != -1 || z != -1
      break
    end
  end
end

printf( '%d %d %d', x, y, z )

結果

スクリーンショット 2019-11-29 21.38.44.png

感想

5000円、10000円と1000円の差分である4000円と9000円で割り切れるかで確認する方法があって処理は早かったけど、流石にコード読むのが難しくなるしなあ・・・

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

HamlをRuby on Railsアプリケーションに導入する方法

Hamlとは

Hamlとは、HTMLを簡潔かつ簡単に記述できるマークアップ言語。

Railsで使用する場合の手順

1. Hamlを導入

Gemfileの一番下にコードを記述すると、すべての環境でhamlが使用できる。
記述後は、bundle installを忘れずに

Gemfile
gem 'haml-rails'

2. erbファイルをHamlに変換

rails newにより、erbファイルがすでに作成されているので
拡張子がerbのファイルをhamlに変換する。

一括変換する方法

ターミナルから以下のコマンドを実行。

$ rails haml:erb2haml

ここで、

Would you like to delete the original .erb files? (This is not recommended unless you are under version control.) (y/n)

と聞かれたら、状況に応じてy/nを選択する。

元のerbファイルを削除して構わなければ yを選択しよう。

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

【Rails】Docker+Rails環境でpryの`edit`コマンドが使えなかったので、使えるようにした

はじめに

DockerとRailsを使った開発環境でpryのeditコマンドが効かなかったので対処してみたときの記録です。

※Docker環境下での話です。ローカルでは普通に動作しましたので、割愛します。

この記事が役に立つ方

  • あれ?なんかeditコマンド効かなくね?と思っている方

この記事のメリット

  • Docker環境下でpryのeditコマンドが使えるようになる

環境

  • macOS Catalina 10.15.1
  • zsh: 5.7.1
  • Ruby: 2.6.5
  • Rails: 5.2.3
  • Docker: 19.03.5
  • docker-compose: 1.24.1

pryのeditコマンドとは?

以下、公式のWikiです。
Editor integration · pry/pry Wiki · GitHub

The edit command is used to invoke your default editor. This command will load your file once you have finished editing it (unless you pass the -n or --no-reload flag to edit).

pryの画面でそのままデフォルトのエディタを開き、ファイルの編集が出来るコマンドです。

これまでpryの画面上からファイルをいじったりはしていなかったのですが、デバッグ効率が上がるかなと思ったので導入します。

【事前準備】

こちらのドキュメントに従ってDocker環境下でgem pry-railsを使うための準備は済ませておきました。

Using pry-rails with Docker · GitHub

上記ドキュメントの流れは以下の通りです。

docker-compose.yml
app:
  tty: true
  stdin_open: true

上記設定を追加。

Gemfile
group: :development
  gem 'pry-rails'
end

gemを追加。

docker-compose build

ビルド。

docker-compose up

起動。

docker ps

RailsアプリのCONTAINER IDNAMESを確認

docker attach `CONTAINER ID``NAMES`

binding.pry
※好きなところで

これで普通にpryでデバッグ可能な状態になります。

とりあえずeditコマンド使ってみようとする

※Docker環境下です。

とりあえずeditコマンドを叩いてみます。

[1] pry(main)> edit
Error: Please set Pry.config.editor or export $VISUAL or $EDITOR

するとエラー発生
Pry.config.editorか、環境変数$VISUAL$EDITORを設定してねという内容。

先程のWikiの、Setting the default editorのくだりに設定方法が記載されていたので、その通りに進めてみます。

1. ~/.pryrcを作成・設定

pryの設定ファイルは.pryrcです。
今回はなかったので作成しました。

以下設定方法についてです。
Editor integration · pry/pry Wiki · GitHub

今回エディタはvimを使いたかったので、以下のように設定。

~/.pryrc
Pry.config.editor = proc { |file, line| "vim #{file}:#{line}" }

該当のファイルと行に飛んでくれる仕様に例にならってみましたが、うまくいきません。

ローカルの設定ファイルは読み込まれないんですね。

2.Railsアプリのディレクトリ内に.pryrcを配置する。

下記記事を参照し、Railsアプリのルートディレクトリに.pryrcを配置してみることにしました。

docker-compose上のRailsのデバッグを行う - My External Storage

./.pryrc
Pry.config.editor = proc { |file, line| "vim #{file}:#{line}" }

※設定は先程と同様です。

もう一回試します。

[1] pry(main)> edit
Error: `vim ファイル名:行数` gave exit status: 127

別のエラー発生。
惜しい!コマンド自体は正しそうに見えますが、127なるエラーが。

何これ?と思って調べると、
「コマンドが見つからない、もしくはタイポ」
らしいと判明。

...ということは、そもそもvimがない?

3.vimをインストールする

なければいくらコマンドが正しくても、どうしようもないですよね。

調べると、こちらの記事を発見しました。
Docker — docker コンテナの中で vim が使えない場合 - Qiita

Dockerfile
apt-get install vim

上記を追記。

docker-compose build

docker-compose up


rails cなどでpry起動。

[1] pry(main)> edit
#=>vim起動

無事起動!

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

vimがないという環境もあるのかとびっくりしましたが、ローカルとDocker環境の境目を意識するいい経験になりました:point_up:

これでデバッグ効率が上がりそうです:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

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

インスタのような感じのアプリを作るスパイラル仕様

1.文字投稿サイト
ヘッダーのパーシャルを使う。
文字投稿はRESTfulな入力フォームを使う。

2.ユーザー登録機能
ヘッダーにifでログイン状態のときに表示を変える。
投稿に対して名前が付与される。この時に関連付けが利用される。
postテーブルの関連付けからuser.nameをひっぱる。postにはuser_idを追加

簡略化のために名前とメールアドレスだけで認証を行う。
パスワードはあとでok

2.5投稿に○○さんの投稿と名前をつける
ログインしていない投稿にはifでゲストさんにしておいた。

3.画像機能
画像が投稿で追加される。
取得できればそんなに難しくない。

3.5 いいね機能
そんなに難しくはないと思う。

4.関連付けでフォロー
できないことはないと思う。

これ以降
細かい機能を取り付け
・投稿、ユーザーの検索
・投稿への返信機能
・ユーザーへのメッセージ機能
・フォロワー通知
・バリデーション
・デザイン
・Restfulな書き方、美コード化
・各機能等のテスト方法考案
・フォロー機能
・いいね機能
・基本的なログイン(パスワード仕様)
・発展的ログイン(クッキー)
・sns紐付けログイン
・投稿詳細ページ
・ユーザー詳細ページ
・アカウント有効化メール
・パスワード再設定機能(メールなし・あり
・投稿の削除・編集
・ユーザーの削除・編集

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

【Ruby on Rails】開発環境構築(MacOS)

MacOSの状態を確認

Catalina以降の場合とMojave以前の場合で環境構築の設定が異なります。

OSがMojave以前の場合の環境構築

❶Command Line Toolsを用意

Command Line ToolsはWebアプリケーション開発に必要なソフトウェアをダウンロードするために必要な機能です。

$ xcode-select --install

❷Homebrewを用意

$ cd  #ホームディレクトリに移動
$ pwd #ホームディレクトリにいるかどうか確認
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"  #コマンドを実行

※処理に時間がかかる可能性のある操作です。

Press RETURN to continue or any other key to abort」と表示されたら、「続けるにはエンターキーを、やめるにはそれ以外の入力をしてください」という意味ですので、エンターキーを入力します。

Password:」と表示されたら、PCを開く際に求められるパスワードを入力します。ターミナル上で文字は表示されませんが、間違いなく入力はされています。パスワードを入力し終わったらエンターキーを押します。

$ brew -v  #Homebrewがインストールされているか確認
Homebrew 1.8.0
$ brew update  #Homebrewを最新状態にする
$ sudo chown -R `whoami`:admin /usr/local/bin  #Homebrewの権限を変更

❸新しいバージョンのRubyをインストール

$ brew install rbenv ruby-build  #Homebrewを用いてrbenvとruby-buildをインストール
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile  #PCにおけるどこのディレクトリからも使用できるようにする
$ source ~/.bash_profile  #bash_profileの変更を反映
$ brew install readline  #readlineをインストール
$ brew link readline --force  #どこからも使用できるように
# rbenvを利用してRubyをインストール
$ RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)"
$ rbenv install 2.5.1
$ rbenv global 2.5.1  #利用するRubyのバージョンを指定
$ rbenv rehash  #rbenvを読み込んで変更を反映
$ ruby -v  #Rubyのバージョンを確認

先ほどインストールした2.5.1であることが表示されれば完了です。

❹MySQLを用意

MySQLは、Webアプリケーションにおけるデータを蓄積する場所のこと。

# MySQLのインストール
$ brew install mysql@5.6
$ brew reinstall https://raw.githubusercontent.com/Homebrew/homebrew-core/f171f1c74/Formula/mysql@5.6.rb

※処理に時間がかかる可能性のある操作です。

# MySQLの自動起動設定
$ mkdir ~/Library/LaunchAgents 
$ ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plist
# mysqlのコマンドをどこからでも実行できるようにする
$ echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile
# mysqlのコマンドが打てるか確認する
$ which mysql
# 以下のように表示されれば成功
/usr/local/opt/mysql@5.6/bin/mysql

❺Railsを用意

Railsというフレームワークを用いて、Rubyでアプリケーションを作成します。

$ gem install bundler  #bundlerをインストール
$ gem install rails --version='5.2.3'  #Railsをインストール

※処理に時間がかかる可能性のある操作です。

$ rbenv rehash  #rbenvを再読み込みしておく
$ rails -v  #Railsが導入できたか確認

Mojave以前の場合は、これにてWebアプリケーション開発のための環境構築は完了です。

OSがCatalina以前の場合の環境構築

# zshをデフォルトに設定
$ chsh -s /bin/zsh
# ログインシェルを表示
$ echo $SHELL
# 以下のように表示されれば成功
/bin/zsh

もしecho $SHELLコマンドで、/bin/zshと表示されなかった方は、一度ターミナルを閉じて再度開いた上でもう一度echo $SHELLコマンドを実行します。(“$”が“%”になっている可能性もあります)

❶Command Line Toolsを用意

$ xcode-select --install  #Command Line Toolsをインストール

❸Homebrewを用意

$ cd  #ホームディレクトリに移動
$ pwd #ホームディレクトリにいるかどうか確認
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"  #コマンドを実行

※処理に時間がかかる可能性のある操作です。

Press RETURN to continue or any other key to abort」と表示されたら、「続けるにはエンターキーを、やめるにはそれ以外の入力をしてください」という意味ですので、エンターキーを入力します。

Password:」と表示されたら、PCを開く際に求められるパスワードを入力します。ターミナル上で文字は表示されませんが、間違いなく入力はされています。パスワードを入力し終わったらエンターキーを押します。

$ brew -v  #Homebrewがインストールされているか確認
$ brew -v
Homebrew 2.1.13
$ brew update  #Homebrewをアップデート
$ sudo chown -R `whoami`:admin /usr/local/bin  #Homebrewの権限を変更

❸新しいバージョンのRubyをインストール

$ brew install rbenv ruby-build  #Homebrewを用いてrbenvとruby-buildをインストール
$ echo 'eval "$(rbenv init -)"' >> ~/.zshrc  #PCにおけるどこのディレクトリからも使用できるようにする
$ source ~/.zshrc  #zshrcの変更を反映
$ brew install readline  #readlineをインストール
$ brew link readline --force  #どこからも使用できるように
# rbenvを利用してRubyをインストール
$ RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)"
$ rbenv install 2.5.1

※処理に時間がかかる可能性のある操作です。

$ rbenv global 2.5.1  #利用するRubyのバージョンを指定
$ rbenv rehash  #rbenvを読み込んで変更を反映
$ ruby -v  #Rubyのバージョンを確認

❹MySQLを用意

# MySQLのインストール
$ brew install mysql@5.6
$ brew reinstall https://raw.githubusercontent.com/Homebrew/homebrew-core/f171f1c74/Formula/mysql@5.6.rb

※処理に時間がかかる可能性のある操作です。

# MySQLの自動起動設定
$ mkdir ~/Library/LaunchAgents 
$ ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plist
# mysqlのコマンドをどこからでも実行できるようにする
$ echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.zshrc
$ source ~/.zshrc
# mysqlのコマンドが打てるか確認する
$ which mysql
# 以下のように表示されれば成功
/usr/local/opt/mysql@5.6/bin/mysql

❺Railsを用意

$ gem install bundler  #bundlerをインストール
$ gem install rails --version='5.2.3'  #Railsをインストール
$ rbenv rehash  #rbenvを再読み込みしておく
$ rails -v  #Railsが導入できたか確認

Catalina以降の場合は、これにてWebアプリケーション開発のための環境構築は完了です。

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

#Rails ( or #Ruby + ActiveSupport ) : Time.zone.now VS Time.current what is difference? when specify Time.zone then used ActiveSupport::TimeWithZone class else use Time class

require 'active_support/core_ext'


# No Time.zone setting

Time.use_zone(nil) { Time.current }
# => 2019-11-29 07:38:04 +0900

Time.use_zone(nil) { Time.current.class }
# => Time



# with Time.zone setting
# Time.current class changed Time to ActiveSupport::TimeWithZone

Time.use_zone('UTC') { Time.current }
# => Thu, 28 Nov 2019 22:38:20 UTC +00:00

Time.use_zone('UTC') { Time.current.class }
# => ActiveSupport::TimeWithZone

Time.use_zone('UTC') { Time.zone.now }
# => Thu, 28 Nov 2019 22:38:26 UTC +00:00

Time.use_zone('UTC') { Time.zone.now.class }
# => ActiveSupport::TimeWithZone



require 'active_support/testing/time_helpers'
include ActiveSupport::Testing::TimeHelpers

# Freeze time
Time.use_zone('UTC') { travel_to Time.parse('2020-01-01 00:00') }

Time.use_zone('UTC') { Time.current == Time.zone.now }
# => true


# NOTE
# Time.parse class not changed to ActiveSupport::TimeWithZone

Time.parse('2020-01-01 00:00')
# => 2020-01-01 00:00:00 +0900

Time.parse('2020-01-01 00:00').class
# => Time

Time.use_zone('UTC') { Time.zone.parse('2020-01-01 00:00') }
# => Wed, 01 Jan 2020 00:00:00 UTC +00:00

Time.use_zone('UTC') { Time.zone.parse('2020-01-01 00:00').class }
# => ActiveSupport::TimeWithZone

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2778

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

#Rails ( or #Ruby + ActiveSupport ) + travel_to : freeze to localtime beggining of year and ref UTC date and time and timestamp

# REQUIRE : on Ruby

require 'active_support/testing/time_helpers'
# => true

require 'active_support/core_ext'
# => true

include ActiveSupport::Testing::TimeHelpers
# => Object


# Travel to JST beggining of Year

travel_to Time.use_zone('Tokyo') { Time.parse('2020-01-01 00:00') }
# => nil


# JST

Time.use_zone('Tokyo') { Time.current }
# => Wed, 01 Jan 2020 00:00:00 JST +09:00

Time.use_zone('Tokyo') { Time.zone.now }
# => Wed, 01 Jan 2020 00:00:00 JST +09:00

Time.use_zone('Tokyo') { Date.current }
# => Wed, 01 Jan 2020


# UTC


Time.use_zone('UTC') { Time.current }
# => Tue, 31 Dec 2019 15:00:00 UTC +00:00

Time.use_zone('UTC') { Time.zone.now }
# => Tue, 31 Dec 2019 15:00:00 UTC +00:00

Time.use_zone('UTC') { Date.current }
# => Tue, 31 Dec 2019


# Unix Timestamp
# are All same at any Timezone

Time.use_zone('Tokyo') { Time.zone.now.to_i }
# => 1577804400

Time.use_zone('UTC') { Time.zone.now.to_i }
# => 1577804400

Time.now.to_i
# => 1577804400

Time.current.to_i
# => 1577804400


# UTC vs JST near date change line

travel_to Time.use_zone('Tokyo') { Time.parse('2020-01-01 08:59') }

Time.use_zone('UTC') { Date.current }
# => Tue, 31 Dec 2019

travel_to Time.use_zone('Tokyo') { Time.parse('2020-01-01 09:00') }
# => nil

Time.use_zone('UTC') { Date.current }
# => Wed, 01 Jan 2020

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2777

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

#Rails ( or #Ruby + ActiveSupport ) の Time.zone.now と Time.current の違いは Time.zone 指定の有無であり、Time class と ActiveSupport::TimeWithZone class が切り替わることが判明

require 'active_support/core_ext'


# No Time.zone setting

Time.use_zone(nil) { Time.current }
# => 2019-11-29 07:38:04 +0900

Time.use_zone(nil) { Time.current.class }
# => Time



# with Time.zone setting
# Time.current class changed Time to ActiveSupport::TimeWithZone

Time.use_zone('UTC') { Time.current }
# => Thu, 28 Nov 2019 22:38:20 UTC +00:00

Time.use_zone('UTC') { Time.current.class }
# => ActiveSupport::TimeWithZone

Time.use_zone('UTC') { Time.zone.now }
# => Thu, 28 Nov 2019 22:38:26 UTC +00:00

Time.use_zone('UTC') { Time.zone.now.class }
# => ActiveSupport::TimeWithZone



require 'active_support/testing/time_helpers'
include ActiveSupport::Testing::TimeHelpers

# Freeze time
Time.use_zone('UTC') { travel_to Time.parse('2020-01-01 00:00') }

Time.use_zone('UTC') { Time.current == Time.zone.now }
# => true


# NOTE
# Time.parse class not changed to ActiveSupport::TimeWithZone

Time.parse('2020-01-01 00:00')
# => 2020-01-01 00:00:00 +0900

Time.parse('2020-01-01 00:00').class
# => Time

Time.use_zone('UTC') { Time.zone.parse('2020-01-01 00:00') }
# => Wed, 01 Jan 2020 00:00:00 UTC +00:00

Time.use_zone('UTC') { Time.zone.parse('2020-01-01 00:00').class }
# => ActiveSupport::TimeWithZone


Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2779

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

初めてのRuby on Rails

はじめに

このQiitaは、プログラミング未経験の初心者がRailsチュートリアルに取り組み
学んだことを記録、振り返るために書いていきます。

今までやったこと

Progateで

  • HTML
  • CSS
  • Ruby
  • Ruby on Rails5
  • Git
  • CommandLine
  • SQL
  • HTMLとCSSを学んだ後に一度架空のホームページの見た目だけ作成

PC環境 

  • MacBook Pro Mid2012(古いです)

  • macOS Mojave ver 10.14.6

エディタ

  • Visual Studio Code ver 1.40.2

最初のアプリケーション

「Hello,World」を表示するプログラム

1.ターミナルでcd..... ホームディレクトリに移動
2.cd environment.....作成したenvironmentディレクトリに移動
3.rails new.....railsアプリの骨組みを作成

ターミナルで

$ rails _5.1.6_ new hello_app

これでrails 5.1.6を使ったhello_appというファイルが作られる

rails newとは

新規ファイルの作成

RsilsはRubyを使ったフレームワークなのである程度必要なファイルなどを自動作成してくれる。

$ rails new blog_app(作りたいプロジェクト名)

これによりblog_appというファイルが作られ、必要なものが中に自動生成される。

rails newのオプション

rails newをする際に様々な条件を付けることができる

Railsのバージョンの指定

railsのバージョンを指定して作成できる

$ rails new _5.1.6_ new blog.app

5.1.6の部分に好きなバージョンを入れることで、そのバージョンで作成できる。

データベースの指定

rails new時、データベースのデフォルトはsqliteになる。
rails new した後に変更することも可能だが、最初から使うデータベースが決まっているなら最初に設定しておける。

$ rails new blog_app -database=mysql

これはMySQLを使うときのコマンド

$rails new blog_app -database=postgresql

これはPostgresqlを使うときのコマンド

そのほかにもrails new するときのオプションはたくさんある

また今度調べよう

Bundler

Bundlerは必要なgemをインストールするコマンド

$ budle

※railsにおけるgemとはrubyのライブラリのこと
rubyに必要な機能の管理場所のようなもの

通常rails newをすると最低限必要なgemは自動でbundlerしてくれる

チュートリアル通りgemの内容を変更し、bundlerを再実行する

gemはテキストエディタでhello_appディレクトリにあるgemfileで触れる

今回は下記の様にgemを書き換える

source 'https://rubygems.org'

gem 'rails',        '5.1.6'
gem 'puma',         '3.9.1'
gem 'sass-rails',   '5.0.6'
gem 'uglifier',     '3.2.0'
gem 'coffee-rails', '4.2.2'
gem 'jquery-rails', '4.3.1'
gem 'turbolinks',   '5.0.1'
gem 'jbuilder',     '2.6.4'

group :development, :test do
  gem 'sqlite3',      '1.3.13'
  gem 'byebug', '9.0.6', platform: :mri
end

group :development do
  gem 'web-console',           '3.5.1'
  gem 'listen',                '3.1.5'
  gem 'spring',                '2.0.2'
  gem 'spring-watcher-listen', '2.0.1'
end

書き換えたらターミナルでbundle

発生したエラー

You have requested:
  spring = 2.0.2 

The bundle currently has spring locked at 2.1.0.
Try running `bundle update spring`

If you are updating multiple gems in your Gemfile at once,
try passing them all to `bundle update`

あなたがリクエストしたのはspring = 2.0.2だけどspring = 2.1.0で設定してるから無理だよ!
変えるならbundle updateしてね!
的なことを言っているので

素直にbundle updateを実行
エラーは治った

そして再度bundle

正常に作動して必要なgemをインストールした

Railsサーバーの立ち上げ

$ rails server

railsはこのコマンドを入れるだけで、ローカルWebサーバーを立ち上げることができる

$ rails s

でもいいらしい

rails s をするときは別ターミナルタブで行った方が他のコマンドも打てるので良い

入力したら

http://localhost:3000/

にアクセスしてみる

サーバーの立ち上げに成功していれば

Yay! You’r on Rails! と可愛らしいイラスト付きで表示が出る。

Railsのサーバーの立ち上げができた

立ち上げること自体は本当に簡単でたった数個の記述で出来た
数行の記述でブログが作れる!みたいな記事もあったので
このお手軽さがrailsが初学者に人気の理由なのかな

Railsチュートリアル自体が最初は細かい部分が省かれている感じなので
rails全体を掴むために疑問点はメモしておき、
確認の1週目ということにして書いてある通りにさくさく進めてみよう

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

RubyからRustを呼び出すいくつかの方法のまとめ

この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc.(エイチーム引越し侍、エイチームコネクト) Advent Calendar 2019 1日目の記事です。

プログラムのパフォーマンス向上のためにRubyからCで書かれた処理を呼び出したいという場合があるとき、拡張ライブラリを書いたりFFIを使って処理を呼び出したりします。
今回はRustで書かれた処理をRubyで呼び出す方法を調べてみました。

拡張ライブラリを書く方法

RubyにはCのためのAPIがあり、これを使うことで拡張ライブラリを作ることができます。RustにはCのプログラムとリンクさせるライブラリを作ることができるので、その機能を利用してRustでRubyの拡張ライブラリを作ることができます。

詳細は以下の記事をご参照ください。
RustだけでRuby native extensionを書く

FFIで呼び出す方法

Cで拡張ライブラリを記述するよりも、より簡単な方法としてFFI(Foreign function interface)を利用する方法もあります。
FFIを使った場合にはC拡張を書くことなく、利用することができるので、より簡単に使うことができます(内部的にはC拡張の仕組みと同じものが動きます)。

詳細は以下の記事をご参照ください。
RubyからRustの関数をつかう → はやい

Helixを使う方法

Rustで簡単に拡張ライブラリを書くための方法としてhelixというgemが
あります。Qiita界隈ですとHelixといえばネームスペースはキーボードに占有されている気もしますが、RustとRubyのブリッジというこよで実在するが名前の由来のようです。
Getting Startedのページを見るといきなりRuby on Railsのプロジェクトから作り始めるなかなかに攻めた内容になっていますが、今回はシンプルにgemを作って、それを呼び出す方法を見てみます。
前章の参照記事で書かれているものとほぼ同じフィボナッチ数を求める関数を作成していきます。

gemテンプレートの作成

まずはgemのプロジェクトの雛型を作ります。

$ bundle gem helix_fib
Creating gem 'helix_fib'...
MIT License enabled in config
Code of conduct enabled in config
      create  helix_fib/Gemfile
...
$ cd helix_fib

プロジェクトの作成が完了したら、helix_fib.gemspecに
以下の内容を追記します。

spec.add_runtime_dependency "helix_runtime", "= 0.7.5"

またspec.authorsなどTODOが記載されている項目は適当に記入します。
記入が終わったらbundle installを行って依存関係にあるhelix_runtimeをインストールします。

bundle install --path=vendor/bundle

次にRakeファイルを以下のように書き換えてbuild時にhelix_runtimeが使われるように修正します。

require 'bundler/setup'
require 'helix_runtime/build_task'

HelixRuntime::BuildTask.new do |t|
end

task :default => :build

rustテンプレートの作成

まず以下のコマンドを実行してrustのプロジェクトを作成します。

$ cargo init --lib

このコマンドによってCargo.tomlとsrc/lib.rsが作られます。
他言語用のダイナミックなシステムライブラリを作成したいためにCargo.tomlに以下の記載を加えます。

[lib]
crate-type = ["cdylib"]

またdependenciesにはhelixを加えます

[dependencies]
helix = "0.7.5"

次にRustのコードを書いていきます。src/lib.rsに以下の内容を記述します。

#[macro_use]
extern crate helix;

ruby! {
    class HelixFIB {
        struct {
        }

        def initialize(helix) {
            HelixFIB{helix}
        }

        def fib(&self, n: u32) -> u32 {
            if n <= 1 {
                n
            } else {
                self.fib(n - 1) + self.fib(n - 2)
            }
        }
    }    
}

lib/helix_fib.rbの先頭に以下の2行を加えてビルドが通るように修正します。

require 'helix_runtime'
require 'helix_fib/native'

ビルドと動作確認

rakeコマンドを行ってrustのコードをビルドします。

$ bundle exec rake
{}
cargo rustc --release -- -C link-args=-Wl,-undefined,dynamic_lookup
...
    Finished release [optimized] target(s) in 0.08s

ビルドが完了したらbin/consoleで動作を確認します。

$ bin/console
irb(main):001:0> helix_fib = HelixFIB.new
=> #<HelixFIB:0x00007fffc4a4afd0>
irb(main):002:0> helix_fib.fib(40)
=> 102334155

rustで書いたコードがrubyのクラスとして呼び出されて、関数が機能していることが確認できます。

gem化とrubyプロジェクトからの呼び出し

gemのパッケージ化をした際にinstall時にビルドされる必要があるためhelix_fib.gemspecに以下の内容を追記します。

spec.extensions = %w[extconf.rb] 

またextconf.rbには以下の内容を記述します。

execonf.rb
abort "Rust compiler required (https://www.rust-lang.org/)" if `which rustc`.empty?

File.open("Makefile", "wb") do |f|

  f.puts(<<EOD)
all:
\tbundle --deployment --path vendor/bundle
\tbundle exec rake
clean:
install:
\trm -r vendor/bundle target
EOD

end

gemのbuildの際にディレクトリの内容をgit ls-filesで検索して依存性を解決するので、これまでの作業内容をgitに記録します。

git add .

gem buildを行うとgemファイルが完成します。

$ gem build helix_fib
  Successfully built RubyGem
  Name: helix_fib
  Version: 0.1.0
  File: helix_fib-0.1.0.gem

新しいディレクトリを作成して、bundle initして作成されたGemfileにhelix_runtimeとhelix_fibを追記します。

gem "helix_runtime"
gem "helix_fib"

vender/cacheのディレクトリを作成したあとに先ほど作成したhelix_fib-0.1.0.gemをコピーして、bundle install --path=vendor/bundleを行います。
helix_fibを呼び出す処理は以下のように記述します。

sample.rb
require 'helix_fib'

helix_fib = HelixFIB.new
puts helix_fib.fib(40)

作成したファイルを実行すると以下のような結果が返ってきてRustで作成したファイルが実行されていることがわかります。

$ bundle exec ruby sample.rb
102334155

Wasmを使う方法

Rustで書かれたコードをWasmにコンパイルする手法はよく知られているものだと思いますが、最近ではCloudfrare Workersや、Compute@Edgeなど、フロントエンドだけでなく、サーバーサイドよりの領域でも使われるようになっていました。そしてWASIというWASMをウェブブラウザー以外から呼び出すための仕様も策定されています。
今回はRustで書かれたコードをWasmにコンパイルして、それをRubyから呼び出す方法について記載しようと思います。
wasmerというスタンドアローンなWasmのランタイムとそれを呼び出すgemが存在するので、今回はこれを利用します。

環境の準備

まずはrustupを使ってターゲットとするコンパイラにwasmを追加します。

rustup target add wasm32-unknown-unknown

プロジェクトの作成とCargo.tomlの整備

cargoコマンドを使ってWasm用のプロジェクトを作成します。

$ cargo new --lib wasm_fib

Cargo.tomlに以下の内容を追記します。

Cargo.toml
...
[lib]
crate-type =["cdylib"]

プログラムの作成

src/lib.rsに以下の内容を記載します。多言語から呼び出すので、[no_mangle]を付けて関数のマングリングをなくします。

lib.rs
#[no_mangle]
pub extern fn fib(n: u32) -> u32 {
    if n <= 1 {
        n
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

Wasmへのコンパイル

cargo buildでwasmへのコンパイルを行います。

$ cargo build --target wasm32-unknown-unknown

コンパイルが完了するとtarget/wasm32-unknown-unknown/debugにwasm_fib.wasmが作成されます。

Rubyのプロジェクトの作成

Wasmを呼び出すためのRubyのプロジェクトを作成します。
新しいディレクトリを作成してbundle initを行って作成されたGemfileに以下の記述を追加して、bundle installを行います。

gem "wasmer"

Wasmを呼び出すプログラムの作成

前章で作成したwasm_fib.wasmをプロジェクトのディレクトリにコピーした後にsample.rbというファイルを作成し、以下の記述を行います。

sample.rb
require "wasmer"

bytes = IO.read "wasm_fib.wasm", mode: "rb"
instance = Wasmer::Instance.new bytes
puts instance.exports.fib 40

wasmのバイト列をWasmer::Instance.newの引数として渡すことで、wasmがインスタンス化され、関数を呼べるようになります。
プログラムを実行して結果を確認します。

$ bundle exec ruby sample.rb
102334155

ベンチマーク

rubyのベンチマーク関数を使ってフィボナッチ数の関数に40を渡したときの結果を出力してみます。

ruby

$ ruby fib.rb
                 user     system      total        real
fibonatti   11.750000   0.000000  11.750000 ( 11.748531)
102334155

ffi

$ ruby rust-fib.rb
                 user     system      total        real
fibonatti    0.593750   0.000000   0.593750 (  0.588983)
102334155

拡張ライブラリ

$ ruby rust-fib.rb
                 user     system      total        real
fibonatti    1.218750   0.000000   1.218750 (  1.232219)
102334155

Helix

 bundle exec ruby sample.rb
                 user     system      total        real
fibonatti    0.625000   0.000000   0.625000 (  0.627916)
102334155

Wasm

$ bundle exec ruby sample.rb
                 user     system      total        real
fibonatti    5.125000   0.000000   5.125000 (  5.144103)
102334155

結果としては、Ruby < Wasm < 拡張ライブラリ < Helix < FFI
の順番になりました。拡張ライブラリがHelixより遅いのはもっと検証が必要かもしれませんが、Wasmはランライムの呼び出しにコストがかかっているように感じました。
しかし、いずれの方法でもRubyに比べてパフォーマンスは向上しているようです。

まとめ

Helixをつかうことによって、RubyのC APIの内容をあまり知らずとも、Rustで拡張ライブラリを書くことができるようになりました。
ruby!やclassというおよそRustとは思えないコードであったり、
多言語から呼び出す処理に#[no_mangle]がないなど、不思議な感じが
する部分もありますが、簡単に拡張ライブラリが書けるということには少なからずメリットがあると思います。
Wasmに関してはFFIと同じような感覚があります。Rustで記述されたものをRubyで呼び出すためにWasmにする必要はなさそうに思えますが、wasmerの他にもwasmtimelucetなど、いくつかランタイムが登場していているので、興味深い分野だと思いました。

お知らせ

エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページWebエンジニア詳細ページよりお問い合わせ下さい。
あるいは、Qiita Jobs引越し侍Webエンジニアチーム自社サービスWebエンジニア/インフラからサービス改善まで!求む!引越し侍大規模リプレイスにおけるフロントエンドのリードエンジニア募集!の記事をみて、興味が出てきた方は是非Qiita Jobsのチャットでメッセージをください。

明日

明日はfujimkの記事です。
きっとこれがQiita初投稿かな。皆さんお楽しみに。

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

rubyのバージョンアップ

rubyのバージョンアップする時の方法

この記事は私の備忘録として記載していきます。
何か間違っている場合があれば、ご指摘していただけると助かります。

現在の環境

ruby 2.6.3
rails 5.2.3

目的

rubyのバージョンを2.6.5にしたいです。

環境にバージョンがあるか確認します。

$ rbenv install --list

もし、指定したいバージョンがなければ

$ brew upgrade ruby-build

をしてください。

もう一度確認で

$ rbenv install --list

rbenvでインストール

指定したいバージョンがあれば次に、rbenvでインストールをします。

$ rbenv install 2.6.

インストールされました!!

インストールしたバージョンを使用する時

$ rbenv versions
  system
  2.4.1
  2.5.1
  2.5.3
* 2.6.3 (set by /Users/user_name/hoge/.ruby-version)
  2.6.
  • 環境全体にrubyバージョンを反映したい時
$ rbenv global 2.6.5
  • 特定のプロジェクトのみ反映したい時
$ rbenv local 2.6.5

もう一度確認で

$ rbenv versions
  system
  2.4.1
  2.5.1
  2.5.3
  2.6.3 
* 2.6.5(set by /Users/user_name/hoge/.ruby-version)

反映されました。

rubyのバージョンごとにgemをインストールしなければいけないので

$ gem install bundler

インストールできました。
Gemfileに使用するバージョンを書き換えて

$ bundle install --path=vendor/bundle

できました。

rubyのバージョンを上げた後にrailsコマンドを使おうとすると、下記のようになってしまった。

ruby.rb
$ rails --version
rbenv: rails: command not found

The `rails' command exists in these Ruby versions:
  2.6.5

新しくインストールしたrubyの中にrailsというgemが入っていないために起こっているようだ。

目的

新しくインストールしたruby環境にrailsをインストールをする
まず、gemをアップデートします

$ gem update --system

次にbundlerをインストールします

$ gem install bundler

その次にrailsをインストールします

$ gem install rails

バージョンを確認します。

$ rails --version
Rails 5.2.3

バージョンが表示されたらrails コマンドが使えるようになっている

まとめ

初めてのQiita記事でした。記事一つ書くだけでも長時間かかってしまいました。
本日はバージョンのアップデートやバージョン指定など頻繁にあるので、備忘録としてここに書かせて頂きました。
どんどん記事を書いてアウトプットしていきたいと思います。

この記事は、下記の記事を参考にさせて頂きました。

rbenvでRubyのバージョンを最新安定版にupdate

rbenvでrubyのバージョンを上げたときに The `rails' command exists in these Ruby versions: となる

とても、勉強になりました。ありがとうございます。

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

【Ruby on Rails】新規アプリケーション作成まとめ

Rubyとは

Ruby(プログラミンング言語)

Rubyはプログラミング言語の一つであり、Javaなどの他のプログラミング言語に比べて、少ないソースコードの記述量でプログラムを組めるなどの特徴があります。(Ruby公式サイト

Ruby on Rails

Ruby on Rails

RubyのWebアプリケーションフレームワークの一つであり、Webアプリケーションフレームワークとは、Webアプリケーションを簡単に作れるようにする骨組みのことです。
フレームワークを使用することで、開発者はより少ない労力で開発を行うことが可能になります。

Rails開発環境の構築

Rails開発ではデータベース(MySQL)やRails自体のインストールなど、まずは開発のできる環境を整える必要があります。(Rails開発環境構築

  • Homebrewのインストール・アップデート
  • rbenv・ruby-buildのインストール
  • Ruby ver.2.5.1のインストール
  • MySQLのインストール
  • MySQLのインストール

新規アプリケーションを作成

$ rails new アプリケーション名
# アプリケーションを新規作成

$ rails new アプリケーション名 -オプション名
# オプションを付けてアプリケーションを作成

Gemの追加

Gem

RailsはGem(じぇむ)と呼ばれる便利なrubyライブラリをインストールして利用することで、より簡単にアプリケーション開発をすることができます。gemには開発を効率化してくれたり、会員登録サービスが簡単にできるなど便利なものがたくさんあります。Railsアプリケーションは全てをゼロから作るのではなくgemのライブラリも利用することでより効率的に開発することができます。

Gemfileに追加したら必ずターミナルでbundle installを実行します。

gem 'Gem名'
# Gemの追加
$ bundle install
# Gemをインストール

データベースの作成

データベース

データベースとはたくさんの情報(データ)が入っている箱のようなものです。この中には様々な種類のデータが存在していて、データを格納したり、取り出すことができます。Railsでは運用環境ごとにひとつのデータベースを持っています。

運用環境

運用環境 概要
development 開発環境。通常、開発をする際に使用する環境。
production 本番環境。アプリケーションを実際にリリースする際に使用する環境。
test テスト環境。アプリケーションの動作をテストする際に使用する環境。

database.ymlファイルの内容に基づいてデータベースを新規作成します。

$ rake db:create
# データベースの作成
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Rails ( or #Ruby + ActiveSupport ) + travel で 現地時刻 = JST で特定日付に時間を固定して、それぞれUTC JST timestamp で現在時刻・日付を得る例

# REQUIRE : on Ruby

require 'active_support/testing/time_helpers'
# => true

require 'active_support/core_ext'
# => true

include ActiveSupport::Testing::TimeHelpers
# => Object


# Travel to JST beggining of Year

travel_to Time.use_zone('Tokyo') { Time.parse('2020-01-01 00:00') }
# => nil


# JST

Time.use_zone('Tokyo') { Time.now }
# => 2020-01-01 00:00:00 +0900

Time.use_zone('Tokyo') { Time.current }
# => Wed, 01 Jan 2020 00:00:00 JST +09:00

Time.use_zone('Tokyo') { Time.zone.now }
# => Wed, 01 Jan 2020 00:00:00 JST +09:00

Time.use_zone('Tokyo') { Date.current }
# => Wed, 01 Jan 2020


# UTC


Time.use_zone('UTC') { Time.now }
# => 2020-01-01 00:00:00 +0900

Time.use_zone('UTC') { Time.current }
# => Tue, 31 Dec 2019 15:00:00 UTC +00:00

Time.use_zone('UTC') { Time.zone.now }
# => Tue, 31 Dec 2019 15:00:00 UTC +00:00

Time.use_zone('UTC') { Date.current }
# => Tue, 31 Dec 2019


# Unix Timestamp
# are All same at any Timezone

Time.use_zone('Tokyo') { Time.now.to_i }
# => 1577804400

Time.use_zone('Tokyo') { Time.zone.now.to_i }
# => 1577804400

Time.use_zone('UTC') { Time.now.to_i }
# => 1577804400

Time.use_zone('UTC') { Time.zone.now.to_i }
# => 1577804400

Time.now.to_i
# => 1577804400

Time.current.to_i
# => 1577804400


# Date.today
# Not considered Timezone with Time.zone setting
# its Ruby method refers TZ env

Time.use_zone('Tokyo') { Date.today }
# => Wed, 01 Jan 2020
Time.use_zone('UTC') { Date.today }
# => Wed, 01 Jan 2020

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2776

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

やさしい図解で学ぶ モデル マイグレーション その2 アソシエーション

その1ではテーブルの設計図となるマイグレーションファイルにて紹介しましたが今回は「モデルについて」と「アソシエーション (関連性)」をモデルファイルへ記述する方法をカンタンに学んでいきます。

⭐️モデルについて

まずモデルファイルとは
DBとのデータのやりとりをするために重要なファイルとイメージしてください。

ファイルには
モデル(テーブル)間の関係性を記述したり、
DBにあるテーブルに直接データの保存、呼び出しを可能にする機能を基本的に備えています。

スクリーンショット 2019-11-27 18.11.52.png

上の図では
モデルファイルからDBへ「ActiveRecord」と書かれたパイプや橋のようなものがあり、userやtweetモデルからDB内の同じ名前のテーブルに伸びています。

*ActiveRecordというのは厳密にいうとRailsに標準ライブラリとして採用されているO/Rマッパーのことです。専門用語を使わず簡単に表現すると

DBとアプリケーションとの 「 橋渡し  or  通訳 」

のようなものだと思ってください。

もし、モデルがないと

スクリーンショット 2019-11-28 12.06.15.png

Railsアプリケーションではプログラミング言語「Ruby」を。
DBでは「SQL」をそれぞれ使っているので、言語の違いからそのままではやりとりができません。

そこで
スクリーンショット 2019-11-28 12.44.47.png

とざっくりなイメージですが、ありがたいことにRailsではモデルファイル(ActiveRecord)が通訳のようにRuby→SQLに、SQL→Rubyにと言語を変換してデータベースとのやりとりを円滑に行ってくれているんです。

⭐️アソシエーション(テーブル間の関連性)

アソシエーションとは、つまり
テーブル同士の関連性のことで、テーブル間にみられる対1や対多などの関係性をモデルファイルに明示しなければいけません。

例えば下のER図の場合
スクリーンショット 2019-11-19 15.42.33.png

中間テーブルを含む3つのテーブルがあります。
これらの関係性を各テーブルのモデルファイルに記述していきます。

まず
今回のテーブル群のモデルファイルへの記述方法は以下の3パターンを使用します。
(そのほかにもhas_oneやhas_and_belongs_to_manyなどがあり、オプションも存在しますが今回はこちらの3つのみ)

スクリーンショット 2019-11-25 12.22.31.png

記述方法は
スクリーンショット 2019-11-29 11.15.56.png

belongs_toやhas_oneの場合は単数、
has_manyなどは複数形で記述します。

実際にuserのモデルファイルに記述するとこのようになります。
スクリーンショット 2019-11-27 18.34.37.png

またそのほかのモデルファイルにもアソシエーションを記述し、
さらにER図のようにイメージして書くとこのような関係性になります。

スクリーンショット 2019-11-25 11.53.03.png

中間テーブルcourse_usersにはそれぞれ接続されているテーブルの主キー、つまりは、外部キーuser_idcourse_idを持っていますのでこのような記述となります。

そして中間テーブルと接続しているテーブルのモデルにはそれぞれアソシエーションとして以下のように記述します。
スクリーンショット 2019-11-29 13.42.13.png

今回はイメージを含めて基本的なもののみの紹介です。
そのほかにも便利なオプションや外部キー関連の記述もありますので徐々に学んでいきましょう。

⭐️補足:belongs_to

はじめはbelongs_toというのがちょっとわかりづらいと思いますが、考え方としてはテーブル内に外部キーを持っていた場合、そのモデルファイルにはbelongs_toを書くと思ってください。

例えば、
usesテーブルとtweetsテーブルなるものがあり、関係性が図のようだとすると

tweetsテーブルには外部キーであるuser_idを持っています。
モデルファイルには以下のように記入します。

スクリーンショット 2019-11-29 12.16.40.png

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

やさしい図解で学ぶ モデル マイグレーション その2

その1ではテーブルの設計図となるマイグレーションファイルにて紹介しましたが今回は「モデルについて」と「アソシエーション (関連性)について」を基礎に学んでいきます。

⭐️モデルについて

まずモデルファイルとは
DBとのデータのやりとりをするために重要なファイルとイメージしてください。

ファイルには
モデル(テーブル)間の関係性を記述したり、
DBにあるテーブルに直接データの保存、呼び出しを可能にする機能を備えています。

スクリーンショット 2019-11-27 18.11.52.png

上の図では
モデルからDBへ「ActiveRecord」と書かれたパイプや橋のようなものがあり、userやtweetモデルからDB内の同じ名前のテーブルに伸びています。

*ActiveRecordというのはRailsに標準ライブラリとして採用されているO/Rマッパーのことです。

専門用語を使わず簡単に表現すると

DBとアプリケーションとの 「 橋渡し  or  通訳 」

のようなものだと思ってください。

もし、モデルがないと

スクリーンショット 2019-11-28 12.06.15.png

Railsではプログラミング言語「Ruby」を。
DBでは「SQL」をそれぞれ使っているので、言語の違いからそのままではやりとりができません。

そこで
スクリーンショット 2019-11-28 12.44.47.png

Railsではモデルファイル(ActiveRecord)が通訳のようにRuby→SQLに、SQL→Rubyにと言語を変換してデータベースとのやりとりを円滑に行ってくれているんです。

⭐️アソシエーション(テーブル間の関連性)

アソシエーションとは、つまり
テーブル同士の関連性のことで、テーブル間にみられる対1や対多などの関係性をモデルファイルに明示しなければいけません。

例えば下のER図の場合
スクリーンショット 2019-11-19 15.42.33.png

中間テーブルを含む3つのテーブルがあります。
これらの関係性を各テーブルのモデルファイルに記述していきます。

今回の記述方法は以下の3パターンを使用します。
(そのほかにもhas_oneやhas_and_belongs_to_manyなどがあり、オプションも存在しますが今回はこちらの3つのみ)

スクリーンショット 2019-11-25 12.22.31.png

記述方法は
スクリーンショット 2019-11-29 11.15.56.png

belongs_toやhas_oneの場合は単数、
has_manyなどは複数形で記述します。

実際にuserのモデルファイルに記述するとこのようになります。
スクリーンショット 2019-11-27 18.34.37.png

またそのほかのモデルファイルにもアソシエーションを記述し、
さらにER図のようにイメージして書くとこのような関係性になります。

スクリーンショット 2019-11-25 11.53.03.png

中間テーブルcourse_usersにはそれぞれ接続されているテーブルの主キー、つまりは、外部キーuser_idcourse_idを持っていますのでこのような記述となります。

has_many through

中間テーブルと接続しているテーブルのモデルにはそれぞれアソシエーションとして以下のように記述します。
スクリーンショット 2019-11-29 13.42.13.png

中間テーブルを通ってその先のインスタンスを複数持っていますよという記述になります。

今回も基礎的な部分を図解を用いて記載致しましたので実際の開発ではモデルファイルにもう少し細かく書いていきます。

今回の記事でモデルに関して少しでもイメージが湧いていただけたのなら幸いです。

⭐️補足:belongs_to

初見ではbelongs_toというのがちょっとわかりづらいと思いますが、考え方としてはテーブル内に外部キーを持っていた場合、そのモデルファイルにはbelongs_toを書くと思ってください。

例えば、
usesテーブルとtweetsテーブルなるものがあり、関係性が図のようだとすると

スクリーンショット 2019-11-29 12.16.40.png

tweetsテーブルには外部キーであるuser_idを持っています。
モデルファイルには以下のように記入します。

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

selectタグで複数選択に変えたら、ストロングマラメーターによってパラメーターが排除されてしまった

webサイトを作成中にselect_tagを複数選択に変えたら、 入力した値が排除されてしまい少しハマってしまったので同じミスをしないためにまとめます。

エラーが発生した状況

スクリーンショット 2019-11-29 0.19.10.png
上の写真のようにselect_tagを複数選択に変えたところ、これまで正常に登録ができていたにもかかわらず、登録できないエラーが発生した。
ログをみてみると、「Unpermitted parameter: :category_id」となっており、ストロングパラメータで弾かれ、paramsの値がnilになっていることが原因のようだった。
スクリーンショット 2019-11-29 0.20.07.png
コンソールでも確認してみたところnilになっている
スクリーンショット 2019-11-29 0.21.00.png

解決のために試したこと

ストロングパラメータを書いたコードをみて、paramsで指定した値の入力ミスがあるか確認。
しかし、タイポしているわけではなさそう。複数選択を解除すれば問題なく通ったため、複数選択が原因であることは間違えなさそう。
スクリーンショット 2019-11-29 0.18.41.png

複数選択の時と一つしか選択できない時のparamsに入っている値を確認してみる。
・一つ選択の時
スクリーンショット 2019-11-29 0.20.32.png
・複数選択の時(pryを使用してparamsの値を確認)
スクリーンショット 2019-11-29 0.20.47.png

確認してみると、複数選択の場合、値が配列になっていることからストロングパラメーターにもそれを追記しなければいけないのではと思い実行したら、うまく作動できた!
スクリーンショット 2019-11-29 12.54.48.png

params.require(:posting_thread_categories).permit(:category_id)
# 上記から以下に変更
params.require(:posting_thread_categories).permit(category_id: [])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル学習メモ2

データ更新(rails cから)

[1] pry(main)> post = Post.find_by(id: 1)
[2] pry(main)> post.content = "ああああああ"
[3] pry(main)> post.save

データの削除(rails cから)

[1] pry(main)> post = Post.find_by(id: 1)
[2] pry(main)> post.destroy
[3] pry(main)> post.save

link_toでmethodを指定する

link_to("文言", "パス", {method: "post"})

modelにvalidationを設定

modes/post
validates :content,  {presence: true, length: {maximum: 100}}
// presenceはカラムが存在するか確認する

renderメソッド

renderメソッドを使うと、redirect_toメソッドを使った場合と違い、そのアクション内で定義した@変数をビューでそのまま使える。

エラーメッセージはRails側で自動で保存される

@post.errors.full_messages

view側でのエラーメッセージ表示

test.html.erb
          <% @post.errors.full_messages.each do |message| %>
            <div class="form-error">
              <%= message %>
            </div>
          <% end %>

ん、railsってメッセージデフォルトで予測してくれて入っちゃってる感じ?

フラッシュメッセージ

flash[:notice] = "投稿を編集しました"

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

【Ruby】配列のnilチェック+部分一致検索 (備忘録)

Rubyの超初心者(自分)向けの備忘録

やりたいこと

  1. Rubyハッシュ(連想配列)のnilチェック
  2. 配列の部分一致検索

1. Rubyのハッシュ(連想配列)のnilチェック

  • 最初はif文でnilチェックをやろうと思ったけど、以下のようにdig()を使う方が簡単だった
    (Ruby2.3で導入されたらしい)
params.dig(:q, :name)

2. 配列の部分一致検索

search_text = '検索したい文字'
array.select { |e| e =~ %r{^.*#{search_text}.*} }
  • #{}で囲うとで変数を入れられる
  • %r{}で正規表現オブジェクトを作成した場合は、パターン内に「/」が含まれていてもエスケープは不要らしい。便利!

「もっとこうした方が楽だよ!」と言った知見があれば、コメントお待ちしてます

参考サイト

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

【Ruby】配列のnilチェック+部分一致検索

やりたいこと

  1. Rubyハッシュ(連想配列)のnilチェック
  2. 配列の部分一致検索

1. Rubyのハッシュ(連想配列)のnilチェック

  • 最初はif文でnilチェックをやろうと思ったけど、Ruby2.3で導入されたdig()を使う方が簡単だった
params.dig(:q, :name)

2. 配列の部分一致検索

search_text = '検索したい文字'
array.select { |e| e =~ %r{^.*#{search_text}.*} }
  • #{}で囲うとで変数を入れられる
  • %r{}で正規表現オブジェクトを作成した場合、パターン内に「/」が含まれていてもエスケープは不要らしい。便利!

「もっとこうした方が楽だよ!」と言った知見があれば、コメントお待ちしてます

参考サイト

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

Ruby on Rails と React Native で作る web & モバイルアプリ [webアプリ編]

イントロダクション

目的

なんか web アプリケーションを作って運用していたら、どうやらモバイルアプリ需要が出てきたことが分かってバタバタ後追いでモバイルアプリ対応することってあるじゃないですか。今回は演習式でそのシミュレーションをします。まず、Ruby on Rails で web アプリケーションを作った後に、API を生やして React Native で web アプリケーションと同じような仕様のモバイルアプリケーションを作ります。

今回作るアプリケーションの完成品は以下のリポジトリに置いていますので、React Native 側の演習のみを行いたい方は Ruby on Rails アプリケーションは以下より clone してデプロイし、モバイルアプリ編 へ進んでください。

フレームワーク リポジトリ
Ruby on Rails (web) https://github.com/ogihara-ryo/zone-web
React Native (mobile) https://github.com/ogihara-ryo/zone-mobile

いきなり言い訳なんですが、Advent Calendar の1枠としてやるには明らかにやりすぎな記事になってしまいました。お詫び申し上げます。Ruby on Rails と React Native のカレンダーで1枠ずつ頂くのではなく1人 Advent Calendar をやれば良かったなぁと12月に入ってから今更少し後悔しています。ただ、私は深く反省しているので、てへぺろ1つで許されることは目に見えています。張り切っていきましょう。

今回作るシステム

スーパーウルトラシンプルなサンプルアプリケーションを作ることにしましょう。今日やるべきタスクを雑に登録してひたすら消化するためだけのタスク管理システムを作りましょう。大人の都合で認証機能を設けた方が記事的に映えるのでログイン必須のシステムが良いですね。/users/:id/tasks を叩けば誰でもそのユーザーの今日のタスク状況が見られるようにしましょう。

スクリーンショット 2019-12-13 11.25.13.pngスクリーンショット 2019-12-13 11.26.12.png

ユースケースとしては、まず各人は朝に今日中に片付けたいタスクを雑に列挙して(まあ朝起きられないおまえらは昼かもしれませんが)、各タスクが終わるたびにチェックを付けていき、プロジェクトマネージャーだか上司だかは URL を共有してもらっているので各人のタスク状況を web から確認できる、みたいな感じにしますか。それ Google スプレッドシートでできるよ。

web アプリケーションでできること

  • サインアップ
  • ログイン
  • タスクの CRUD
  • 他人のタスクも閲覧のみ可能

モバイルアプリケーションでできること

  • ログイン (サインアップは web でやれ)
  • 自身のタスクの CRUD (他人のタスクは web で見ろ)

想定読者

記事を書いた動機は、最近 Ruby on Rails で作った web アプリを React Native アプリ対応しようと思ったら参考日本語記事が少なくて苦労したためですが、この記事としては Ruby on Rails の心得が少しだけある初学者レベルを想定して書いています。Ruby on Rails 自体が初めての方も、写経で雑に理解しながら進めていくことはできるようには少しだけ配慮していますが、あまりにも初歩的で詳細すぎる説明は避けるようにも配慮しています。本記事(webアプリ編)だけでも、ちょっとした Ruby on Rails アプリケーションを作ることができるので、初学者の方は挑戦してみてほしいなぁ...という下心があります。

もし、この記事を進めていく上で躓いた場合や理解できないことがあった場合は @OgiharaRyo までご連絡頂くか、現在300人ほど参加している技術質問 slack コミュ二ティを運営していますので、こちらで質問して頂ければと思います。(slack 招待リンク)

環境

2019年12月初旬執筆時点の最新版を使用します。

% ruby -v
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]

% rails -v
Rails 6.0.1

% psql -V
psql (PostgreSQL) 12.1

謝辞

以下のツイートで記事のテストプレイヤーを募集したところ、にしこさんとnonoさんにお声がけ頂き、記事中の不備や手順の漏れを確認及び指摘して頂きました。ありがとうございました。

Ruby on Rails アプリケーションの開発

rails new

オプション欲張りセットの rails new を実行します。本記事では web アプリケーションを Heroku で公開するため、データベースに PostgreSQL を指定しています。その他のたっぷりあるオプションは今回の記事に関係のないものを全て排除しています。ちなみにアプリケーション名に zone と入っているのは、集中して没頭している意識状態になることを「ゾーンに入る」と表現することがあるので、そこから拝借しました。日次でタスクを可視化して消化するだけで、結構集中状態に入りやすくなる気がしませんか?まあそんなことは良いので早く rails new してください。Ruby on Rails アプリケーション開発の始まりです!

% rails new zone-web --database=postgresql --skip-action-mailer --skip-action-text --skip-active-storage --skip-action-cable --skip-javascript --skip-turbolinks --skip-test

データベースを作ってから一応起動を確認しておきましょう。

% cd zone-web
% rails db:create
% rails s

環境にもよりますが、大人の都合で以下よりあなたの開発サーバーが localhost:3000 であることを前提にリンクを記述していきますので異なる方は適時読み替えてください。ということで、localhost:3000 にアクセスしてご機嫌な表示がされたら ok です。

スクリーンショット 2019-11-30 18.55.58.png

モデリング

とりあえず User モデルと Task モデルを作成します。

User

説明
name string ユーザーの名前を保存します。
account_id string 今回はメール認証等は行わないので、とりあえず任意のアカウントID(文字列)とパスワードのペアでログインできるようにします。ここをメールアドレスにしてメール認証のプロセスを挟んでも良いでしょう。
password_digest string ハッシュ化されたパスワードを保存します。このパスワードは復号できません。つまり、ユーザーがどんなパスワードを入力したのかはシステムが後から知ることはできません。

Task

説明
user_id string どのユーザーのタスクであるのかを保存します。
name string タスクの名前を入力します。見積もり時間も詳細も不要です。ただ1行完結なタスク内容が書ければ良いのです。日次でタスクを表示するので作成日も必要ですが、 rails g(enerate) で生成されたマイグレーションファイルはデフォルトで created_at という列にレコードの作成日時が入るようになるのでこれを利用します。ちなみにこれはあまり良い方法ではないので実務では created_at を業務上の都合と結び付けないようにしましょう。

scaffold 等は行わずに Controller と View は温もりのあるお手製で作ります。モデルとマイグレーションファイルは冷たく Generator に頼ります。

% rails g model User name:string account_id:string password_digest:string
% rails g model Task user:belongs_to name:string finished:boolean
% rails db:migrate

migrate した結果はこのようになっているはずです。

db/schema.rb
  create_table "tasks", force: :cascade do |t|
    t.bigint "user_id", null: false
    t.string "name"
    t.boolean "finished"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_tasks_on_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "account_id"
    t.string "password_digest"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  add_foreign_key "tasks", "users"

Task モデルには generator が belongs_to :user を追加してくれていますが、User 側が Task を複数持つことはモデルが知らないので一文追加しておきましょう。

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

サインアップ

とりあえず User を作る機能を実装しなければ始まらないので、サインアップ機能を提供しましょう。

サインアップフォーム表示

ルーティング

/signup という URL にアクセスされたら UsersControllernew アクションが呼ばれるようにします。また、サインアップの submit で /signup という URL に POST された時に UsersControllercreate アクションが呼ばれるようにします。

config/routes.rb
  Rails.application.routes.draw do
+   get 'signup', to: 'users#new'
+   post 'signup', to: 'users#create'
  end

Controller

新しく app/controllers/users_controller.rb を作成し、以下のコードを仮置きします。今回は new アクションでフォームが表示されることだけを目的とするので、create アクションはまだ書きません。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end
end

パスワードハッシュ化の準備

Gemfile でデフォルトでコメントアウトされている bcrypt をコメントインします。

Gemfile
- # gem 'bcrypt', '~> 3.1.7'
+ gem 'bcrypt', '~> 3.1.7'

bundle install します。ちなみに bundle だけ叩くと bundle install されます。

% bundle

User モデルで has_secure_password をコールします。今インストールした bcrypt の力で、これをコールしておくことでモデルの属性に passwordpassword_confirmation(確認入力用) が追加され、合致して save した時に自動で password_digest にハッシュ化されたパスワードがセットされます。

app/models/user.rb
  class User < ApplicationRecord
+   has_secure_password
  end

View

新しく app/views/users/new.html.erb を作成し、ユーザー情報の入力フォームをマークアップしていきましょう。

app/views/users/new.html.erb
<%= form_with model: @user, url: signup_path, local: true do |f| %>
  <p>
    <%= f.label :name, 'お名前' %>
    <%= f.text_field :name %>
  </p>

  <p>
    <%= f.label :account_id, 'アカウントID' %>
    <%= f.text_field :account_id %>
  </p>

  <p>
    <%= f.label :password, 'パスワード' %>
    <%= f.password_field :password %>
  </p>

  <p>
    <%= f.label :password_confirmation, 'パスワード(確認)' %>
    <%= f.password_field :password_confirmation %>
  </p>

  <p><%= f.submit '送信' %></p>
<% end %>

これで localhost:3000/signup にアクセスするとフォームが表示されます。
スクリーンショット 2019-12-01 18.56.01.png

サインアップ処理実装

今の状態で submit すると(送信ボタンを押すと) 「create アクションが見つからないんだが?!」というエラーで怒られます。初学者のあなたは一度 submit してみてエラー画面を目に焼き付けておきましょう。エラーメッセージを目に焼き付けた数が経験値になります。さて、早速 Controller に create アクションを実装していきましょう。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    def new
      @user = User.new
    end
+
+   def create
+     @user = User.new(user_params)
+     render :new unless @user.save
+   end
+
+   private
+
+   def user_params
+     params.require(:user).permit(:name, :account_id, :password, :password_confirmation)
+   end
  end

user_params メソッドの中身は Strong Parameters といい、ブラウザーから POST されるはずのパラメーターのキーを個別に許可しています。これは、id 等といったサインアップフォームからは設定してほしくない属性(フォームにフィールドを置いていない属性)を特殊な方法で POST されてもデータベースに保存しないための防御機構です。ここで許可した属性以外は、users テーブルにセットしないようにしているわけですね。

続いて、作成後に「ご登録ありがとうございます。」とのテキストを表示できるようにします。新しくapp/views/users/create.html.erb を作ってマークアップします。

app/views/users/create.html.erb
<p>ご登録ありがとうございます。</p>

さて、実装できたら以下のような情報を入力してテストアカウントを作成してみましょう。

項目 入力する情報
お名前 テストアカウント
アカウントID test
パスワード password
パスワード(確認) password

スクリーンショット 2019-12-01 20.57.31.png

送信すると、先ほどの create.html.erb にマークアップしたテキストが表示されるはずです。

スクリーンショット 2019-12-01 21.09.20.png

上手く実装できていれば、users テーブルに1レコード作成されているはずです。rails console から見てみましょう。

% rails c
Running via Spring preloader in process 48857
Loading development environment (Rails 6.0.1)
irb(main):001:0>

一番最近作られた User を参照します。このように先ほど作ったユーザーが表示されれば ok です。

irb(main):001:0> User.last
  User Load (5.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 1, name: "テストアカウント", account_id: "test", password_digest: [FILTERED], created_at: "2019-12-01 11:58:46", updated_at: "2019-12-01 11:58:46">

「ご登録ありがとうございます。」との表示を確認したのに User の情報が表示されず nil が返ってくる場合はコードを見直してみてください。Strong Parameters あたりが怪しいと思います。

異常処理

今のサインアップ機構には以下の問題点があります。

  1. 名前やアカウントIDやパスワードが空でも登録できてしまう
  2. パスワードとパスワード(確認)が違っている場合にユーザーに何も通知されない
  3. 同一のアカウントIDを登録できてしまう

同一のアカウントIDを登録できてしまうと、ログインする時に入力されたアカウントIDでどのユーザーで認証すれか良いか分からなくなるので、アカウントIDは既に登録されている値を使えなくする必要があります。これから、それらの問題を解決していきます。

サーバーサイドバリデーション

まず、Model 側でバリデーションを設定してレコードを作成する時に値の有効性を検証することにしましょう。パスワードを必須であるかや、パスワードとパスワード(確認)が一致しているかは、has_secure_password が検証してくれるので、残りの名前とアカウントIDに関するバリデーションを定義していきます。

app/models/user.rb
  class User < ApplicationRecord
    has_secure_password
+
+   validates :name, presence: true
+   validates :account_id, presence: true, uniqueness: true
  end

これで、名前が必須となり、アカウントIDが必須かつ重複不可となりました。rails console で試してみましょう。名前を空にして、アカウントIDを先ほど画面から作った test にして、パスワードとパスワード(確認)に別の文字列を入れてみます。

irb(main):001:0> User.create!(name: '', account_id: 'test', password: 'password', password_confirmation: 'invalid')
   (0.1ms)  BEGIN
  User Exists? (0.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."account_id" = $1 LIMIT $2  [["account_id", "test"], ["LIMIT", 1]]
   (0.3ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):2
ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password, Name can't be blank, Account has already been taken)

今回重要なのは最終行です。

ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password, Name can't be blank, Account has already been taken)

と書かれていますね。つまり、パスワード(確認)がパスワードと合っていないよ、名前は空にできないよ、アカウントが重複しているよ、と言われています。問題なさそうですね。ちなみに勿論、データベースには保存されていません。不安であれば再び User.last で確かめてみましょう。ちなみに、エラーになる条件は他にもパターンがあります。アカウントIDやパスワードが空の場合です。勿論このままそういったパターンの検証を続けても良いですが、実際の開発ではこういった挙動確認はテストコードを書いて行うことが多いです。ただし、テストの話をすると私がうるさくなるので、本記事ではノーテストでいきます。「ノーテストで開発するのはノーヘルでバイクに乗るようなものです。つまり気持ちいいってことです。」みたいなことを誰か偉い人が言っていたような気がします。わかる。

エラーメッセージの通知

さて、サーバーからバリデーションエラーで弾かれたことをユーザーに通知してやる必要があります。エラーメッセージは、@user.errors.full_messages で配列で取り出すことができます。View の form_with ブロックの一番上にエラーメッセージ表示用のマークアップを行いましょう。

app/views/users/new.html.erb
  <%= form_with model: @user, url: signup_path, local: true do |f| %>
+   <% if @user.errors.any? %>
+     <ul>
+       <% @user.errors.full_messages.each do |message| %>
+         <li><%= message %></li>
+       <% end %>
+     </ul>
+   <% end %>
    ...
  <% end %>

これで、@user インスタンスがエラー情報を持っている場合のみバリデーションエラーのリストを表示することができます。早速動かしてみましょう。先ほど rails c で試してみたことをブラウザーから試してみましょう。以下のスクリーンショットのように、名前を空にして、アカウントIDを先ほど画面から作った test にして、パスワードとパスワード(確認)に別の文字列を入れてみます。

スクリーンショット 2019-12-02 2.08.57.png

この状態で送信ボタンを押して submit すると以下のようにエラーメッセージが表示されます。先ほど rails c で確認したエラーメッセージと同様ですね。

スクリーンショット 2019-12-02 2.10.23.png

ちなみにこのメッセージを日本語にすることもできるのですが、少しコードが散らかって今回作りたいシステムの目的から脱線するので今回は英語のままにしておきます。変なところで投げっぱなし、それがこの記事の雑さです。また、上記2つのスクリーンショットを見比べてみると、ラベルと入力フィールドの間の改行状態が変わっていることが分かります。これは Ruby on Rails (f.text_field)が気を利かせて生成される HTML にエラーであることが分かるような手を加えてくれているのですが、ここで詳しくは解説しません。気になる方は、ブラウザーのデベロッパーツールから生成された HTML のソースコードを読んでみてください。変なところで投げっぱなし、それがこの記事の雑さです。

クライアントバリデーション

さて、ついでにクライアント(ブラウザー)側でも簡易的なバリデーションを設定しておきましょう。今回はとても簡単な「フィールドを必須にする」という実装だけを行います。なんと、HTML5 の input 要素は required 属性を与えるだけでそのフィールドが必須であることをブラウザーに指示してくれます。Ruby のフォームビルダーのメソッド(f.text_field 等)で required 属性を指定するには required: true というオプションを与えてやるだけで ok です。

app/views/users/new.html.erb
    <p>
      <%= f.label :name, 'お名前' %>
-     <%= f.text_field :name %>
+     <%= f.text_field :name, required: true %>
    </p>

    <p>
      <%= f.label :account_id, 'アカウントID' %>
-     <%= f.text_field :account_id %>
+     <%= f.text_field :account_id, required: true %>
    </p>

    <p>
      <%= f.label :password, 'パスワード' %>
-     <%= f.password_field :password %>
+     <%= f.password_field :password, required: true %>
    </p>

    <p>
      <%= f.label :password_confirmation, 'パスワード(確認)' %>
-     <%= f.password_field :password_confirmation, required: true %>
+     <%= f.password_field :password_confirmation, required: true %>
    </p>

この状態でフィールドを空にして submit しようとすると、ブラウザーから以下のスクリーンショットのように警告が表示されてキャンセルされると思います。このスクリーンショットは macOS の Chrome です。

スクリーンショット 2019-12-02 2.57.29.png

「どの道サーバーで弾かれてエラーメッセージも表示されるんだからクライアント側のバリデーションは冗長では?」と思う方もいらっしゃるかもしれませんが、サーバーに問い合わせなくても入力段階で弾けるものは弾いてしまっておいた方が、ユーザーの手間を煩わせずに済むことが多いため、UX の観点からクライアント側でもバリデーションを行うことが多いです。変なところでこだわる、それがこの記事の雑さです。

スタイリング

無事にサインアップの機能を作ることができましたが、フォームがあまりにも簡素で気分が上がらないので少しだけ見た目に気を使うことにしましょう。ただ、この記事では「CSS 絶対に書きたくないでござる!」という気持ちと、「CSS フレームワークに依存した class 付けは絶対にしたくないでござる!」という気持ちと、「CSS フレームワークの導入は CDN だけで終わらせたいでござる!」という気持ちが強いので、CSS を書かなくても class を付けなくても良くて CDN で読み込むだけでそれっぽい見た目になる方法を探します。そこで mini.css を利用します。記事執筆時点で、ここまでに書いたサインアップフォームを一切触らなくても CDN へのリンクを書くだけでスタイルが当たるものを探し回ったところ mini.css が一番それっぽくなったような気がしました。ということで、早速 CDN から mini.css を読み込みます。たった1行書くだけで CSS を丸ごと配信してもらえるなんて至れり尽くせりですね。また、mini.css の ドキュメント(Getting started) を見ると、「viewport はちゃんと指定しておけよ」と書いてあるので従います。各ページ共通のレイアウト(head 要素等) は app/views/layouts/application.html.erb に書かれているので、ここに viewport の指定と CDN へのリンクを設定します。

app/views/layouts/application.html.erb
    <head>
      <title>ZoneWeb</title>
      <%= csrf_meta_tags %>
      <%= csp_meta_tag %>
+     <meta name="viewport" content="width=device-width, initial-scale=1">
+
+     <link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css">
      <%= stylesheet_link_tag    'application', media: 'all' %>
    </head>

この状態でサインアップフォームにアクセスすると、それっぽいデザインになっています。required 属性が付いた input 要素のフィールドが未入力だと赤枠で囲まれるのが素敵ですね。

スクリーンショット 2019-12-02 3.22.01.png

まあいろいろと、もう少し何とかしたい感はありますが、一切の class 付けもカスタマイズも行わずにただ CSS を読み込むだけで、ここまでスタイリングしてもらえれば充分です。今回はスタイリングのことは考えずに進めたいので、一旦はこのまま進もうと思います。

認証

次は、サインアップしたユーザーのアカウントIDとパスワードでログインできるようにします。この章では以下のような仕様で実装していきます。

  • ログインしたらトップページへ遷移
  • ログインしていない状態でトップページへアクセスされたらログインページへリダイレクト

トップページ

まず、ログインした時に表示するためのトップページを作ります。トップページはタスクの一覧をいきなり表示するので、TasksControllerindex アクションにルーティングします。

config/routes.rb
  Rails.application.routes.draw do
+   root 'tasks#index'

    get 'signup', to: 'users#new'
    post 'signup', to: 'users#create'
  end

app/controllers/tasks_controller.rb を作成し、 TasksControllerindex アクションを仮実装します。今は、ログイン後にページが表示されれば何でも良いので空っぽのアクションで ok です。

app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index; end
end

続いて app/views/tasks/index.html.erb を作成し、View を仮実装します。テンションの上がりそうなテキストを置いておきましょう。

app/views/tasks/index.html.erb
<p>Welcome!!</p>

これで、トップページ(localhost:3000)にアクセスするとテンションが上がります。
スクリーンショット 2019-12-02 11.16.20.png

ログインフォーム実装

サインアップページやトップページを作った時と同様に、サクサクとルーティングと Controller と View を実装していきましょう。少しずつ慣れてきましたか?

config/routes.rb
  Rails.application.routes.draw do
    root 'tasks#index'

    get 'signup', to: 'users#new'
    post 'signup', to: 'users#create'
+
+   get 'login', to: 'sessions#new'
+   post 'login', to: 'sessions#create'
  end
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new; end

  def create; end
end
app/views/sessions/new.html.erb
<%= form_with scope: :session, url: login_path, local: true do |f| %>
  <p>
    <%= f.label :account_id, 'アカウントID' %>
    <%= f.text_field :account_id, required: true %>
  </p>

  <p>
    <%= f.label :password, 'パスワード' %>
    <%= f.password_field :password, required: true %>
  </p>

  <p><%= f.submit 'ログイン' %></p>
<% end %>

<p><%= link_to 'サインアップはこちら', signup_path %></p>

サクサクと実装できましたか?これでログインページ(localhost:3000/login)にアクセスするとフォームが表示されます。
スクリーンショット 2019-12-03 6.11.50.png

あとは、サインアップ完了画面にログインリンクを追加しておきましょう。

app/views/users/create.html.erb
  <p>ご登録ありがとうございます。</p>
+ <p><%= link_to 'ログインページ', login_path %>からログインしてご利用を開始してください。</p>

認証

さて、それではログイン処理を実装していきましょう。いきなり難易度が跳ね上がりますが、あれだったらコピペして後は見なかったことにして切り抜けてください。まずは認証のコア部分を書いていきます。今回は超シンプルな認証にするので devise 等の Gem は使わずに remember me も実装せず、大したセキュリティも意識せずにサクッとお手製で作っていきます。認証系を手書きするのは地獄だと思われるかもしれませんが、これだけです。

app/helpers/sessions_helper.rb
module SessionsHelper
  def login(user)
    session[:user_id] = user.id
  end

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  def logged_in?
    current_user.present?
  end
end

login メソッドはただの代入のように見えますが、これは User の ID をクライアントの Cookie に暗号化して詰めています。この Cookie の中身を復号した値は代入時と同じように session メソッドから呼び出せます。つまり、session[:user_id]User の ID を詰めておけば、次回以降のリクエストでユーザーに Cookie が残っていれば session[:user_id] を参照することで読み出すことができます。

current_user メソッドは、上述の session[:user_id] を使って User のインスタンスを取得して返します。User.find_by(id: session[:user_id]) で Cookie に入っているユーザーの ID でデータベースを照会している訳ですね。||= という演算子の使い方は見慣れない方もいるでしょう。これは、演算子の短絡評価の特性を使って左辺が偽の場合のみ代入が実行するための文法です。論理和演算(||)における短絡評価とは、「左辺が偽なら右辺が真であろうが偽であろうがどうせ偽になるので右辺をそもそも評価する必要ないよね」という評価法です。左辺が真であって初めて右辺を気にするということですね。ここで ||= を使う目的は、同じリクエスト内で複数回 current_user メソッドがコールされた時のデータベースアクセスを初回以外スキップすることです。つまり初回でデータベースから User のインスタンスを引っ張ってきたら、次回以降はインスタンス変数を参照することでデータベースにアクセスするコストを防いでいるということです。

logged_in? メソッドは、見ての通り current_userUser のインスタンスを引っ張れたかどうかを確認するメソッドです。find_by は見つからなかった時に nil を返すので、session[:user_id] が空の場合や存在しないユーザーの ID 等が入っている場合は、logged_in? メソッドが false を返すということです。

さて、ログイン機構を作ったところで、ログインフォームから POST されてきた時の処理を実装しましょう。SessionsControllercreate アクションを実装していきます。

app/controllers/sessions_controller.rb
  def create
    user = User.find_by(account_id: params[:session][:account_id])
    if user&.authenticate(params[:session][:password])
      login user
      redirect_to root_path
    else
      flash.now[:alert] = 'アカウントID、またはパスワードが間違っています。'
      render :new
    end
  end

ユーザーから POST された値(ログインフォームに入力されたアカウントIDとパスワード)は、params[:session] に入っているので、まず User.find_by(account_id: params[:session][:account_id]) でそのアカウントIDのユーザーを引っ張り、次行の if user&.authenticate(params[:session][:password]) でパスワードが正しいかを確認しています。authenticate メソッドは User モデルでコールした has_secure_password メソッドによって提供されていて、引数に与えたパスワードが正しいかどうかを返してくれます。&. という演算子も見慣れない方がいるかもしれません。これは Safe Navigation Operator という演算子で、レシーバーが nil であれば NoMethodError を投げずに nil を返してくれます。昔の Ruby を知っている方は user.try!(:authenticate, params[:session][:password]) と同じだと考えると良いかもしれません。つまり、ユーザーが存在しないアカウントIDを POST してきて find_byUser のインスタンスが引っ張れずに nil を返してきた時に、else に入るために利用しています。if user.present? && user.authenticate(params[:session][:password]) という上述の演算子の短絡評価を使った書き方の方が明示的で好みな方もいるかもしれませんね。余談ですが、& はひとりぼっちで体育座りしている様子に見えるため &. は通称「ぼっちオペレーター」と呼ばれていたりします。

そんなこんなで認証できた場合は if が真となるので、先ほど SessionsHelper に定義した login メソッドをコールして Cookie に User の ID を詰めてトップページにリダイレクトしています。else に入った場合は、POST されたアカウントIDが存在していなかったか、アカウントIDは存在していたけどパスワードが間違っていたかのいずれかなので、エラーメッセージを flash に詰めて new.html.erb を再描画します。詰めたエラーメッセージを表示する処理は後ほど書きましょう。その前に、今のままだと SessionsHelperlogin メソッドが SessionsController からは見えていない問題を解決します。SessionsHelpercurrent_user はあちこちで使うことになるので全ての Controller が継承している ApplicationController で Mix-in します。

app/controllers/application_controller.rb
  class ApplicationController < ActionController::Base
+   include SessionsHelper
  end

これで、ApplicationController を継承している全ての Controller で SessionsHelpercurrent_userlogged_in? を利用できるようになりました。Helper を Controller に Mix-in するのって気持ち悪いですが、現状スッキリとした方法が思い浮かばないのでこのままいきます。

そして、ログインページに、ログインに失敗した時のエラーメッセージを表示します。先ほど Controller の flash.now[:alert] で詰めたエラーメッセージが表示されます。if flash[:alert].present? しても良いですが、まあ flash[:alert] の中身が nil ならどうせ何も出ないので雑に一行置いておきましょう。

app/views/sessions/new.html.erb
  <%= form_with scope: :session, url: login_path, local: true do |f| %>
+   <%= flash[:alert] %>

    <p>
      <%= f.label :account_id, 'アカウントID' %>
      <%= f.text_field :account_id, required: true %>
    </p>
    ...
  <% end %>

最後に、ログインに成功してトップページにリダイレクトされた後、本当にログインに成功しているのかを確かめるために「Welcome!!」 の文字列を「Welcome [ユーザーの名前(User#name)]!!」に変更しましょう。

app/views/tasks/index.html.erb
- <p>Welcome!!</p>
+ <p>Welcome <%= current_user.name %>!!</p> 

これでログイン処理周りの実装は完了です。早速動かしてみましょう!まずは、適当なアカウントIDやパスワードを入力してログインに失敗した場合にエラーメッセージが表示されるかを確認しましょう。
スクリーンショット 2019-12-03 6.12.49.png

次に、正しいアカウントIDとパスワードでログインしてみましょう。先ほど作ったテストアカウント(アカウントID: test, パスワード: password) でログインしてみましょう。いろいろあってテストアカウントがない場合はサインアップページ(localhost:3000/signup)から作りましょう。ログインに成功してユーザー名が表示されれば ok です。
スクリーンショット 2019-12-03 6.06.20.png

ログアウト

ログインできるようになったのでログアウトも実装しましょう。サクサクといきましょう。まずはルーティングを追加します。/login に対する DELETE リクエストを SessionsControllerdestroy アクションにルーティングします。

config/routes.rb
    get 'login', to: 'sessions#new'
    post 'login', to: 'sessions#create'
+   delete 'logout', to: 'sessions#destroy'

SessionsControllerdestroy を実装していきます。この後追加する SessionsHelperlogout メソッドをコールしてログインページにリダイレクトします。

app/controllers/sessions_controller.rb
    def create
      ...
    end
+
+   def destroy
+     logout
+     redirect_to login_path
+   end
  end

そして、実際のログアウト処理です。Cookie から User の ID 情報を削除して、インスタンス変数も nil で初期化するだけです。

app/helpers/sessions_helper.rb
    def logged_in?
      current_user.present?
    end
+
+   def logout
+     session.delete(:user_id)
+     @current_user = nil
+   end
  end

最後にログアウトのリンクを置きます。まあとりあえずトップページにでも置いておきましょう。

app/views/tasks/index.html.erb
  <p>Welcome <%= current_user.name %>!!</p>
+
+ <%= link_to 'ログアウト', logout_path, method: :delete %>

最後と言ったな、あれは嘘だ。まだ大きな問題があります。上記のコードでは link_tomethod オプションを与えることで、生成される a 要素に data-method="delete" 属性を追加して GET ではなく DELETE でリクエストを送ることを期待しています。しかし、現状はそんなことはできずに GET リクエストで /logout にリクエストされてルーティングエラーになります。「えっ?よくやってるけど」と思う方もいらっしゃるかもしれませんが、これは jquery-ujs が提供している機能です。今回は --skip-javascriptrails new したため jquery-ujs なんて高級なものは入っていません。よし、CDN から読み込みましょう。(雑)

app/views/layouts/application.html.erb
      <link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css">
      <%= stylesheet_link_tag    'application', media: 'all' %>
+
+     <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
+     <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ujs/1.2.2/rails.js"></script>
    </head>

ちなみに data-method 属性を持った a 要素で任意のリクエストを投げるためのコードはこれです。なんと、a 要素が押された瞬間に data-method に従ったメソッドでリクエストを投げられるような form 要素を錬金して強制 submit しています。最初から form を組み立てておいても良いなら link_to ではなく button_to を使う方法もあるのですがデザインがいい感じにならなかったので jquery-ujs を読み込んじゃうことにしました、てへぺろ。まあ今回はしばらく使い古した web アプリケーションのモバイルアプリ化が目的なので、ちょっと jQuery ぐらい置いてたり、設計が歪だったりした方がリアルじゃないですか。ちなみに rails-ujs の話がタイムリーなことに別の Advent Calendar の記事で行われていたので、詳しく知りたい方はこちらの記事をどうぞ。

さて、ということで、ログアウト処理が実装できました。ログインしてトップページからログアウトリンクを踏むとログインページに戻されるためです。ブラウザーのデベロッパーツールでログアウト前とログアウト後で Cookie がどのような動きをするかを確認してみても良いかもしれませんね。

スクリーンショット 2019-12-03 23.27.28.png

ログイン必須化

ログインとログアウトの処理は実装しましたが、もう1つだけ問題があります。それは、ログアウトした状態であってもトップページにアクセスできてしまうことです。ログインしていない状態で localhost:3000 にアクセスしてみましょう。このように「current_usernil だから name なんかにはアクセスできませんぞ!!」というエラーが表示されます。

スクリーンショット 2019-12-03 23.29.16.png

ログアウト状態では、session[:user_id] が空なので current_usernil を返すのでしたね、覚えていますか?回避方法としては先ほどの Safe Navigation Operator (通称ぼっちオペレーター)を使って current_user&.name にする手もありますが、そもそも今回の要件としては、トップページはログインユーザー以外アクセスできてはならないということなので、未ログインのユーザーがトップページにアクセスしてきたらログインページにリダイレクトするようにしましょう。

app/controllers/tasks_controller.rb
  class TasksController < ApplicationController
+   before_action :login_required

    def index; end
+
+   private
+
+   def login_required
+     redirect_to login_path unless logged_in?
+   end
  end

before_action にシンボルで渡したメソッドは、この Controller のアクションがコールされる前に実行されます。ここでは、unless logged_in? の時、つまり未ログインの時にログインページにリダイレクトしています。では、ログアウトした状態でトップページにアクセスしてみましょう。ログインページにリダイレクトされており、ログインしたらトップページが表示されれば ok です。あとは肝心のタスク管理ができるようになれば web アプリケーションとしては完成です。

タスクの CRUD

CRUD とは、データの Create, Read, Update, Delete の頭文字を取ったものです。つまり、タスクを作ったり読み出したり更新したり消したりといったコードをこれから書いていきます。今回は Delete は実装しないので、CRU かもしれませんね。

一覧ページと作成フォーム

タスクの一覧ページはトップページとユーザーIDを指定したページの2種類用意します。つまり、localhost:3000localhost:3000/users/:id/tasks の2種類です。:id の部分にはその Userid 属性が入ります。また、タスクの一覧ページに正しくタスクが表示されているかを確認するために、一気にタスク作成フォームまで作ってしまうので create アクションも実装します。今回は index ページにタスク追加フォームを配置するので new の専用フォームはありません。indexcreate を同時に実装していくのは難しそうに感じるかもしれませんが、大丈夫、あなたは写経するだけです。

config/routes.rb
    get 'login', to: 'sessions#new'
    post 'login', to: 'sessions#create'
    delete 'logout', to: 'sessions#destroy'
+
+   resources :users, only: [] do
+     resources :tasks, only: :index
+   end
+   resources :tasks, only: :create

それでは、タスクの一覧を表示するための Controller 処理を書いていきます。今回少し癖があるのは、/users/:id/tasks の URL でアクセスされた場合は :id で指定された User のタスクを取得しますが、そうでない場合はログイン中の User(current_user) のタスクを取得する必要があるということです。

app/controllers/tasks_controller.rb
  class TasksController < ApplicationController
    before_action :login_required
+   before_action :set_user

-   def index; end
+   def index
+     @tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
+   end
+
+   def create
+     @task = Task.create(task_params.merge(user: current_user))
+     redirect_to Rails.application.routes.recognize_path(request.referer)
+   end

    private

    def login_required
      redirect_to login_path unless logged_in?
    end
+
+   def set_user
+     @user = (params[:user_id].present? ? User.find(params[:user_id]) : current_user)
+   end
+
+   def task_params
+     params.require(:task).permit(:name, :finished)
+   end
  end

まずは before_action で今回タスクを表示する User のインスタンスを @user に代入しています。もし、params[:user_id] が存在しているのであれば、/users/:id/tasks からのリクエストなので User.find(params[:user_id]) で対象の User のインスタンスを取得します。params[:id] が存在していなければトップページからのリクエストなので current_user を取得します。index アクションでは、この @user が持っている今日付けのタスクを全て取得しています。create アクションでは、飛んできたパラメーターにログインユーザーの user_id をセットして tasks テーブルのレコードを作成し、元いたページ(トップページ or /users/:id/tasks)にリダイレクトしています。

次に View を書いていきましょう。Welcome メッセージとログアウトリンクだけであったページを、タスク一覧とタスク作成フォームの表示ページとしてマークアップしていきます。

app/views/tasks/index.html.erb
<p><%= @user.name %></p>

<% if @user == current_user %>
  <%= form_with model: Task.new, local: true do |f| %>
    <%= f.text_field :name, required: true %>
    <%= f.submit '作成' %>
  <% end %>
<% end %>

<ul>
  <% @tasks.each do |task| %>
    <li>
      <%= check_box_tag "finished#{task.id}", true, task.finished, data: { id: task.id }, disabled: (@user != current_user) %>
      <%= label_tag "finished#{task.id}", task.name %>
    </li>
  <% end %>
</ul>

<%= link_to 'ログアウト', logout_path, method: :delete %>

<% if @user == current_user %>
  <script>
    $(document).ready(function(){
      $('#task_name').focus();
    });
  </script>
<% end %>

<style>
  li { list-style: none; }

  :checked + label {
    color: gray;
    text-decoration: line-through;
  }
</style>

今回のアプリケーションでは、タスクのチェックボックスにチェックを入れた状態を完了状態とします。ただし、人のタスクを勝手に完了にするわけにはいかないので、disabled: (@user != current_user) で今見ているページが自分のページでない時に限りチェックボックスを非活性にしています。チェックボックスをチェックした時にサーバーにリクエストを送って finished を変更する処理は後ほど書いていきます。今はただチェックボックスを表示するだけのコードです。check_box_tag の各種引数は覚えるかググるかしかないので、まあ適当に察してください。フォームヘルパーの引数順は何百回ググっても覚えられませんね。まあ、都度ググれば良いのです。チェックボックスの活性状態と同様に、タスクの新規作成フォームも if @user == current_user で今見ているページが自分のページである場合のみ表示しています。

今回の View には純粋な HTML の文書構造だけではなく、script 要素と style 要素を使っています。script 要素では、JavaScript でページを表示した時にタスク名の入力フォームにフォーカスが当たるようにしています。こうすることで、タスク名を打って return キーを叩く操作を繰り返すだけで次々にタスクを作っていくことができて快適になります。ここでも jQuery を利用しています。style 要素では軽くデザインを当てていて、li 要素の list-style: none; で箇条書きの を非表示にして、チェックされたチェックボックスの後続の label 要素のテキストカラーをグレーにして取り消し線を付けています。まあどちらも触ってみれば効果が分かるでしょう。

ということで動作確認です。少し複雑な仕様で作り込んだのでテストも複雑です。テストコードを書いた方が早くテストできるのでストレスは溜まりますが、一生懸命ブラウザーで手動確認していきましょう。準備として、別のユーザーをサインアップフォームから作成しておいてください。サインアップを何度か繰り返していて、もう今のデータベースの状態がよく分からなくなってしまった方は、一度データベースをリセットしてゼロから作っていきましょう。

% rails db:migrate:reset
% rails c
irb(main):001:0> User.create(name: 'テストアカウント', account_id: 'test', password: 'password', password_confirmation: 'password')
irb(main):002:0> User.create(name: 'テストアカウント2', account_id: 'test2', password: 'password', password_confirmation: 'password')
irb(main):003:0> exit

2ユーザーをシステムに作成できたらログインとログアウトを繰り返しながら双方のタスクをいくつか作ってみて、以下を確認してみましょう。

  • トップページ(localhost:3000)
    • 自分のタスクが表示されること
    • タスクの作成が行えること
    • チェックボックスをチェックできること

スクリーンショット 2019-12-05 7.43.18.png

  • 自分のタスク一覧ページ(localhost:3000/users/1/tasks)
    • 自分のタスクが表示されること
    • タスクの作成が行えること
    • チェックボックスをチェックできること

スクリーンショット 2019-12-05 7.41.48.png

  • 他人のタスク一覧ページ(localhost:3000/users/2/tasks)
    • 他人のタスクが表示されること
    • タスク作成フォームが表示されていないこと
    • チェックボックスをチェックできないこと

スクリーンショット 2019-12-05 7.44.45.png

タスクの更新

チェックボックスをチェックすることで、tasks テーブルの finished 列を更新します。今回はチェックボックスの状態変更のイベントを拾って Ajax で非同期にサーバーにステータス更新リクエストを投げることにします。まずはルーティングに update アクションを追加します。

config/routes.rb
    resources :users, only: [] do
      resources :tasks, only: :index
    end
-   resources :tasks, only: :create
+   resources :tasks, only: %i[create update]

次に TasksControllercreate アクションの下に update アクションを追加しましょう。上述のルーティングによって /tasks/:id という URL でリクエストが送られてくるので、params[:id]Task を更新します。ただし、更新しようとしているタスクがログインしているユーザーのタスクであった場合のみ更新を行い、ログインしているユーザーのタスクでなかった場合はログインページにリダイレクトしています。勝手に他人のタスクを弄られないようにする配慮です。

app/controllers/tasks_controller
  def update
    task = Task.find(params[:id])
    redirect_to login_path if task.user != current_user
    task.update(task_params)
  end

そして、View の script 要素内にチェックボックスの状態が変化した時に上記の Controller までリクエストを投げる処理を追加します。

app/views/tasks/index.html.erb
    <script>
      $(document).ready(function(){
        $('#task_name').focus();
      });
+
+     $('input[type="checkbox"]').change(function() {
+       $.ajax('/tasks/' + $(this).data('id'),
+         {
+           type: 'patch',
+           data: {
+             task: {
+               finished: $(this).prop('checked')
+             }
+           },
+           dataType: 'json'
+         }
+       )
+     });
    </script>

チェックボックスを触る度にページをリロードされてはストレスなので、Ajax で非同期にリクエストを投げることでページ遷移を挟まずにリクエストを投げています。やや複雑に見えるコードですが、落ち着いて1行ずつ読んでみれば意味は分かるはずです。$('input[type="checkbox"]').change はチェックボックスが変更された場合イベントを拾おうとしています。$.ajax('/tasks/' + $(this).data('id') では Ajax でリクエストを送る URL を組み立てています。変更されたチェックボックスである $(this)data-id 属性を使って /tasks/1 といった URL を作っています。data-id 属性についてピンと来なければ check_box_tag にどんなパラメーターを与えたかと、結果としてどのような HTML が生成されているかをブラウザーからソースコードを見て確認してみましょう。type: 'patch' は PATCH リクエストを送るためです。PATCH がピンと来なければ rails routes コマンドを実行して config/routes.rb によってどのような URL とメソッドがアプリケーションに定義されているかを確認してみましょう。data: { task: { finished: $(this).prop('checked') } } はサーバーに送るパラメーターです。動作確認する時にチェックボックスを操作した時に rails s に出るログを確認してチェックを付けた時と外した時でそれぞれどんなパラメーターが飛んできているかを確認してみましょう。ということで、早速動かしてみましょう。チェック状態がページをリロードしても変わらなければ ok です。

reload.gif

なんと、これで web アプリケーションとしては完成です。日次のタスクをひたすら登録して消化していくには充分な機能ができました。勿論、まだまだ改善の余地はあるのでスタイリングをするなり、ユーザーのお気に入り機能を付けるなり、他人のタスクを見るだけならログイン必須ではなくしたり、いろいろ取り組んでみると力が付くかもしれませんね。とりあえず、本記事で作りたかった web アプリケーションはこれで完成です。お疲れ様でした。

モバイルアプリケーション用 API 実装

ここからは、モバイルアプリケーションからこの web アプリケーションを扱えるように API を実装していきます。外部からでもタスクの情報を取得したり、タスクの完了状態を変更したりできるようにするためのインターフェースを提供するということですね。早速やっていきましょう。

認証

今作った web アプリケーションでは、タスクを見るにも作るにも更新するにもアカウントIDとパスワードによるログイン認証が必要でした。モバイルアプリケーションも同様で、正しいアカウントIDとパスワードを入力したモバイルアプリケーションにのみタスクの操作を許してあげる必要があります。web アプリケーションは、モバイルアプリケーションが正しいアカウントIDとパスワードを送ってきた場合は API トークンを返します。web アプリケーションは、モバイルアプリケーションがパラメーターに乗せてきた API トークンでログインユーザーを判別します。何を言っているのか分からないと思いますので、実装を始めてしまいましょう。作っていくうちに何となく分かってくるはずです。

APIトークンの発行

まずは users テーブルに api_token の列を追加します。

% rails g migration AddApiTokenToUsers api_token:string
% rails db:migrate

そして、users テーブルにレコードを追加する時に自動でAPIトークンを埋めるようにします。何と1行追加するだけで bcrypt が勝手にやってくれます。

app/models/user.rb
  class User < ApplicationRecord
    has_many :tasks

    has_secure_password
+   has_secure_token :api_token

    validates :name, presence: true
    validates :account_id, presence: true, uniqueness: true
  end

これで新規の User に関しては自動で api_token がセットされるようになりましたが、既に存在する users テーブルのレコードの api_token は空のままなので rails console からワンライナーで一気に発行してしまいましょう。ちゃんと発行されたか不安であれば User.first.api_token 等で確認してみましょう。これで API トークンの発行準備は完了です。

% rails c
irb(main):001:0> User.find_each { |user| user.regenerate_api_token }

ログイン

次に、ログイン用のルーティングを追加します。API に関する URL は必ず /api で始めることにしましょう。また、API 用の Controller は Api という名前空間で括ることにしましょう。つまり、ログイン API は URL が /api/login で Controller は app/controllers/api/login_controller.rb にある Api::LoginController クラスとなります。

config/routes.rb
    resources :tasks, only: %i[create update]
+
+   namespace :api do
+     get 'login', to: 'login#show'
+   end

次に Controller を実装していきます。

app/controllers/api/login_controller.rb
class Api::LoginController < ApplicationController
  def show
    @user = User.find_by(account_id: params[:account_id])
    if @user&.authenticate(params[:password])
      render status: :ok, json: { api_token: @user.api_token }
    else
      render status: :unauthorized, json: {}
    end
  end
end

それでは、localhost:3000/api/login?account_id=test&password=password にアクセスしてみましょう。このような API トークンが表示されれば ok です。

スクリーンショット 2019-12-06 3.00.08.png

また、パラメーターに誤ったアカウントIDかパスワードを指定してみましょう。この時は API トークンは返ってきません。

スクリーンショット 2019-12-06 2.59.36.png

タスク CRUD API

認証と一覧の取得

自身のタスクの一覧を返したり、更新させたりという機能を書いていくわけですが、それを要求してきたユーザーが上記の API トークンをちゃんと持っているかを確認する必要があります。ルールとして、リクエストのパラメーターに必ず api_token を与えてもらうことにします。api_token のパラメーターが渡されていない場合、あるいは誤っている場合のリクエストは無視します。

app/controllers/api/tasks_controller.rb
class Api::TasksController < ApplicationController
  before_action :authenticate_by_token

  def index
    @tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
    render json: @tasks.map { |task| { id: task.id, name: task.name, finished: task.finished } } 
  end

  private

  def authenticate_by_token
    @user = User.find_by(api_token: params[:api_token])
    render status: :unauthorized, json: 'Invalid API token' if @user.blank?
  end
end

before_action では、トークンの認証を行い失敗した場合に Invalid API token という文字列とステータスコード 401 を返します。

スクリーンショット 2019-12-06 3.56.08.png

正しい API トークンを与えた場合はタスクの一覧の json が返ってきます。これが @tasks.map { |task| { name: task.name, finished: task.finished } } の結果です。モバイルアプリケーションは、この情報を元にタスクの一覧画面を組み立てるわけですね。

スクリーンショット 2019-12-06 3.57.38.png

作成と更新

続いて作成と更新の API を一気に書いていきます。やることは web アプリケーションとほとんど同じなのでサクサクといきましょう。まずはルーティングに createupdate アクションを追加します。

config/routes.rb
    namespace :api do
      get 'login', to: 'login#show'
-     resources :tasks, only: :index
+     resources :tasks, only: %i[index create update]
    end

そして、Controller に web の時と同様に createupdate アクションを実装していきます。

app/controllers/api/tasks_controller.rb
  class Api::TasksController < ApplicationController
+   skip_forgery_protection

    before_action :authenticate_by_token

    def index
      @tasks = @user.tasks.where(created_at: Time.zone.today.all_day)
      render json: @tasks.map { |task| { id: task.id, name: task.name, finished: task.finished } } 
    end
+
+   def create
+     task = Task.create(task_params.merge(user: @user))
+     render json: { id: task.id, name: task.name, finished: task.finished }, status: :created
+   end
+
+   def update
+     task = Task.find(params[:id])
+     (task.user == @user) ? task.update(task_params) : render(status: :unauthorized)
+   end

    private

    def authenticate_by_token
      @user = User.find_by(api_token: params[:api_token])
      render status: :unauthorized, json: 'Invalid API token' if @user.blank?
    end
+
+   def task_params
+     params.require(:task).permit(:name, :finished)
+   end
  end

create アクションと update アクションの中身はほとんど前回と同じなので問題ないでしょう。問題があるとすれば先頭にしれっと追加された skip_forgery_protection という一行です。これは話すと長くなるのですが、Ruby on Rails はデフォルトで CSRF(Cross-Site Request Forgeries) 保護が行われています。外部の攻撃者から不正なリクエストを受けても弾けるように、POST や PATCH リクエストは認証トークンを添えて送らなければリクエストを処理しません。認証トークンで認証できない場合は Controller の before_action 時点で弾かれるので createupdate といったアクションに入ってくることはありません。skip_forgery_protection はこれを skip_before_action するためのものです。この後 curl で API を叩いてみるので興味のある方は skip_forgery_protection をコメントアウトした状態で API を叩いて rails server のログを確認してみると良いでしょう。Can't verify CSRF token authenticity. というエラーが表示されているはずです。そして作成や更新は行われずにデータベースに変化は起こっていないはずです。ただ、この Controller に関しては、authenticate_by_token メソッドで自前の セキュリティゆるゆるの トークン認証を実装しているので、一旦は CSRF 保護をスキップします。

それでは、動作を確認してみましょう。まずは作成です。ターミナルから curl コマンドを実行してサーバーに POST リクエストを送ってみます。api_token はご自身のものに差し替えてください。先ほどログイン API で GET した API トークンです。

% curl -X POST -d 'task[name]=by API&api_token=8YKPhsDNphD91j4EaQxyf6JF' localhost:3000/api/tasks

実行したらブラウザーのタスク一覧画面か、一覧 API で返ってくる json を確認してみましょう。新しいタスクが追加されていれば ok です。

スクリーンショット 2019-12-06 4.29.17.png

POST による作成が問題なければ、次は更新の PATCH リクエストを送ってみましょう。今作ったタスクを完了状態にしてみましょう。API トークンを差し替えるのは勿論のこと、URL の末尾の Task の ID も今作った Task の ID に差し替えてください。rails consoleTask.last.id を叩けば最後に作った Task の ID が取れます。

% curl -X PATCH -d 'task[finished]=true&api_token=8YKPhsDNphD91j4EaQxyf6JF' localhost:3000/api/tasks/7

再びブラウザーのタスク一覧画面か、一覧 API で返ってくる json を確認してみましょう。完了状態になっている、あるいは finishedtrue になっていれば ok です。

スクリーンショット 2019-12-06 4.31.23.png

ここまで実装できれば、モバイルアプリケーションからタスク管理するための準備は整いました。お疲れ様でした。

デプロイ

最後にこの web アプリケーションをどこかのサーバーに置く必要があります。本記事では無料かつクレジットカードの登録も必要がない Heroku にデプロイする手順を雑に書きますが、お好きなサーバーに上げてもらって大丈夫です。どこにも上げたくなくてお手軽にやりたければ、ngroklocalhost:3000 をポートフォワーディングする手もあります。本記事では「web アプリケーションを作ったった!!」という気分になりたいので、とりあえず Heroku の無料枠にデプロイします。

まずは Heroku にサインアップしてインストールガイドに従って heroku-cli をインストールします。Mac であれば現状は以下です。

% brew tap heroku/brew && brew install heroku

Heroku にログインします。下記のコマンドを実行するとブラウザーからのログインが求められます。

% heroku login

Heroku では Git のリポジトリを push する形でデプロイするので、もしここまで Git 管理していなかった人は雑に commit してください。

% git commit -am "Create task management application"

Heroku にアプリを作ります。このタイミングで URL が割り振られます。

% heroku create
Creating app... done, ⬢ limitless-earth-31665
https://limitless-earth-31665.herokuapp.com/ | https://git.heroku.com/limitless-earth-31665.git

お好みで名前を変更してください。ドメインになります。勿論人と被ると弾かれます。zone-web.herokuapp.com は頂いた。

% heroku rename zone-web
Renaming limitless-earth-31665 to zone-web... done
https://zone-web.herokuapp.com/ | https://git.heroku.com/zone-web.git

heroku create した段階で、git remoteheroku が自動登録されているので、ここに push するだけでデプロイできます。3分ぐらい時間がかかると思うので、お茶でも飲みながら休憩しましょう。

% git push heroku master

無事にデプロイできたら、Heroku 上で rails db:migrate を実行してもらいます。heroku run を頭に付けるだけでコマンドを走らせられるのは素晴らしいですね。

% heroku run rails db:migrate

これで準備は完了です。heroku createheroku rename の時に割り振られた URL へアクセスしてみましょう。ログインページにリダイレクトされれば、ちゃんとアプリケーションが動いています。サインアップして自由にアプリケーションを操作してみましょう。

スクリーンショット 2019-12-06 7.23.52.png

これで、web アプリ編の演習は終了です。お疲れ様でした。モバイルアプリ編 でお会いしましょう。

終わりに

今回の演習では、最小限の認証付きタスク管理アプリケーションを開発し、モバイルアプリケーションに向けた API の実装を行いました。もし完走した方がいらっしゃいましたら、こっそり私(@OgiharaRyo)まで教えて頂けると嬉しいです。冒頭でも言い訳しましたが、Advent Calendar でやるには本当に本当に長い演習になってしまいました。重ねてお詫び申し上げます。

Ruby on Rails Advent Calendar 17日目は bake0937 さんです。

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

#Rails ( #Ruby + ActiveSupport ) + Time.use_zone + travel_to で現地時刻=日本時刻=JSTを固定して UTC 変換結果を見て遊ぶ例

  • タイムゾーンの9時間の扱いを一生かかっても覚えられる気がしない
  • 足し算と引き算を間違える確率が65%
  • Rubyで遊んでいたらだんだん分かってきた気がするけれど
  • タイムゾーンに慣れ親しむだけで1週間かかって、3日で忘れる気がした
require 'active_support/core_ext'

# 日本時間の年始の時間
jst_beggining_of_year = Time.use_zone('Tokyo') { Time.local(2020, 01, 01, 00, 00, 00) }

# travel するために必要
require 'active_support/testing/time_helpers'
include ActiveSupport::Testing::TimeHelpers

# 日本時間の年始に時間を固定する
travel_to jst_beggining_of_year

# 今日の日付は 2020-01-01
Date.current
# => Wed, 01 Jan 2020

# 現在時刻は 2020-01-01 00:00:00 で JST のタイムゾーンも持っている
Time.now
# => 2020-01-01 00:00:00 +0900

# こちらも同じく
Time.current
# => 2020-01-01 00:00:00 +0900

# Time.nowはRubyのclassだがちゃんとtravelできている
Time.now.class
# => Time

# Time.currentはRails=ActiveSupportのclassみたいだがtravelできている
# Time.zone= を指定していない場合は Ruby の Time.now と同じように働く気がしたが合ってるかな
Time.current.class
# => ActiveSupport::TimeWithZone

# Time.zone.now メソッドでは、Time.zoneの指定をしていないので何も得られない
# Time.currentとは扱いが違うみたいだ
Time.zone.now
# NoMethodError: undefined method `now' for nil:NilClass

# Time.zone 指定を日本時間にする
Time.zone = 'Tokyo'
# => "Tokyo"

# 現在時刻は 2020-01-01 00:00:00 で JST のタイムゾーンや曜日も持っている
Time.zone.now
# => Wed, 01 Jan 2020 00:00:00 JST +09:00

# Time.zone.now は ActiveSupport の class を持っている
Time.zone.now.class
# => ActiveSupport::TimeWithZone

# 今日の日付は 2020-01-01だ
# こちらも Ruby ではなく Rails = ActiveSupport のもの
Date.current
# => Wed, 01 Jan 2020

# Time.zone 指定を日本時間にする
Time.zone = 'UTC'
# => "UTC"

# UTCでいうと今日の日付はまだ、前年の年末だ
Date.current
# => Tue, 31 Dec 2019

# 日本時間の年始の0時は、UTCでいうと9時間遅い、年末の15時だ 
Time.zone.now
# => Tue, 31 Dec 2019 15:00:00 UTC +00:00


Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2775

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

#Ruby と #Rails の Time や Time.zone の扱いががマジで分からないし混沌としてるのでちょっとだけ整理したい。

Try at

この時間に試しました

  • JST 2019-11-28 hour 19
  • UTC 2019-11-28 hour 10

For Ruby

require ‘active_support/core_ext’

No Time zone setting

タイムゾーン指定がないとメソッドが使えなかったり

Time.zone
# nil

Time.zone.now
# NoMethodError: undefined method `now’ for nil:NilClass

Timezone指定

Time.zone は ActiveSupportのインスタンスになる
タイムゾーンを切り替えるには Time.zone= メソッドを実行

Railsであればapplication.rb の configに書くだろうが

タイムゾーン指定自体は Tokyo でも Asia/Tokyo でも同じようにはからってくれるっぽい
ちなみに Time.zone = ‘JST’ じゃ動かないとか信じられるかい

Time.zone = 'UTC'
=> "UTC"
Time.zone
=> #<ActiveSupport::TimeZone:0x00007f8610941ef0 @name="UTC", @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, @utc_offset=nil>

=> #<ActiveSupport::TimeZone:0x00007f86108d1a38 @name="Asia/Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil>
Time.zone = 'Tokyo'
=> "Tokyo"
Time.zone

Time.zone = 'Asia/Tokyo'
=> "Asia/Tokyo"
Time.zone
=> #<ActiveSupport::TimeZone:0x00007f86108d1a38 @name="Asia/Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil>


Time.zone = 'JST'
# ArgumentError: Invalid Timezone: JST

UTC

UTCをtimezone指定した場合

Time.zone = UTC
# => “UTC”

Time.zone
# => #<ActiveSupport::TimeZone:0x00007fe40e15ed08 @name=“UTC”, @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, @utc_offset=nil>

Time.zone.now
# => Thu, 28 Nov 2019 10:00:31 UTC +00:00

Time.zone.parse(2020-01-01 00:00:00)
# => Wed, 01 Jan 2020 00:00:00 UTC +00:00

JST

JSTをtimezone指定した場合

Time.zone = Tokyo
# => “Tokyo”

Time.zone
# => #<ActiveSupport::TimeZone:0x00007fe40d2721a0 @name=“Tokyo”, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil>

Time.zone.now
# => Thu, 28 Nov 2019 19:00:47 JST +09:00

Time.zone.parse(2020-01-01 00:00:00)
# => Wed, 01 Jan 2020 00:00:00 JST +09:00

Time系

こちらはRuby系なので Time.zone を参照しない、というより Time.zone なんていうメソッドもクラスもない
Rails = ActiveSupport の Time.zone 系メソッドとは分けて考えること

Ruby で最初から出来るかと思いきや、 一部のメソッドは requireが必要という罠 -> Time.parse とか
require active support でも動くようになるので、まさかのactivesupportのメソッドかと思って驚いたが違った

タイムゾーンを変えるには ruby実行時の環境変数を変える

TZ=UTC ruby ...
TZ=Asia/Tokyo ruby ...

環境変数指定もしくはシステムのタイムゾーンによって結果が変わる
Rubyだけの処理でもタイムゾーン情報を持っているようだ

Time.now
# => 2019-11-28 10:19:37 +0000

Time.now
# => 2019-11-28 19:19:43 +0900

# Time.parse / Time.zone.parse

Time.parse(2020-01-01 00:00:00)
# NoMethodError: undefined method `parse’ for Time:Class

Time.parse(2020-01-01 00:00:00)
# => 2020-01-01 00:00:00 +0900

Time.zone = UTC
# => “UTC”

Time.zone.parse(2020-01-01 00:00:00)
# => Wed, 01 Jan 2020 00:00:00 UTC +00:00

RubyっぽいのにRails? current系

nowじゃなくてcurrentっていう名前のメソッドはRubyじゃなくてRails/AcviveSupportのものだ

Time.current
# NoMethodError: undefined method `current’ for Time:Class

require active_support/core_ext

Time.current
# => 2019-11-28 19:21:56 +0900

なのにclassTimeという奇特なやつ

Time.current.class
# => Time

ちなみに Date.today は Ruby のものだが Date.current Date.yesterday Date.tomorrow は Railsのものだという大いなる罠もある

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2774

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

Railsでのjoinsの結果のidについて

Railsでテーブル結合してデータを取得した時、idを参照元の主キーにしたいのに参照先の主キーになってしまっていた。
結論として

Hoge.joins(:fuga).where(fuga_name:"hoge").select("fuga.*,hoge.*")

このようにselectで最後に指定したテーブルの主キーが結合したデータの主キーとなる。

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

金井の戯言【NIT-Numazu Advent Calendar 2019】

書くことないんですけど

なんかプロコン同好会で参加して欲しいとか言われたから参加したけどさあ
どうすんのこれマジで
現時点で書くこと無いし、何も強い人じゃないから既に劣等感しか無いんですけど
技術的なこと書ける人、強くていいですね!^ ^

…ぼやいててもしょうがないので取り敢えず自己紹介書きます。

とってつけたような自己紹介


名前 : 金井 一真
趣味 : ぷろぐらみんぐ、原付で小旅行、ピアノ
使用言語 : C / C++ / Ruby / HTML / CSS /
コメント : 最近基本情報受かりました、おら基本情報技術者様だぞ道を開けろ
関連リンク : ついたー ぎとはぶ

適当に書いてたら方針が決まった

ぼく結構何事も浅く広くな人間なので(信条は狭く深くなのに!)、色んなことをちまちま並べて書いていくことにする

一記事で二度美味しいって言え!!!!!!!!!!!!!!!

それじゃいきます

vimのはなし

今これ書いてるのもvimです。vimを弄り回す人たち凄いですね、これmarkdownなのにリアルタイムで変更が見れるプラグイン使って書いてるんですよ。
あ、URL貼っとこ。
https://github.com/suan/vim-instant-markdown
というわけで個人的にすこポイントを幾つか挙げていく。

キーボードから手が離れない

これ90%くらいを占めてます。それくらいキーボードから手が離れないのは重要。
いちいちマウス触るのめんどくさいんすよね。それだけ。
自分とマウスは一体だから問題ないですよって人は別にいいんじゃないですかね?わからん。

なんかかっこいい


逆にこれがかっこいいと思わん人とは感性が合わないんやろなって思います。
画面分割とか、ひたすらキーボードカタカタやって作業してるのオシャレじゃないですかね?そんなことない?

強めのオタクたちのプラグインがたくさんある

多分これを一番推すべきなんでしょうね。
ぼくはまだvimの素の機能すら完全に使いこなせていないので、あんまたくさん入れてないんですよ。今はmarkdown周りだけかな?
vimスーパー完璧人間になってからあれこれいれると作業効率がとんでもないことになると思います。そう信じてます。

とりあえずこんなもんですかね。実際vimmerになってから作業効率は間違いなく上がってるし、そこそこ使いやすいエディタだと思います。端末からのアクセスが早いのも良し、軽いのも良し。かっこいい。いいことずくめ。
ただ惜しむらくは、日本語入力との相性が悪いことですかね…
編集モードと入力モードを行ったりきたりするエディタなので、編から入に切り替えようってときに日本語のままの状態になってると、

こうなる。

(なんか伝わりにくいスクショですね、vim触ったことある人は言わんとすることわかるはず)
これ急いでるときにやられるとほんま迷惑で、編集モードに入ったら自動で英語入力に戻すシステムだかプラグインだか入れなきゃいけない。あるのかな?探してみるか。
といった感じでvimの話は以上。みんなもvim使ってね!

bot開発のはなし

ぼくの趣味みたいになってる奴です。ついったのbotと、でぃすこのbotがあります。
これなんでハマってるかというと、自分のした改造がレスポンスとしてわかりやすく現れる、ってのと、色んな人に遊んでもらえるってところかなって思います。
  
↑ これは僕のbotをつっつくたべさん
要は、わかりやすく評価されるから嬉しいよねって話です。
他にもいいところが有って(活かせてはいない)、色んなAPIとか.jsonファイルとかの扱いが上手になります。外部のAPIを使うとbotの完成度が上がるので良いです。ぼくのbotでは天気予報が出来たりします。すごい!すごいって言え!
ただ、あんまりdiscordの方のファイル構造がうまくいってなくて困ってます。ファイル構造云々は多分rubyの理解度の問題なんだよな、精進します。
みなさんのdiscordにもかめせんにんを入れて遊んでみてね!
ついったーのbotの方は、僕のツイを分解してマルコフ連鎖で繋げてツイートする、なんてことをしている奴です。あの、案外面白いです。僕のことをフォローしている人は是非セットでフォローしていただきたい。
というわけで以上、bot開発のはなしでした。みんなもbot開発、やってみよう!

Josephineのはなし

かっこつけて英語使いたかっただけです。ジョセフィーヌのはなしです。
ジョセフィーヌって誰だよ、何だよ、って方もいらっしゃると思うのでご紹介します。

この亀のぬいぐるみの名前です。かわいい。
この子は、八景島シーパラダイスで開催された「鎮守府第三次瑞雲祭り」にいった際にお土産で買ってきました。
会場内を歩き回って疲れたときに亀を見て癒されたのが妙に印象に残っており、「せっかくだから」と言って買いました。
名前の由来は…

父に強引に命名されました

これマジ?(由来とか)ないじゃん…
ちなみに父に命名される前は「亀五郎」と呼んでました。なんで五郎なんだ。
ちなみに、ついったのFFの絵を描く人にお願いして、擬人化を描いて貰ったりしました。

かわいい。
僕の顔みたいな存在なので、ぜひ覚えてあげてください、ジョセフィーヌのはなしでした。

もう十分だろ

書くことがないとぼやきから始まった記事でしたが、なんだかんだそこそこ書けたような気がします。埋めるだけならついったーらんどで鍛えた文章力が火を噴きます。噴くな。
記事作成を完走した感想(激うまギャグ)ですが、専門的な内容が結局書けなかったのが悔しい、是れに尽きます。色々なことを齧っている割には深く学習は出来ていないので、今の僕はなんとも中途半端なエンジニアなのかなと。まぁでも、ひとまずやることは応用情報の勉強でしょうし、専門化はまだまだ先のお話となりそうです。
というわけで、この記事もそろそろ終わりです。如何だったでしょうか?もしちょっとでも面白いと思っていただけたのであれば、僕のついったーの方をフォローしていただけると泣かない程度に喜びます。
御拝読頂きまして、ありがとうございました。お疲れやまでした。

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

email_fieldの入力フォームに初期値を反映させる方法

bootstrapを使用しながらemail_fieldの入力フォームに初期値を反映させる方法。

ポートフォリオを採用側に見てもらう時少しでも手間を省くため、ワンクリックで済むようにと初期値設定を考えました。
text_fieldは大量に検索ヒットしたけどemail_fieldとpassword_fieldは出てこなくて苦戦したので、自分用メモとして。

new.html.erb
        <%= form_with(url: login_path, scope: :session, local: true) do |f| %>

      <div class="form-group">
        <%= f.label :email, 'メールアドレス' %>
        <%= f.email_field :email, class: 'form-control', placeholder: 'hoge@gmail.com' %>
      </div>

      <div class="form-group">
        <%= f.label :password, 'パスワード' %>
        <%= f.password_field :password, class: 'form-control', value: 'hoge' %>
      </div>
      <%= f.submit 'ログイン', class: 'btn btn-primary btn-block' %>
    <% end %>

email_fieldの方には placeholder: 'hoge@gmail.com'
password_fieldの方には value: 'hoge'

下記のURLのchapter 7−4を参考にしてください。
email_fieldとpassword_field初期値設定

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

#Ruby で #Rails と同じ Time.zone.now を使う

  • activesupportをinstall & require する
  • Time.zone を指定する
  • あとは Rails と同じようにする
gem install activesupport
require 'active_support/core_ext'
# => true

Time.zone
# => nil

Time.zone = 'Tokyo'
# => "Tokyo"

Time.zone
# => #<ActiveSupport::TimeZone:0x00007fded1c02c10 @name="Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil>

Time.zone.now
# => Thu, 28 Nov 2019 17:41:36 JST +09:00

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2773

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

#Ruby / #Rails で日付を UTC あるいは local時刻 = JSTなどで time に変換するにはするには引数を指定せよ

Rails

Date.parse('2020-01-01').to_time(:utc)
# => 2020-01-01 00:00:00 UTC

Date.parse('2020-01-01').to_time(:local)
# => 2020-01-01 00:00:00 +0900

Ruby の場合はgem installが必要

gem install activesupport
 require 'active_support/date'
 require 'active_support/core_ext'

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2772

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

AndroidのlogcatをTSV形式に加工するrubyスクリプト

Androidのlogcatを解析する際にExcelやgoogleスプレッドシートに貼り付けて、
フィルタ機能を使って一部のログのみ抽出して表示したいので作ってみました。

変換スクリプト

locat2tsv.rb
#!/usr/bin/env ruby

while line = ARGF.gets
    (date, time, pid_package, level_tag, message) = line.split(" ",5)
    (pid, package) = pid_package.split("/")
    (level, tag) = level_tag.split("/")

    data = ["#{date} #{time}", pid, package, level, tag, message]
    puts data.join("\t")
end

変換結果をgoogleスプレッドシートに貼り付けた例

スクリーンショット 2019-11-29 0.17.59.png

使い方

cat env-logcat.log | ./locat2tsv.rb | pbcopy
./locat2tsv.rb env-logcat.log | pbcopy

変換後のTSVのカラムは次の通りです
1. 日時
2. pid
3. パッケージ名
4. ログ出力レベル
5. tag
6. ログメッセージ

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

[Ruby] 無限フィボナッチ

無限フィボナッチ

Yo! クリぼっちでも フィボナッチ:sunglasses:

旧コード

大好きな DelegateClass と Enumerator を組み合わせて書いてみた。後で書き直しました (後述) 。

class FibonacciNumbers < DelegateClass(Enumerator)
  def initialize
    super(fibonacci_numbers)
  end

  private

  def fibonacci_numbers
    Enumerator.new do |y|
      memo = [0, 1]

      loop.with_index do |_, i|
        if i < 2
          y << memo[i]
        else
          memo = memo[1], memo.sum
          y << memo[1]
        end
      end
    end
  end
end

fibonacci_numbers = FibonacciNumbers.new
fibonacci_numbers.first(10) #=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
fibonacci_numbers.first(10_000).last #=> 20793608237133498072112648988642836825087036094015903119682945866528501423455686648927456034305226515591757343297190158010624794267250973176133810179902738038231789748346235556483191431591924532394420028067810320408724414693462849062668387083308048250920654493340878733226377580847446324873797603734794648258113858631550404081017260381202919943892370942852601647398213554479081823593715429566945149312993664846779090437799284773675379284270660175134664833266377698642012106891355791141872776934080803504956794094648292880566056364718187662668970758537383352677420835574155945658542003634765324541006121012446785689171494803262408602693091211601973938229446636049901531963286159699077880427720289235539329671877182915643419079186525118678856821600897520171070499437657067342400871083908811800976259727431820539554256869460815355918458253398234382360435762759823179896116748424269545924633204614137992850814352018738480923581553988990897151469406131695614497783720743461373756218685106856826090696339815490921253714537241866911604250597353747823733268178182198509240226955826416016690084749816072843582488613184829905383150180047844353751554201573833105521980998123833253261228689824051777846588461079790807828367132384798451794011076569057522158680378961532160858387223882974380483931929541222100800313580688585002598879566463221427820448492565073106595808837401648996423563386109782045634122467872921845606409174360635618216883812562321664442822952537577492715365321134204530686742435454505103269768144370118494906390254934942358904031509877369722437053383165360388595116980245927935225901537634925654872380877183008301074569444002426436414756905094535072804764684492105680024739914490555904391369218696387092918189246157103450387050229300603241611410707453960080170928277951834763216705242485820801423866526633816082921442883095463259080471819329201710147828025221385656340207489796317663278872207607791034431700112753558813478888727503825389066823098683355695718137867882982111710796422706778536913192342733364556727928018953989153106047379741280794091639429908796650294603536651238230626

なお

def fibonacci_numbers
  memo = [0, 1]

  Enumerator.new do |y|
    # 略
  end
end

と、ローカル変数 memo の代入を Enumerator.new のブロックの外で行うと挙動が変わってしまうので注意。

# Enumerator.new のブロックがクロージャとしてローカル変数 memo の状態を保持してしまう。
# そのせいで first に 3 以上の整数を渡して呼び出すと、つど結果が変わってしまう。
fibonacci_numbers.first(3) #=> [0, 1, 1]
fibonacci_numbers.first(3) #=> [1, 1, 2]
fibonacci_numbers.first(3) #=> [1, 2, 3]

新コード

@kts_h さんのコメントを参考 (というかほぼそのままですが……) にして、以下の方針でよりシンプルなコードにしました。

  • オブジェクトを生成せずに FibonacciNumbers の Module 自体に機能をもたせる。
  • 他のオブジェクトに委譲するのではなく FibonacciNumbers 自体を Enumerable にする。
  • loop メソッドのブロック内の if 式を取り除く。
module FibonacciNumbers
  extend Enumerable

  def self.each
    return enum_for unless block_given?

    a, b = 0, 1
    loop do
      yield a
      a, b = b, a + b
    end
  end
end

FibonacciNumbers.first(10) #=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
FibonacciNumbers.first(10_000).last #=> 20793608237133498072112648988642836825087036094015903119682945866528501423455686648927456034305226515591757343297190158010624794267250973176133810179902738038231789748346235556483191431591924532394420028067810320408724414693462849062668387083308048250920654493340878733226377580847446324873797603734794648258113858631550404081017260381202919943892370942852601647398213554479081823593715429566945149312993664846779090437799284773675379284270660175134664833266377698642012106891355791141872776934080803504956794094648292880566056364718187662668970758537383352677420835574155945658542003634765324541006121012446785689171494803262408602693091211601973938229446636049901531963286159699077880427720289235539329671877182915643419079186525118678856821600897520171070499437657067342400871083908811800976259727431820539554256869460815355918458253398234382360435762759823179896116748424269545924633204614137992850814352018738480923581553988990897151469406131695614497783720743461373756218685106856826090696339815490921253714537241866911604250597353747823733268178182198509240226955826416016690084749816072843582488613184829905383150180047844353751554201573833105521980998123833253261228689824051777846588461079790807828367132384798451794011076569057522158680378961532160858387223882974380483931929541222100800313580688585002598879566463221427820448492565073106595808837401648996423563386109782045634122467872921845606409174360635618216883812562321664442822952537577492715365321134204530686742435454505103269768144370118494906390254934942358904031509877369722437053383165360388595116980245927935225901537634925654872380877183008301074569444002426436414756905094535072804764684492105680024739914490555904391369218696387092918189246157103450387050229300603241611410707453960080170928277951834763216705242485820801423866526633816082921442883095463259080471819329201710147828025221385656340207489796317663278872207607791034431700112753558813478888727503825389066823098683355695718137867882982111710796422706778536913192342733364556727928018953989153106047379741280794091639429908796650294603536651238230626

これまでに試した似たやつ

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