20200712のRubyに関する記事は29件です。

[Ruby on Rails] date_selectで分割されたパラメーターをFormObjectに渡す。

一つのフォームの送信で複数のモデルを更新したいときなど、FormObjectを利用するかと思います。
(FormObjectについて知りたい方はこちら:【Rails】FormObjectを使ってほしい

そのFormObjectにdate_selectで分割されたパラメーターをそのまま渡すことできなく困っていた矢先、FormObjectにinclude ActiveRecord::AttributeAssignmentを宣言したら、まとめて渡すことができたので以下にて紹介したいと思います。

説明

状況としては、新規登録画面で誕生日を登録する場面です。

画像のCreateUserボタンを押すと、コントローラーのcreateアクションが走ります。user_paramsメソッドでStrongParameterの仕組みを使って、分割されたパラメーター<ActionController::Parameters {"birthday(1i)"=>"2020", "birthday(2i)"=>"7", "birthday(3i)"=>"13"} permitted: true>を取得。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    @users = User.all
  end

  # GET /users/1
  # GET /users/1.json
  def show
  end

  # GET /users/new
  def new
    @user = Form.new
  end

  # GET /users/1/edit
  def edit
  end

  # POST /users
  # POST /users.json
  def create
    @user = Form.new(user_params)
    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /users/1
  # PATCH/PUT /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to @user, notice: 'User was successfully updated.' }
        format.json { render :show, status: :ok, location: @user }
      else
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /users/1
  # DELETE /users/1.json
  def destroy
    @user.destroy
    respond_to do |format|
      format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def user_params
      params.require(:user).permit(:birthday)
    end
end
app/forms/form.rb
class Form
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :birthday, :date

  def to_model
    User.new(birthday: birthday)
  end

  def save
    return false if invalid?
    to_model.save
  end
end

@user = Form.new(user_params)で記載されているように、
分割されたパラメーターをFormObjectの初期値で渡そうと思うと、分割されているが故にエラーが起きてしまう。<ActionController::Parameters {"birthday(1i)"=>"2020", "birthday(2i)"=>"7", "birthday(3i)"=>"12"} permitted: true>をまとめてbirthdayパラメーターとして渡したい。
image.png

そこで、include ActiveRecord::AttributeAssignmentを宣言すると、分割されたパラメーターをまとめてattributeに渡してくれる。一件落着。

app/forms/form.rb
class Form
  include ActiveModel::Model
  include ActiveModel::Attributes
  # 追加
  include ActiveRecord::AttributeAssignment

  attribute :birthday, :date

  def to_model
    User.new(birthday: birthday)
  end

  def save
    return false if invalid?
    to_model.save
  end
end

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

[Ruby on Rails] date_selectで分割されたパラメーターFormObjectに渡す。

一つのフォームの送信で複数のモデルを更新したいときにFormObjectを利用するかと思います。
(FormObjectについて知りたい方はこちら:【Rails】FormObjectを使ってほしい

そのFormObjectにdate_selectで分割されたパラメーターをそのまま渡すことできなく困っていた矢先、FormObjectにinclude ActiveRecord::AttributeAssignmentを宣言したらいい感じにまとめて渡せたので以下にて紹介したいと思います。

説明

状況としては、新規登録画面で誕生日を登録する場面です。
image.png

画像のCreateUserボタンを押すと、コントローラーのcreateアクションが走り、StrongParameterを取得するuser_paramsメソッドにて、分割されたパラメーター<ActionController::Parameters {"birthday(1i)"=>"2020", "birthday(2i)"=>"7", "birthday(3i)"=>"12"} permitted: true>が取得される。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    @users = User.all
  end

  # GET /users/1
  # GET /users/1.json
  def show
  end

  # GET /users/new
  def new
    @user = Form.new
  end

  # GET /users/1/edit
  def edit
  end

  # POST /users
  # POST /users.json
  def create
    @user = Form.new(user_params)
    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /users/1
  # PATCH/PUT /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to @user, notice: 'User was successfully updated.' }
        format.json { render :show, status: :ok, location: @user }
      else
        format.html { render :edit }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /users/1
  # DELETE /users/1.json
  def destroy
    @user.destroy
    respond_to do |format|
      format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def user_params
      params.require(:user).permit(:birthday)
    end
end
app/forms/form.rb
class Form
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :birthday, :date

  def to_model
    User.new(birthday: birthday)
  end

  def save
    return false if invalid?
    to_model.save
  end
end

@user = Form.new(user_params)で記載されているように、
分割されたパラメーターをFormObjectの初期値で渡そうと思うと、分割されているが故にエラーが起きてしまう。<ActionController::Parameters {"birthday(1i)"=>"2020", "birthday(2i)"=>"7", "birthday(3i)"=>"12"} permitted: true>をまとめてbirthdayパラメーターとして渡したい。
image.png

そこで、include ActiveRecord::AttributeAssignmentを宣言すると、分割されたパラメーターを一旦まとめて渡してくれる。一見落着。

app/forms/form.rb
class Form
  include ActiveModel::Model
  include ActiveModel::Attributes
  # 追加
  include ActiveRecord::AttributeAssignment

  attribute :birthday, :date

  def to_model
    User.new(birthday: birthday)
  end

  def save
    return false if invalid?
    to_model.save
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TECH CAMP学習 個人アプリ④git ignore

5Kって何だ!?

個人アプリを作成中に、Git Hubにプルリクエストをしてない事をユーザー管理作成中の時に気づいてしまいました。
かなりデータが溜まっていって、ワークスペースになんか『5K』と表示されてしまい全てをプルリクエストできません、と注意で書かれてありました。

Git Hubデスクトップにもchangesが500と表示されてしまい500!!??となりました。5Kの意味はこれの事なのかな??

そしてやはり遅かったのか、どうすればプルリクエストできるのか、ファイルのどれかを消さないといけないのかと思い何となく一部のファイルを消去。

ただそれだけでは全くうまくいかず、少なくならず、ひたすらにググってググって、やっと見つけたのがgit ignore!!!
VS code 内で.git ignoreファイルを作り、以下のコードを入れました。
image.png

これで一気にchangesが91まで縮小され、プルリクエストができるようになりました。

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

【ターミナル】ターミナル強制終了後、rails sしたらA server is already runningとなった時の対処法【Rails】

「controll+z」でサーバーを切ったら、rails sが効かなくなった。どうしよう・・という問題。

【バージョン】

Rails 5.2.4.3
Ruby 2.5.1
macOS Catalina 10.15.4

【経緯】

ローカルで作業中、rails sで立ち上げたlocalhost:3000を「controllキー + c」でサーバを切り、再度「rails s」で起動させようとしたらうんともすんともいわず・・・ほかの切り方がないか調べたところ「controllキー + z」で強制終了できることが判明。

しかし、「controllキー + z」で強制終了後、
再度、rails sで立ち上げたところ、localhost:3000が立ち上がらず、下記のエラーが出た。

ターミナル
A server is already running. Check /Users/名前/XXX/XXXXXX/tmp/pids/server.pid.

「サーバーはすでに起動しています」と言われた。

【解決方法】

なので、

ターミナル
% lsof -i:3000

でどんなプロセスが動いているのかを確認してみる。

lsofコマンド
…「LiSt Open Files」(開いているファイル群を列挙する)という言葉に由来するようです。その名の通り、「プロセスが開いているファイル」を表示するコマンドらしいです。



ターミナル
COMMAND  PID            USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ruby    6792 satokokinoshita   11u  IPv4 0x95b341c28591923f      0t0  TCP localhost:hbci (LISTEN)
ruby    6792 satokokinoshita   22u  IPv6 0x95b341c27813ce6f      0t0  TCP localhost:hbci (LISTEN)



実際の画面はこんな感じだった
quiita.png

ここで、COMMANDがrubyとなっているPIDの番号 (ここだと6792、下の画面だと3405) を、○に入れる。

ターミナル
% kill -9 〇〇〇〇



これで、rails sで再度起動できるようになりました。

※ちなみにkillコマンドについてはkill -9(強制終了)以外は試しておりません。

localhost:3000のサーバーを切った時に消えるはずのプロセスが、強制終了したことで一部残ってしまっていたのだと思います。

「PID(プロセスアイディー)」は、プロセスを識別するための一意の数字になりますが、
PIDを指定してkillコマンドで切ることで、サーバーを切った状態に戻ったということになったのだと思います。

【参考にさせていただいたサイト】

このポートで実行中のプロセスはどれ? lsofコマンドの使い方

プロセスを止める最終手段killコマンドの種類・シグナルの使い方



【当該エラーに直面した時の感想的な】

Railsの画面に表示されるエラーは慣れたけど、ターミナルのエラーってあまりないので変な汗がでました?
ターミナルコマンドはどんどん慣れていきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人アプリ作成#2

一覧の作成

スクリーンショット 2020-07-12 21.47.19.png

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

Digdag公式ドキュメントからDigdagを学ぶ-Scheduling workflow

目標

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

目次

Getting started
Architecture
Concepts
Workflow definition
Scheduling workflow
Operators
Command reference
Language API -Ruby
REST API
Internal architecture
Release Notes

Scheduling workflow

Setting up a schedule

ワークフローを定期的に実行するには、ワークフロー定義の最初にschedule: オプションを追加します。

簡単なワークフローを生成します。日本時間の毎日9時に実行させる場合以下のようにスケジュールオプションを追加します。

workflows.dig
timezone: Asia/Tokyo

schedule:
  daily>: 09:00:00

+step1:
  sh>: echo start schedule ${session_time}

Digdagサーバーを起動してworkflowsプロジェクトをDigdagサーバーにプッシュします。

$ digdag server --memory
$ digdag push workflows

ワークフローの詳細を見るとセッションに9時の実行履歴が追加されてStatusはSuccessになっています。
スクリーンショット 2020-07-12 9.00.34.png

SessionsのSuccessをクリックすると実際試行情報も確認できます。
スクリーンショット 2020-07-12 9.08.42.png

schedule: オプション

構文 説明
hourly>: MM:SS 毎時 30分に実行 hourly>: 30:00
daily>: HH:MM:SS 毎時 7時に実行 daily>: 07:00:00
weekly>: DDD,HH:MM:SS 毎週9時に実行 weekly>: Sun,09:00:00
monthly>: D,HH:MM:SS 毎月一日の9時に実行 monthly>: 1,09:00:00
minutes_interval>: M 30分ごとにこのジョブを実行する minutes_interval>: 30
cron>: CRON 複雑なスケジューリングにはcron形式を使用

digdag checkコマンドは、最初のスケジュールがいつ開始するかを示します。

check
$ digdag check
2020-07-12 09:23:10 +0900: Digdag v0.9.41
  System default timezone: Asia/Tokyo

  Definitions (1 workflows):
    workflows (2 tasks)

  Parameters:
    {}

  Schedules (1 entries):
    workflows:
      daily>: "09:00:00"
      first session time: 2020-07-13 00:00:00 +0900
      first scheduled to run at: 2020-07-13 09:00:00 +0900 (in 23h 36m 47s)

注意

hourly、ddaily、weekly、またはmonthly使用する場合、セッション時間は実際の実行時間と異なる場合があります。
セッション時間は、実際の実行日の00:00:00です(hourlyの場合、時間は00:00に設定される)。

スケジュール例(システム時間:2019-02-24 14:20:10 +0900)
セッション時間は全て「00:00:00」になっている

schedule first session time first scheduled to run at
hourly>: 32:32 2019-02-24 14:00:00 +0900 2019-02-24 14:32:32 +0900
daily>: 10:32:32 2019-02-25 00:00:00 +0900 2019-02-25 10:32:32 +0900
weekly>: 2,10:32:32 2019-02-26 00:00:00 +0900 2019-02-26 10:32:32 +0900
monthly>: 2,10:32:32 2019-03-02 00:00:00 +0900 2019-03-02 10:32:32 +0900

Running scheduler

digdag schedulerはスケジューラを起動します。

$ digdag scheduler

ワークフロー定義を変更すると、スケジューラはdigdag.digファイルを自動的に再ロードするため、再起動する必要はありません。

以前9時に設定されている時間を12時に変更してdigdag schedulerを実行してみたがエラーが出ました。
digdagのバグかもと思って調べたら以下のようなISSUEがありました。
schedulerはメンテも活発に行ってなさそうだったのでdigdag schedulerよりはdigdag serverをお勧めします。

workflows.dig
timezone: Asia/Tokyo

schedule:
  daily>: 12:00:00

+step1:
  sh>: echo start schedule ${session_time}
$ digdag scheduler
1) Error injecting constructor, java.lang.IllegalArgumentException: Configured authenticatorClass not found: io.digdag.standards.auth.jwt.JwtAuthenticator
  at io.digdag.server.ServerModule$AuthenticatorProvider.<init>(ServerModule.java:176)
  while locating io.digdag.server.ServerModule$AuthenticatorProvider
  while locating io.digdag.spi.Authenticator
    for the 1st parameter of io.digdag.server.AuthRequestFilter.<init>(AuthRequestFilter.java:28)
  while locating io.digdag.server.AuthRequestFilter
  while locating io.digdag.server.AuthRequestFilter annotated with @com.google.inject.internal.UniqueAnnotations$Internal(value=3)
1) Error injecting constructor, java.lang.IllegalArgumentException: Configured authenticatorClass not found: io.digdag.standards.auth.jwt.JwtAuthenticator
  at io.digdag.server.ServerModule$AuthenticatorProvider.<init>(ServerModule.java:176)
  while locating io.digdag.server.ServerModule$AuthenticatorProvider
  while locating io.digdag.spi.Authenticator
    for the 1st parameter of io.digdag.server.AuthRequestFilter.<init>(AuthRequestFilter.java:28)
  while locating io.digdag.server.AuthRequestFilter
  while locating io.digdag.server.AuthRequestFilter annotated with @com.google.inject.internal.UniqueAnnotations$Internal(value=3)

