20200714のRubyに関する記事は23件です。

【before_action】

before_actionとは

before_actionはコントローラーで定義された処理を実行する前に共通の処理を行うことができるメソッドのこと

今回はコントローラー内のメソッドの実行内容が重複している場合のbefore_actionの使用例を書いていきます。

set_action1.png

上の画像のようにeditアクションとshowアクションの実行内容が同じである場合は共通の処理としてまとめてしまった方が可読性も上がり、変更の際も便利なので処理をまとめる。

set_action2.png

まず該当のアクションを削除。

set_action3.png

その後先ほど削除した共通していた処理内容をprivateメソッドの部分にset_actionとして定義する。

set_action5.png

最後にコントローラーの上部にbefore_actionを記述する。
今回の場合はset_actionを適用させたいのはeditとshowアクションのみなのでオプションとしてonlyを記述することで該当のアクションにのみset_actionを実行させれば良い。

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

Ruby on RailsのポートフォリオにDockerを組み込む!

現在の状態

みなさんこんにちわ今回Qiita初投稿を行います!
間違っているところがあると思いますがその時は優しく教えてください(笑)
今回、AwsとDockerを用いて既に作成済のRailsのポートフォリオをデプロイしています。
現在の状態は、railsとAws用いてポートフォリオを作成したのですが、環境構築が大変だと思い、環境周りをコードで管理しやすいDockerを用いてポートフォリオを際デプロイしたいと思います。

構成図

デプロイの際の構成図は以下の通りです。
スクリーンショット 2020-07-14 21.27.20.png
めちゃくちゃ簡単に説明するとクライアントから通信リクエストが来るとNginxに行き動的な処理が必要な際はpumaに通信を行いその際にpumaとmysqlも通信する流れとなっています。
今回はこの構成をDockerを用いて作成していきたいと思います。

必要な物

今回デプロイする際はAwsのEC2とRDSをを使用します。
Dockerで環境構築を行いますが、データの永続化の観点から今回データベースはDockerではなくRDSのMysqlをしようします。
以下作業に必要な準備と作業です。

・AwsでEC2、RDSを用いてデプロイする際に必要な準備を行う(ネットワーク構成など...)
・local環境とEc2にDokcerをインストール
・local環境とEc2にDocker-composeをインストール

参考にした記事

EC2上でRailsアプリケーションにDockerを導入する(Rails、Nginx、RDS)
Rails On DockerでのAWSデプロイができたので,中身を整理します。

作業

まずはじめにDockerファイルを作成します。
ここではDockerfileを用いてRuby周りの環境を構築するコードを書きます。

FROM ruby:2.5.7

RUN apt-get update -qq && \
    apt-get install -y build-essential \
                       libpq-dev \
                       nodejs \
                       vim

RUN mkdir /アプリの名前

WORKDIR /アプリの名前

ADD Gemfile /アプリの名前/Gemfile
ADD Gemfile.lock /アプリの名前/Gemfile.lock

RUN gem install bundler
RUN bundle install

ADD . /アプリの名前

RUN mkdir -p tmp/sockets
RUN mkdir -p tmp/pids

次にDocker-composeを作成します。
ここではDockerコンテナの管理やマントする箇所の指定を行っていきます!

Docker-compose.yml
version: '3'
services:
  app:
    build: .
    command: bundle exec puma -C config/puma.rb -e production
    volumes:
      - .:/アプリの名前:cached
      - public-data:/アプリの名前/public
      - tmp-data:/アプリの名前/tmp
      - log-data:/アプリの名前/log

  web:
    build:
      context: containers/nginx
    volumes:
      - public-data:/アプリの名前/public
      - tmp-data:/アプリの名前/tmp
    ports:
      - 80:80

volumes:
  public-data:
  tmp-data:
  log-data:

次に以下のファイルを作成します。
ここではNginxコンテナの設定を行っていきます。

FROM nginx:1.15.8