Checking scheduling status

クライアントモードのコマンドを使用して、スケジュールを管理できます。
クライアントモードのコマンドについては今後整理します。早めにみたい方は以下のリンク参照
http://docs.digdag.io/command_reference.html#client-mode-commands

スケジューラコマンドは、デフォルトでhttp://127.0.0.1:65432をリスンします。 127.0.0.1(localhost)からの接続のみ受け付けます。これはセキュリティ上の理由から、パブリックネットワークへのポートは開かれません。
リスンアドレスを変更するには、-bind ADDRESSオプションを使用してください。

Setting an alert if a workflow doesn’t finish within expected time

例がなかったので簡単なコードで確認します。
ワークフローの実行時間が1秒以上かかる場合、警告を表示

workflows.dig
timezone: Asia/Tokyo

schedule:
  daily>: 11:45:00
sla:
  duration: 00:00:01
  +notice:
    sh>: echo alert! so slow

+step1:
  sh>: ./your_script.sh

sleep 5にして5秒遅延させる

your_script.sh
#!/bin/bash
sleep 5
echo start script

サーバーのLogをみたい場合、起動の時にtask-logオプションを追加してください。

$digdag server --memory --task-log ./task_log

1秒で設定したワークフローの実行時間がが5秒かかったのでLogにaletメッサージが表示されています.
StatusはSuccessになっています。

スクリーンショット 2020-07-12 11.46.19.png

Options

このパラメーターは、fail:BOOLEANおよびalert:BOOLEANオプションをサポートしています。
cを設定すると、ワークフローが失敗します。
alert:trueは、上記の通知メカニズムを使用して通知を送信します。

fail:trueを設定すると、ワークフローが失敗します。
alert:trueを設定すると、上記の通知メカニズムを使用して通知を送信します。上記の例では時間が過ぎてもStatusがSuccessになっていたのはこの設定がDefaultだと思います。

fail:true を追加してみると実行時間が1秒超えた場合StatusがFailureになるのがわかる。

add_fail_option
timezone: Asia/Tokyo

schedule:
  daily>: 12:03:00
sla:
  duration: 00:00:01
  fail: true
  +notice:
    sh>: echo alert! so slow

+step1:
  sh>: ./your_script.sh

スクリーンショット 2020-07-12 12.05.06.png

Skipping a next workflow session

セッション間の継続時間よりも長い時間がかかるワークフロー(30分または60分ごとのセッションなど)を頻繁に実行している場合があります。
ワークフローの期間におけるこの変動は、いくつかの理由で発生する可能性があります。たとえば、通常処理しているデータの量が増加している場合が該当します。

たとえば、1時間ごとに実行されるワークフローがあり、通常30分しかかからないとします。しかし、これは休日であり、現在はサイトの使用率が大幅に増加した結果ワークフローで1時間30分かかっている大量のデータが処理されています。
この期間中、2番目のワークフローが次の1時間実行開始されます。両方が同時に実行されるため、使用可能なリソースにさらに負担がかかります。

この場合は、次の1時間のワークフローセッションをスキップし、代わりに後続のセッションを利用して2時間のデータを処理するのが最善です。これを行うために、以下を追加しました。

skip_on_overtime:true | false: セッションが既に実行されている場合にスケジュールされたセッションの実行をスキップするかどうかを制御するために使用

スケジュールされたワークフローセッションには、以前に実行されたセッション時間を含むlast_executed_session_time変数があります。通常last_session_timeと同じです。skip_on_overtime:trueが設定されている場合かセッションが最初の実行である場合は値が異なります。

毎分実行されるワークフローを定義

workflows.dig
timezone: Asia/Tokyo

schedule:
  minutes_interval>: 1
  skip_on_overtime: true
+step1:
  sh>: ./your_script.sh
your_script.sh
#!/bin/bash
sleep 120
echo start script

毎分実行予約しても実際スクリプトは2分かかるサンプルです。
skip_on_overtime: trueにより1分ごとに実行設定しても実際は最初の実行が終わった後の1分後に次の実行時間が決まります。
スクリーンショット 2020-07-12 12.33.17.png

Skipping backfill

skip_delayed_by オプションを使用するとbackfill コマンドで、指定した時間だけ遅延したセッションの作成をスキップできます。

Digdagが再起動すると、スケジュールのセッションが、last_session_timeの次のセッションまで自動的に作成されます。

schedule:
  minutes_interval>: 1
  skip_delayed_by: 3m

+setup:
  sh>: echo ${session_time}

スケジュール停止の時
スクリーンショット 2020-07-12 13.16.51.png

スケジュール再開の時(17分に再開)
スクリーンショット 2020-07-12 13.17.38.png
再開時間の3分前の14分、15分、16分は再実行される、3分以上過ぎたセッションはSkipしている。

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

ActionTextのデータ構成はどうなってる?

個人的にActionTextを導入する機会があったのでメモ。
blogサービスのようなものを開発する際に、ArticleモデルにどのようにActionTextを導入すればいいのか悩んだので、誰かの役に立てればと思います。

ActionTextとは

rails6から使えるようになった機能の1つ。
Railsにリッチテキストコンテンツと編集機能を導入できる。

今回やりたいこと

記事投稿サービスを作りたいと思います。
タイトル、説明文、本文の3つのデータをもつArticleモデルを作成し、本文の編集にActionTextを導入します。

データ構成

ActionTextを導入するモデル作成

Articleモデルを生成します。
titleは記事のタイトル、descriptionは記事の説明文を表します。本文のデータを格納するカラムは作成しないことに注意してください。

rails g model Article title:string description:text

ActionTextを導入する

ActionTextは以下のコマンドでインストールできます。

rails action_text:install

ActionTextをインストールすると、2つのmigrationファイルが生成されます。
1つはActive Storageに関わるファイルで、もう一つがActionTextのテーブルを作成するファイルです。
後者のテーブルがこの記事で説明したいデータ構成の鍵となります。

リッチテキストコンテンツは独自のRichTextモデルに保存され、このモデルはアプリケーションの既存のあらゆるActive Recordモデルと関連付けられます。 あらゆる埋め込み画像(およびその他の添付ファイル)は自動的にActive Storageに保存され、includeされたRichTextモデルに関連付けられます。

https://railsguides.jp/action_text_overview.html#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB

class CreateActionTextTables < ActiveRecord::Migration[5.2]
  def change
    create_table :action_text_rich_texts do |t|
      t.string     :name, null: false
      t.text       :body, limit: 16777215
      t.references :record, null: false, polymorphic: true, index: false

      t.datetime :created_at, null: false
      t.datetime :updated_at, null: false

      t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
    end
  end
end

上記のデータを、Articleモデルと紐づけることで、記事の内容をリッチテキストで編集することが可能になります。
データの紐付けは簡単で、以下のようにモデルに追加してください。

article.rb
class Article < ApplicationRecord
  has_rich_text :content
end

どのようなデータ構造になっているのか

上記で説明した通り、Articleテーブルでtitledescriptionを保持し、記事の内容(content)は、ActionText::RichTextというテーブルに保持されています。以下のコンソールの結果を見るとイメージをしやすいと思います。

# Articleモデルのデータを取得
irb(main):001:0> Article.all.first
=> #<Article id: 1, created_at: "2020-07-11 13:19:00", updated_at: "2020-07-11 13:19:00", title: "テスト", description: "テストです">

# Articleモデルに紐づいているActionText::RichTextのデータを取得
irb(main):002:0> Article.all.first.content
=> #<ActionText::RichText id: 1, name: "content", body: #<ActionText::Content "<div class=\"trix-conte...">, record_type: "Article", record_id: 1, created_at: "2020-07-11 13:19:00", updated_at: "2020-07-11 13:19:00">

# 記事の内容を取得するには?
irb(main):003:0> Article.all.first.content.body
=> #<ActionText::Content "<div class=\"trix-conte...">

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

Action Textのデータ構成について

個人的にActionTextを導入する機会があったのでメモ。
blogサービスのようなものを開発する際に、ArticleモデルにどのようにActionTextを導入すればいいのか悩んだので、誰かの役に立てればと思います。

ActionTextとは

rails6から使えるようになった機能の1つ。
Railsにリッチテキストコンテンツと編集機能を導入できる。

データ構成

ActionTextを導入するモデル作成

Articleモデルを生成します。
titleは記事のタイトル、descriptionは記事の説明文を表します。

rails g model Article title:string description:text

ActionTextを導入する

ActionTextは以下のコマンドでインストールできます。

rails action_text:install

ActionTextをインストールすると、2つのmigrationファイルが生成されます。1つはActive Storageに関わるファイルで、もう一つがActionTextのテーブルを作成するファイルです。後者のテーブルがこの記事ではターゲットとなります。ActionTextで編集された内容は、このテーブルに格納されるからです。

class CreateActionTextTables < ActiveRecord::Migration[5.2]
  def change
    create_table :action_text_rich_texts do |t|
      t.string     :name, null: false
      t.text       :body, limit: 16777215
      t.references :record, null: false, polymorphic: true, index: false

      t.datetime :created_at, null: false
      t.datetime :updated_at, null: false

      t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
    end
  end
end

上記のデータを、Articleモデルと紐づけることで、記事の内容をリッチテキストで編集することが可能になります。
データの紐付けは簡単で、以下のようにモデルに追加してください。

article.rb
class Article < ApplicationRecord
  has_rich_text :content
end

どのようなデータ構造になっているのか

上記で説明した通り、Articleテーブルでtitledescriptionを保持し、記事の内容(content)は、ActionText::RichTextというテーブルに保持されています。以下のコンソールの結果を見るとイメージをしやすいと思います。

# Articleモデルのデータを取得
irb(main):001:0> Article.all.first
=> #<Article id: 1, created_at: "2020-07-11 13:19:00", updated_at: "2020-07-11 13:19:00", title: "テスト", description: "テストです">

# Articleモデルに紐づいているActionText::RichTextのデータを取得
irb(main):002:0> Article.all.first.content
=> #<ActionText::RichText id: 1, name: "content", body: #<ActionText::Content "<div class=\"trix-conte...">, record_type: "Article", record_id: 1, created_at: "2020-07-11 13:19:00", updated_at: "2020-07-11 13:19:00">

# 記事の内容を取得するには?
irb(main):003:0> Article.all.first.content.body
=> #<ActionText::Content "<div class=\"trix-conte...">

結論

ActionTextは本当に簡単に導入できますが、データの仕組みが少し複雑なので注意してください。
また、私自身railsを勉強し始めて半年の未熟者ですので、間違いや不適切な表現があればご指摘ください。

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

Rails5でECサイトを作る⑧ ~Productモデル編、親子ともに条件つきで絞り込み表示~

はじめに

架空のベーカリーで買い物できるECサイトを作るシリーズ、Rails5でECサイトを作る⑦の続きです。
今回は商品の一覧、詳細を表示するProductモデルの実装です。商品の管理はadminサイト側で行うので、customerサイト側では画面を表示する処理のみになります。

ただし、商品を束ねるGenreモデル、その下に属するProductモデルの両方に「販売中・停止」のステータスが存在します。商品ごとの販売中・停止を設定するほかに、ジャンルごとでも同様の設定を行えるということです。customerサイトで表示するのはGenre、Product両方で「有効」のものだけなので、その絞り込みが少々難しめです。

ソースコード

https://github.com/Sn16799/bakeryFUMIZUKI

Modelのアソシエーション

association.jpg

controller

app/controllers/products_controller.rb
class ProductsController < ApplicationController

  # サイドバー表示用
  before_action :set_genres

  def show
    @product = Product.find(params[:id])
    @cart = @product.cart_items.build
  end

  def index
    # ジャンルが有効 かつ 商品ステータスが有効 な商品を絞り込み
    @products = Product.includes(:genre).where(genres: {validity: true}).is_active.page(params[:page]).per(9)
  end

  private

  def set_genres
    @genre = Genre.is_valid
  end

  def product_params
    params.require(:product).permit(:name,:price,:image_id, :genre_id)
  end
end

indexにおいて、Genreモデル(親)のvalidityカラムと、Productモデル(子)のstatusカラムに条件を設定して絞り込みをかけています。これで、「ジャンルが有効」かつ「商品ステータスが有効」の商品群を取り出すことができました。また、ページャ(kaminari)を用意して、1ページにつき9個の商品が表示されるようにしています。

app/models/product.rb
scope :is_active, -> { where(status: true) }
app/models/genre.rb
scope :is_valid, -> { where(validity: true) }

controller内にあるis_activeとis_validは、いずれも商品、ジャンルの有効・無効を絞り込むスコープです。今回のアプリでは何回も行う処理ではないので別に必須ではないのですが、スコープを定義しておくことで後から修正しやすくなったり、コードソース上で何の処理をしているのか、視覚的に分かりやすくなったりします。

view

index画面

app/views/products/index.html.erb
<div class="col-lg-10 space">
  <div class="container">
  <h2>
    <span style="display: inline-block;">全商品一覧</span>
    <span style="display: inline-block;"><%= @products.count %>種)</span>
  </h2>
</div>
  <div class="container">
    <div class="row">
    <% @products.each do |gp| %>
      <%= render 'products/box', product: gp %>
    <% end %>
  </div>
  </div>
  <%= paginate @products %>
</div>
<%= render 'genres/index', genres: @genres %>

部分テンプレートは、'products/box'が商品1個ごとに写真・名前・値段などが載ったち小さなdivで、'genres/index'はジャンルごとに商品を見られるリンクを集めたサイドバーです。HTMLソースは前回の記事で公開しています。

show画面

app/views/products/show.html.erb
<div class="col-lg-10 space">
  <div class="container">
    <div class="row">
      <div class="col-lg-4 offset-lg-2">
        <%= attachment_image_tag(@product, :image, :fill, 560, 420, fallback: "no_img.jpg") %>
      </div>
      <div class="col-lg-4">
        <h3>
          <%= @product.name %>
        </h3>
        <h4>商品説明</h4>
        <%= @product.introduction %>
        <h4>
          <%= price_include_tax(@product.price) %>
        </h4>
      </div>
    </div>
  </div>

  <div class="container">
    <div class="w-50 offset-lg-6">
    <%= form_with(model: [@product, @cart], local: true, url: {controller: 'cart_items', action: 'create'}) do |f| %>
    <%= f.label :ご購入個数 %>
    <%= f.number_field :quantity, value: 1 ,min:1, max:99 %><%= f.hidden_field :product_id, value: @product.id  %>
    <%= f.submit "カートに入れる", class: "btn btn-danger" %>
    <% end %>
  </div>
  </div>
</div>

<%= render'genres/index', genres: @genres %>

show画面では、商品の説明を見て、気に入ったものをカートに入れることができます。

空欄のフォームを表示するようにはなっていますが、CartItemControllerに何も書いていないため、現時点で「カートに入れる」ボタンを押すとエラーになります。ご注意を。

後記

商品がたくさん並んだ画面を作ったことで、見た目にはパン屋さんのサイトになりました。相変わらず機能面は皆無ですが。また、ページャや商品のボックスにCSSの加工を一切施しておらず、少し殺風景な感じは拭えません。装飾は最後にまとめて行う予定なので、まずは機能の完成を急ぎたいところ。

このECサイトの原型となっているのはスクールのチーム実装で作ったアプリなのですが、今見るとバリデーションなど意外と抜け落ちているところがあります。3人がかりで取り組んでも見逃す部分はあるのだなあと思いましたが、もしかしたら単にそこまで細かく要件定義された課題ではなかっただけかも知れません。

今回は2回目の開発なので、その辺りもこだわって実装していきたいと思います。次回へ続く!

参考

Rails小モデルから親モデルの情報で検索したい

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

Ruby で 繰返し二乗法 を解くなら Integer#pow がお薦め

はじめに

AtCoder Problems の Recommendation を利用して、過去の問題を解いています。
AtCoder さん、AtCoder Problems さん、ありがとうございます。

事の始まり

Ruby と Perl と Java と Python で解く AtCoder ATC 002 B 繰返し二乗法 でコメントをいただきました。

Ruby だと専用メソッド Integer#pow がありますね。

早速試しました。

AtCoder Typical Contest 002 B - n^p mod m

pow.rb
n, m, p = gets.split.map(&:to_i)
puts n.pow(p, m)
zisaku.rb
n, m, p = gets.split.map(&:to_i)
def mpow(n, p, mod)
  r = 1
  while p > 0
    r = r * n % mod if p % 2 == 1
    n = n * n % mod
    p >>= 1
  end
  r