RUN rm -f /etc/nginx/conf.d/*

ADD nginx.conf /etc/nginx/conf.d/アプリの名前.conf

CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
containers/nginx/nginx.conf
upstream FashionInformation_app {
  server unix:///アプリの名前/tmp/sockets/puma.sock;
}

server {
  listen 8000;
  server_name ドメイン名;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  root /アプリの名前/public;

  client_max_body_size 100m;
  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;
  try_files  $uri/index.html $uri @アプリの名前;
  keepalive_timeout 5;

  location @アプリの名前 {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://アプリの名前;
  }
}

Railsアプリケーションをデプロイする

ここまできたらEC2にgitをクローンします!

ec2-user@ip-xxx-xx-xx-xxx ~]$ git clone GitHubのリポジトリのURL

イメージのビルド

ec2-user@ip-xxx-xx-xx-xxx]$ cd myapp
[ec2-user@ip-xxx-xx-xx-xxx myapp]$ docker-compose build

サーバー起動前の準備

[ec2-user@ip-xxx-xx-xx-xxx myapp]$ docker-compose run app rails assets:precompile RAILS_ENV=production

サーバー起動前の準備

[ec2-user@ip-xxx-xx-xx-xxx myapp]$ docker-compose up -d

データベースの作成、マイグレーションファイルの読み込み

[ec2-user@ip-xxx-xx-xx-xxx myapp]$ docker-compose exec app rails db:create db:migrate RAILS_ENV=production

パブリックIPにアクセスして、正しく表示されれば成功です!

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

Kinx ライブラリ - JIT ライブラリ(番外編)

Kinx ライブラリ - JIT ライブラリ(番外編)

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。今回は JIT ライブラリ番外編です。

ここ で紹介した JIT ライブラリに新規追加した機能です。

JIT ライブラリ に任意のバイナリコードを実行できる機能を追加してみました。本気で細かく制御したい人向けです。こちらはがっつりアーキテクチャ依存です。そして、クラッシュさせるのも簡単です。

サンプル

まずはサンプルです。

okay.kx
using Jit;

var code
    = System.PLATFORM == "X86_64-WIN" ? <0x48, 0x89, 0xc8, 0xc3>    // mov rax, rcx | ret
    : System.PLATFORM == "X86_64"     ? <0x48, 0x89, 0xf8, 0xc3>    // mov rax, rdi | ret
    : null;
if (code.isBinary) {
    Jit.dump(code);
    var runner = new Jit.Runner(code);
    System.println(runner.run(100));
}
$ ./kinx okay.kx
       0:   48 89 f8                                    mov rax, rdi
       3:   c3                                          ret
100

さて、引数で与えた数値を単に返すだけの関数で、破壊するレジスタも無いのでプロローグもエピローグもなく、第一引数に来た値を復帰値(rax)に設定して ret するだけのものです。

System.PLATFORM

System.PLATFORM で x64 でかつ Windows か Windows ではないかを切り分けてます。生のアセンブラだとこうやって切り分けないといけないのが面倒ですね。しかし、何でもできる という危険で甘い香りのするメリットを存分に享受できます。

Windows だと Microsoft 呼び出し規約に基づくので第一引数は rcx レジスタに入ってきます。それに対し、ほぼほぼマイクロソフト以外が採用している System V 呼び出し規約では rdi レジスタに第一引数が入ってきます。

System.PLATFORM で具体的には何が返るかというと...

Value Window?
"X86_32-WIN" O
"X86_64-WIN" O
"ARM_THUMB2-WIN" O
"ARM_V7-WIN" O
"ARM_V5-WIN" O
"ARM_64-WIN" O
"X86_32"
"X86_64"
"ARM_THUMB2"
"ARM_V7"
"ARM_V5"
"ARM_64"
"PPC_64"
"PPC_32"
"MIPS_32"
"MIPS_64"
"SPARC_32"
"TILEGX"
"UNSUPPORTED"

です。

やってはいけないこと

crash.kx
using Jit;
var code = <0x48, 0x31, 0xc0,   // xor rax, rax
            0x48, 0x8b, 0x00>   // mov rax, [rax]
            ;
Jit.dump(code);
var runner = new Jit.Runner(code);
System.println(runner.run());
$ ./kinx crash.kx
       0:   48 31 c0                                    xor rax, rax
       3:   48 8b 00                                    mov rax, [rax]
Segmentation fault (core dumped)

ふふふ...。危ないなぁ。

おわりに

一言でいうと、"Take your own risk." といったところですね。しかし、こういうところに踏み込むスクリプト言語はなかなか無いと思うので、それなりの価値はあるのではないでしょうか。例えば、このライブラリがあれば Xbyak なんかも移植できそうですね! Kinx には演算子オーバーライド(オーバーロードとは呼ばない)もありますし。

ますます JIT が身近になりますね。

ではまた。

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

[Rails]payjp(API)を利用する仕組みと方法をかなり丁寧に書いてみる(クレカ登録編)

はじめに

スクールの課題でフリマアプリを作る中で、payjpを利用したクレカ登録・決済機能の実装がありました。
検索すると以前の卒業生が多数記事を書いてくれてるのでコードは見つかるのですが、何をやっているの?という所は結局公式のリファレンス読むのが一番だよねとなったので理解したことについて記載しておくものです。

この記事では
payjpを利用するってそもそもどういうこと?(APIの説明)
payjpの呼び出し方
クレカ登録の実装
登録したクレカによる決済実装

上記について記載していきます。

※現在payjpはv2バージョンを利用することが推奨されております。よりセキュアな実装として登録フォーム自体もpayjp側で用意してくれているのがv2なのですが、今回の課題はフォームの実装もこちら側のタスクとして含まれていたため、旧型のv1で実装を進めています。
とはいえ呼び出し方や使えるメソッドが異なるだけで、どちらも公式を読みながらやればあまり差がなく実装できるかと思います。

環境

ruby 2.6.5
rails 6.0.3

payjpを利用するってそもそもどういうこと?

コードを書いていくにしても、前段としてここの理解がまず大事だと思っています(あまりここに触れた記事はなかった)

payjpは、APIの1種です。
APIっていうのはググれば色々説明が出てくるんですが、「外部向けにソフトウェアの機能を一部提供してあげるよ」って感じです。

用意してくれてる側の指示に従いうまくAPIを呼び出すことで、自分のアプリケーションの中でそのソフトウェアの機能を一部使えることができる様になる、という仕組みですね。

具体例を言うと、例えばGogole Mapのサービスがあります。
あれもgoogleが用意してくれたAPIを利用し、指示にしたがってコードを記載することで、自分のアプリケーション上にgoogle mapを表示することが可能になるという仕組みです。

payjpもクレカに関するAPIであり、利用することでクレジットカードの登録や決済が可能になります。

APIを利用する時に考え方として個人的に大事だと思っているのが、相手に用意してもらっているという流れをちゃんと把握するということです。

上記を認識した上で実装の流れを考えると、
用意してもらったやり方で、APIを呼び出す準備をする
用意してもらったやり方で、向こうのアプリケーションとやりとりをし、処理を行う

といった感じになります。このイメージがあると、以降の作業が具体的に何をしているのか分かりやすくなると思うので重要です。

それが故、用意してくれてる側の公式ドキュメントでやり方を理解する、と言うのが一番適切なアプローチになります。公式の説明書を読む様なものなので。

payjpの呼び出し方

前提として、payjpに登録しテスト公開鍵とテスト秘密鍵を取得しましょう。
APIの認証のために必要なキーです。payjp側はこれらを持って、僕らが「利用を許されたユーザー」であることを判断しています。

登録が終わったらv1の公式リファレンスを読みましょう。
アクセスして早々、埋め込むべきスクリプトがちゃんと記載されています。
また、少し下に読み進めると、公開鍵による認証方法も書いています。
実際の記載については次項で見ていきます。

クレカ登録の実装

上記の呼び出し方も踏まえ、先に実際のコードを載せます。
その上で流れを解説していきます。
※各ファイル、説明に不要な部分の記載は適宜削除しています。

application.html.haml
!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %script{src: "https://js.pay.jp/v1/", type: "text/javascript"}
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application'
new.html.haml
    .creditCreate
      .creditCreate__title
        %h1 クレジットカード情報入力
      .creditForm
        = form_with model: @credit, method: :post, id: "cardCreateForm" do |f|
          .creditForm__numberfield
            %label(for="cardnumber-input") カード番号
            %span.creditPoint 必須
            %br
            = f.text_field :card_number, type: "text", class: 'cardnumber-input', id:'card-number', placeholder: "半角数字のみ", maxlength: 16
            .creditslist
              = image_tag "jcb.gif", class:"creditsIcon"
              = image_tag "visa.gif", class:"creditsIcon"
              = image_tag "master.gif", class:"creditsIcon"
              = image_tag "amex.gif", class:"creditsIcon"
          .creditForm__datefield
            %label 有効期限
            %span.creditPoint 必須
            %br
            = f.select :exp_month, [["01",1],["02",2],["03",3],["04",4],["05",5],["06",6],["07",7],["08",8],["09",9],["10",10],["11",11],["12",12]],{} , class: 'dateSelect', name: 'exp_month'= f.select :exp_year, [["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]],{} , class: 'dateSelect', name: 'exp_year'.creditForm__securityfield
            %label セキュリティコード
            %span.creditPoint 必須
            %br
            = f.text_field :cvc, type: 'text', class: 'securityInput', id: 'cvc', placeholder: 'カード背面4桁もしくは3桁の番号', maxlength: "4"
          #card_token.creditForm__submitfield
            = f.submit '追加する', class: 'creditsSubmit', id: 'token_submit'

payjp.js
$(function() {
  $('#cardCreateForm').on('submit', function(e) {
    e.preventDefault()
    Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);
    var card = {
      number: document.getElementById("card-number").value,
      exp_month: document.getElementById("credit_exp_month").value,
      exp_year: document.getElementById("credit_exp_year").value,
      cvc: document.getElementById("cvc").value
    };
    if (card.number == "" || card.cvc == "") {
      alert("入力もれがあります");
    } else {
      Payjp.createToken(card, function(status, response) {
        if (status === 200 ) {
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          $('#cardCreateForm').get(0).submit();
          alert("登録に成功しました");
        } else {
          alert("カード情報が正しくありません");
        }
      });
    }
  });
});
credits_controller.rb
require 'payjp'

def create
    Payjp.api_key =  ENV['PAYJP_SECRET_KEY']
    if params['payjp-token'].blank?
      render :new
    else
      customer = Payjp::Customer.create(
        email: current_user.email,
        card: params['payjp-token'],
        metadata: {user_id: current_user.id}
      )
      @credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @credit.save
        redirect_to user_path(current_user.id)
      else
        render :new
      end
    end
  end

以上がクレカ登録に関するコードです。
処理の流れは以下の通りです。この流れに沿って説明をしていきます。

①payjpのAPIを利用するセッティングをする
②jsファイルにて、フォーム送信時に入力内容とpajypを紐付け登録の準備をする
③コントローラーにアクションを飛ばし、payjp上で顧客データを生成・顧客データを紐づくidをテーブルに保存する

①payjpのAPIを利用するセッティングをする

application.html.hamlをみましょう

application.html.haml
!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %script{src: "https://js.pay.jp/v1/", type: "text/javascript"}
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application'

scriptの部分がpayjpを呼び出す記載です。
呼び出し方は公式リファレンスに書いてあります。ただ転記するだけです。公式をみていればただ転記するだけなのです。

②jsファイルにて、フォーム送信時に入力内容とpajypを紐付け登録の準備をする

jsファイルをみましょう。
フォーム送信のhamlファイルと対応した記載になってるので、並べてみながらだと理解がしやすいと思います。

payjp.js
$(function() {
  $('#cardCreateForm').on('submit', function(e) {
    e.preventDefault()
    Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);
    var card = {
      number: document.getElementById("card-number").value,
      exp_month: document.getElementById("credit_exp_month").value,
      exp_year: document.getElementById("credit_exp_year").value,
      cvc: document.getElementById("cvc").value
    };
    if (card.number == "" || card.cvc == "") {
      alert("入力もれがあります");
    } else {
      Payjp.createToken(card, function(status, response) {
        if (status === 200 ) {
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          $('#cardCreateForm').get(0).submit();
          alert("登録に成功しました");
        } else {
          alert("カード情報が正しくありません");
        }
      });
    }
  });
});

分割して解説していきます。

payjp.js
$(function() {
  $('#cardCreateForm').on('submit', function(e) {
    e.preventDefault()

jQueryで、フォームボタンをクリックした際の挙動ですと定義しています。

payjp.js
Payjp.setPublicKey(['PAYJP_PUBLIC_KEY']);

認証(payjpの利用登録ができていますよ!と言うこと)を示すため、登録し取得したテスト公開鍵をセットします。
これを持ってpayjp側が利用を許可してくれます。

setPublicKeyという書き方がいきなり出てきますが、これも公式リファレンスで指定された方法で記述してます。ただのコピペです。恐ることはありません。

payjp.js
    var card = {
      number: document.getElementById("card-number").value,
      exp_month: document.getElementById("credit_exp_month").value,
      exp_year: document.getElementById("credit_exp_year").value,
      cvc: document.getElementById("cvc").value
    };

JSの記載です。get element by id と言う書き方の通り、各フォーム欄のidを指定・特定することで、それぞれの入力内容を定義し、cardと言う変数に代入し定義しています。

payjp.js
    if (card.number == "" || card.cvc == "") {
      alert("入力もれがあります");

jsの記載です。定義した変数cardの、numberもしくはcvcが空だった時に受付をせずエラーを返す処理をしています。

payjp.js
    } else {
      Payjp.createToken(card, function(status, response) {
        if (status === 200 ) {
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          $('#cardCreateForm').get(0).submit();
          alert("登録に成功しました");
        } else {
          alert("カード情報が正しくありません");
        }
      });
    }
  });
});

カード情報を基にトークンを生成します。
これもpayjp APIの公式リファレンスを読んでください。

トークンを生成の部分を読むと、トークンはどのように生成するのかと言う文章での説明と、実際の記載方法が右側に記載されているはずです。

いきなりPayjp.create~と言う記載が出てきましたが、これはリファレンスに「この通り書いたらできるよ」って書いてくれてるだけなので、ちゃんと読めばその通りにするだけです。
ここでnumber等4つの値を渡す必要があると書かれいてがために、先ほどcard変数に4つの値を定義した訳です。順番的にはここで必要とされてるから定義している訳です。

if status === 200とは、カード情報が有効であり、正しくトークンが生成された場合にpayjp側が返してくれるステータス値になります。
なのでelseでエラーを返しているのは「正常に登録できなかった場合」を示しているわけです。

ここからさらに細かくみていきます。

payjp.js
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");

JS,jQueryの記載です。
カードが有効だった場合、セキュリティの観点からそれぞれのフォームに入力した値を取り除いています(idで指定したフォームのname属性をremoveする、と言う処理です)

payjp.js
          $("#card_token").append(
            $('<input type="hidden" name="payjp-token">').val(response.id)
          );
          $('#cardCreateForm').get(0).submit();
          alert("登録に成功しました");

カードが有効な際にPayjpから帰ってきたデータ(response.id)を、フォームに返す処理をappendで行っています。
type = hiddenを指定することで、あくまでユーザーからは見えないものの、フォームにデータを送っている形です。

その上で、submitすることで、Payjpから返してもらったデータをこの後controllerに送りcreateアクションを行っていく、と言うことを実現しています。

コントローラーにうまくデータを飛ばすための記載ということですね。
これを持って次にcontrollerの処理をみていくことができます。

③コントローラーにアクションを飛ばし、payjp上で顧客データを生成・顧客データを紐づくidをテーブルに保存する

credits_controller.rb
require 'payjp'

def create
    Payjp.api_key =  ENV['PAYJP_SECRET_KEY']
    if params['payjp-token'].blank?
      render :new
    else
      customer = Payjp::Customer.create(
        email: current_user.email,
        card: params['payjp-token'],
        metadata: {user_id: current_user.id}
      )
      @credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @credit.save
        redirect_to user_path(current_user.id)
      else
        render :new
      end
    end
  end

ここまでうまく処理ができて、最後にcontrollerでデータ作成となります。
順を追ってみていきましょう。

credits_controller.rb
require 'payjp'

def create
    Payjp.api_key =  ENV['PAYJP_SECRET_KEY']
    if params['payjp-token'].blank?
      render :new

まずrequire 'payjp'でpayjpを使える様にします。
次にSECRET_KEYを指定し、こちらがpayjpの利用権を思ったユーザーであることを示します(環境変数を用いて記載しています)

その上で先ほど正常なレスポンスだった場合に送られてきたデータがparams['payjp-token']ですので、そちらがからだった場合は処理を行わずrenderする処理を記載しています。

credits_controller.rb
def create

    else
      customer = Payjp::Customer.create(
        email: current_user.email,
        card: params['payjp-token'],
        metadata: {user_id: current_user.id} #ここは任意
      )

ここで、Payjpの顧客データを作成し、変数customerに代入しています。
いきなりこの書き方が出てきました。
何度も言う様ですが公式リファレンスを見れば全てが買いてあります。

今回行いたいのは顧客データの作成なので、リファレンスの中の顧客データの欄をみます。
すると、Payjp::Customer.createという記載方法や、その引数が書かれているはずです。

引数の内容をみていけば何をいれるべきなのかは簡単にわかるはずです。
例えばcardと言う引数はトークンIDを指定と書かれているので、先ほど送る様に設定したトークンIDを設定すればいいことがわかります。

credits_controller.rb
def create

      @credit = Credit.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @credit.save
        redirect_to user_path(current_user.id)
      else
        render :new
      end
end

ここからはRails側のテーブル処理の話です。
先ほどのリファレンスを参照すれば、Payjpの顧客が持つレスポンスデータについても確認することができます。
先ほど作成したPayjpの顧客データは変数customerに代入していたため、customer.id等で指定することで、レスポンスデータをテーブルに保存することができるわけです。

(法律上クレカデータを自分のテーブルに保存することはできないため、そのデータ自体はpayjp側に保存してもらい、そのデータを呼び出すための紐付けとしてcustomer_idやcardのdataをテーブルに保存しておくわけです。

終わりに

結構な長文になってしまったので登録のみで終わらせますが、大事なのは流れを理解し、リファレンスを読むこと、これに尽きると思います。

この登録したデータを基にカード決済を行う流れについても、リファレンスを読みながら決済にはどの様な記述が必要か?引数には何を指定するのか?を見ることでわりとすんなり実装できました。
(先ほどテーブルに紐付けたことからわかる通り、決済に必要なカードデータを呼び出して引数に渡してやれば良い、と言うのはそれほど難しくなく想像できるのではないでしょうか)

とはいえAPIに慣れていない状態だったのでQiitaの記事にも大分助けられつつの実装にはなりました。が、結論公式が最強、この言葉を実感を持って理解できる経験だったので記録しておきます。

*初学者ゆえ何かあればご指摘いただけると嬉しいです。

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

文字列の間を正規表現で抜き出したい

文字列の間の抜き出しに手間取ったので、メモ。

経緯

文字列の前後が固定で、間にある文字を抜き出す

例:
下記を連結したファイル名や定数からサービス名部分を抜き出したい

COMPANY_SERVICE_OPTION
※命名規則がサービス会社_サービス名_OPTION

パターン1

こちらを参考にして取得

"COMPANY_SERVICE_OPTION".slice(/COMPANY_(.+)_OPTION/)
puts $+

SERVICE

「$+」はRubyの組み込み変数で他にもいろいろとある模様
しかしながら、非推奨との記載もちらほら見かけたため下記パターン2を使用した。

パターン2

こちらのgsub記載を参考にして取得

puts "COMPANY_SERVICE_OPTION".gsub(/COMPANY_(.+)_OPTION/,'\+')

SERVICE

以上です。
いいねやQiitaやTwitterのフォローいただけると励みになります!
他にも方法がありましたら、コメントお待ちしております。
宜しくお願いします〜

参考

正規表現で間の文字列を抜き出したい
[Ruby] Kernelの特殊変数をできるだけ$記号なしで書いてみる
String - Rubyリファレンスマニュアル

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

【HTTPメソッドのPATCH】

PATCHとは

ブラウザからサーバーに情報を送信して、サーバー内の情報を書き換えることができるメソッドであり、登録情報の更新をする際などに利用される。

railsでeditアクションをして編集画面に遷移した後に実際に元あるデータを編集するときは
updateアクションを実行することになるが、その際のルーティングとしてHTTPメソッドのPATCHを利用することでupdateアクションを起動させてデータを更新することができる。

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

Rails5でECサイトを作る⑩ ~注文機能を作る~

はじめに

架空のベーカリーで買い物できるECサイトを作るシリーズ、Rails5でECサイトを作る⑨の続きです。このサイトの主要機能であり最も難しい箇所であるOrderモデル(注文機能)周辺を実装します。

ユーザの動きは以下のようになります。

カートに商品を入れて「注文画面に進む」ボタン押下
↓
注文情報入力(orders/new)画面
・支払方法
・お届け先
を入力、「確認画面へ進む」ボタン押下
↓
注文情報確認(orders/confirm)画面
内容を確認して「注文を確定する」ボタン押下
↓
orders/thanks画面を表示(静的ページ)

注文履歴を一覧、詳細画面で確認できる

ポイントは、orders/new画面で入力するお届け先が、「自分の住所」「登録した住所」「新しい住所」から選べることです。新しい住所を選択してnew画面のフォームに内容を入力すると、Orderのデータとして保存されるとともに、Addressモデルにも新規データとして保存されます。
フォームを入れ子にすることと、そのデータを保存する前に確認画面を経由することが、この機能の実装を難しくしている要因です。

ソースコード

https://github.com/Sn16799/bakeryFUMIZUKI

Modelのアソシエーション

fumizuki_ER.jpg

Controller

app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  before_action :authenticate_customer!
  before_action :set_customer

  def index
    @orders = @customer.orders
  end

  def create
    if current_customer.cart_items.exists?
      @order = Order.new(order_params)
      @order.customer_id = current_customer.id

      # 住所のラジオボタン選択に応じて引数を調整
      @add = params[:order][:add].to_i
      case @add
        when 1
          @order.post_code = @customer.post_code
          @order.send_to_address = @customer.address
          @order.addressee = full_name(@customer)
        when 2
          @order.post_code = params[:order][:post_code]
          @order.send_to_address = params[:order][:send_to_address]
          @order.addressee = params[:order][:addressee]
        when 3
          @order.post_code = params[:order][:post_code]
          @order.send_to_address = params[:order][:send_to_address]
          @order.addressee = params[:order][:addressee]
      end
      @order.save

      # send_to_addressで住所モデル検索、該当データなければ新規作成
      if Address.find_by(address: @order.send_to_address).nil?
        @address = Address.new
        @address.post_code = @order.post_code
        @address.address = @order.send_to_address
        @address.addressee = @order.addressee
        @address.customer_id = current_customer.id
        @address.save
      end

      # cart_itemsの内容をorder_itemsに新規登録
      current_customer.cart_items.each do |cart_item|
        order_item = @order.order_items.build
        order_item.order_id = @order.id
        order_item.product_id = cart_item.product_id
        order_item.quantity = cart_item.quantity
        order_item.order_price = cart_item.product.price
        order_item.save
        cart_item.destroy #order_itemに情報を移したらcart_itemは消去
      end
      render :thanks
    else
      redirect_to customer_top_path
   flash[:danger] = 'カートが空です。'
    end
  end

  def show
    @order = Order.find(params[:id])
    if @order.customer_id != current_customer.id
      redirect_back(fallback_location: root_path)
      flash[:alert] = "アクセスに失敗しました。"
    end
  end

  def new
    @order = Order.new
  end

  def confirm
    @order = Order.new
    @cart_items = current_customer.cart_items
    @order.how_to_pay = params[:order][:how_to_pay]
    # 住所のラジオボタン選択に応じて引数を調整
    @add = params[:order][:add].to_i
    case @add
      when 1
        @order.post_code = @customer.post_code
        @order.send_to_address = @customer.address
        @order.addressee = @customer.family_name + @customer.first_name
      when 2
        @sta = params[:order][:send_to_address].to_i
        @send_to_address = Address.find(@sta)
        @order.post_code = @send_to_address.post_code
        @order.send_to_address = @send_to_address.address
        @order.addressee = @send_to_address.addressee
      when 3
        @order.post_code = params[:order][:new_add][:post_code]
        @order.send_to_address = params[:order][:new_add][:address]
        @order.addressee = params[:order][:new_add][:addressee]
    end
  end

  def thanks
  end

  private
  def set_customer
    @customer = current_customer
  end

  def order_params
    params.require(:order).permit(
      :created_at, :send_to_address, :addressee, :order_status, :how_to_pay, :post_code, :deliver_fee,
      order_items_attributes: [:order_id, :product_id, :quantity, :order_price, :make_status]
      )
  end

end

いつものようにform_withで送られた情報をそのまま保存できれば楽なのですが、form_withは確認画面をはさむことができません。そのため、confirm、createアクションではviewから受け取ったパラメータをparams[:hoge]の形で取り出しています。

また、createにおいては一つのアクション内で,「Orderデータを新規作成」「CartItemからOrderItemに商品データを移す(OrderItemの新規作成と登録済みCartItemの削除)」Addressモデル内に一致するデータがない場合のみ新規登録」の3つをこなさなければなりません。一つ一つの動作は複雑ではありませんが、全体のコード量がかなり多くなります。

View

new

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">

  <div class="row">
    <div class="col-lg-4">
      <h2>注文情報入力</h2>
    </div>
  </div>

  <%= form_with(model: @order, local: true, url: {action: 'confirm'}) do |f| %>

  <!-- 支払方法 -->
  <div class="row space">
    <h3><strong><%= f.label :支払方法 %></strong></h3>
  </div>

  <div class="row">
    <div class="col-lg-4 btn-group" data-toggle="buttons">
      <label class="btn btn-outline-secondary active" style="width:50%">
        <%= f.radio_button :how_to_pay, true, {checked: true} %> クレジットカード
      </label>
      <label class="btn btn-outline-secondary" style="width:50%">
        <%= f.radio_button :how_to_pay, false, {} %> 銀行振込
      </label>
    </div>
  </div>

  <!-- お届け先 -->
  <div class="row space">
    <h3><strong><%= f.label :お届け先 %></strong></h3>
  </div>
  <!-- 自身の住所 -->
  <div class="row">
    <p>
      <label><%= f.radio_button :add, 1, checked: true, checked: "checked" %>ご自身の住所</label><br>
      <%= @customer.post_code %>
      <%= @customer.address %>
      <%= @customer.full_name %>
    </p>
  </div>

  <!-- 登録済み住所 -->
  <div class="row space-sm">
    <p>
      <label><%= f.radio_button :add, 2, style: "display: inline-block" %>登録住所から選択</label><br>
      <%= f.collection_select :send_to_address, @customer.addresses, :id, :address %>
    </p>
  </div>

  <!-- 新しい住所 -->
  <div class="row space-sm">
    <p><label><%= f.radio_button :add, 3 %>新しいお届け先</label></p>
  </div>
  <div class="row">
    <div class="col-lg-12">
      <%= f.fields_for :new_add do |na| %>
      <div class="row">
        <div class="col-lg-3">
          <strong>郵便番号(ハイフンなし)</strong>
        </div>
        <div class="col-lg-6">
          <%= na.text_field :post_code, class: 'form-control' %>
        </div>
      </div>

      <div class="row">
        <div class="col-lg-3">
          <strong>住所</strong>
        </div>
        <div class="col-lg-6">
          <%= na.text_field :address, class: 'form-control' %>
        </div>
      </div>

      <div class="row">
        <div class="col-lg-3">
          <strong>宛名</strong>
        </div>
        <div class="col-lg-6">
          <%= na.text_field :addressee, class: 'form-control' %>
        </div>
      </div>
      <% end %>
    </div>
  </div>
  <!-- お届け先ここまで -->

  <div class="row space">
    <div class="col-lg-2 offset-lg-7">
      <%= f.submit "確認画面へ進む", class: "btn btn-danger"%>
    </div>
  </div>

  <% end %>
</div>

confirm

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">
  <div class="row">
    <h2>注文情報確認</h2>
  </div>

  <%= form_with(model: @order, local: true) do |f| %>

  <div class="d-none d-lg-block space">
    <div class="row">
      <div class="col-lg-5"><h4>商品名</h4></div>
      <div class="col-lg-2"><h4>単価(税込)</h4></div>
      <div class="col-lg-2"><h4>数量</h4></div>
      <div class="col-lg-2"><h4>小計</h4></div>
    </div>
  </div>

  <% sum_all = 0 %>
  <% @cart_items.each do |cart_item| %>
  <div class="row space-sm">
    <div class="col-lg-3">
      <%= link_to product_path(cart_item.product) do %>
      <%= attachment_image_tag(cart_item.product, :image, :fill, 100, 100, fallback: "no_img.jpg") %>
      <% end %>
    </div>
    <div class="col-lg-2">
      <%= link_to product_path(cart_item.product) do %>
      <%= cart_item.product.name %>
      <% end %>
    </div>
    <div class="col-lg-2">
      <%= price_include_tax(cart_item.product.price) %>
    </div>
    <div class="col-lg-2">
      <%= cart_item.quantity %>
    </div>
    <div class="col-lg-2">
      <%= sum_product = price_include_tax(cart_item.product.price).to_i * cart_item.quantity %><% sum_all += sum_product %>
    </div>
  </div>
  <% end %>

  <div class="row space">
    <div class="col-lg-12">
      <div class="row">
        <div class="col-lg-3">
          <strong>送料</strong>
        </div>
        <div class="col-lg-3">
          <%= @order.deliver_fee %></div>
      </div>
      <div class="row">
        <div class="col-lg-3">
          <strong>商品合計</strong>
        </div>
        <div class="col-lg-3">
          <%= sum_all.to_i %></div>
      </div>
      <div class="row">
        <div class="col-lg-3">
          <strong>ご請求額</strong>
        </div>
        <div class="col-lg-3">
          <% billling_amount = sum_all + @order.deliver_fee.to_i %>
          <%= billling_amount.to_i %></div>
      </div>
    </div>
  </div>

  <div class="row space-sm">
    <div class="col-lg-2">
      <h3>支払方法</h3>
    </div>
    <div class="col-lg-4">
      <%= how_to_pay(@order.how_to_pay) %>
    </div>
  </div>

  <div class="row space-sm">
    <div class="col-lg-2">
      <h3>お届け先</h3>
    </div>
    <div class="col-lg-4">
      <%= @order.post_code %>
      <%= @order.send_to_address %>
      <%= @order.addressee %>
    </div>
  </div>

  <%= f.hidden_field :customer_id, :value => current_customer.id %>
  <%= f.hidden_field :post_code, :value => "#{@order.post_code}" %>
  <%= f.hidden_field :send_to_address, :value => "#{@order.send_to_address}" %>
  <%= f.hidden_field :addressee, :value => "#{@order.addressee}" %>
</div>

<div class="row space">
  <div class="col-lg-2 offset-lg-5">
    <%= f.submit "購入を確定する", class: "btn btn-danger btn-lg" %>
  </div>
</div>


<% end %>
</div>

thanks

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">
  <h2>ご購入ありがとうございました!</h2>
  <h3><%= link_to 'TOPへ戻る', customer_top_path %></h3>
</div>

「購入を確定する」ボタンを押した後に遷移するページです。静的なページでこれといった機能はないので、上記のように質素な文言だけ載せるも良し、スライダーなどで写真を表示するも良しです。

index

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">

  <div class="row">
    <h2>注文履歴一覧</h2>
  </div>

  <div class="d-none d-lg-block space">
    <div class="row">
      <div class="col-lg-2">注文日</div>
      <div class="col-lg-3">配送先</div>
      <div class="col-lg-2">注文商品</div>
      <div class="col-lg-2">支払金額</div>
      <div class="col-lg-1">状況</div>
      <div class="col-lg-2">注文詳細</div>
    </div>
  </div>

  <% @orders.each do |order| %>
  <div class="row space-sm">
    <div class="col-lg-2">
      <%= simple_time(order.created_at) %>
    </div>
    <div class="col-lg-3">
      <div class="row">
        <%= order.post_code + " " + order.send_to_address %>
      </div>
      <div class="row">
        <%= order.addressee %>
      </div>
    </div>
    <div class="col-lg-2">
      <% sum_all = 0 %>
      <% order.order_items.each do |order_item| %>
      <%= order_item.product.name %><br>
      <% sub_total = price_include_tax(order_item.order_price).to_i * order_item.quantity %>
      <% sum_all += sub_total.to_i %>
      <% end %>
    </div>
    <div class="col-lg-2">
      <%= sum_all += order.deliver_fee.to_i %></div>
    <div class="col-lg-1">
      <%= order_status(order) %>
    </div>
    <div class="col-lg-2">
      <%= link_to '表示する', order_path(order), class: "btn btn-sm btn-danger" %>
    </div>
  </div>
  <% end %>

</div>

show

app/views/orders/.html.erb
<div class="col-lg-10 offset-1 space">
  <div class="row">
    <h2>注文履歴詳細</h2>
  </div>

  <div class="row">
    <div class="col-lg-7">
      <div class="row space">
        <h3>注文情報</h3>
      </div>
      <div class="row">
        <div class="container">
          <div class="row space-sm">
            <div class="col-lg-3">
              <strong>注文日</strong>
            </div>
            <div class="col-lg-9">
              <%= simple_time(@order.created_at) %>
            </div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-3">
              <strong>配送先</strong>
            </div>
            <div class="col-lg-9">
              <%= @order.send_to_address %>
            </div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-3">
              <strong>支払方法</strong>
            </div>
            <div class="col-lg-9">
              <%= how_to_pay(@order.how_to_pay) %>
            </div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-3">
              <strong>状況</strong>
            </div>
            <div class="col-lg-9">
              <%= order_status(@order) %>
            </div>
          </div>
        </div>
      </div>

      <div class="row space">
        <h3>注文内容</h3>
      </div>
      <div class="d-none d-lg-block">
        <div class="row">
          <div class="col-lg-4">
            <strong>商品</strong>
          </div>
          <div class="col-lg-3">
            <strong>単価(税込)</strong>
          </div>
          <div class="col-lg-2">
            <strong>個数</strong>
          </div>
          <div class="col-lg-2">
            <strong>小計</strong>
          </div>
        </div>
      </div>
      <% sum_all = 0 %>
      <% @order.order_items.each do |order_item| %>
      <div class="row space-sm">
        <div class="col-lg-4">
          <%= order_item.product.name %>
        </div>
        <div class="col-lg-3">
          <%= price_include_tax(order_item.order_price) %>
        </div>
        <div class="col-lg-2">
          <%= order_item.quantity %></div>
        <div class="col-lg-2">
          <%= sub_total = price_include_tax(order_item.order_price).to_i * order_item.quantity %><% sum_all += sub_total %>
        </div>
      </div>
      <% end %>
    </div>

    <div class="col-lg-5">
      <div class="row space">
        <h3>請求情報</h3>
      </div>
      <div class="row">
        <div class="container">
          <div class="row space-sm">
            <div class="col-lg-6">
              <strong>商品合計</strong>
            </div>
            <div class="col-6">
              <%= sum_all %></div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-6">
              <strong>配送料</strong>
            </div>
            <div class="col-lg-6">
              <%= @order.deliver_fee %></div>
          </div>
          <div class="row space-sm">
            <div class="col-lg-6">
              <strong>ご請求額</strong>
            </div>
            <div class="col-lg-6">
              <%= sum_all + @order.deliver_fee.to_i %></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

orders_show.jpg

上図において、「請求情報」欄に商品の合計額を表示したいのですが、上から順番に画面を構築すると「請求情報」と「注文内容」でそれぞれループを回す必要が出てきて二度手間になってしまいます。そこで、ズルいやり方だよなと思いつつ、colで画面を縦に2分割してまず「注文情報」「注文内容」の欄を作り、「注文内容」のループ内でついでに合計額を計算して変数に格納し、「請求情報」欄で変数を呼び出す、ということをしています。
(「請求情報」欄において、DB内で配送料のdefault値を設定し忘れたので商品合計とご請求額が同じ金額になっています。本来は商品合計に配送料800円がご請求額に加算される仕様で、viewのコードもそれに対応したものとなっています。)

後記

このECサイトの中枢機能だけあって、コード量も多いし内容も複雑でしたね。今回の実装では、おおよその部分では以前私がスクールのチーム実装で作ったものを踏襲しつつ、ヘルパーやモデルのメソッドを新たに定義してコードを見やすくしたり、画面をレスポンシブ対応にしたりといった改変を加えました。

createアクションで新しい住所を選択したらOrderと同時にAddressにも住所データが登録される仕様について追記です。上記の記述ではAddressモデルのaddressカラムを検索して、一致するものがなければ登録する流れになっています。より正確な検索をするなら、

unless Address.find_by(addressee: @order.addressee, address: @order.send_to_address).exists?

として、宛名・住所ともに一致するかを確かめても良いかも知れません。コードは余計に長くなりますが。

何はともあれ、これでcustomerサイトの機能は揃いました。めでたしめでたし。

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

【Prefix】

Prefixとは

prefix.png

ルーティングを確認する際にrails routesコマンドを実行すると上の画像のような画面が表示されるが、上の画像の一番左の項目のことであり、何を表しているかというと真ん中あたりの項目のURI patternのルーティングを変数にしたもの。

なのでlink_toメソッドを使用する際の遷移先の記述を以下のように

prefix4.png

としてルーティングにURI patternを記述していたところを以下のように

prefix2.png

のようにPrefixで記述することができる。
二つとも遷移先は全く同じ。

tweet_pathというprefixの引数であるtweet.idは今回の場合はdestroyアクションへのルーティングなのでどのツイートを削除するのかという情報が必要なため該当のツイートのidを引数に指定している。

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

【ストロングパラメーター】

ストロングパラメーターとは何か

ストロングパラメーター.png

コントローラーにアクションを定義する際に出てくるストロングパラメータとは何か解説していきます。

createアクションの実行内容の部分にcreate(tweet_params)と記述がありますがこの引数であるtweet_paramsはprivateメソッドの中に定義されています。

privateメソッド

privateとはクラスの外から呼び出すことのできないメソッドのことです。
privateメソッドを使うことのメリットは以下の二点です

1.classの外部から呼び出されると困るメソッドを隔離する

メソッドの中にはclassの外部から呼び出されるとエラーを起こすメソッドもあるため隔離しておくことでエラーを事前に防ぐことができる。

2.コードの可読性を高める

privateとそうでない部分を明確に分離することでコードとしての可読性が上がる

このprivateメソッドの中に
tweet_paramsというメソッドが定義されており、処理の内容は以下のようになっています。

ストロング2.png

requireの引数にtweetモデルを取り、
permitの引数に:name :image :textを取っています。

この意味はフォームから送られてきたデータのうち、:name :image :textというpermit以下で指定したキーを持つパラメーターのみを受け取るように制限するということです。

この指定したキーを持つパラメーターのみを受け取る仕組みのことをストロングパラメータといいます。

ストロングパラメータを指定しておくことによって仕様以外のパラメータが送信されてくることを防ぎ、意図しないデータの更新をされないようにすることができます。

例えば、他人のログインパスワードを更新するパラメータを送信すれば勝手に他人のパスワードを変更できてしまうため、
こういったことを防ぐためにストロングパラメータを使用する必要があります。

再度コードを見ていくと

ストロングパラメーター.png

createメソッドは引数にtweet_paramsを指定しているため、tweet_paramsメソッドを経由して新たなデータが作成、保存される。
そのためこの場合は新しいツイートは必ずpermitの引数に指定したストロングパラメータのみを持っているということになります。

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

【Rails】form_withに記述するlocal: trueについて

なぜlocal: trueを記述するのか

railsでフォームを送信する際にform_with以下にlocal: trueを記述するが、
最初は意味も分からずにそういうのなんだろうと深く考えもせずに真似てコードを記述していました。

form_with.png

しかし意味も分からずに学習を進めるのは気持ち悪いため改めて調べて見たところ、
rails5においてはlocal: trueを記述しないとajax通信によるフォームの送信という意味になってしまうためHTMLとしてフォーム送信をする場合はlocal: trueの記述が必要とのことです。

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

macOS CatalinaにRailsをインストールする

はじめに

macOS CatalinaにRailsをインストールする方法です。
Railsチュートリアルなどでローカル環境にセットアップする場合は、ご参考ください。

前提条件

  • OS: macOS Catalina
  • バージョン: 10.15.5
  • Ruby: 2.7.1
  • Rails: 5.1.6

インストール手順

1. Command line tools をターミナルからインストール:
 xcode-select --install
2. Homebrewをインストール:
 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
3. rbenvをインストール:
 brew update
 brew install rbenv
 echo 'eval "$(rbenv init -)"' >> ~/.zshrc
 source ~/.zshrc
 ※macOS Catalinaからbashからzshに変更になっています。
4. rbenvを使ってRubyをインストール:
 rbenv install 2.7.0
5. デフォルトのRuby を設定:
 rbenv global 2.7.0
6. 必要なソフトウェアをインストール:
 brew install yarn
7. Railsのインストール:
 gem install rails -v 5.1.6
 ※ターミナルを再起動しないとデフォルトのRubyが呼び出されてエラーになる可能性があります。

参考

Rails Girls インストール・レシピ

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

コードを書くということ Ruby

rubyでコードを書くって行為とは・・・

 rubyの書き方、思考方法は本で読んで、取り敢えず動かしてみるだけでは見えてこないものが多いです。他者のコードを読むにも読解力が低い状態だと、習慣的な表現にさえ詰まる。また、こちらがその習慣を知らないと習慣慣れした人たちから逆に自分の書いたコードで驚かれることだってある。前に書いた命名の仕方だったりがそれにあたりますが、今回はそれとはまた別種の学んだものを纏めておこうと思います。

クラスメソッドのself

 このクラスメソッドに書かれてるselfってなんなのか・・・書かないとそのメソッドを呼ぶこともできない。なんでやねんって話なんですよね。一番最初に思う疑問は。オブジェクト指向にそもそも慣れてないと、ここで躓くのです。

答えはクラスメソッドを示すためのものです。レシーバーがクラス名になるメソッドです。
下記は書籍で見たクラスメソッドの書き方です。呼び出す際に、クラス名であるGreeterをレシーバーにして、greetメソッドを呼んでいます。

class_method.ruby
class Greeter
    class << self
        def greet
            puts "おはこんばんちわ"
        end
    end
end

Greeter.greet

下記はselfをレシーバーみたいに使った書き方ですね。クラス自身のgreetメソッドということだと思っています。初っ端はこれだけを見たのでなんのこっちゃわかりませんでした。

self.rb
class Greeter
    def self.greet
        puts "おはこんばんちわ"
    end
end

Greeter.greet

クラス自身のgreetメソッド、と捉える理由として、下記の書き方ができるからです。selfではなく、直接クラス名をレシーバーみたいに書いても、greetメソッドを呼び出すことができます。

class.rb
class Greeter
    def Greeter.greet
        puts "おはこんばんちわ"
    end
end

Greeter.greet

ではなぜ、クラスメソッドにおいて、selfを使うのか?

  • selfを書いておいて、インスタンスメソッドと差異化
  • 二度書き防止

の2点にあると思われます。後日、インスタンスメソッドとの違いも記述する予定です。

クラス関係の余談

 とあるコードではrailsのActive Supportにおけるclass_attributeにおいても、slefが使用されているのを見たことがあります。これもselfの部分をFirebaseクラスに書き換えてやっても動いたことから、このclass_attributeも呼んで字の如く、Class#に分類されるもののようですね。

firebase.rb
class Firebase
  require 'google/cloud'

  class_attribute :contact
  self.contact = Google::Cloud.new(ENV['GOOGLE_PROJECT']).firestore
end

ロジックの初手は肝要

ビリヤード台で左上から右下45度でボールを発射して壁で理想的な反射をする場合、ボールはどの座標を通って何ステップでコーナーの穴に落ちるか

ゲームプログラミングのコードを課題として頂いた時(ゲームプログラミングであることは後になって気づいたことですが)に、今まで有限ループでのロジック組みに取り組んでいたために、(例えば、1ヶ月のユーザー登録者数を計算するので、DB上から取り出したユーザーの分だけループさせたいのような)この手の無限ループロジックの発想が出てこなかった。そも思考ロジックの土台すらままなっておらず、この課題は答え合わせになるまで解く事ができなかったものです。

billiard.rb
x_max = ARGV[1]
y_max = ARGV[0]

if !x_max || !y_max
  puts "引数を指定してください"
  exit 1
end

x_max =  x_max.to_i
y_max =  y_max.to_i

#状態
x = 1
y = 1
step = 1
x_way = 1
y_way = 1

puts " #{step} (#{x},#{y})"
x += 1
y += 1
loop do
  step += 1
  puts " #{step} (#{x},#{y})"

  if y == 1 && x == x_max || x == 1 && y == y_max || x == 1 && y == 1 || x == x_max && y == y_max
    puts "GOAL!!"
    break
  elsif x == x_max
    x_way = -1
  elsif y == y_max
    y_way = -1
  elsif x == 1
    x_way = 1
  elsif y == 1
    y_way = 1
  end

  x += x_way
  y += y_way
end

以下、学んだものを纏めます。

状態(state)管理

プログラムは扱うデータの状態変化である。
今回のビリヤード問題において具体的化させると、

  • 引数で与えられた数によって作られる盤面
  • ボールの初期値
  • ボールの進行方向

何の状態を変化させ、何が固定化されていると目的のコードを作れるかを考える。

境界条件

何がきっかけで、状態の変化に繋がるのか、もしくは終了になるのかを見極めて条件式、状態に対し動きを与える処理を書く。
今回のビリヤード問題において具体的化させると、

  • 四方の壁にあるx軸とy軸の方向にある最小値と最大値

ということになります。これを抑えることでどのタイミングでボールの移動状態が変化するかを判断できます。条件式を考える上で、重要になる。ロジックの走りは、境界条件を意識する。

正しい無限ループ

プログラミングにおける無限ループは、

ループを実行しながら終了条件が見えてくるものに関して無限ループを使用する。言い換えると、プログラムを書いてる時に終了条件が明らかに分からないものを利用して無限ループを使用する。

ゲームのプログラムなんてのはまさにそれキャラのダッシュモーションはユーザーの入力に依存するので、入力される限り無限ループさせる必要がある、という事ですね。

無限ループにおける利用はDB関係でも必要になる。大量のデータをループで処理する場合、殆ど無限ループと変わらない数のループを要するので、一定条件を満たしたら、終了するような設計にする必要がある。

感想

ロジックを組むにおいて、そもそも有限ループか、一定条件まで続くループなのか、でコードを書く事ができていなかった故に、今回の課題はろくに辿り着けることができなかったが、重要な思考としての無限ループは理解できてよかった。

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

1つの商品に複数画像を登録。ちょっと何言ってるか、わかる!!~part3~

前提

  • ruby on rails 6.0.0 を使用。
  • ユーザー機能はdeviseにより導入されているものとする。
  • viewファイルは全てhaml形式とする。
  • ちなみに使っているのはMacBook Air(Retina, 13-inch, 2020)です。

はじめに

前回(part2)のあらすじです。carrierwaveのuploaderを使って商品登録の際に画像を投稿できる機能を実装しました。

前置きや手順などは part1 に詳しく記載してあるので気になったら騙されたと思ってクリックしてLGTMを押してみましょう。特に何も起こりません。

前回で画像1枚のみの投稿はできるようになったので今回は最大5枚まで一気に登録できるようにしていきます。手順の3番目にあたる、 「jQueryを導入して複数画像の投稿を実装」ですね。

1つ画像を登録するのに3万年かかったのに5つ登録しようと思ったらそれはもう15万年かかるのでは?(あほ)

そんなことはさておき、

画像を複数投稿する

前置きが長くなってしまいましたが、尻込みしていても仕方がないのでさっそくやっていきましょう。

現在の状態だと、画像のフォームは一つしか表示されておらず、一つ選択してしまったらそれで終わりとなってしまっています。そこで登場するのがjavascript。(今回はjQueryを使います)
最終的に、一つ画像を入力したら新しいフォームが出現し、最大5回まで画像を登録できるといった状態ができればゴールとします。

jQueryについて

まずはjQueryの導入からですが、Rails6において、私の知る限りjQueryの導入方法には2つの方向性があります。

  • gemとしてjQueryをインストールする方法
  • webpackerを通してjQueryを呼び出す方法

今回は先述のgemを通した方法でやりました。混乱する人も多いと思うのでjQueryの導入方法は番外編として後ほど投稿しようと思います。というか僕が大混乱しました。

時を飛ばそう

というわけで、無事jQueryが導入できたていで進めていきましょう。

app/views/products/_form.html.haml
= form_with model: @product, local: true do |f|
  = f.text_field :name, placeholder: 'name'
  #image-box
    = f.fields_for :images do |i|
      .group{ data: { index: i.index } }
      = i.file_field :src, class: 'file'
  = f.submit 'SEND'

まずはformのビューファイルにクラスやIDをつけていきます。{ data: ~ } の部分はカスタムデータ属性の指定です。他は特に問題ないと思います。基礎的なことをhamlで書いているだけです。

app/assets/javascripts/product.js
$(function() {
  const buildFileField = (index)=> {
    const html = `<div data-index="${index}" class="group">
                    <input class="file" type="file"
                    name="product[images_attributes][${index}][src]"
                    id="product_images_attributes_${index}_src"><br>
                    <div class="remove">削除</div>
                  </div>`;
    return html;
  }

  let fileIndex = [1,2,3,4,5];

  $('#image-box').on('change', '.file', function(e) {
    $('#image-box').append(buildFileField(fileIndex[0]));
    fileIndex.shift();
    fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
  });

  $('#image-box').on('click', '.remove', function() {
    $(this).parent().remove();
    if ($('.file').length == 0) $('#image-box').append(buildFileField(fileIndex[0]));
  });
});

こちらがjQueryの記述となります。番外編で詳しく書きますが、Rails6のassetsフォルダにjavascriptはないので、自分で作成していただく必要があります。

それでは順を追って解説していきましょう。

let fileIndex = [1,2,3,4,5];

$('#image-box').on('change', '.file', function(e) {
  // ファイルが選択されたときfileIndexの最初の数字をindexとして持ったフォームを新しく作成する。
  $('#image-box').append(buildFileField(fileIndex[0]));
  // fileIndexの最初の数字を削除して数字をひとつずつ左へずらす。
  fileIndex.shift();
  // fileIndexの最後の数字に1を足した数字を最後尾に挿入する。
  fileIndex.push(fileIndex[fileIndex.length - 1] + 1)
  });

まずはこちら、表示されているフォームで画像を選択した際に、新しいフォームが表示される。といった処理ですね。一行ずつの考え方を追記しておきました。これを繰り返すことで、ひとつ一つのフォームに固有のindexを持たせることができます。

const buildFileField = (index)=> {
    const html = `<div data-index="${index}" class="group">
                    <input class="file" type="file"
                    name="product[images_attributes][${index}][src]"
                    id="product_images_attributes_${index}_src"><br>
                    <div class="remove">削除</div>
                  </div>`;
    return html;
  }

先ほどの buildFileField の処理を記述したものです。先ほど作った固有のindexを引数として渡しています。``で囲われている部分は、_form.html.haml で記述されていたフォームをhtmlとして記述し直し、あとで参照できるようにidとnameをつけているだけです。

$('#image-box').on('click', '.remove', function() {
  // クリックされた.removeの親要素を削除する。
  $(this).parent().remove();
  // フォームの数が0になった際、新しいフォームを表示させる。
  if ($('.file').length == 0) $('#image-box').append(buildFileField(fileIndex[0]));
});

最後にフォームを削除する処理ですね。こちらも一行ずつ書いておきました。

さて、ここまでで登録画面における処理はひとまずできました。jQueryの書き方さえ分かっていれば特に難しいこともなかったかと思います。
そういえばfileやremoveなど、一部メソッドなのかクラスなのかがわかりにくかったかもしれませんが、'.~'という形で記述されているのは全てクラスになります。慣れれば簡単に見分けられます。

最後に

大袈裟な前置きをしていましたが、実はデータベースへの複数登録自体は前回で終わっているのです。なので今回はフロントにおける作業がメインでしたね。

ようやく形になってきました。今回で画像の複数登録はできたので、次のpartでは複数画像の編集を行っていきたいと思います。

ではまた次のpartでお会いしましょう。

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

[Rails]ransackで関連するモデル(親や子)のカラムをまたいで検索する方法

実現したいこと

1つのモデルに関連(ネスト)するモデルのカラムまで、検索対象にしたい

具体的には、古着屋の店舗名だけでなく、エリア名(1対多)や取り扱いブランド名(多対多)まで含めて一括検索したい。

スクリーンショット 2020-07-11 17.45.47.png

結論

フォームタグの要素名に、関連するモデル名_関連するモデルのカラム名を指定する

関連するモデルが、対1(belongs_to: hoge、やhas_one: fuga)

例えば、shopモデルに紐づくareaモデルのエリア名(name)を検索条件にしたい時
= f.フォームヘルパー :要素名要素名area_name_contとする。

分解すると
area → 関連するモデル名
name → 関連するモデルのカラム名
cont → 部分一致を指定する述語

となります。

= search_form_for(@q, url: shop_search_path) do |f|
  = f.text_field :area_name_cont
  # shopモデルに紐づくareaモデルの、エリア名(name)

関連するモデルが、対多(has_many: hogesなど)

例えば、shopモデルに紐づくbrandsモデルのブランド名(name)を検索条件にしたい時
= f.フォームヘルパー :要素名要素名brands_name_contとする。

分解すると
brands → 関連するモデル名 ※shop has_many: brandsなので複数形
name → 関連するモデルのカラム名
cont → 部分一致を指定する述語

となります。

= search_form_for(@q, url: shop_search_path) do |f|
  = f.text_field :brands_name_or_genres_name_cont
  # shopモデルに紐づくbrandモデルの、ブランド名(name)
  # shopモデルに紐づくgenreモデルの、ジャンル名(name)
  # ※紐づくモデルが複数の時は、モデル名が複数形になることに注意

ちなみに、_or_などでカラム名を繋ぐと、複数カラムを検索対象にできます。

実際のransackの使い方などについては、[Rails]ransackを利用した色々な検索フォーム作成方法まとめなどの記事を参考にしてください。

モデル間のアソシエーション

※関連する箇所のみ記載

スクリーンショット 2020-07-11 18.17.14.png

shop.rb
# shopモデル
  belongs_to :area, optional: true
  has_many :shop_genres
  has_many :shop_brands
  has_many :genres, through: :shop_genres
  has_many :brands, through: :shop_brands
area.rb
# areaモデル
  has_many :shops
brand.rb
# brandモデル
  has_many :shop_brands
  has_many :shops, through: :shop_brands
shop_brand.rb
# shop_brandモデル
  belongs_to :shop
  belongs_to :brand
shop_genre.rb
# shop_genreモデル
  belongs_to :shop
  belongs_to :genre

関連を調べる方法

ransackable_associationsというメソッドを使うと便利です。

1. アプリケーションディレクトリでrails c

terminal
# 該当のアプリケーションディレクトリで実行

$ rails c
Running via Spring preloader in process 61541
Loading development environment (Rails 5.0.7.2)
[1] pry(main)> 

2. モデル名.ransackable_associationsを実行

terminal
# 今回はShopモデルとの関連を調べたいので、Shop.ransackable_associationsとすると
  Shopモデルに紐づくモデルが表示される
[1] pry(main)> Shop.ransackable_associations
=> ["user", "area", "shop_genres", "shop_brands", "genres", "brands"]
[2] pry(main)> 

参考

Ransackで簡単に検索フォームを作る73のレシピ -026 関連
Ransackで親テーブルや子テーブルのカラムで複数検索する方法

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

【随時更新】Ruby on Rails 便利なメソッド

alias化されたattributeを知りたい。

pry(main)> Member.attribute_aliases
=> { "user_name"=>"UserName", "mail_address"=>"MailAddress",}

methodsメソッドで取得したメソッド一覧からgrepで検索したい。

pry(main)> Member.methods.grep /attribute_aliases/i
=> [:attribute_aliases, :attribute_aliases?, :attribute_aliases=]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Digdag公式ドキュメントからDigdagを学ぶ-Language API-Ruby

目標

Digdagの公式サイトのドキュメントのOperatorsの翻訳+α
DigdagのRubyを使ってRailsにバッチを作るまでが最後の目標
http://docs.digdag.io/operators/scripting.htmlhttp://docs.digdag.io/operators/scripting.html

目次

Getting started
Architecture
Concepts
Workflow definition
Scheduling workflow
Operators
Command reference
Language API -Ruby

Language API-Ruby

Programmable workflow in Ruby

workflow1.dig
_export:
  rb:
    require: 'tasks/my_workflow'

+step1:
  rb>: MyWorkflow.step1

+step2:
  rb>: MyWorkflow.step2
my_workflow.rb
class MyWorkflow
  def step1
    puts "ruby step1"
  end

  def step2
    puts "ruby step2"
  end
end
結果
$ digdag run workflow1.dig --rerun
2020-07-12 17:37:09 +0900 [INFO] (0017@[0:default]+workflow1+step1): rb>: MyWorkflow.step1
ruby step1
2020-07-12 17:37:10 +0900 [INFO] (0017@[0:default]+workflow1+step2): rb>: MyWorkflow.step2
ruby step2

Defining variables

step1でmy_valueに1を保存してstep2では保存した変数を出力する

class MyWorkflow
  def step1
    Digdag.env.store(my_value: 1)
  end

  def step2
    puts "step2: %s" % Digdag.env.params['my_value']
  end
end

Method argument mapping

step1で設定した変数はstep2で関数のパラメーターとして受け取れる

my_workflow.rb
class MyWorkflow
  def step1
    Digdag.env.store(my_value1: 1)
    Digdag.env.store(my_value2: 2)
  end

  def step2(my_value1: 0, my_value2: 0)
    puts "my_value1: #{my_value1} my_value2: #{my_value2} "
  end
end
結果
$ digdag run workflow1.dig --rerun
2020-07-12 17:47:41 +0900 [INFO] (0017@[0:default]+workflow1+step1): rb>: MyWorkflow.step1
2020-07-12 17:47:42 +0900 [INFO] (0017@[0:default]+workflow1+step2): rb>: MyWorkflow.step2
my_value1: 1 my_value2: 2 

Generating child tasks

Digdag.env.add_subtaskを使ってRubyでサーブタスクを生成可能です。

my_workflow.rb
class MyWorkflow
  def step1
    puts "step1 start"
    Digdag.env.add_subtask(MyWorkflow, :step3, arg1: 3)
    Digdag.env.store(my_value: 2)
  end

  def step2(my_value: "default")
    puts "step2: %s" % my_value
  end

  def step3(arg1:)
    puts "step3: %s" % arg1
  end
end

結果を見るとわかると思いますがstep1が実行される時、step1で追加したサブタスクstep3が実行される。

結果
$ digdag run workflow1.dig --rerun
2020-07-12 17:56:27 +0900 [INFO] (0017@[0:default]+workflow1+step1): rb>: MyWorkflow.step1
step1 start
2020-07-12 17:56:27 +0900 [INFO] (0017@[0:default]+workflow1+step1^sub+subtask0): rb>: ::MyWorkflow.step3
step3: 3
2020-07-12 17:56:28 +0900 [INFO] (0017@[0:default]+workflow1+step2): rb>: MyWorkflow.step2
step2: 2

これでDigdagとRubyとの連携についての説明は完了です。
次回からRubyでバッチを作ってDigdag上で実行してみたいと思います。

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

Rubyで配列の中のハッシュの値を取り出す方法

久しぶりにRubyに触れたらびっくりするぐらいわからなかったので調べました。

こんな気持ち悪い配列とハッシュの組み合わせの場合。

test.rb
user_data = [
  { user: { profile: { name: 'George', sex: 'man' } } },
  { user: { profile: { name: 'Alice', sex: 'woman' } } },
  { user: { profile: { name: 'Taro', sex: 'man' } } }
]

配列の中に、userというハッシュがあり、更にネストしてprofileというハッシュがあり、nameというキーに値が入ってる・・・もはやわけがわからんですね。

ではこれを取り出してみます。

'George'を取り出す。

こんな感じ

test.rb
puts user_data[0][:user][:profile][:name]

'Alice'を取り出すなら当然こんな感じ

test.rb
puts user_data[1][:user][:profile][:name]

配列の位置を変えただけ。

'Alice'のハッシュ内にある性別を取り出す場合は

test.rb
puts user_data[1][:user][:profile][:sex]

:nameを:sexに変えただけ。

配列内にある:nameをすべて取り出す

配列なのでeachを使って1つずつ取り出す。

test.rb
user_data.each do |values|
  puts values[:user][:profile][:name]
end

結果はこうなる

test.rb
George
Alice
Taro

digメソッドを使う場合

Ruby 2.3以降からはハッシュクラスで新しく使えるメソッドとしてdigというものが用意されています。

digメソッドで'Gorge'を取り出してみる

test.rb
puts user_data.dig(0, :user, :profile, :name)

配列に含まれたハッシュなら、何番のデータがほしいか引数に入れるだけ。

digメソッドで ':name'全て取り出してみる

test.rb
user_data.each do |values|
  puts values.dig(:user, :profile, :name)
end

digメソッドを使うメリット

キーが見つからない場合にエラーで返すかnilで返すかの違いのようです。digを使えばnilで返してくれるのでエラーへの対策が不要になるとか。(それまではfetchメソッドを使うなどしていたそうです)

test.rb
user_data = [
  {user: {}},
  {user: {}},
  {user: {}}
]

# digを使った場合
user_data.each do |values|
  puts values.dig(:user, :profile, :age)
end

# digを使わなかった場合
user_data.each do |values|
  puts values[:user][:profile][:age]
end

こんな感じの結果となる

test.rb
% ruby tmp/test.rb
#=> nil

#=> undefined method `[]' for nil:NilClass (NoMethodError)

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

【rspec】fill_inがあっているのにfetureテストが成功しない原因の1つ

rspecのfeatureテストが通らない

capybaraでわかりやすくて便利なのですが、fill_inハマったので内容をまとめます。

expect内のfiii_inのところでどうしてもエラーが起きてしまう。

検証して生成されたhtmlから見つけたidを指定しても、新たにidを作っても、findを使ってもできませんでした
。。

しかし数時間の格闘の末結論が出ました。

この記事のゴール

fill_inのエラー解決策の1つになれば幸いです。

結論 ログインに失敗している

失敗したコード

topics_spec.rb
scenario "user creates a new topic" do
    user = FactoryBot.create(:user)
    visit root_path
    click_link "ログイン"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
ココ→ click_link "ログイン"

    expect {
      visit new_topic_path
      fill_in "topic_description", with: "test"
      click_button "投稿"

      expect(page).to have_content "投稿しました"
      expect(page).to have_content "test"
      expect(page).to have_content "#{user.name}"
    }.to change(user.topics, :count).by(1)

スクリーンショット 2020-07-14 9.39.41.png

click_linkでログインを指定ためsubmitではなくヘッダーのリンクをクリックしていました。
そのためログインページでメールアドレスとパスワードを入力して、もう一度ログインページをクリックしていました。。
なんと不毛なことを。。。。

ということでclick_linkではなく、click_buttonを指定。

topics_spec.rb
scenario "user creates a new topic" do
    user = FactoryBot.create(:user)
    visit root_path
    click_link "ログイン"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
ココ→ click_button "ログイン"

    expect {
      visit new_topic_path
      fill_in "topic_description", with: "test"
      click_button "投稿"

      expect(page).to have_content "投稿しました"
      expect(page).to have_content "test"
      expect(page).to have_content "#{user.name}"
    }.to change(user.topics, :count).by(1)

これでちゃんと通りました。

まとめ

fill_inでエラーが起きているのでfill_inばかり見ていましたが、一度立ち止まって全体を見ることの重要性を再認識できました。

参考になれば嬉しいです!

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

【rspec】fill_inがあっているのにfetureテストが失敗する原因 ログインで失敗してない?

rspecのfeatureテストが通らない

capybaraでわかりやすくて便利なのですが、fill_inハマったので解決策の1つを紹介します。

expect内のfiii_inのところでどうしてもエラーが起きてしまう。

検証して生成されたhtmlから見つけたidを指定しても、新たにidを作っても、findを使ってもできませんでした
。。

しかし数時間の格闘の末結論が出ました。

この記事のゴール

fill_inのエラー解決策の1つとして検証してみてください!

結論 ログインに失敗している

自分の場合は結論ログインがうまくできていないためエラーが起きていました。

失敗したコード

topics_spec.rb
scenario "user creates a new topic" do
    user = FactoryBot.create(:user)
    visit root_path
    click_link "ログイン"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
ココ→ click_link "ログイン"

    expect {
      visit new_topic_path
      fill_in "topic_description", with: "test"
      click_button "投稿"

      expect(page).to have_content "投稿しました"
      expect(page).to have_content "test"
      expect(page).to have_content "#{user.name}"
    }.to change(user.topics, :count).by(1)

スクリーンショット 2020-07-14 9.39.41.png

click_linkでログインを指定ためsubmitではなくヘッダーのリンクをクリックしていました。
そのためログインページでメールアドレスとパスワードを入力して、もう一度ログインページをクリックしていました。。
なんと不毛なことを。。。。

ということでclick_linkではなく、click_buttonを指定。

topics_spec.rb
scenario "user creates a new topic" do
    user = FactoryBot.create(:user)
    visit root_path
    click_link "ログイン"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
ココ→ click_button "ログイン"

    expect {
      visit new_topic_path
      fill_in "topic_description", with: "test"
      click_button "投稿"

      expect(page).to have_content "投稿しました"
      expect(page).to have_content "test"
      expect(page).to have_content "#{user.name}"
    }.to change(user.topics, :count).by(1)

これでちゃんと通りました。

まとめ

fill_inでエラーが起きているのでfill_inばかり見ていましたが、一度立ち止まって全体を見ることの重要性を再認識できました。

参考になれば嬉しいです!

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

railsにgRPCクラアント導入

経緯

会社でPMを担当しているプロダクト(Rails)の実装で他のサービスから情報を取得する必要があり、そのサービスがOpenAPIでなく、gRPCでAPIインタフェースが定義されていたので、gRPCのクライアントを実装しました。
僕が前職でrailsでのgRPC周りを少し触っていたので、相対的に僕がやった方がチームの開発工数が増えると思ったので自分でやることにしました。
それについてのアウトプットです。

実装手順

1. gem導入

※前職で使っていたgrufというライブラリを使いました。

Gemfile
gem "google-protobuf" 
gem "grpc-tools"
gem "gruf"

※ webというコンテナで開発してる

docker-compose run --rm web bundle install
2. protoファイルが管理されているリポジトリをsubmoduleでアプリケーションのサブディレクトリとして登録して、protoファイルを元のリポジトリから取ってくる
$ git submodule add [web URL or ssh key] proto
$ git submodule init
$ cd proto
$ git submodule update
3. protoファイルをrubyファイルにコンパイル
docker-compose run --rm web grpc_tools_ruby_protoc -I [コンパイル対象のprotoディレクトリ] --ruby_out=[コンパイル後のファイル保存先のディレクトリ] --grpc_out=[コンパイル後のファイル保存先のディレクトリ] [コンパイル対象のprotoディレクトリ内の対象ファイル]
4. コンパイル後のrubyファイル(*_pb.rb)の読み込み設定

コンパイル後のファイルは、ファイル名と、クラス名が噛み合っておらず、Railsの読み込み規則に則っていなく自動読み込まれないので、指定してあげる必要がある。

config/initializers/gruf.rb
require "gruf"

Gruf.configure do
  Dir.glob(Rails.root.join("[コンパイル後のファイル保存先のディレクトリ]/*_pb.rb")).each do |file|
    require file
  end
end

コンパイル後のファイルでは下記のように自動で指定されており、auto_load_pathに追加しておく必要がある。
e.g. gruf-demoから
require 'Products_pb'

※コンパイル後のファイルは基本修正しないので。

config/application.rb
  class Application < Rails::Application
    config.paths.add [コンパイル後のrubyファイルディレクトリ], eager_load: true
  end
5. クライアントがサーバをコールする部分の実装

ここまでで、全てのコンパイル後のrubyファイルは使えるようになったでのクライアントの実装。

moduleにしようかとか悩みましたが、既存の実装でクライアント系の処理はservice層にまとめていたので、今回もそれに習う形にしました。

※特にmetadata周りはサーバ側の実装に依存するので注意。
grufのwikiだと、クライアントの初期化(Gruf::Client.new)時のoptions引数のキーでusernameを入れていたりとこの辺が今回の実装と違っており、若干悩みました。

app/services/grpc_client_service.rb
class GrpcClientService
  def initialize
    @metadata = {
      login: ENV["GRPC_CLIENT"],
      password: ENV["GRPC_PASSWORD"]
    }
  end

  def run(service_klass, method, request)
    client = Gruf::Client.new(
      service: service_klass,
      options: {
        hostname: ENV["GRPC_HOST"],
        channel_credentials: :this_channel_is_insecure
      }
    )

    client.call(method, request.to_h, @metadata)
  end
end

導入してみての感想とか

前職でgrufは使っていたので、余裕かと思っていましたが、やはり導入するのと、ただ使うだけでは結構違うなと思いました。
しっかり設定周りのコードを読んでおけば良かったと後悔してます。

また、今回gRPCサーバがgoで書かれており、クライアントからのコールがうまく行かないときにコード読むのに苦労して結局諦めたので、goも勉強したいなと思いました。

また少し詰まったのは、クライアントの初期化(Gruf::Client.new)時にmetadataを入れると謎の勘違いしており(本当はcall時の引数に入れる)、ライブラリのWikiに書いてないことはやはり、しっかりコード読まないといけないなと初歩的なことを改めて実感しました。

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

Rubyで与えられた配列の部分集合を列挙(+α)

競技プログラミングの問題を解いていたら、単に場合の数を数え上げるだけでなく、各場合の状態を作成して処理する必要が時々出てきた。Rubyだと Array#combinationArray#repeated_permutation などが用意されているが、他のが欲しいときにパッと組み立てられず時間を取られてしまっている。

なので練習も兼ねて、そのとき欲しかったメソッドを作ってみる。

部分集合

総当たり的に解きたいときなど、各要素について含む/含まないパターンを網羅したいことがある。場合の数は 2 ** ary.size

Array#combination を利用

要素数は 0 から ary.size まで考えられるので、それぞれについて組合せを列挙すればいい。楽なうえほとんどの処理をRubyの内部実装に任せられるため速い。

each_subset.rb
def each_subset(ary, &block)
    0.upto(ary.size) { |n| ary.combination(n, &block) }
end

if $0 == __FILE__
    each_subset([*0..2]) { |subset| p subset }
end
output
[]
[0]
[1]
[2]
[0, 1]
[0, 2]
[1, 2]
[0, 1, 2]

二進表記を利用

列挙の順番 k (0始まり)を二進表記したときの 1 0 がそのまま各要素の含む/含まないに対応するようにしてみる。

素直にやるならこんな感じ。Rubyは Integer#[]i ビット目の値を取得できる。

each_subset.rb
def each_subset(ary, &block)
    (1 << ary.size).times do |k|
        subset = ary.select.with_index { |_, i| k[i] == 1 }
        block.call(subset)
    end
end

if $0 == __FILE__
    each_subset([*0..2]) { |subset| p subset }
end
output
[]
[0]
[1]
[0, 1]
[2]
[0, 2]
[1, 2]
[0, 1, 2]

ary の先頭要素 0 は最下位ビット(=偶奇)に対応させているため有無が毎回切り替わり、一方で末尾要素 2 は最上位ビットに対応させているため後半のみに固まって現れている。

実装は見ての通り回数固定の二重ループになっていて、時間計算量は O(2N N) 。 2N からすれば N は大したことないので、実用上(要素数が16程度)は問題ないはず。どちらかというと #select でひたすら評価していることのほうが気になる。


一応 O(2N) も試しておく。再帰で実装し、 ary がひとつ少ない場合の処理に落とし込む。

※破壊的メソッドを多用してオブジェクトを使い回しているので、安全のため Array#dup で保護している。これを全て外せば倍以上速くなる。

each_subset.rb
def each_subset(ary, selected = [], &block)
    if ary.empty?
        block.call(selected.dup)  # 配列が同一オブジェクトだと危険すぎるので複製
        return
    end

    ary = ary.dup  # 入力配列を壊さないように複製

    elem = ary.pop
    each_subset(ary, selected, &block)  # elem を含まない
    selected.unshift(elem)
    each_subset(ary, selected, &block)  # elem を含む
    selected.shift
    # ary << elem  # 複製してあれば不要

    return  # ひとまず nil を返す
end

if $0 == __FILE__
    each_subset([*0..2]) { |subset| p subset }
end

分割

ary = [0, 1, 2, 3, 4] に対して、例えば [[0, 1], [2], [3, 4]] などを作りたい。

見方を変えると、「配列の各要素間について区切りを入れる/入れない」ということなので、場合の数は 2 ** (ary.size - 1) 。空配列のときは何を返すべきか謎なので、実装に都合良い形にする。

こちらも再帰で実装する。 ary から何個かをグループ化して抽出すれば、残ったものでまたグループを作ればいい。

each_split.rb
def each_split(ary, selected = [], &block)
    if ary.empty?
        block.call(selected.map(&:dup))  # 配列が同一オブジェクトだと危険すぎるので複製
        return
    end

    # ary == part1 + part2 を保つように操作していく
    part1 = []
    part2 = ary.dup  # 入力配列を壊さないように複製

    selected << part1
    until part2.empty?
        part1 << part2.shift  # 分割位置を右へずらす
        each_split(part2, selected, &block)
    end
    selected.pop

    # ary.replace(part1)  # 複製してあれば不要

    return  # ひとまず nil を返す
end

if $0 == __FILE__
    each_split([*0..4]) { |sp| p sp }
end
output
[[0], [1], [2], [3], [4]]
[[0], [1], [2], [3, 4]]
[[0], [1], [2, 3], [4]]
[[0], [1], [2, 3, 4]]
[[0], [1, 2], [3], [4]]
[[0], [1, 2], [3, 4]]
[[0], [1, 2, 3], [4]]
[[0], [1, 2, 3, 4]]
[[0, 1], [2], [3], [4]]
[[0, 1], [2], [3, 4]]
[[0, 1], [2, 3], [4]]
[[0, 1], [2, 3, 4]]
[[0, 1, 2], [3], [4]]
[[0, 1, 2], [3, 4]]
[[0, 1, 2, 3], [4]]
[[0, 1, 2, 3, 4]]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Twitterをスクレイピングしてもっとも古いぴえんツイートを見付ける

スクリーンショット 2020-07-14 3.47.33.png

Twitterをスクレイピングして過去のツイートを取得できる twitterscraper-ruby gem を作成しました。

このtwitterscraper-ruby gemを使えば「もっとも古いツイート」や「ある単語をツイートした最初の人」を簡単に見付けることができます。

告知

SNSデータを用いた分析、Ruby on Railsを用いたWeb開発について、ご依頼やご相談は @ts_3156 までお気軽にご連絡ください。

なぜいまさらスクレイピングなのか

ツイートを大量に取得する方法は、大別するとTwitter Search API(無料版)、Twitter Search API(有料版)、Twitterのスクレイピングの3通りがあります。

Twitter Search API(無料版)

おそらく世の中の99%の方はこの方法を使ってツイートを取得しています。ツイッターが提供しているAPIなので安心して利用できる一方、利用回数に強力な制限があり、また、最大のデメリットとして「直近の7日間のツイート」しか取得できません。このため、「最近のツイートを少しだけ取得する」ことしかできません。

Twitter Search API(有料版)

このAPIを使うと、月々あたり数百万円ほど支払うことで、過去の全てのツイートを検索することができるようになります。利用回数に制限はありますが、比較的緩やかな制限でありツイートの取得という意味では不自由することはなくなります。

※有料のAPIには様々な種類があり、正確な名称は異なります

Twitterのスクレイピング

twitterscraper-ruby gemが利用している方法です。Twitter Search APIのデメリットである利用回数の制限や対象期間の制限を気にすることなく、高速に大量のツイートを取得することができます。ただし、スクレイピングは利用規約で明確に禁止されている行為であり、完全に自己責任で行う必要があります。

この記事では、できるだけツイッターに負荷をかけない方法でいくつかの調査を行っています。

「ぴえん」をツイートした最初の人を調べる

最近流行っている「ぴえん」を最初にツイートした人を調べてみました。意外に歴史は古く、2008年5月22日の時点で現在のぴえんとほぼ同じ意味で使った人が見つかりました。

最初にツイートされた「ぴえん」のURLはこちら

スクリーンショット 2020-07-14 3.15.52.png

twitterscraper-rubyをインストール後に下記のコマンドを実行すれば、最初の「ぴえん」ツイートを取得することができます。

$ twitterscraper --query 'ぴえん' --start_date 2008-03-21 --end_date 2009-03-21 --lang ja --limit 10 --proxy --threads 10

ぴえんローの元ツイートURLはこちら

ちなみに、現在のぴえんと違う意味であれば、もっとも古いぴえんは2008年1月24日にツイートされています。ぴえんロー(ピェンロー鍋)やぴえん粥(ヤーピエンジョウ)という食べ物があり、この意味でのツイートでした。

スクリーンショット 2020-07-14 3.24.35.png

ぴえん粥の元ツイートURLはこちら

スクリーンショット 2020-07-14 3.25.31.png

年号として「令和」をツイートした最初の人を調べる

この予言ツイートのことは、2019年の春頃にネットでとてつもなくバズっていたので知っている方も多いと思います。

平成の次の年号として「令和」が最初にツイートされたのは「2016年7月13日」です。twitterscraper gemを使えばこのツイートを簡単に見付けることができます。

令和予言ツイートのURLはこちら

令和予言ツイートのスクリーンショット

twitterscraper-rubyをインストール後に下記のコマンドを実行すれば、令和予言ツイートを取得することができます。

$ twitterscraper --query '令和' --start_date 2016-07-13 --end_date 2016-07-14 --limit 10

ちなみに、「年号としての令和」ではなく「ただの文字列としての令和」であれば、もっと前にツイートしている人はたくさんいます。中国語で偶然同じ並びになることがあるようです。

たまたま令和と書かれたツイートのURLはこちら

スクリーンショット 2020-07-14 1.52.52.png

取得できる中でもっとも古いツイートを見付ける

公式のTwitter Searchであれば、最大で「2006-03-21」まで遡ってツイートを取得することができます。

試しに、もっとも古いツイートを取得してみました。その結果、2006年3月22日のjust setting up my twttrがもっとも古いツイートであることが判明しました。

もっとも古いツイートのURLはこちら

スクリーンショット 2020-07-14 2.41.08.png

twitterscraper-rubyをインストール後に下記のコマンドを実行すれば、もっとも古いツイートを取得することができます。

twitterscraper --query 'just' --start_date 2006-03-21 --end_date 2006-03-22 --limit 10

告知

SNSデータを用いた分析、Ruby on Railsを用いたWeb開発について、ご依頼やご相談は @ts_3156 までお気軽にご連絡ください。

参考リンク

https://github.com/ts-3156/twitterscraper-ruby

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

【Rails】セッションタイムアウトがうまくいかない事象の解決

前提

・タイムアウトのメソッドを定義
・コード自体間違っていないにもかかわらず、セッションがnilと判定されてしまい、メソッドがうまく処理されずエラーとなる。

タイムアウトのメソッド

sessions_controller.rb
  def create
# ...

   session[:last_access_time] = Time.current

# ...
  end
application_controller.rb
  TIMEOUT = 5.minutes

  def time_out
     if session[:last_access_time] > TIMEOUT.ago
       session[:last_access_time] = Time.current
     else
       session.delete(:user_id)
       flash[:danger] = "タイムアウトしました。"
       redirect_to :login
      end
    end

上記のメソッドを組み、その後ログインを試みるとエラーが発生する。

NoMethodError (undefined method `>=' for nil:NilClass):
# session[:last_access_time] がnil判定される。

解決方法

ブラウザ側のクッキーを削除することで解決しました。
おそらくログインしている状態でメソッドを定義したから、
「session[:last_access_time]なんかないぞ」って怒られてしまったのかもしれません。

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