end
puts mpow(n, p, m)
Integer#pow 自作mpow
コード長 (Byte) 50 183
実行時間 (ms) 68 62
メモリ (KB) 14356 14356

AtCoder Regular Contest 066 C - Lining Up

pow.rb
n = gets.to_i
a = gets.split.map(&:to_i)
h = Hash.new(0)
a.each do |x|
  h[x] += 1
end
f = true
h.each do |k, v|
  if n.odd? && k == 0
    if v != 1
      f = false
      break
    end
  elsif v != 2
    f = false
    break
  end
end
MOD = 1_000_000_007
puts (f ? 2.pow(n / 2, MOD) : 0)
Integer#pow 自作mpow
コード長 (Byte) 305 438
実行時間 (ms) 101 100
メモリ (KB) 22676 22488

エイシング プログラミング コンテスト 2020

pow.rb
n = gets.to_i
x = gets.chomp
xs = x.to_i(2)
xc = x.count('1')
def mpow(n, p, mod)
  return 0 if mod.zero?
  n.pow(p, mod)
end
def popcount(u)
  return 0 if u.zero?
  a = u % u.to_s(2).count('1')
  1 + popcount(a)
end
xsp = mpow(xs, 1, xc + 1)
xsm = mpow(xs, 1, xc - 1)
n.times do |i|
  if x[i] == '0'
    y = xsp + mpow(2, n - i - 1, xc + 1)
    y = mpow(y, 1, xc + 1)
  elsif xc == 1
    puts '0'
    next
  else
    y = xsm - mpow(2, n - i - 1, xc - 1)
    y = mpow(y, 1, xc - 1)
  end
  puts popcount(y) + 1
end
Integer#pow 自作mpow
コード長 (Byte) 541 629
実行時間 (ms) 421 625
メモリ (KB) 14704 15392

いずれも同等以上の成績でした。
rubyの時代が来ましたね。

まとめ

  • Integer#pow を使おう
  • Ruby の時代が来た

参照したサイト
instance method Integer#**
Ruby と Perl と Java と Python で解く AtCoder ATC 002 B 繰返し二乗法
Ruby と Python で解く AtCoder ARC 059 C 最小二乗法
Ruby と Python で解く AtCoder AISING2020 D 繰返し二乗法

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

[WIP] Ruby Tips

a

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

VagrantでRuby on Railsの開発環境を構築する手順

環境

macOS 10.15.4

Vagrantをインストール

Vagrantをホームページからインストールします。

スクリーンショット 2020-07-12 16.09.53.png
https://www.vagrantup.com

Cent OSをインストール

まずはVagrantがインストールできているか確認をします。

terminal
$ vagrant -v
Vagrant 2.2.9

ディレクトリを作成し、移動します。

terminal
$ mkdir -p vagrant/centos7
$ cd vagrant
$ cd centos7

移動したディレクトリで下記のコマンドを実行します。

terminal
$ vagrant init centos/7

インストール完了後、エディタを開き下記の1行をコメントアウトから外します。

Vagrantfile
# config.vm.network "private_network", ip: "192.168.33.10"

スクリーンショット 2020-07-12 16.22.25.png

下記のコマンドを実行し、少し待ちます...

terminal
$ vagrant up

CentOSにログイン

下記のコマンド実行しログインします。

terminal
$ vagrant ssh
[vagrant@localhost ~]$  

rbenv をインストール

yumパッケージを準備します。

terminal
$ sudo yum install -y git gcc openssl-devel readline-devel zlib-devel sqlite-devel gcc-c++ libicu-devel cmake vim

rbenvをインストールします。

terminal
$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv    
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile

インストールが完了しているかどうか確認をします。

terminal
$ rbenv --version
rbenv 1.1.2-30-gc879cb0

ruby-buildプラグインを追加

下記のコマンドを実行しruby-buildプラグインを追加します。

terminal
$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build

Rubyをインストール

Rubyをインストールします。

※余談ですがわたしが他者様のサイトを参照にしたときにバージョンが2.4でした。しかしRails5以上と現在主流のRailsのバージョンには2.5以上が必要になりますので、バージョン指定などに気をつけてください。

terminal
$ rbenv install 2.7.1
$ rbenv global 2.7.1
$ rbenv rehash
$ ruby -v 

インストールの完了が確認できましたら次に進みます。

Bundlerをインストール

Bundlerのインストールを行うため下記のコマンドを実行します。

terminal
$ gem install bundler
$ rbenv rehash
$ bundle -v
Bundler version 2.1.4

Ruby on Railsをインストール

インストールするためのディレクトリの作成と移動を行います。

terminal
$ mkdir -p app/memo_app
$ cd app
$ cd memo_app

そしてインストールするためのコマンドを実行します。

terminal
$ bundle init
$ sed -i 's/# gem "rails"/gem "rails", "~> 5.1.0"/g' Gemfile
$ bundle install --path vendor/bundler
$ bundle exec rails new .
$ sed -i "s/# gem 'therubyracer'/gem 'therubyracer'/g" Gemfile
$ bundle install

サーバーを起動

下記のコマンドを実行し下記のアドレスにアクセスしてください。

terminal
$ ./bin/rails s -b 0.0.0.0

http://192.168.33.10:3000/

スクリーンショット 2020-07-12 12.29.09.png

これで手順は終了になります^_^

(補足)シャットダウンとログイン/ログアウトについて

一度閉じてしまったときや再度開くときに必要になると思いますのでシャットダウンとログイン/ログアウトについても説明します。

ログアウト

terminal
# CentOSからログアウトする場合のコマンド
$ exit

#CentOSをシャットダウンする場合のコマンド
$ vagrant halt

ログイン

terminal
# Cent OSにログインする場合のコマンド
$ vagrant up

$ vagrant ssh
[vagrant@localhost ~]$

Vagrantの開発環境でも高機能エディタ(VSCode)でアプリを作成できるようにする手順

Vagrantの仮想環境が構築されましたがこれを高機能エディタ(VSCode)でも作成や編集が行えるように接続する方法を解説していますので合わせてご覧ください^_^

https://qiita.com/chisaki0606/items/982f296389ffcb850994

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

hamlでYouTube動画をDBから呼び出して埋め込み表示させてみた

自作アプリでYouTubeの動画を埋め込み表示させてみた時に見付けた方法

自作アプリの作成でお気に入りの動画を一旦DBに保存して、呼び出す際に埋め込み表示させたかったので、色々模索したやり方を紹介します。

一般的な埋め込みの方法

好きなYouTube動画の共有ってところを選ぶ

スクリーンショット 2020-07-12 16.16.00.png

さらにそこから埋め込みってやつを選ぶ
スクリーンショット 2020-07-12 16.16.33.png

動画の埋め込み用のURLが出るので、コピーをクリックする
スクリーンショット 2020-07-12 16.16.59.png

これをHTML(haml)に直接貼り付ければ完成

出来上がりがこんな感じ
スクリーンショット 2020-07-12 16.21.46.png

ただ、これでDBに投稿して、hamlファイルから呼び出そうとすると文字列で出てしまったので、それを解消する方法です。

実際に行ったやり方

%iframe#player{frameborder: "0", height:"390", src: (movie.url), type: "text/html", width: "640"}
※今回はMovieモデルと言うのを作って、カラム名をurlにしてます

簡単に言うと、
・外枠の部分はhamlファイルにしておく(サイズ等はお好みで変えられるはずです)
・DBにはURL部分のみ保存→呼び出す記述にしてます
・タイトルや内容なども別のカラムで保存

こうする事で、DBに投稿→呼び出しの際も埋め込み表示に出来ました。

実際の画像(まだ開発中ですが)

スクリーンショット 2020-07-12 16.36.11.png
↑無事に全部投稿機能で実装が出来ました(パチパチ)

1つ注意ポイント

私が使ったURLですが、この青下線の部分です(ダブルクォーテーションは要らないです)
スクリーンショット 2020-07-12 16.38.35.png

スクリーンショット 2020-07-12 16.16.33.png

こっちの青下線のURLではダメでした。
おそらく埋め込み用のURLでは無いからだと思います。

以上、同じ様な実装をされたい方の参考になれば幸いです

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

Rubyで値オブジェクトの親クラスを作ってみた

ドメイン駆動設計にかぶれて値オブジェクトを作ってみたんですが、結構面倒くさかったんで、親クラスを作ってみました。
(あくまでも個人開発で開発スピードを早めるために作っていますので、製品でそのまま使えるかは保証しません)

値オブジェクトとは

https://qiita.com/kichion/items/151c6747f2f1a14305cc
に書いてあるような特徴を持つオブジェクトです。

Rubyで書いてみると↓みたいなオブジェクトになります。この例はユーザの名前を入れる値オブジェクトです。
(正統な定義をするならequal的なものを定義したほうが良いんでしょうが、あまり使わないので今は定義していません。)

domain/user/name.rb
module Domain::User
  class Name
    attr_reader :first_name, :last_name

    def initialize(first_name, last_name)
      @first_name = first_name
      @last_name = last_name
    end

    def update_first_name(first_name)
      self.new(first_name, @last_name)
    end

    def update_last_name(last_name)
      self.new(@first_name, last_name)
    end

    def fullname
      "#{@last_name} #{@first_name}"
    end
  end
end

使うのには便利なんですが、いちいちこのお決まりのフィールドのゲッター、セッターの何行かを書くのが面倒くさいんですよね。
なので、これらを勝手に定義してくれる親クラスを作ってみました。

値オブジェクトの親クラス

domain/base/value_object.rb
module Domain::Base
  class ValueObject
    attr_reader :changed_fields

    FIELDS = []

    class << self
      # 次のようなメソッドを定義 new_date = date.update_month(11)
      def attr_updater(*attrs)
        attrs.map(&:to_sym).each do |defining_attr|

          define_method("update_#{defining_attr}") do |defining_value|
            values = attrs.map { |attribute| attribute == defining_attr ? defining_value : instance_variable_get("@#{attribute}")}
            changed_fields = @changed_fields.include?(defining_attr) ? @changed_fields : @changed_fields + [defining_attr]
            self.class.new(*values, changed_fields: changed_fields)
          end

        end
      end
    end

    # NOTE
    # - 基本的に値オブジェクト側ではFIELDSとアクセサだけ定義させる
    # - initializeは親のこのクラスの物を使わせる。
    # - initializeの独自実装をするのは可能。しかしその場合、値オブジェクト側からこの親クラスのinitializeを使うだけで、値オブジェクト使用者にはこのクラスのinitializeを直接は使わせない
    #   - changed_fieldsなどの変更は内部でのみ行う
    #   - 値オブジェクト自体の初期化引数はできるだけ単純になるようにケア
    def initialize(*field_values, fields: self.class::FIELDS, changed_fields: [])
      define_fields(fields, field_values)
      @changed_fields = changed_fields
    end

    private

    def define_fields(fields, field_values)
      fields.zip(field_values).each do |field, field_value|
        instance_variable_set("@#{field}", field_value)
      end
    end
  end
end

これを使うと以下の書き方で、元々のメソッドが提供できます。

domain/user/name.rb
module Domain::User
  class Name < ::Domain::Base::ValueObject
    FIELDS = %I(first_name last_name)
    attr_reader *FIELDS
    attr_updater*FIELDS

    def fullname
      "#{@last_name} #{@first_name}"
    end
  end
end

備考

  • メタプログラミングの黒魔術を使いすぎると、追えなくなる(grepでも目視でも)ので、複雑なメソッドは定義していません
  • attr_readerとかも自動化できるけど、attr_readerとかがあると読み手に負荷をかけないので、基本のRubyの書き方ができるところは踏襲しています。
  • この書き方だと、initialize時にFILEDどおりの順番で引数を入れることを暗黙に示していており、それを明示していないんですがが、自分ではその問題をわかっているので、今の所運用で回避しています。
  • changed_fieldsはDB更新時に更新があったフィールドだけ更新したかったので好みで作りました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] enumを使って下書き機能を作った

はじめに

下書き機能を作ろうと思った時にあまり記事がなかったので記録として残しておこうと思いました。

開発環境

ubuntu(WSL)
Ruby 2,5.1
Rails 6.0.2

事前準備

以下の機能は作成済みとします。

  • 投稿機能・詳細・削除・編集(Post)
  • ユーザーの作成・詳細(User)
models/post.rb
  belongs_to :user
models/user.rb
  has_many :microposts, dependent: :destroy

Postテーブルにカラムを追加する

まずはPostモデルにstatusカラムを追加しboolean型にします。
booleanでなくintergerでも可能です。
以下のコマンドを入力します。

bin/rails g migration AddStatusToPost status:boolean

マイグレーションファイルを編集します。

migrationfile.rb
  def change
    add_column :microposts, :status, :boolean, default: true, null: false
  end

編集したらマイグレートします。

Postモデルにenumを追加する。

models/post.rb
  enum status: { draft: false, published: true }

statusカラムのdraft(下書き)をfalseに指定し、statusカラムのpublished(公開)をtrueに指定します

ユーザーごとの情報を取得

@userでuserのidを取得。

users_controller.rb
  #下書き用
  def confirm
    @user = User.find(params[:user_id])
    @microposts = @user.microposts.draft.page(params[:page])
  end

  #公開用
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.published.page(params[:page])
  end

ルーティングを追加

collection をつけるとURLにidが付かなくなります

routes.rb
resources :users do
  get 'confirm'
end

Viewの設定

関係あるところだけを抜粋しました。

view/users/show.html.slim
//ユーザーの詳細画面
= link_to "投稿一覧", @user
= link_to  "お気に入り", user_likes_path(current_user)
= link_to "下書き一覧", user_confirm_path(current_user)

//自分の投稿を表示する
- if @microposts.present?
  = render "microposts/list", microposts: @microposts
- else
  h4 投稿はありません

下書き一覧は自分の好みに合わせて書いてください。

view/users/confirm.html.slim
h4 下書き一覧

table.table.table-hover
  thead.thead-default
    tr
      th = Micropost.human_attribute_name(:title)
      th = Micropost.human_attribute_name(:content)
      th = Micropost.human_attribute_name(:created_at)
      th
  tbody
    - @microposts.each do |micropost|
      tr
        td = link_to micropost.title, micropost
        td = link_to micropost.content, micropost
        td

icroposts/listの中身は上記と同じです。

終わりに

間違いがありましたら編集リクエスト、コメントをお願いします。

参考文献

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

投稿ページのビュー画面を直す

アウトプット用です。
今回はこちら18c7038dfaec3788b2be3c423fd05bc2-2.png

投稿はできているが、何故はみ出るんだ?
インデントも問題無い

top_page>index.html.haml
.top
  %ul.top__side
    ジャンル
    %li.top__side__love
      恋愛
    %li.top__side__work
      仕事
    %li.top__side__money
      お金
    %li.top__side__friend
      友人
    %li.top__side__school
      学校
    %li.top__side__other
      その他
  .top__consultation
    .top__consultation__title
      お悩み一覧
    .top__consultation__text
    - @toppages.each do |top_page|
      = top_page.contents
      .top__consultation__text__time
        = top_page.created_at

色々試行錯誤した結果each文の後が元の定義したクラスの中に収まらないことが判明しました。
これ、何ででしょうかね??

ということでそれならとeach文を一番最初につけました

top_page>index.html.haml
- @toppages.each do |top_page|
  .top
    %ul.top__side
      ジャンル
      %li.top__side__love
        恋愛
      %li.top__side__work
        仕事
      %li.top__side__money
        お金
      %li.top__side__friend
        友人
      %li.top__side__school
        学校
      %li.top__side__other
        その他
    .top__consultation
      .top__consultation__title
        お悩み一覧
      .top__consultation__text
        = top_page.contents
        .top__consultation__text__time
          = top_page.created_at

割愛してますが、インデントを最初の"top"と合わせたらSyntaxError
エラーになったので地味に気をつけたいところ。全て1個インデントを下げる必要があります(当たり前だけど)

adb62a3f262ca3cb8d40312aba48b4f8.png

ということで解決です。
ただ何故はみ出てしまうのか原因はわかりましたが、言語化できません。
htmlのルールと言ってしまえば終わりな気はしますが。

訂正
修正したコードで複数投稿したら一番上にeach分があるためにお悩み一覧や左のカテゴリまで
全て表示されていました汗
正しくは下記ですね

top_page>index.html.haml
.top
  %ul.top__side
    ジャンル
    %li.top__side__love
      恋愛
    %li.top__side__work
      仕事
    %li.top__side__money
      お金
    %li.top__side__friend
      友人
    %li.top__side__school
      学校
    %li.top__side__other
      その他
  .top__consultation
    .top__consultation__title
      お悩み一覧
    - @toppages.each do |top_page|
      .top__consultation__text
        = top_page.contents
        .top__consultation__text__time
          = top_page.created_at

ただ要領は変わらないですね。インデント一個下げただけであっさり解決しました笑

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

フロント側でAPIからのSet-Cookieを無効にする方法

背景

deviseを使って作成したRails製Webアプリのモバイルアプリ版を作成するため、devise-token-auth でトークン認証出来るようにしようとしたところCookieが効いて別のユーザー情報が返ってきてしまう。

やっていたこと

devise-token-authで作成したエンドポイントをフロントから叩いてチェックしていました。
直接curlで叩いたら上手くいくのに....

解決策

import { NativeModules } from 'react-native';

NativeModules.Networking.clearCookies(() => { });

参考記事そのままですが、フロント(ReactNative)側でこのようにするとAPIから送られたCookieがフロント側でクリアされ期待した通りの動作をするようになりました。

今回はフロント側で対応しましたが、バックエンド側でdevise-token-authのcontrollerをオーバーライドして対応することも出来るようです。

なぜCookieが保存されていたか

ネイティブアプリではデフォルトでCookieが保存されるようです。
ちなみに、RailsのAPIモードではデフォルトでCookieが使えないようになっているものの、今回は既存のWebアプリにdevise-token-authでトークン認証をつけようとしていたためCookieが渡されるようになっていました。

参考記事

https://qiita.com/kakken1988/items/504e679504b9e710cf21

ちなみに

Memopicというアルバム・写真共有サービスを作っています。是非使ってみてください!
ご意見・ご要望などあればこちら(memopic.bamboo@gmail.com)までお願いします。

ここだけでなくdevise-token-authを導入する時もdeviseとの共存させるのに少しハマったので、そこについての記事も近々書こうと思ってます〜

ご指摘、ご意見お待ちしてます!

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

Railsで表示時間を日本時間に設定する方法

やりたいこと

投稿時間を国際標準時間から日本時間の表示に変更したい

変更前(GMT標準時間)▼
Screenshot from Gyazo
変更後(日本時間)▼
Screenshot from Gyazo

設定の方法

  • config/application.rb 内に config.time_zone = 'Tokyo' の記述を追加
application.rb
# ↑これ以前のコードは割愛
module App
  class Application < Rails::Application
    config.time_zone = 'Tokyo'
  end
end
  • サーバーが立ち上がっている場合は一度シャットアウトして再度立ち上げ直す
    ※dockerを起動している場合は、下記でコンテナを立ち上げ直す(ターミナルで下記コマンドを実行)
    docker-compose stop
    docker-compose up -d

これで日本時間で表示されるが、フォーマットを別途変更する必要あり

  • strftimeメソッド を使って、表示フォーマットを変えたい箇所にメソッドを当てる
sample.html.erb
# 下記はサンプル
<td><%= @tweet.created_at.strftime('%Y年%m月%d日 %H時%M分') %></td>

変換のメソッドを定義する方法

やること:Initializeにフォーマット変換を定義する

  • config/initializers配下time_formats.rbというファイルを作成する
  • Time::DATE_FORMATS[:datetime_jp] = '%Y年%m月%d日 %H時%M分' と記述する
time_formats.rb
Time::DATE_FORMATS[:datetime_jp] = '%Y年%m月%d日 %H時%M分'

※ [:datetime_jp] は任意の命名でOK
※ '%Y年%m月%d日 %H時%M分' には変換したいフォーマットを記述する

  • 使いたい箇所(viewファイル内)で .to_s(:datetime_jp]) と記述して使う
sample.html.erb
# 下記はサンプル
<td><%= @tweet.created_at.to_s(:datetime_jp) %></td>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】プロフィールに自己紹介機能を実装するシンプルな方法

プロフィールに自己紹介機能を実装する

ポートフォリオ完成後、追加実装したので順序を備忘録として残します。
ユーザー名・email・パスワードなどの既存カラムに、introductionカラムを新たに追加します。
なるべくシンプルに記します!

どうやって?

以下の順序です。

  1. git checkout -b introduction (ブランチ切る)
  2. usersテーブルにintroductionカラムを追加する
  3. データベースに反映させる
  4. usersコントローラーに必要コードの追記
  5. userモデルに必要コードの追記
  6. ユーザー詳細ページ(MYPAGE)に表示させる
  7. ユーザー編集ページ(EDIT)に表示させる
  8. 完成!

以下順番に記します!

1. git checkout -b introduction (ブランチ切る)

新たな機能を実装するため、ブランチを切リます。

2. usersテーブルにintroductionカラムを追加する

ターミナル
$ rails generate migration AddIntroductionToUsers introduction:text
      invoke  active_record
      create    db/migrate/20200712005652_add_introduction_to_users.rb

上記rails gコマンドでintroductionカラムを追加します。

3. データベースに反映させる

ターミナル
$ docker-compose run  web rails db:migrate
== 2020~~~~ AddIntroductionToUsers: migrating ===========================
-- add_column(:users, :introduction, :text)
   -> 0.0518s
== 2020~~~~ AddIntroductionToUsers: migrated (0.0519s) ==================

4. usersコントローラーに必要コードの追記

private methodに追記
introduction属性を付与する

users_controller.rb
   def user_params_update
      params.require(:user).permit(:name, :email, :image, :introduction) # introdution追加
   end

これでintroductionをupdate可能になります

5. userモデルに必要コードの追記

バリデーションを追加します。自己紹介は50文字以内で入力させます。
文字数は自由に設定してください。
※presenseですが、falseにしないと新規登録時に作用してintroductionがnilとなって新規登録できなくなりますので注意してください。新規登録時に自己紹介も入力必要な場合とするならtrueでOKです。

user.rb
validates :introduction, presence: false, length: { maximum: 50 } # 自己紹介の最高文字数は50文字

6. ユーザー詳細ページ(MYPAGE)に表示させる

show.html.slim
 = @user.introduction

SCSSなどで適宜修正してください

7. ユーザー編集ページ(EDIT)に表示させる

ユーザー編集はユーザー名・emailのみでしたがそこにintroductionを追記します。

edit.html.slim
.form-group
  = f.label :introduction
  = f.text_area :introduction, class: 'form-control', id: 'user_introduction'

完成

以上で自己紹介をusersテーブルに追加し、introduction属性を追加完了しました。
途中でintroductionをintroduceと書いていたりしたので、皆様も注意してください!

スクリーンショット 2020-07-12 13.33.19.png

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

Ruby と Python で解く AtCoder AISING2020 D 繰返し二乗法

はじめに

エイシング プログラミング コンテスト 2020 に参加しました。
AtCoder さん、AtCoder Problems さん、ありがとうございます。

今回のお題

AtCoder エイシング プログラミング コンテスト 2020 D - Anything Goes to Zero
Difficulty: 1294

今回のテーマ、繰返し二乗法

0 1 1 0 1 10進数表記
元の数値 $$0*2^4$$ $$1*2^3$$ $$1*2^2$$ $$0*2^1$$ $$1*2^0$$ 13
0桁目ビット反転 $$1*2^4$$ 13+16=29
1桁目ビット反転 $$0*2^3$$ 13- 8= 5
2桁目ビット反転 $$0*2^2$$ 13- 4= 9
3桁目ビット反転 $$1*2^1$$ 13+ 2=15
4桁目ビット反転 $$0*2^0$$ 13- 1=12

与えられた数値を01101としますと、上の表の様に分解できます。
また、分配則として下記が成り立ちますので、
$$(a+b)\%m=(a\%m+b\%m)\%m$$
ビット反転した桁の数値を出し入れすることにより、高速に全体の数値を求めることができます。

Ruby

ruby.rb
n = gets.to_i
x = gets.chomp
xs = x.to_i(2)
xc = x.count('1')
def mpow(n, p, mod)
  return 0 if mod.zero?
  r = 1
  while p > 0
    r = r * n % mod if p & 1 == 1
    n = n * n % mod
    p >>= 1
  end
  r
end
def popcount(u)
  return 0 if u.zero?
  a = u % u.to_s(2).count('1')
  1 + popcount(a)
end
xsp = mpow(xs, 1, xc + 1)
xsm = mpow(xs, 1, xc - 1)
n.times do |i|
  if x[i] == '0'
    y = xsp + mpow(2, n - i - 1, xc + 1)
    y = mpow(y, 1, xc + 1)
  elsif xc == 1
    puts '0'
    next
  else
    y = xsm - mpow(2, n - i - 1, xc - 1)
    y = mpow(y, 1, xc - 1)
  end
  puts popcount(y) + 1
end

以前投稿しました Ruby と Perl と Java と Python で解く AtCoder ATC 002 Bmpowメソッドコピペ使用します。

mpow.rb
def mpow(n, p, mod)
  return 0 if mod.zero?
  r = 1
  while p > 0
    r = r * n % mod if p & 1 == 1
    n = n * n % mod
    p >>= 1
  end
  r
end

但し、今回は除数が0になる可能性がありますので、それに対応しています。

inout.rb
n.times do |i|
  if x[i] == '0'
    y = xsp + mpow(2, n - i - 1, xc + 1)
    y = mpow(y, 1, xc + 1)
  elsif xc == 1
    puts '0'
    next
  else
    y = xsm - mpow(2, n - i - 1, xc - 1)
    y = mpow(y, 1, xc - 1)
  end
  puts popcount(y) + 1
end

ここで、ビット反転した桁の数値の出し入れを行っています。
ここでも、除数が0の場合の処理が必要です。

popcount.rb
def popcount(u)
  return 0 if u.zero?
  a = u % u.to_s(2).count('1')
  1 + popcount(a)
end

2回目以降のpopcountを再帰計算しています。
ここをメモ化するともう少し速くなります。

Python

python.py
from sys import stdin

def mpow(n, p, mod):
    if mod == 0:
        return 0
    r = 1
    while p > 0:
        if p & 1 == 1:
            r = r * n % mod
        n = n * n % mod
        p >>= 1
    return r
def popcount(u):
    if u == 0:
        return 0
    a = u % bin(u).count('1')
    return 1 + popcount(a)
def main():
    input = stdin.readline
    n = int(input())
    x = '0b' + input()
    xs = int(x, 0)
    xc = x.count('1')
    xsp = mpow(xs, 1, xc + 1)
    xsm = mpow(xs, 1, xc - 1)
    for i in range(2, n + 2):
        if x[i] == '0':
            y = xsp + mpow(2, n - i + 1, xc + 1)
            y = mpow(y, 1, xc + 1)
        elif xc == 1:
            print(0)
            continue
        else:
            y = xsm - mpow(2, n - i + 1, xc - 1)
            y = mpow(y, 1, xc - 1)
        print(popcount(y) + 1)
main()
Ruby Python
コード長 (Byte) 629 872
実行時間 (ms) 625 1048
メモリ (KB) 15392 9636

まとめ

  • AISING2020 D を解いた
  • Ruby に詳しくなった
  • Python に詳しくなった

参照したサイト
pythonで2進数を表す

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

undefined method `user_signed_in?'と出たときの対処法

日々学んだことやつまずいたことについてまとめていきます。
記載に誤りがありましたら、ご指摘していただけると助かります!
いつも他のかたの記事に助けられているので、少しでもお役に立てればと思います。

どういうエラーなのか

deviseで使えるようになる、 user_signed_in ヘルパーメソッドが定義されていないというエラー。

どんなときに起こるのか

1.deviseがインストールされていないとき。
2.routes.rbにdevise_for :users などの記述がないとき。

対処法

1.
Gemfileに以下を記述。

gem 'devise'

ターミナルで以下を実行。

gemをインストール

bundle install

設定ファイルを作成

rails g  devise:install

ログイン機能に対応したモデルを作成

rails g devise user

ログイン機能に関連するテーブルを作成

bundle exec rake db:migrate

2.
routes.rbに以下を記述

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

【rails】ランダムのパスワードを作成しパスワードフィールドに入力する方法

この記事を参考にすればできること

-ランダムのパスワードを生成

-

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

【rails】ランダム英数字のパスワードを作成しパスワードフィールドに入力する方法

パスワードフィールドにパスワードを生成して入力させよう

ポートフォリオの投稿機能にパスワード機能を設定したのですが、
パスワードを自動的に生成、入力までしてくれる機能あったら便利じゃん。

ということで作りました。

以下画像のように「パスワードを生成する」ボタンをクリックすると、
自動的に入力できるようにしましょう!
パスワード生成.gif

パスワードを生成する

まずはコントローラーでランダムの英数字を作れるようにします。

topics_controller.rb
def new
  @topic    = Topic.new
  @password = SecureRandom.alphanumeric(6)
  -> ランダムの英数字(A-Z, a-z, 0-9)を生成
   (6)は6桁、指定なしの場合は16桁になります。
end

パスワードをパスワードフィールドに入力しよう

次に先ほど作したパスワードをpassword_field(今回はtext_fieldにしています)に
入力できるようにします。

new.html.erb
<div class="topic-new-wrapper" >
  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
          <%= form_for @topic do |f| %>
            <div class="form-group">
              <%= f.label :password, 'password(任意)' %>
              <%= f.text_field :password, class: 'form-control', id: 'password' %>
              <%= button_tag 'パスワードを生成する', id: 'auto-fill-link' %>
            </div>

            <%= f.submit 'パスワード登録', class: 'btn btn-black btn-block' %>

          <% end %>
      </div>
    </div>
  </div>
</div>

<script>
  $(function(){
    autoFill();
    function autoFill() {
      $('#auto-fill-link').click(function(){
        $('#password').val("<%= @password %>");
      });
    }
  })
</script>

auto-fill-linkというidを含むリンクがクリックされると、passwordというidを含む
fieldに@passwordが入力されます。

ただこれだとbutton_tagがクリックされたときにsubmitされてしまうため、
ただのボタンとして利用する際は以下のコードを追加してあげます。

type: "button"

new.html.erb
<div class="topic-new-wrapper" >
  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
          <%= form_for @topic do |f| %>
            <div class="form-group">
              <%= f.label :password, 'password(任意)' %>
              <%= f.text_field :password, class: 'form-control', id: 'password' %>
              <%= button_tag 'パスワードを生成する', id: 'auto-fill-link', type: "button" %>
            </div>

            <%= f.submit 'パスワード登録', class: 'btn btn-black btn-block' %>

          <% end %>
      </div>
    </div>
  </div>
</div>

<script>
  $(function(){
    autoFill();
    function autoFill() {
      $('#auto-fill-link').click(function(){
        $('#password').val("<%= @password %>");
      });
    }
  })
</script>

まとめ

パスワードの生成から入力までしてくれる機能を作成しました。

簡単なので上記のコードぜひ使ってくださいね。

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

【rails】ボタンをクリックするとランダムの英数字パスワードを作成、パスワードフィールドに入力する方法

パスワードを自動生成したい

ポートフォリオの投稿機能にパスワード機能を設定したのですが、
パスワードを自動的に生成、入力までしてくれる機能あったら便利じゃん。

ということで作っていきましょう!

この記事のゴール

「パスワードを生成する」ボタンをクリックして
自動的に入力できるようにしましょう!
パスワード生成.gif

前提

rails:5.2
ruby:2.6.3
bootstrap
jquery

パスワードを生成する

まずはコントローラーでランダムの英数字を作れるようにします。

topics_controller.rb
def new
  @topic    = Topic.new
  @password = SecureRandom.alphanumeric(6)
  -> ランダムの英数字(A-Z, a-z, 0-9)を生成
   (6)は6桁、指定なしの場合は16
end

ボタンクリックでフィールドに入力できるようにする

次に先ほど作したパスワードをpassword_field(今回はtext_fieldにしています)に
入力できるようにします。

new.html.erb
<div class="topic-new-wrapper" >
  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
          <%= form_for @topic do |f| %>
            <div class="form-group">
              <%= f.label :password, 'password(任意)' %>
              <%= f.text_field :password, class: 'form-control', id: 'password' %>
              <%= button_tag 'パスワードを生成する', id: 'auto-fill-link' %>
            </div>

            <%= f.submit 'パスワード登録', class: 'btn btn-black btn-block' %>

          <% end %>
      </div>
    </div>
  </div>
</div>

<script>
  $(function(){
    autoFill();
    function autoFill() {
      $('#auto-fill-link').click(function(){
        $('#password').val("<%= @password %>");
      });
    }
  })
</script>

"auto-fill-link"というidを含むリンクがクリックされると、"password"というidを含む
fieldに@passwordが入力されます。

ただこれだとbutton_tagがクリックされたときにsubmitされてしまうため、
ただのボタンとして利用する際は以下のコードを追加してあげます。

type: "button"

new.html.erb
<div class="topic-new-wrapper" >
  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
          <%= form_for @topic do |f| %>
            <div class="form-group">
              <%= f.label :password, 'password(任意)' %>
              <%= f.text_field :password, class: 'form-control', id: 'password' %>
この行->       <%= button_tag 'パスワードを生成する', id: 'auto-fill-link', type: "button" %>
            </div>

            <%= f.submit 'パスワード登録', class: 'btn btn-black btn-block' %>

          <% end %>
      </div>
    </div>
  </div>
</div>

<script>
  $(function(){
    autoFill();
    function autoFill() {
      $('#auto-fill-link').click(function(){
        $('#password').val("<%= @password %>");
      });
    }
  })
</script>

まとめ

パスワードの生成から入力までしてくれる機能を作成しました。

簡単なので上記のコードぜひ使ってくださいね。

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

【Rails】RspecでCapybaraを使用した際に「undefined method `visit'」というエラーが出た場合の解決方法

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・rspec-rails: 4.0.1
・Capybara: 3.32.2
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装

原因

Capybaraが読み込めていない。

解決方法

1.requireで読み込む

spec_helper.rb
require 'capybara/rspec' # 追記
RSpec.configure do |config|
end

2. 解決方法1で解決しない場合はDSLで強制的に読み込む

spec_helper.rb
require 'capybara/rspec'
RSpec.configure do |config|
  config.include Capybara::DSL # 追記
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

booleanのvalidateについて

booleanのカラムについて、バリデーションをかけたがvalid?がtrueにならず、期待していた実行とは違うものになってしまった!

そこで、booleanのカラムについて調べたことを備忘録としてまとめておく!

ダメなパターン:Notnull制約でfalseが入っているので、presence: trueをすれば良いと思い下記のようにした!

validates :check, presence: true

しかし、これだとcheck=falseの時にエラーが出てしまう!

調べてみると、下記のようにする必要があることがわかった!

validates :check, inclusion: {in: [true, false]}

参考資料
https://qiita.com/mktakuya/items/a13c2175f0f0d9871038

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

Ruby on Rails seeds.rbファイルを使って、コマンド1つでレコードを大量作成

seeds.rbとは?

app/db ディレクトリにあるファイルで、レコードの作成に使うファイル。

レコードを作成する際に手作業で1つ1つデータを登録しなくても、
seeds.rbを使用すると大量のレコードをコマンド1つで作成することができる。

seeds.rbを使用したレコード作成方法

  1. seeds.rb ファイルにレコード作成の処理を記述
  2. rails db:seed を実行
  3. seeds.rb ファイルの処理が実行される

例:usersテーブルに100人分の名前とメールアドレスを登録

app/db/seeds.rb に以下のように記述し、

seeds.rb
100.times do |n|
  User.create(name: "name#{n}", email: "mail#{n}@gmail.com")
end

rails db:seed を実行すると、
users テーブルには name, email の値がそれぞれ

"name0, mail0@gmail.com",
"name1, mail1@gmail.com",



"name99, mail99@gmail.com"

のようにレコードが100件登録される。

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

Ruby + SinatraでLINE Botを作ろう 〜 オウム返し編 〜

こんにちは。
この記事では、複数回に渡ってRubyとSinatraを使って

  • 本の裏にあるISBNコードを送信すると検索して本の画像を表示してくれる
  • その本を記録しておいて、あとから参照できる

という機能を搭載した「ほんめも!」というLINE Botを作ってみたいと思います。

0. この記事で作るもの

この記事では、オウム返しするLINE Botを作っていきます。
プログラムの流れは下記の通りです。
スクリーンショット 2020-07-12 3.23.08.png

1. プロジェクトを準備する

まずはプロジェクトの雛形を作りましょう。既に雛形がある場合や、既存のコードに組み込む場合は次の章に進んでください。

ターミナル
$ bundle init

bundle initコマンドでGemfileを生成します。
そして、下記のgemを追記していきましょう。

Gemfile
# 中略
gem "sinatra"
gem "sinatra-contrib"
gem "dotenv"
gem "line-bot-api"

記述が完了したらbundleコマンドでインストールしましょう。

ターミナル
$ bundle

続いて、コアのファイルを作っていきます。今回はapp.rbというファイル名にします。

app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/reloader' if development?

get '/' do
    "Hello world!"
end

さて、実行して動作テストをしてみましょう。

ターミナル
$ ruby app.rb -o 0.0.0.0

とコマンドを入力して、プログラムを起動します。
Sinatraのデフォルトのポート番号は4567なので、http://localhost:4567/にアクセスしてみましょう。
「Hello world!」と表示されれば、Sinatraの環境構築は完了です!(スクリーンショットのポート番号が違うのはDockerを噛ませている為なのであまり気にしないでください><)

スクリーンショット 2020-07-12 1.53.41.png

2. LINE Developersに登録しよう

2_1. ログインしよう

https://developers.line.biz/ja/
にアクセスしてログインしましょう。普段LINEを使っていればQRコードでログイン出来ます。

ログインすると下のような画面に移動するはずです。とりあえず右下のボタンから言語を変更出来るので、日本語にしておくと楽です。

スクリーンショット 2020-07-12 1.56.50.png

2_2. プロバイダーとチャネル?

LINE Botの開発をすると必ず「プロバイダー」と「チャネル」という単語が出てきます。
これらは、ざっくり説明すると

  • プロバイダー: 開発者アカウント
  • チャネル: Botアカウント

という意味になります。
プロバイダーは会社や個人、開発者のグループ等の単位で作成し、チャネルは1つのプロバイダーに属します。

2_3. プロバイダーを作ろう

というわけで、まずはプロバイダーを作成しましょう。
「新規プロバイダー作成」をクリックすると、プロバイダー名を入力する画面が出てくるので、好きなプロバイダー名を入力しましょう。 

スクリーンショット 2020-07-12 2.03.05.png

ここで入力したプロバイダー名はBotの作者として 公開 されます。本名等は控えたほうが良いでしょう。
作成ボタンを押すとプロバイダーが作成されます。

2_4. チャネルを作ろう

続いて、Botのアカウントとなるチャネルを作りましょう。
Botで使うチャネルは「Messaging API」なので、Messaging APIをクリックします。

スクリーンショット 2020-07-12 2.05.33.png

新しく作るチャネルの設定画面が出てきます。必要な内容を埋めていきましょう。

項目 内容
チャネルの種類 Messaging API
プロバイダー 先程作ったもの
チャネルアイコン 今は設定しなくてOK
チャネル名 Bot名
チャネル説明 Botの説明
大業種 Botのジャンル
小業種 Botの詳細ジャンル
メールアドレス Botに関する連絡が来るメールアドレス。連絡が受けられるメールアドレスにしておくと良いです。
プライバシーポリシーURL 今は設定しなくてOK
サービス利用規約URL 今は設定しなくてOK

必要な規約に同意した後、作成ボタンを押すとチャネルが作成されます。

3. Botに必要なキーを.envファイルに書き込もう

Botに必要なチャネルの準備が出来たので、キーやシークレットを.envファイルに書き込んでいきましょう。
プログラム内に直接書き込むことも出来ますが、セキュリティの事を考えるとあまりよろしくありません。

.env
LINE_CHANNEL_ID=xxxxxxxxxx
LINE_CHANNEL_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
LINE_CHANNEL_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

チャネルIDとチャネルシークレットは「チャネル基本設定」
チャネルトークンは「Messaging API設定」の中に「チャネルアクセストークン(長期)」という名前で記載されています。
表示されていない場合は発行ボタンを押して発行しましょう。

プログラム内で.envに記述した変数が利用出来るようにするためDotenv.loadをrequireの下に書いておきます。

app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/reloader' if development?
Dotenv.load # <- 追記

get '/' do
  "Hello world!"
end

4. Webhookを受け付けるようにしよう

4_1 Webhookって何?

Webhookとは、相手のサービス(この記事ではLINE)でイベントが発生したら、事前に設定しておいたURLにイベントの内容をPOSTしてもらう仕組みのことです。

例えば、自分の作ったサービスの/callbackというURLをLINEのチャネルに設定しておけば、チャネルにメッセージが来た時に/callbackにメッセージの内容をPOSTしてもらう事が出来ます。

ただし、LINEのサーバーからアクセス出来るURLである必要があるので、サービスを実際に公開していないときちんと動作しません。例えば開発中のlocalhost:4567は自分のPCからしかアクセス出来ないURLなので、localhost:4567/callbackを設定した状態でメッセージが来てもlocalhost:4567/callbackは呼び出されません。

この仕様があるため、LINE Botを開発する時は基本的に毎回デプロイする必要があります。
ポート開放をすればいちいちデプロイする手間は省けますが、セキュリティ上のリスクもあるのでここでは紹介しません。

4_2 Webhookを受け取って返信しよう

基本的にGitHubのREADMEと一緒ですが、画像の処理を抜いてシンプルなコードにしています。
https://github.com/line/line-bot-sdk-ruby

app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/reloader' if development?
Dotenv.load

# ====== 追記ここから ======

def client
  @client ||= Line::Bot::Client.new { |config|
    config.channel_id = ENV["LINE_CHANNEL_ID"]
    config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
    config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
  }
end

post '/callback' do
  body = request.body.read

  signature = request.env['HTTP_X_LINE_SIGNATURE']
  unless client.validate_signature(body, signature)
    error 400 do 'Bad Request' end
  end

  events = client.parse_events_from(body)
  events.each do |event|
    if event.is_a?(Line::Bot::Event::Message)
      if event.type === Line::Bot::Event::MessageType::Text
        message = {
          type: 'text',
          text: event.message['text']
        }
        client.reply_message(event['replyToken'], message)
      end
    end
  end

  "OK"
end

# ====== 追記ここまで ======

get '/' do
  "Hello wolrd!"
end

コードの解説をこの下でしていきます。

4_3 コード解説

def client
  @client ||= Line::Bot::Client.new { |config|
    config.channel_id = ENV["LINE_CHANNEL_ID"]
    config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
    config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
  }
end

これはLINE Botを操作するための「client」を使えるようにするためのコードです(これはline-bot-apiの機能です)。
Line::Bot::Client.newでclientを作る事が出来ますが、1つのサービスで1つのclientがあれば十分なのでこのような実装になっています。
変数名の頭に@を付ける & ||=演算子を使う事で、@clientが空の時はLine::Bot::Client.newしてそれを渡す。@clientが既に入ってる時はそれを渡す。という処理を実現しています。

post '/callback' do
  body = request.body.read

  signature = request.env['HTTP_X_LINE_SIGNATURE']
  unless client.validate_signature(body, signature)
    error 400 do 'Bad Request' end
  end

post '/callback' doのブロックは少し長いので分けて説明します。
body = request.body.readは送られてきたデータをbody変数に代入しているだけです。

signature以降では、送られてきたデータが本当にLINEサーバからのものかをチェックしています。
LINEサーバから送信されるデータには必ず「HTTP_X_LINE_SIGNATURE」というものが含まれており、その中身を見るとLINEサーバから送られてきたデータかどうかを確認する事が出来ます。
LINEサーバかどうかの確認はline-bot-apiに実装されており、先程作ったclientを通して利用する事が出来ます。
確認の処理はclient.validate_signature(body, signature)という部分で実行されています。

悪意のある人がLINEサーバをなりすましてメッセージを送り込んできていないかをチェックするという重要なコードです。

  events = client.parse_events_from(body)
  events.each do |event|
    if event.is_a?(Line::Bot::Event::Message)
      if event.type === Line::Bot::Event::MessageType::Text
        message = {
          type: 'text',
          text: event.message['text']
        }
        client.reply_message(event['replyToken'], message)
      end
    end
  end

  "OK"
end

events = client.parse_events_from(body)では送られてきたデータをrubyで扱いやすい形に変換してもらっています。
変換した結果はeventsという名前からも分かるように、イベントの配列になります。

events.each do |event|では複数あるイベントを1件ずつ処理しています。これは一度に複数のイベントが同時に送信される事がある為です。

if event.is_a?(Line::Bot::Event::Message)ではイベントの種類がMessageかどうかを確認しています。メッセージ以外のイベントには、「友だち追加」や「ブロック解除」などがあります。

if event.type === Line::Bot::Event::MessageType::Textではメッセージの種類がテキストであることを確認しています。テキスト以外のメッセージの種類には、画像や動画、スタンプなどがあります。

つまり、上から4行までのコードで「送信されたデータを解析して、テキストメッセージのみを絞り込む」という処理をしています。


続いてif文の中のコードを見てみましょう

message = {
  type: 'text',
  text: event.message['text']
}
client.reply_message(event['replyToken'], message)

上4行でLINEサーバに送るメッセージを組み立てて、最後の1行で返信を送信しています。
event['replyToken']はイベントに含まれている返信用のトークンです。


最後に"OK"と書いていますが、処理が正常に成功したら正しいレスポンスを返す必要があるというLINE Bot APIのルールに則ったものです。何を返してもOKです。

4_4 デプロイしよう

さあ、コードは完成したので実行してみましょう!と言いたい所ですが、先程解説したとおり残念ながらローカルでの実行では動作しません。
なので、今回はHerokuへデプロイすることにします。

herokuへのデプロイの詳細は割愛しますが、Procfileだけ作成しておきましょう。

Procfile
web: bundle exec ruby app.rb -o 0.0.0.0 -p $PORT
ターミナル
$ git init
$ git add -A
$ git commit -m "first commit"
$ heroku create
$ git push heroku master

デプロイできたらアプリケーションを開いてみましょう。「Hello wolrd!」と表示されればデプロイ完了です!

4_5 Webhookを設定しよう 

LINE Developersのサイトに行きチャネル設定の画面に移動しましょう。
「Messaging API設定」を開き、「Webhook URL」の編集をクリックします。
URLを入力する枠が出てくるので、先程デプロイしたURL+/callbackと入力して更新ボタンを押しましょう。

例えばデプロイしたURLが
https://xxxxxxx-yyyyyy-zzzzz.herokuapp.com
だったら
https://xxxxxxx-yyyyyy-zzzzz.herokuapp.com/callback`
となります。

その後、Webhookの利用にチェックを入れておきましょう。

`スクリーンショット 2020-07-12 4.14.21.png

検証ボタンを押すとサーバーの動作チェックが出来ます。成功と表示されれば問題ありません。


また、現時点では自動応答メッセージが有効になっているためBotのメッセージが送信出来ません。なので、自動応答を無効にします。

スクリーンショット 2020-07-12 4.17.19.png

Messaging API設定の中の「応答メッセージ」の編集ボタンをクリックします。
すると、応答設定というページが表示されるので、下にある詳細設定の

  • 応答メッセージを オフ
  • Webhookを オン

設定します。

スクリーンショット 2020-07-12 4.18.33.png

これでWebhookを利用する準備が出来ました。

5. 試してみよう

さて、準備は全て整いました!Botを友だちに追加して実際にメッセージを送信してみましょう!
「Messaging API設定」の中にQRコードがあるので、これをLINEで読み込んでみましょう。
作ったBotと友達になれるはずです。

友達になれたら何かメッセージを送信してみましょう。
送ったメッセージと同じメッセージが帰ってきたら成功です!おつかれさまでした!

IMG_1505A6F48E6C-1.jpeg

6. まとめ

この記事では送られてきたメッセージをオウム返しするBotを作りました。
とてもシンプルなものですが、Bot作りに欠かせない大切なコードが沢山含まれているのでよく理解しておくことをおすすめします!

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

all_month が気になって ActiveSupport DateAndTime::Calculations を読んでみた

これ何

先輩にレビュー中に、 all_month 使った方がいいんじゃない?と言われたので、気になって調べてみた。
ActiveSupport の DateAndTime::Calculations に定義されていたので、周りのmethodも読んでみる

範囲指定のmethod

all_monthの内部実装を眺めてみた。rails/railsの内部実装だが、読めるコードでわかる。

def all_month
  beginning_of_month..end_of_month
end

ref: activesupport/lib/active_support/core_ext/date_and_time/calculations.rb

同じファイルの中を見てみるとall_month 以外にも all_xxx methodが存在した。対象範囲が変わるだけで、使えそうなmethodが多い。

  • all_day
  • all_month
  • all_quarter:3ヶ月ごとの日時情報が取れる
  • all_week:週の開始日を決めることができる
  • all_year

気づき 1. future?, past?

未来・過去かを判定するmethodが存在する!再定義しなくても、rails/railsであるものを使える!!気づき。

Date.yesterday.past?
=> true
Date.yesterday.future?
=> false
Date.tomorrow.future?
=> true
Date.tomorrow.past?
=> false

気づき 2. all_xxxと同様にnext_xxx, prev_xxxも存在する

next_xxx, prev_xxx が定義されている。 prev_xxx は last_xxx にaliasされている。

Date.current
=> Sun, 11 Jul 2020
Date.current.next_week
=> Mon, 13 Jul 2020
Date.current.prev_week
=> Mon, 29 Jun 2020
Date.current.last_week
=> Mon, 29 Jun 2020

参考文献

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