20201010のRubyに関する記事は28件です。

【RSpec】ヘルパーメソッドが使われたviewファイルをテストしたらActionView::Template::Error:が出た。。

個人開発のアプリに通知機能を実装し、その後GitにpushしてCircleCIのテスト結果を確認したら突然大量のテストが失敗していて少し困ったので、その解決方法をここで共有させて頂きます。

エラー内容

Failure/Error: -if unchecked_notifications.any?

ActionView::Template::Error:
  undefined local variable or method `unchecked_notifications' for #<#<Class:0x0000555732134140>:0x00005557326f7f78>

通知が来ている場合と来ていない場合で表示するviewを変えるために、unchecked_notificationsという、ヘルパーメソッドを定義しました。しかし、RSpec実行時にはこのヘルパーメソッドが読み込まれておらずエラーが出てテストに失敗してしまったようです。

notifications_helper.rb
module NotificationsHelper
  def unchecked_notifications
    @notifications = current_user.passive_notifications.where(checked: false)
  end
end

解決策

解決策は簡単です。
テストを実行するファイルに、モジュールをインクルードするれば解決するはずです。↓例

post_spec.rb
require 'rails_helper'
#ヘルパーメソッドをインクルード
include NotificationsHelper

RSpec.describe '投稿機能', type: :system do
  #テスト処理
end

最後まで読んでいただきありがとうございます!

日々学習したことをアウトプットしてます!ご指摘などあればコメントいただけますと幸いです!

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

each文の中にeach文でいい気分!?

はじめに

each文が入れ子になると、読みにくいので、どう考えれば良いのか、まとめてみた。

配列の中の配列の中の配列

shopping_price = [["野菜", [200, 250, 220]], ["フルーツ", [1200, 1500]]]

そもそも配列の中に配列がある場合、どのようにして中の値を取り出すのか。
それは、添字を重ねることで、取り出すことができる。
上の配列で1200を取り出すなら、

puts shopping_price[1][1][0]

となる。

each文

基本的な記述は

配列.each do |ブロックパラメーター(変数)|
 #繰り返したい処理
end

ブロックパラメーターはdo~endの中でのみ扱える変数。イメージとしては、配列の中が一つ一つ入っていく感じ。

配列の名前が複数形の場合、その単数形を入れることが多い。

each文の中にeach文

shopping_price = [["野菜", [200, 250, 220]], ["フルーツ", [1200, 1500]]]

shopping_price.each do |shopping|
 sum = 0
 shopping[1].each do |price|
  sum += price
 end
 puts "#{shopping[0]}の合計金額は#{sum}"
end
#=>野菜の合計金額は670円
#=>フルーツの合計金額は2700円

外のeach文には、
1回目に["野菜", [200, 250, 220]]
2回目に["フルーツ", [1200, 1500]]
|shopping|の中に入る。

中のeach文の配列には、shopping[1]が当てられている。
つまり、
1回目に[200, 250, 220]
2回目に[1200, 1500]
priceの中に入る。

1回目の計算は、
sum = 0 + 200
sum = 200 + 250
sum = 450 + 220

となる。

shopping[0]には、野菜フルーツがそれぞれ入る。

最後に

[角括弧]がたくさん出てきて、読みにくいが、どの[角括弧]も必要。
each文の中にeach文や配列の中に配列は読みにくいので、リファクタリングが必要だろう。

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

検索機能の実装

はじめに

個人アプリの作成にて、投稿した記事の内容を検索で表示出来るように検索機能の実装を行う。

ルーティングの設定

今回は:idを指定してページに遷移しないので、collectionを使用してルーティングを設定します。

routes.rb
resources :posts do
  get :search, on: :collection
  resource :likes, only: [:create, :destroy] -いいね機能実装
end

モデルの設定

LIKE句
LIKE句は、文字列の検索をすることができる。
whereメソッドと一緒に使う。

実行例 詳細
where('title LIKE(?)', "a%") aから始まるタイトル
where('title LIKE(?)', "%b") bで終わるタイトル
where('title LIKE(?)', "%c%") cが含まれるタイトル
where('title LIKE(?)', "d_") dで始まる2文字のタイトル
where('title LIKE(?)', "_e") eで終わる2文字のタイトル
app/models/post.rb
def self.search(search)
  return Post.all unless search
  Post.where('body LIKE(?)', "%#{search}%")
end

コントローラーにsearchアクションを定義

post_controller.rb
def search
  @posts = Post.search(params[:keyword])
end

viewの実装

renderメソッドを使って、部分テンプレートを行っているものとします。
{ post: post } の右側のpostはeachメソッド内の変数としてのpostpostのインスタンスを示しています。左側のpostは部分テンプレート内での変数の名前を表しています。

search.html.erb
<% @posts.each do |post| %>
   <%= render partial: "post", locals: { post: post } %>
<% end %>

おわりに

比較的簡単に検索機能の実装は完了しました。
他にも機能を実装し、出来ることを増やしていこうと思います。
最後まで見ていただきありがとうございます:grin:

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

Ruby Leet文字列に変換

はじめに

こちらは学習メモです。

Leetとは?

たとえば、「Warez」という語を leet で表記すると、「W@rez」や「W4r3z」などとなるように、一部のアルファベットを形の似た数字や記号などに変化させること。

今回は、Rubyで以下のように変換します。

アルファベット⇄記号変換表
アルファベット 記号
A
B
C
D
E

入力された文字列に対してLeetで文字を変換する。その後、文字列を出力する。

コード

word = gets.chomp.split('')
word.each do |w|
  case w
  when 'A'
    print '6'
  when 'B'
    print '8'
  when 'C'
    print '5'
  when 'D'
    print '3'
  when 'E'
    print '1'
  else
    print w
  end
end

入力例

ABKTED

[実行結果]

68KT13

解説

word = gets.chomp.split('')

・1行目では入力された文字を一文字ずつ区切って配列にし、word変数に代入
  getsメソッド: 入力を一行ごとに「文字列」で受け取る。
  chompメソッド: 文字列の改行を取り除く。
  splitメソッド: 文字列を分解して配列にする。

word.each do |w|

・word変数の要素をw変数に代入する
 2行目以降はcaseによって一致判定・文字の入れ替えを一文字ずつ行っています。

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

【Ruby】ハッシュテーブルの順番を逆にしたい

はじめに

たとえば、reputation = {very_bad: '最悪', bad: '悪い', good: '良い', very_good: '最高'}みたいなハッシュテーブルがあったとして、その順番を{very_good: '最高', good: '良い', bad: '悪い', very_bad: '最悪'}のように後ろから並べたい。

やり方

pry(main)> reputation.to_a.reverse.to_h 

 #=> {very_good: '最高', good: '良い', bad: '悪い', very_bad: '最悪'}

これだけです。

一応説明すると、reverseっていう配列の順番を逆にするメソッドを使いたいんだけどreputationはhashなので使えない。なので一度to_aでhashを配列に変換してreverseした後、そのままだと配列のままなのでto_hで元の戻す、ということです。

あとがき

「gemのenum_helpを使ってラジオボタンの表示は日本語表記に、内部ではint型で処理したい。評価が低い方から数字も低い方を割り当てたい、しかしラジオボタンの表示は評価が高い方から表示したい。」という場面で必要でした。
普通enum使うときってvalueが数字になるはずだから並べ替えやすいんですが、enum_help使うと上に書いたようなhashを渡すことになるので「どうやって並べ替えるんだ…」ってなりました。

記事にすることもないような気がしますが、僕のような初心者だとto_sto_iは使っても、to_aとかは使う機会が少なくて詰まることがあると思うので少しでも誰かの役に立てば。

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

ざっくりすぎるdocker-composeでのRails5+MySQL8.0+top-level volumesの環境構築

丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
とほぼ同じ内容です。
なので詳しく知りたい方はそちらへどうぞー
mysqlのバージョンが異なっていたり、top-level volumesを使っているなど少し異なりますー

環境

  • MacOS 10.15.7
  • Docker Desktop for Mac
  • Ruby 2.7.1
  • Rails 5.2.4.4
  • MySQL 8.0.21
$ mkdir rails_docker
$ cd rails_docker
$ vi Dockerfile
Dockerfile
FROM ruby:2.7.1

RUN apt-get update -qq && \
    apt-get install -y build-essential \
                       libpq-dev \
                       nodejs \
    && rm -rf /var/lib/apt/lists/*

RUN mkdir /recruit_web
ENV APP_ROOT /recruit_web
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN bundle install
ADD . $APP_ROOT
$ vi Gemfile
Gemfile
source 'https://rubygems.org'
gem 'rails',  '~> 5.2.4', '>= 5.2.4.4'
$ touch Gemfile.lock
$ vi docker-compose.yml
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0.21
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - rails_docker_network
    environment:
        MYSQL_DATABASE: root
        MYSQL_ROOT_PASSWORD: password
    # mysql8.0の認証プラグイン(caching_sha2_password)をmysql_native_passwordに変更
    command: --default-authentication-plugin=mysql_native_password
    container_name: rails_db_container
  web:
    build: .
    depends_on:
      - db
    command: rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/recruit_web
    networks:
      - rails_docker_network
    ports:
      - "3000:3000"
    container_name: rails_web_container

volumes:
  db_data:

networks:
  rails_docker_network:
    name: rails_docker_network
$ docker-compose run web rails new . --force --database=mysql --skip-bundle
$ vi /confing/database.yml
database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root # 追加
  password: password # 追加
  host: db # 追加
$ docker-compose build
$ docker-compose up -d

以下のコマンドを実行するとデータベースが作られてlocalhost:3000でアクセスできると思いますー

$ docker-compose run web rails db:create

詰まったところ

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0.21
    volumes:
      - db_data:/var/lib/mysql
    environment:
        MYSQL_DATABASE: root
        MYSQL_ROOT_PASSWORD: password
    # 以下を指定していたらエラーが出た
    ports:
      - "3306:3306 

  web:
    build: .
    command: rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app_sample
    ports:
      - "3000:3000"
    # 以下を指定していたらエラーが出た(非推奨+なくても名前解決されるので必要なかった)
    links:
      - db

volumes:
  db_data:

なぜかlinks と db の公開ポートを指定していると

Mysql2::Error::ConnectionError: Access denied for user 'root'@'172.19.0.4' (using password: YES)

と怒られかなり躓きました。。。

とりあえず、ポートは必要ないと気づき削除
ドキュメントよりlinksは非推奨でなくても名前解決できるそうなので削除
https://docs.docker.com/compose/networking/

何か改善点あればよろしくおねがいしますー

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

Ruby 四則演算

はいじめに

学習用のメモになります

四則演算とは?

四則演算とは、足し算・引き算・掛け算・割り算のことをいいます。 ワークシート上やマクロの中でも四則演算は使われています。 四則演算は下記のような記号を使って計算式を作ります。

四則演算

基本的な四則演算を行うための演算子は次の通りです。

演算子 内容
* 乗算 5 * 3
/ 除算 8 / 2
+ 加算 3 + 6
- 減算 5 - 2

また四則演算以外にも次の演算子が用意されています。

演算子 内容
% 剰余 5 % 3
** べき乗 5 ** 2

剰余は演算子の左辺を右辺で割った余りです。「5 % 3」の剰余は「2」となります。
べき乗は演算子の左辺を右辺の値で累乗した値です。「5 ** 2」は「25」となります。

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

Rubyのエラーが出ている原因の見つけ方について

前書き

よく出てくるエラー文を翻訳しなくてさっと確認する方法を解説してみました!

(ArgumentError) 引数エラー

このエラーが出た時は、文字通り引数についてのエラーになります。
例えば、引数の数があっていない時や値が正しくない時出たりします。

下記のようなエラー文が出たとします。

一部分を記載
arguments(given 2, expected 0)

訳すと、
引数(与えられた 2, 期待される 0)
極端な例ですが、渡す引数が2で
受け取る側が引数がない状態なのでエラーが出てますよ

みたいな感じになります。
(ArgumentError) これが出た時は、(given,expected)を探してみてください!

(NameError)名前エラー

名前に関するエラー。
変数名やメソッド名に間違いがある時にでる。
定義した時の名前が間違っているか、メソッドを呼び出した時の名前が間違っている可能性があります。

一部分を記載
undefined local variable or method  '〇〇' 

訳すと、
未定義のローカル変数またはメソッド
〇〇の変数orメソッドに注目してみましょう!

did you mean?(もしかして)
と提案してくている場合も多いのでここもチェック!

(NoMethodError)ノーメソットエラー

メソットに関するエラー。
実行しようとしたメソットの呼び出し時の名前が間違っているか、メソッドが定義時の名前が間違っている可能性がある時にでる。

一部分を記載
test.rb:2in '〇〇'

これはエラーが出ている部分を教えてくれいるので、この部分のメソッドを確認して見よう!

ノーメソッドエラーにも下記の記載があったらチェック!
did you mean?(もしかして)

まとめ

とりあえずエラー文が出た時は、翻訳かけれましょう。
それで意味が分かれば見えてくる部分があると思います。

出てきた意味をググってみたら解決方法が見つかる場合もありますので!

エラーが出た時こそ、成長のチャンスです。
って思えるように頑張ります!

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

Ruby on Rails 変数、定数まとめ

Ruby on Rails 変数、定数まとめ

Railsにおいてよく使う変数、定数についてまとめました。

前提条件

  • 変数
    • 変更可能
  • 定数
    • 定数不可

変数

ローカル変数

  • メソッドやブロック内でのみ有効
  • 変数名
    • 小文字のスネークケース
      user_name = 'jon'

Railsでの活用

  • View
    • View でローカル変数を扱いたい場合は、Viewファイル内で定義する。 [users/show.html.erb]
<% hoge = 'huga' %>
<div><%= hoge %><div>
  • Controller
    • Viewに変数の値をわたす必要がなかったり、Contrller内で変数の値を共有する必要がない場合は、ローカル変数に定義する
def create
  article = Article.find( params[:id])
  return render_404 if article.blank?
end

インスタンス変数

  • 同じコントローラー内で使える変数
  • 変数名
    • 先頭に@を付ける

Rails での活用

  • Controller Action内で定義したインスタンス変数を View へわたす。
def show
  @user = User.find(params[:id])
end

[users/show.html.erb]

<div>ユーザーID:<%= @user.id %><div>

※ローカル変数をわたすことはできない

def show
  user = User.find(params[:id])
end

[users/show.html.erb]

<div>ユーザーID:<%= user.id %><div>

-> 参照不可
undefined local variable or method 'user'

グローバル変数

  • 全てのContrller使える
  • プログラムのどこからでも、変更、参照が可能
  • 変数名
    • 先頭に@を付ける

定数

  • クラス内で定義する

  • 定数名

    • 大文字、スネークケース

Rails での活用

  • [config/initializers/constants.rb]に共通の定数を定義する
    • 全ての Controller, View で参照することができる [config/initializers/constants.rb]
MAX_SIZE = 10
  • 各 Model で使用する定数を定義するには、model ファイルのクラス内に定義する。 [user.rb]
class User
  OFFICIAL_ID = 100
end

Controller や View で参照する場合は、User::OFFICIAL_IDと書く

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

[Rails5.2] Docker内のMysql 5.7の絵文字対応(文字コードをutf8mb4に変更する)

はじめに

Ruby on Railsでポートフォリオを作成しているRails初学者です。
今回はタイトルの通り、tableに絵文字情報を保存するためのmysqlをutf8mb4にした際の覚書です。

Youtube Data APIから動画タイトルを保存しようとしたらエラーが出た。

ActiveRecord::StatementInvalid in VideosController#refresh
Mysql2::Error: Incorrect string value: '\xF0\x9F\x94\xB5\xE8\x87...'

絵文字が入っているタイトルがいくつかあり、それが影響でエラーが出たようです。
mysql上のdbの文字コードを見るとuft8になっており、これを変更しなければいけない様子・・・
ひとまず色々なコードを参考にして以下の通りに実施しました。

環境

Docker for mac を使ってコンテナ内でRailsを開発しています。

ruby 2.4.5
mysql 5.7.31
Ruby on rails 5.0.7.2

流れ

(準備:dumpする)
1. my.cnfを変更する
2. mysqlをリスタートする
3. ActiveRecordにオプションを追加
4. database.ymlの変更
5. docker-compose.ymlの変更し、コンテナの再起動
6. db:migrate:resetを実行
7. dumpデータのrestore

準備:dumpする

そもそもこの作業を行うまでdumpという言葉を知らなかったのですが、dumpとはDBのテーブル内の情報をSQLの形で出力することだそうです。(ちなみに最後に行うrestoreは、dumpした情報をDBに投入すること)

今回データベース設定を変更し、各テーブルの文字コードをutf8mb4にするので、一旦データをファイルとして落としておいて、設定変更後に投入するという形をとりります。

dump/restoreを簡単に行うために以下のgemを使用します。

Gemfile
gem 'yaml_db'

今回使用するのはyaml_dbという、ダンプでyaml形式ファイルを出力してくれるgemです。
GitHub - yamldb/yaml_db

dumpするには以下のコマンドをすればOK

Terminal
bundle exec rails db:data:dump

コマンド実行後、db/data.ymlにファイルが出力されています。
こんな感じで、基本的にはテーブル名、カラム情報、レコードの順番で出力されます。

data.yml
videos:
  columns:
  - id
  - name
  - url
  - upload_at
  - created_at
  - updated_at
  records:
  - - 1
    - "【衝撃】ボーナス支給額をクイズで決める会社"
    - https://www.youtube.com/watch?v=42ofwfioMFM
    - 2020-10-09 09:00:00.000000000 Z
    - 2020-10-10 08:07:38.000000000 Z
    - 2020-10-10 08:07:38.000000000 Z
 :

以下で準備はOKです。

1. my.cnfを変更する

my.cnfファイルを以下のように設定します。
記載されている内容をきちんと理解したかったため、1行ずつ見ていきました。

my.cnf
[mysql]
default-character-set=utf8mb4 #文字コードの設定

[mysqld] #mysqld Mysqlサーバの設定
character-set-server = utf8mb4 #文字コードの設定
skip-character-set-client-handshake #client側で指定した文字コードを無視するためのもの
collation-server = utf8mb4_general_ci #ソート順の指定
init-connect = SET NAMES utf8mb4 #クライアントからserverへの送信に使用される文字セット指定

各項目について、細かく見ていきます。

[mysql]と[mysqld]
mysqldとは、MySQLサーバとも呼ばれる、mysqlでの各種操作を受け持っているメインプログラムのこと。
mysql側の操作は必ずこのmysqldよりされるため、ここに各種設定が必要。

skip-character-set-client-handshake
character-set-client-handshakeとは、クライアント側の文字コードをMySQL側に反映する行為。これをスキップすることで、uft8mb4への設定ができる。

collation-server = utf8mb4_general_ci
collationは、照合順序:ソート順のこと。_で区切られた各単語でそれぞれ設定。
詳細は以下を参照
【MySQL】照合順序とは?

init-connect = SET NAMES utf8mb4
クライアントからサーバへの送信に使用される文字コードの指定
詳細は以下を参照
10.1.4 接続文字セットおよび照合順序

2. mysqlをリスタートする

ひとまず、この設定を反映するためにmysqlをリスタートします。
Docker内のコンテナに入り以下のコマンドを実行します。

もともとの設定を一応確認しておきます

terminal
mysql>status;
:(中略)
Server characterset:    utf8
Db     characterset:    utf8
Client characterset:    utf8
Conn.  characterset:    utf8
:

utf8になっています。
というわけで、一旦リスタートして、もう一度みてみます。

terminal
mysql>service mysql restart
terminal
mysql>status
:(中略)
Server characterset:    utf8mb4
Db     characterset:    utf8mb4
Client characterset:    utf8mb4
Conn.  characterset:    utf8mb4
:

設定できました!

3. ActiveRecordにオプションを追加

次に、ActiveRecordのcreate_tableが実行される際に、utf8mb4を使って登録するよう設定していきます。

新しくconfig/initializers/utf8mb4.rbを作成し、以下のコードを記載。
config/initializersにファイルを投入すると、Railsが起動する前に初期設定として読み込まれるようになります。

config/initializers/utf8mb4.rb
#optionを設定するmodule
module Utf8mb4
  def create_table(table_name, options = {})
    table_options = options.merge(options: 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
    super(table_name, table_options) do |td|
      yield td if block_given?
    end
  end
end

ActiveSupport.on_load :active_record do
  module ActiveRecord::ConnectionAdapters
    class AbstractMysqlAdapter
      #最初にmodule utf8mb4を実行し、その後既存のメソッドを実行(super)      
      prepend Utf8mb4
    end
  end
end

4. database.ymlの変更

以下の内容を追加ないし変更します

config/database.yml
  charset: utf8mb4
  encoding: utf8mb4
  collation: utf8mb4_general_ci

5. docker-compose.ymlの変更

Dockerの設定ファイルも以下のように変更します。

docker-compose.yml
:
  db:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci #utf8mb4をセット 
    container_name: コンテナ名
    volumes:
      - ./my.cnf:/etc/mysql/conf.d/my.cnf #my.cnfを読み込むよう設定
 :

この状態で、一度コンテナを再起動します。

6. db:migrate:resetを実行

ここまでのDBへの設定を反映させるため、DBをリセット→migrateします。

7. dumpデータのrestore

最後に、冒頭でdumpしたデータをもう一度投入すれば完了です。
以下のコマンドでrestoreできます。

terminal
rails db:data:load

以上で設定は完了です!

終わりに

色々と設定してきましたが、dumpなどの機能を知ることができたので、いい勉強になりました。

全面的に参考にさせていただいたサイト様
大変ありがとうございます・・・!
MySQLのencodingをutf8からutf8mb4に変更して寿司ビール問題に対応する
RailsのDBバックアップ(gem:yaml_db)
Rails5で絵文字を保存する utf8mb4 (docker)

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

DockerでのRailsアプリケーション構築

概要

Dockerの勉強会の際、作成したリポジトリがありそのまま埋めてるの勿体無いと思ったので記事にしました。
どなたかの参考になればいいなぁと思ってます。

https://github.com/yodev21/docker_tutorial_rails

作業手順

ファイル作成

下記ファイルを作成

Dockerfile
docker-compose.yml
Gemfile
Gemfile.lock
# イメージ名にRuby(Ver2.6.5)の実行環境のイメージを指定
FROM ruby:2.6.5

# パッケージのリストを更新しrailsの環境構築に必要なパッケージをインストール
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs

# プロジェクト用のディレクトリを作成
RUN mkdir /myapp

# ワーキングディレクトリに設定
WORKDIR /myapp

# プロジェクトのディレクトリにコピー
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock

# bundle install実行
RUN bundle install

# ビルドコンテキストの内容を全てmyappにコピー
COPY . /myapp
docker-compose.yml
version: '3'
services:
  db:
    # postgresのイメージを取得
    image: postgres
    environment:
      POSTGRES_USER: 'postgresql'
      POSTGRES_PASSWORD: 'postgresql-pass'
    restart: always
    volumes:
      - pgdatavol:/var/lib/postgresql/data
  web:
    # Dockerfileからイメージをビルドして使用
    build: .
    # コンテナ起動時に実行
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    # カレントディレクトリを/myappにバインドマウント
    volumes:
      - .:/myapp
    # 3000で公開して、コンテナの3000へ転送
    ports:
      - "3000:3000"
    # Webサービスを起動する前にdbサービスを起動
    depends_on:
      - db
# データ永続化のためにpgdatabolのvolumeを作成し、postgresqlのデータ領域をマウント
volumes:
  pgdatavol:
source 'https://rubygems.org'
gem 'rails', '5.2.4.2'
Gemfile.lock

railsアプリケーション作成

docker-compose run web rails new . --force --database=postgresql

railsプロジェクトに使用するデータベースの設定ファイルを修正

database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  # -------- 追加 --------
  host: db
  username: postgresql
  password: postgresql-pass
  # -------- ここまで --------

デタッチモード(バックグラウンド)で起動

docker-compose up -d

bundle installが反映されない場合の対応

docker-compose build --no-cache

データベース作成コマンド

docker-compose run web rails db:create

Scaffoldにて簡易的なアプリケーション作成

docker-compose run web bin/rails g scaffold User name:string
docker-compose run web bin/rails db:migrate

http://localhost:3000/users

コンテナの停止

docker-compose stop

コンテナ削除

docker-compose down

コンテナ内に移動

docker-compose run web bash

Go言語を使用してみる

ディレクトリ移動

cd doc/golang/
FROM golang:1.9

RUN mkdir /echo
COPY main.go /echo
CMD ["go", "run", "/echo/main.go"]

イメージのビルド

docker image build -t example/echo:latest .

イメージの確認

docker image ls

コンテナの起動

docker container run -d -p 9000:8080 example/echo:latest

GETリクエストの確認

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

投稿者のみアクセスできるようにする記述

開発環境

Ruby 2.6.5
Rails 6.0.3.3

問題点

投稿者のみレビューの削除ができるように実装したいが、NoMethodErrorが出てしまう

原因

当初、条件式を以下のように記述をしておりました。

<% if current_user.id == @review.user_id %>

こちらは現在ログインしているユーザーと投稿者が同じ場合、条件式がTrueとなり処理を実行するという記述です。
しかし、こちらの記述で読み込むとエラーが発生しておりました。

確認したところ正しくは以下のような記述でした。

<% if user_signed_in? && current_user.id == @review.user_id %>

こちらの記述でログインの有無を確認することにより正常に処理を行えるようになりました。
初歩的な箇所ですが、改めて気付いたので、備忘録として投稿いたしました。

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

Ruby × AWS Lambda × CloudWatch Eventsで定期実行プログラムを作成する

本記事で目指す構成

rubyLambdaClowdWach.png
CloudWatch EventsをトリガーにLambdaを起動し、Slackへメッセージを飛ばす。
※ CloudWatch EventsにはあらかじめCron式で実行スケジュールを設定しておく。

スクリーンショット 2020-10-10 17.25.07.png

↑こんな感じで自動で定期実行してくれるようになる。

対象読者

  • Rubyで定期実行プログラムを作成してみたい人
  • Lambdaに触れてみたい人

仕様

言語: Ruby2.5系
インフラ: Lambda、ClowdWatch Events

※ SlackBotは各自あらかじめ作成しておいてください。

参照: ワークスペースで利用するボットの作成

完成形

periodic-slack-bot-on-aws-lambda

※ 一から作るのが面倒な方は↑からgit cloneしてください。

プログラムを作成

Slackへメッセージを飛ばすためのプログラムを作成していく。

ディレクトリを作成

$ mkdir periodic-slack-bot-on-aws-lambda
$ cd periodic-slack-bot-on-aws-lambda

Gitを設定

$ git init
$ touch .gitignore
./.gitignore
.bundle
/vendor/bundle

Gitで管理したくないものを記述。

Rubyのバージョンを指定

$ rbenv local 2.5.1 
# 2.5系を推奨

Gemをインストール

$ bundle init

↑のコマンドでGemfileを生成し、以下のように編集する。

./Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

ruby '2.5.1'

gem 'async-websocket'
gem 'slack-ruby-bot'

その後、必要なGemをインストール。

$ bundle install --path vendor/bundle

※ 後ほどGemも含めてzipファイルにパッケージングする必要があるため、グローバルではなく「vendor/bundle」以下(ローカル)にインストールしなければならない。

app.rbを作成

$ touch app.rb
./app.rb
require 'slack-ruby-client'

Slack.configure do |conf|
  conf.token = ENV['SLACK_BOT_TOKEN']
end

def post_to_slack(event:, context:)
  client = Slack::Web::Client.new
  client.chat_postMessage(channel: ENV['SLACK_CHANNEL_NAME'], text: 'テスト送信 from AWS Lambda', as_user: true)
end

※ 引数に「(event:, context:)」を渡さないと動かないので注意。
参照記事: Python の AWS Lambda 関数ハンドラー

Lambdaへデプロイ

プログラムを作成したら、Lambdaへデプロイするための準備を行う。

AWS CLIをインストール

今回は「AWS CLI」と呼ばれるツールを使いながらデプロイしていくので、まだインストールできてないない場合はインストールしておく。

$ brew install awscli

IAMユーザーを作成

デプロイを行うためのIAMユーザーを作成していく。
スクリーンショット 2020-09-21 22.11.08.png
まずは「IAM」→「ポリシー」→「ポリシーの作成」へと進み、JSONタブから以下の文を貼り付ける。
スクリーンショット 2020-09-21 22.13.26.png

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "apigateway:*",
                "cloudformation:*",
                "dynamodb:*",
                "events:*",
                "iam:*",
                "lambda:*",
                "logs:*",
                "route53:*",
                "s3:*"
             ],
            "Resource": [
                "*"
            ]
        }
    ]
}

参照: Minimal Deploy IAM Policy

スクリーンショット 2020-09-21 22.15.47.png
適当にポリシー名や説明を記述し、「ポリシーの作成」をクリック。

スクリーンショット 2020-09-21 22.17.27.png
次に「IAM」→「ユーザー」→「ユーザーの作成」へと進み、適当な名前を付けた後「プログラムによるアクセス」にチェックを入れて次へ進む。

スクリーンショット 2020-09-21 22.17.47.png
「既存のポリシーを直接アタッチ」から先ほど作成した「MinimalDeployIAMPolicy」を選択し、次へ進む。

スクリーンショット 2020-09-21 22.17.59.png

(タグは任意でOK)最後に確認画面が表示されるので、問題無ければ「ユーザーの作成」をクリック。

スクリーンショット 2020-09-21 22.18.13_censored.jpg
すると「アクセスキーID」と「シークレットアクセスキー」の2つが発行されるので、csvファイルをダウンロードするなりメモするなり大事に保管しておく。

AWS CLIの設定

$ aws configure --profile lambda(任意のプロフィール名でOK)

AWS Access Key ID # 先ほど作成したアクセスキーID
AWS Secret Access Key # 先ほど作成したシークレットアクセスキー
Default region name # ap-northeast-1 
Default output format # json 

ターミナルで「aws configure」と打ち込むと対話形式で色々聞かれるので、それぞれ必要な情報を入力していく。

関数を作成

スクリーンショット 2020-10-10 18.12.38.png

AWSのコンソール画面から「AWS Lambda」を開き、「関数の作成」をクリック。

  • 関数名(任意の名前)
  • ランタイム(Ruby2.5)

を入力して関数を作成する。(その他はデフォルトの値もしくは空欄でOK)

スクリーンショット 2020-10-10 18.17.53.png

「環境変数の編集」から

  • SLACK_BOT_TOKEN
  • SLACK_CHANNEL_NAME

それぞれ環境変数をセット。

スクリーンショット 2020-10-10 18.23.04.png

「基本設定を編集」からハンドラ名を「app.post_to_slack(ファイル名.メソッド名)」に変更。

デプロイ

作成したプログラムをzipファイルとしてパッケージング。

$ zip -r function.zip app.rb vendor

aws cliを使いデプロイ。

$ aws lambda update-function-code --function-name PeriodicSlackBotOnAwsLambda --zip-file fileb://function.zip --profile lambda

{
    "FunctionName": "PeriodicSlackBotOnAwsLambda",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:**************:function:PeriodicSlackBotOnAwsLambda",
    "Runtime": "ruby2.5",
    "Role": "arn:aws:iam::**************:role/service-role/PeriodicSlackBotOnAwsLambda-role-**************",
    "Handler": "app.post_to_slack",
    "CodeSize": 12941753,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2020-10-09T21:08:11.888+0000",
    "CodeSha256": "**************/**************=",
    "Version": "$LATEST",
    "Environment": {
        "Variables": {
            "SLACK_CHANNEL_NAME": "#**************",
            "SLACK_BOT_TOKEN": "xoxb-**************-**************-**************"
        }
    },
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "********-****-****-****-********",
    "State": "Active",
    "LastUpdateStatus": "Successful"
}

※ --function-nameには先ほど作成した関数名をセット。

テスト実行

スクリーンショット 2020-10-10 18.34.15.png
「テストイベントの設定」から適当にテストを作成。(ハッシュは空欄でOK)

スクリーンショット 2020-10-10 18.38.29.png
「テスト」をクリックし、指定したSlackチャンネルへメッセージが飛んでいれば成功。
スクリーンショット 2020-10-10 18.40.17.png

CloudWatch Eventsでスケジュール管理

トリガーの設定

スクリーンショット 2020-10-10 18.43.36.png

「トリガーを追加」をクリック。

スクリーンショット 2020-10-10 18.47.40.png

  • トリガー名: CloudWatch Events
  • ルール: 新規ルールの作成
  • ルール名: test_30minutes(任意)
  • ルールの説明: 30分に1回実行(任意)
  • ルールタイプ: スケジュール式
  • スケジュール式: cron(*/30 * * * ? *)

※ 実行間隔はそれぞれお好みで設定。

cron式の書き方の説明については今回省略。
参照: クーロン(cron)をさわってみるお

スクリーンショット 2020-10-10 17.25.07.png
時間の経過を待ち、しっかりと定期実行されていれば成功。

スクリーンショット 2020-10-10 18.54.50.png

もし上手く行かなかった場合はCloudWatchのロググループ内にログが出力されているはずなので適宜デバッグ。

あとがき

お疲れ様でした!もし記事通りに進めて上手く動作しない箇所があったらコメント蘭などで指摘していただけると幸いです。

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

モンティホール問題をRubyで検証したら、よく理解できてよく解らなくなった話

モンティホール問題って何?

モンティホール問題とは、直感的に納得できない確率の問題です。
ネットで調べてもなんだかスッキリしなかったので自分で検証してみることにしました。

こちらのサイトでも概要は分かりますが、少しこのページでも解説します

モンティホール問題の概要

箱を選んだ段階で、はずれの箱を1つ開けてくれます。
その後選んだ箱を変えることができるとき、選択肢を変える・変えないのどうするのがお得かという問題です。

箱1 箱2 箱3
当たり はずれ はずれ

例えば、上のように3つある箱の中から箱2を選んだ状態で(まだ開けてはない)、ゲームの主催者がはずれの箱を開けたとします。
状況的には下のような感じ

箱1 箱2 箱3
当たり はずれ はずれ
選択中 OPEN

箱3がはずれと分かった時点で選択を変更できる場合に、変更すべきかどうか?という問題です。

直感的には当たり or はずれの2択なのでどちらを選んでも変わらなそうですが、数学的には次のようになります

そのまま 選択肢を変更
当たりの確率 33% 67%

これを最初見たとき「そんなバカな…」って思いました。
解説を見てもいまいち納得できなかったので、プログラミングを使って検証したらとても腑に落ちたので解説していきます。
(でも結局新たな謎が増えました)

さっそくコーディングして検証

Rubyを使って検証していきます。
(もう少しリファクタはできますが、やってることが分かりやすいようにあえて冗長に書いてる部分もあります。それでも少し分かりにくいかも…笑)

コード

ruby
# 試行回数(100万回くらいやれば十分でしょ)
number = 1_000_000

# 当たりとはずれを用意
win = 1
lose = 0

# 箱を用意
boxes = [win, lose, lose]

# ケース1(選択肢を変更しない場合)
count = 0
number.times do
  # ランダムでどの箱を選ぶか決定
  random_number = [0, 1, 2].sample
  selected_box = boxes[random_number]
  # はずれの箱を開ける(次に選択肢を変更しないのでこの処理は特に意味がない)
  random_number != 2 ? boxes.delete_at(2) : boxes.delete_at(1)
  # 選んだ箱のままで当たりかどうか判定
  count += 1 if selected_box == win
end
# 確率の計算
prob_1 = (count.to_f / number.to_f).round(5) * 100

puts "選択肢を変更しない場合の確率: #{prob_1}%"

# ケース2(選択肢を変更する場合)
count = 0
number.times do
  # 箱を選択
  random_number = [0, 1, 2].sample
  selected_box = boxes[random_number]
  # はずれの箱を開けて選んだ箱を変更
  if random_number == 0
    boxes.delete_at(2)
    selected_box = boxes[1]
  elsif random_number == 1
    boxes.delete_at(2)
    selected_box = boxes[0]
  else
    boxes.delete_at(1)
    selected_box = boxes[0]
  end
  # 判定
  count += 1 if selected_box == win
end
# 確率の計算
prob_2 = (count.to_f / number.to_f).round(5) * 100

puts "選択肢を変更した場合の確率: #{prob_2}%"


結果

ファイルを実行した結果です。
確かに直感が間違っていることがわかります
6222a43d3cc8b2ba6e6aba05f660e0b6.png

少し解説

この問題をRubyで解いてみて、そゆことかーと思ったことを解説していきます。
ポイントは最初に選んだ選択肢による場合分けです

箱を選んだ時点ではずれの箱を一つ教えてくれるわけですから、この問題は次の2パターンに場合分けできます。

  • 最初に当たりの箱を選んでいた場合
  • 最初にはずれの箱を選んでいた場合

それぞれの確率は当然次のようになります

最初に当たりの箱を選ぶ確率 最初にはずれの箱を選ぶ確率
33% 67%

では次にここから選択肢を変える・変えないの話をみていきます

選択肢を変えない場合

選択肢を変えない場合、最初に当たりの箱を選ばないといけません。
もともと当たりを引く確率が33%なので、この場合の当たりを引く確率は33%になります

選択肢を変える場合

選択肢を変える場合、最初に当たりの箱を選んでいたらはずれ、はずれを選んでいたら当たりになります。
つまり、最終的に当たりを引くためには最初にはずれの箱を選択する必要があります。
最初にはずれを選ぶ確率は67%ですので、この場合の当たりを引く確率は67%になります

少し疑問

ここまで解説してきましたが、書いてる途中で疑問も出てきました。
はずれの箱が開かれた段階で、別の参加者がやってきたとしましょう。

このとき、別の参加者から見たらどちらの箱が当たりかどうかは五分五分では?

この疑問を思いついたはいいが上手い説明ができないので、数学に詳しい方がいれば是非コメントしてください。

最後に

すこしモヤっとして終わってしまいましたが、面白い問題でした!
モンティホール問題は単純なのに奥が深いですね〜

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

アソシエーション(多対多)!!

アソシエーションって何??:information_desk_person_tone2:

簡単に説明するとモデルを利用したテーブル同士の関連付けのことですね。
テーブル同士で関連付けておき、一方のモデルからもう一方のモデルに
アクセスできるようにするためということですね。

*今回は、「多対多」のアソシエーションの説明になります。

関連付けを行う理由

Railsでは、「関連付け(アソシエーション: association)」とは2つの
Active Recordモデル同士の繋がりのことですね。

2つモデルの間には関連付けを行なう必要がありますが、
その理由を知っていますか?

それはですね、関連付けを行う事でコードの共通操作をより
シンプルで簡単にできるからなんです。

例:アソシエーション(多対多)

今回は、usersテーブルとroomsテーブルで説明したいと思います。

まずは、テーブルごとに、他テーブルとの関係性を考えましょう。

・ユーザーがどのチャットルームに属しているかを管理する
・チャットルームにどのユーザーが存在するかを管理する

今回の下記の画像になります。

多対多.png

ここで注目すべきポイントは、usersテーブルとroomsテーブルの関係性が、
今回のDB設計では「多対多」という関係性が存在していますね。
「多対多」は、関連するテーブルのidをお互いが複数持っている関係性のことです。

今回の場合は、「一人のユーザーは複数のチャットルームに所属する」
「一つのチャットルームには複数ユーザーが所属する」という場合の関係性ですね。

しかしですね、このテーブルの関係性を示すために、外部キーへ複数の値を
保存できれば簡単なのですが、1つのカラムに対して複数の値を保存することはできません。

よく考えられる方法としては、関連するidが増えるごとにカラムを増やすという
パターンがあります。
例としてわかりやすいように下記に画像を載せておきますね。

無駄なカラム.png

画像を見てもらえるとわかるのですが、カラムを不安と
無駄なカラムが生じていますね。

このようなDB設計は、関連するレコードが増えるほどカラムが増えるため、
良くない設計とされているものですね。

これを解決するために使用するのが、中間テーブルです。

中間テーブル

では、中間テーブルって何って思われると思うので簡単に説明します。
中間テーブルとは、その名の通り2つのテーブルの中間にあるテーブルのことです。

今回で言うと、usersテーブルとroomsテーブルの間に中間テーブルを作成します。

中間テーブルは、「多対多」の関係にある2つのテーブルの間に挟まって、2つの組み合わせパターンだけをレコードとして保存することですね。

また、2つのモデルのみでは「多対多」のアソシエーションを
組むことはできません。
そのため、中間テーブルを利用して「多対多」の関係を定義します。

中間テーブル.png

まだこれだけの説明ではイメージがあまりできてないかもしれないので、
次はSNSに例えて説明します。
次の説明からはテーブル名を変えていきますので、先ほどのテーブル名とは異なります。わかりにくかったら申し訳ないです。。

タグ付き写真投稿アプリを例に中間テーブルの役割を確認しましょう

『Instagram』などの、一つの写真に複数のタグ付けができる写真投稿アプリをイメージしてみて下さい。
『Instagram』のようにタグ付け機能を実現するには3つのテーブルが
必要です。タグを保存するテーブル、写真を保存するテーブル、
そして「どの写真にどのタグが登録されているか」を保存するテーブルです。

このうち写真やタグを保存するテーブルの名前はtagsテーブル、photosテーブルで良いでしょう。「どの写真にどのタグが ~」のテーブルは、photos_tagsテーブルとします。関係する2つのテーブルをアンダーバーで繋いだ名前です。

それぞれのテーブルに保存されているレコードの関係性は以下のようになります。

instagram.png

上記の画像の見てもらうとわかるのですが、中間テーブルには、「どの写真とタグが関連づいているか」という情報が記録されています。一つのレコードには「photo_id × tag_id」の組み合わせが記録され、すべての写真とタグの組み合わせの数だけ、レコードが蓄積されていきます。例えば、10個の写真にそれぞれ、異なるタグが3つずつ付いている場合、これらの関係性を表すための中間テーブルには30個のレコードが生成されることになります。

ここまでが、中間テーブルを用いて情報をDBに保存することの説明です。ここからは、中間テーブルを用いてアソシエーションを設定する方法を確認します。

中間テーブルを経由して「多対多」のテーブルへアソシエーションを組むには、これまで使用してきたhas_manyメソッドに、throughオプションを記述する必要があります。
throughオプションの説明を次にさせて頂きます。

throughオプション

has_manyメソッドのthroughオプションは、モデルに多対多の関連を定義するときに利用します。
throughという名前のとおり、「〜を経由する」という意味です。
もっとわかりやすく参考画像を載せておきます。

参考.png

throughオプションを使用し、多対多のアソシエーションを定義する場合は、それぞれのモデルに以下のような記述をします。

models/photo.rb
class Photo < ApplicationRecord
  has_many :photos_tags
  has_many :tags, through: :photos_tags
end
models/tag.rb
class Tag < ApplicationRecord
  has_many :photos_tags
  has_many :photos, through: :photos_tags
end
models/photos_tag.rb
class PhotosTag < ApplicationRecord
  belongs_to :photo
  belongs_to :tag
end

多対多の関係にある2つのテーブルのモデルでは、has_manyメソッドによる「1対多」のアソシエーションを互いに定義するのと合わせて、
throughオプションによって経由する中間テーブルを指定します。

一方、中間テーブルのモデルでは、belongs_toメソッドで多対多の関係にある2つのテーブルを指定します。

以上が、「多対多」のアソシエーションを定義する方法です。

まとめ

・「多対多」のアソシエーションには中間テーブルを用いること
・「多対多」の関係にある2つのテーブルのモデルでは、has_manyメソッド
による「1対多」のアソシエーションを互いに定義するのと合わせて、
throughオプションを使用して経由する中間テーブルを指定すること

今回の説明は、長くなってしまって説明がわかりにくくなっていたら、
すいません。。
私なりに頑張ってまとめたので参考になれば嬉しいです。

以上。

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

部分一致検索機能の実装をRansuckを使用せず行う

部分一致検索機能の実装をRansuckを使用せず行ったのでまとめていく。
今回は名前、メールアドレスを入力するとそれに部分一致で該当するユーザーの情報を取得できるという簡単な機能を作る。

例 
名前フォームに「の」 メールアドレスフォームに「  」で検索
             ↓
野比 のび太 nobinobi@example.com
が出力するみたいな感じ。にしたい。

結論からということでまずはコード載せますね。

routes.rb
Rails.application.routes.draw do
  root 'users#index'
  resources :users
end
index.html
 <h1>ドラえもんキャラを部分一致検索してみよう</h1>
<%= form_with(url: 'users', local: true, method: :get) do |f| %>
  <span class="small">検索対象: 名前 メールアドレス</span>
    <div>
      <%= f.label :name, '名前' %>
      <%= f.text_field :name , value: @search_params[:name] %>
    </div>
    <div>
      <%= f.label :email, 'メールアドレス' %>
      <%= f.text_field :email, value: @search_params[:email] %>
    </div>
  <%= f.submit '検索', class: 'btn btn-default' %>
<%end%>

<h1>検索結果</h1>
  <% @users.each do |user| %>
    <%= user.name %>
    <%= user.email %>
  <% end %>
users_controller.rb
class UsersController < ApplicationController
  def index
    @search_params = search_params
    @users = User.search(search_params)
  end

  private

  def search_params
    params.permit( :name, :email  )
  end
end
user.rb
class User < ApplicationRecord
  scope :search, -> (search_params) do
    return if search_params.blank?
      name_like(search_params[:name] )
      .email_like(search_params[:email])
    end
  scope :name_like, -> (name){where("name LIKE ?" ,"%#{name}%")}
  scope :email_like, -> (email){where("email LIKE ?", "%#{email}%")}
end
schema.rb
ActiveRecord::Schema.define(version: 2020_10_10_011424) do

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

seeds.rb
User.create!(name: "野比 のび太",email: "nobinobi@example.com")
User.create!(name: "剛田 武",email: "soulmate@example.com")
User.create!(name: "骨川 スネ夫",email: "kanemoti@example.com")
User.create!(name: "ドラえもん",email: "dora@example.com")
User.create!(name: "ドラミちゃん",email: "dorami@example.com")
User.create!(name: "源 しずか",email: "piano@example.com")

一個一個説明していきます。
まずviewの説明から

index.html
 <h1>ドラえもんキャラを部分一致検索してみよう</h1>
<%= form_with(url: 'users', local: true, method: :get) do |f| %>
  <span class="small">検索対象: 名前 メールアドレス</span>
    <div>
      <%= f.label :name, '名前' %>
      <%= f.text_field :name , value: @search_params[:name] %>
    </div>
    <div>
      <%= f.label :email, 'メールアドレス' %>
      <%= f.text_field :email, value: @search_params[:email] %>
    </div>
  <%= f.submit '検索', class: 'btn btn-default' %>
<%end%>

<h1>検索結果</h1>
  <% @users.each do |user| %>
    <%= user.name %>
    <%= user.email %>
  <% end %>

まずここなのですが

<%= form_with(scope: :search ,url: 'users', local: true, method: :get) do |f| %>

検索フォームはform_withを使用しています。form_tag と form_forとの違いは下記URLにわかりやすく説明されています。
https://qiita.com/hmmrjn/items/24f3b8eade206ace17e
(以前は関連するモデルがなかったときは form_tag 関連するモデルがある時は form_forを使用していましたがrails5.1で非推奨となっています。具体例として挙げるとUser情報を使いたいだけ(検索時)などはform_tag
User情報を登録したい→登録されるデータベースが存在している時はform_forを使用していた。)
rails5.1以降より推奨されているform_withはそこら辺の処理をうまい感じに切り分けて実行してくれます。またデフォルトで非同期通信のAjaxが実装されてます。今回はJSは使用しない実装なので、リモートフォームを無効にするため local: true と指定し、HTML形式でデータをmethod: get にて通信しています。

続いてコントローラーを見ていきましょう。

users_controller.rb
class UsersController < ApplicationController
  def index
    @search_params = search_params
    @users = User.search(search_params)
  end

  private

  def search_params
    params.permit( :name, :email  )
  end
end

ここでのみそは User.search(search_params) です。こちらのsearchメソッドなのですがUsermodelの方に部分一致検索機能をscopeしています。送られた値に対しsearchでnameとemailに部分一致処理をし返します。続いてmodelを参照してください。

user.rb
class User < ApplicationRecord
  scope :search, -> (search_params) do
    return if search_params.blank?
      name_like(search_params[:name] )
      .email_like(search_params[:email])
    end
  scope :name_like, -> (name){where("name LIKE ?" ,"%#{name}%")}
  scope :email_like, -> (email){where("email LIKE ?", "%#{email}%")}
end

searchスコープはこちらに飛んで一つ一つ部分処理をした後コントローラに返されます。
return if search_params.blank?
を入れておく事で仮に何も値がはいっていなくても処理をそこで止めてコントローラに返します。
検索対象が今回のように:name :emailと複数である場合はparamsを繰り返し処理とし一つ一つ検索を返すようにします。

今回は以上となります。
もしここは違うなどありましたどんどん指摘しください。

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

部分一致検索機能の実装をransuckを使用せず行う

部分一致検索機能の実装をransuckを使用せず行ったのでまとめていく。
今回は名前、メールアドレスを入力するとそれに部分一致で該当するユーザーの情報を取得できるという簡単な機能を作る。

例 
名前フォームに「の」 メールアドレスフォームに「  」で検索
             ↓
野比 のび太 nobinobi@example.com
が出力するみたいな感じ。にしたい。

結論からということでまずはコード載せますね。

routes.rb
Rails.application.routes.draw do
  root 'users#index'
  resources :users
end
index.html
 <h1>ドラえもんキャラを部分一致検索してみよう</h1>
<%= form_with(url: 'users', local: true, method: :get) do |f| %>
  <span class="small">検索対象: 名前 メールアドレス</span>
    <div>
      <%= f.label :name, '名前' %>
      <%= f.text_field :name , value: @search_params[:name] %>
    </div>
    <div>
      <%= f.label :email, 'メールアドレス' %>
      <%= f.text_field :email, value: @search_params[:email] %>
    </div>
  <%= f.submit '検索', class: 'btn btn-default' %>
<%end%>

<h1>検索結果</h1>
  <% @users.each do |user| %>
    <%= user.name %>
    <%= user.email %>
  <% end %>
users_controller.rb
class UsersController < ApplicationController
  def index
    @search_params = search_params
    @users = User.search(search_params)
  end

  private

  def search_params
    params.permit( :name, :email  )
  end
end
user.rb
class User < ApplicationRecord
  scope :search, -> (search_params) do
    return if search_params.blank?
      name_like(search_params[:name] )
      .email_like(search_params[:email])
    end
  scope :name_like, -> (name){where("name LIKE ?" ,"%#{name}%")}
  scope :email_like, -> (email){where("email LIKE ?", "%#{email}%")}
end
schema.rb
ActiveRecord::Schema.define(version: 2020_10_10_011424) do

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

seeds.rb
User.create!(name: "野比 のび太",email: "nobinobi@example.com")
User.create!(name: "剛田 武",email: "soulmate@example.com")
User.create!(name: "骨川 スネ夫",email: "kanemoti@example.com")
User.create!(name: "ドラえもん",email: "dora@example.com")
User.create!(name: "ドラミちゃん",email: "dorami@example.com")
User.create!(name: "源 しずか",email: "piano@example.com")

一個一個説明していきます。
まずviewの説明から

index.html
 <h1>ドラえもんキャラを部分一致検索してみよう</h1>
<%= form_with(url: 'users', local: true, method: :get) do |f| %>
  <span class="small">検索対象: 名前 メールアドレス</span>
    <div>
      <%= f.label :name, '名前' %>
      <%= f.text_field :name , value: @search_params[:name] %>
    </div>
    <div>
      <%= f.label :email, 'メールアドレス' %>
      <%= f.text_field :email, value: @search_params[:email] %>
    </div>
  <%= f.submit '検索', class: 'btn btn-default' %>
<%end%>

<h1>検索結果</h1>
  <% @users.each do |user| %>
    <%= user.name %>
    <%= user.email %>
  <% end %>

まずここなのですが

<%= form_with(scope: :search ,url: 'users', local: true, method: :get) do |f| %>

検索フォームはform_withを使用しています。form_tag と form_forとの違いは下記URLにわかりやすく説明されています。
https://qiita.com/hmmrjn/items/24f3b8eade206ace17e
(以前は関連するモデルがなかったときは form_tag 関連するモデルがある時は form_forを使用していましたがrails5.1で非推奨となっています。具体例として挙げるとUser情報を使いたいだけ(検索時)などはform_tag
User情報を登録したい→登録されるデータベースが存在している時はform_forを使用していた。)
rails5.1以降より推奨されているform_withはそこら辺の処理をうまい感じに切り分けて実行してくれます。またデフォルトで非同期通信のAjaxが実装されてます。今回はJSは使用しない実装なので、リモートフォームを無効にするため local: true と指定し、HTML形式でデータをmethod: get にて通信しています。

続いてコントローラーを見ていきましょう。

users_controller.rb
class UsersController < ApplicationController
  def index
    @search_params = search_params
    @users = User.search(search_params)
  end

  private

  def search_params
    params.permit( :name, :email  )
  end
end

ここでのみそは User.search(search_params) です。こちらのsearchメソッドなのですがUsermodelの方に部分一致検索機能をscopeしています。送られた値に対しsearchでnameとemailに部分一致処理をし返します。続いてmodelを参照してください。

user.rb
class User < ApplicationRecord
  scope :search, -> (search_params) do
    return if search_params.blank?
      name_like(search_params[:name] )
      .email_like(search_params[:email])
    end
  scope :name_like, -> (name){where("name LIKE ?" ,"%#{name}%")}
  scope :email_like, -> (email){where("email LIKE ?", "%#{email}%")}
end

searchスコープはこちらに飛んで一つ一つ部分処理をした後コントローラに返されます。
return if search_params.blank?
を入れておく事で仮に何も値がはいっていなくても処理をそこで止めてコントローラに返します。
検索対象が今回のように:name :emailと複数である場合はparamsを繰り返し処理とし一つ一つ検索を返すようにします。

今回は以上となります。
もしここは違うなどありましたどんどん指摘しください。

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

Trailblazerでcreateを試してみる

はじめに

抽象レイヤーを提供してくれるGem,Trailblazerの存在を知り勉強がてらにCreateを試してみた。
Trailblazerの概要については他の記事でわかりやすく説明していただいてるので割愛とする。

こちらの記事がとてもわかりやすかったです。
TrailBlazer概要まとめてみた

環境

ruby '2.7.2'
'rails', '~> 6.0.3'
template engine: haml

Create

Trailblazerのディレクトリ構成

.
└── todo
    ├── cell
    │   ├── index.rb
    │   └── new.rb
    ├── contract
    │   └── form.rb
    ├── operation
    │   ├── create.rb
    │   └── index.rb
    └── view
        ├── form.haml
        ├── index.haml
        └── new.haml

Operation

module Todo::Operation
  class Create < Trailblazer::Operation
    class Present < Trailblazer::Operation
      step Model(Todo, :new)
      step Contract::Build( constant: Todo::Contract::Form )
    end

    step Subprocess(Present) # present classのstep呼び出し
    step Contract::Validate(key: :todo) # contractを使ってバリデーション
    step Contract::Persist() # モデルに保存する
  end
end

Contract

module Todo::Contract
  class Form < Reform::Form
    include Reform::Form::ActiveModel
    property :title
    property :description
    property :completed

    validates :title, presence: true
    validates :description, length: { minimum: 2 }

    # overrideもできる
    # def title
    #  super.capitalize
    # end
  end
end

Cell

module Todo::Cell
  class New < Trailblazer::Cell
    include ActionView::RecordIdentifier
    include ActionView::Helpers::FormOptionsHelper
    include SimpleForm::ActionViewExtensions::FormHelper
  end
end

View

= simple_form_for(model, html: {novalidate: true}) do |f|
  = f.input :title, placeholder: "Title"
  %br
  = f.input :description, as: :text, placeholder: "Description"
  %br
  = f.submit 'Submit'
  = link_to 'Back', todos_path

確認

trab-todo

今回試したプロジェクト

参考記事

TrailBlazer概要まとめてみた
trailblazer

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

【Rails】deviseのviewやcontrollerの編集とカスタマイズ方法

はじめに

deviseのviewとcontrollerを編集、カスタマイズする方法

deviseを導入した後、見た目が味気なく英語ばかりのviewを整えたい。
deviseのcontrollerに編集を加えてカスタマイズする必要がある。
そんな時の方法を簡単に紹介します。

目次

1.前提
2.devise.rbの設定ファイルを変更
3.contorollerの生成・カスタマイズ
4.viewファイルの生成・編集
5.まとめ

開発環境

ruby 2.6.5
rails 6.0.0
devise 4.7.3

1.前提

devise導入済み
model生成済み
ルーティングは随所で設定

実装

それでは実装します。

2.devise.rbの設定ファイルを変更

config/initializers/devise.rbを開きます。config.scoped_viewsを有効にします。

config/initializers/devise.rb
  # ==> Scopes configuration
  # Turn scoped views on. Before rendering "sessions/new", it will first check for
  # "users/sessions/new". It's turned off by default because it's slower if you
  # are using only default views.
  config.scoped_views = true #←デフォルトはfalse#

Railsサーバを再起動します。
「gemを入れた時」などと同様で再起動が必要です。これを怠るとソースが反映されず「エラーは起きずないが、思い通りにならないという」原因を突き止めるのが難しい状況に陥ります。

3.controllerファイルの生成・編集

deviseのcontrollerを生成します。

$ rails g devise:controllers users

この時の公文は
 rails g devise:controllers モデル名
です。
今回はdeviseモデルをuserにしているので(大体userにしますが)その名前を使います。
app > controllers > users >
の下にファイルができます。
スクリーンショット 2020-10-10 11.27.44.png
例えば、ユーザー登録関連のcontrollerをカスタマイズしたければ、regisrations_controller.rbファイルを編集します。

ここで注意が必要なのが、ルーティングの設定です。
ルーティングで記述している

[コントローラー名]#[アクション名]
と実際の
[コントローラー]#[アクション]

が違うと、当たり前ですがうまく行きません。
なので、rails routes 等でしっかり確認する事をおすすめします。

4.viewファイルの生成・編集

deviseのviewファイルを生成・編集します。

$ rails g devise:views

app > views > devise >
devise直下にフォルダができます。

スクリーンショット 2020-10-10 11.04.57.png

例えば登録画面を編集したければ、regisrationsのディレクトリにあるファイルを編集します。

ここで注意するべきなのは、viewのファイル名です。
deviseではrenderやredirect_toで遷移先を指定しなければ、コントローラーの処理後メソッド名に遷移する様になってます。
なので、それぞれの名前を合わせる必要があります。

5.まとめ

contorollerの生成・カスタマイズとviewファイルの生成・編集を紹介しました。
deviseのcontrollerを使うと、デフォルトで持ってる機能を自動で使えると言うメリットがあります。
しかし、その反面に間違った記述をしてても、裏でdeviseが動いてくれるので、思い通りの挙動にならずエラーも出ないという状況にも陥ります。
その為にも、ルーティング、コントローラー、ビューがどうの様に辿ってきてるか注意する事をおすすめします。

最後に

私はプログラミング初学者ですが、自分と同じ様にエンジニアを目指す方々の助けになればと思い、記事を投稿しております。
それではまた次回お会いしましょう〜

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

「もっとプログラム脳を鍛える数学パズル」_Q41 (code:Ruby) -> Rust

「もっとプログラマ脳を鍛える数学パズル」をRustで書き直すのは、ボケ防止にちょうど良いかもしれない、と思った。

Q41:スタートメニューのタイル

こういう問題、どういう発想で考えてるんだろう。図形を対象とする問題は楽しい。

Ruby

q41_2.rb
W, H = 10, 10

@memo = {}
def search(tile)
    return @memo[tile] if @memo[tile]
    return 1 if tile.min == H

    pos = tile.index(tile.min)
    cnt = 0
    [[1,1],[2,2],[4,2], [4,4]].each do |px,py|
        check = true
        px.times do |x|
            if (pos + x >= W) || (tile[pos + x] + py > H)
                check = false
            elsif tile[pos + x] != tile[pos]
                check = false
            end
        end
        next if !check

        px.times do |x|
            tile[pos + x] += py
        end
        cnt += search(tile)
        px.times do |x|
            tile[pos + x] -= py
        end
    end
    @memo[tile.clone] = cnt
end

puts search([0] * W)

いやさすがに圧縮しすぎな気がする。アルゴリズムの図解が書籍中には無いので、変数名の適当さとデータ構造がむき出しであることが相まって理解が難しい。どういう判断によって、次の状態がどうなっていくのか、を図解しないと、再帰的探索のイメージがつかない。私だけかもしれないが。

Rust

main.rs
use std::collections::HashMap;

fn main() {
    let space_width = 10;
    let space_height = 10;
    let mut q41 = Q41 {
        memo: HashMap::new(),
    };
    let space = Space::new(space_width,space_height);
    println!(
        "{}",
        q41.patterns(&space, &vec![(1, 1), (2, 2), (4, 2), (4, 4)])
    );
}

struct Q41 {
    memo: HashMap<Space, u64>,
}

impl Q41 {
    pub fn patterns(&mut self, space: &Space, tiles: &Vec<(u64, u64)>) -> u64 {
        match self.memo.get(space) {
            Some(v) => *v,
            _ => {
                if space.is_filled() {
                    return 1;
                }
                let mut count = 0;
                for t in tiles {
                    if space.can_be_placed(t) {
                        let new_space = space.place(t);
                        count += self.patterns(&new_space.unwrap(), tiles);
                    }
                }
                self.memo.insert(space.clone(), count);
                return count;
            }
        }
    }
}

#[derive(Eq, Hash)]
struct Space {
    rows: Vec<u64>,
    width: usize,
}

impl PartialEq for Space {
    fn eq(&self, other: &Self) -> bool {
        self.rows == other.rows
    }
}

impl Space {

    pub fn new(width:u64, height:u64) -> Space {
        Space {
            rows: vec![0u64; height as usize],
            width: width as usize,
        }
    }

    pub fn height(&self) -> usize {
        self.rows.len()
    }

    pub fn min_value(&self) -> u64 {
        let mut min = std::u64::MAX;
        for i in 0..self.height() {
            if self.rows[i] < min {
                min = self.rows[i];
            }
        }
        min
    }

    pub fn index_of_min_value(&self) -> usize {
        let min_value: u64 = self.min_value();
        self.rows.iter().position(|&v| v == min_value).unwrap()
    }

    pub fn is_filled(&self) -> bool {
        for i in 0..self.height() {
            if self.rows[i] != self.width as u64 {
                return false;
            }
        }
        true
    }

    pub fn place(&self, tile: &(u64, u64)) -> Option<Space> {
        if !self.can_be_placed(tile) {
            return None;
        }

        let mut new_space: Space = self.clone();

        let py = self.index_of_min_value();
        for y in py..py + tile.1 as usize {
            new_space.rows[y] += tile.0;
        }

        return Some(new_space);
    }

    pub fn can_be_placed(&self, tile: &(u64, u64)) -> bool {
        let index_of_min = self.index_of_min_value();
        let (tw, th) = tile;
        if index_of_min + *th as usize > self.height() {
            // height over!
            return false;
        }
        for dh in 0..*th {
            let ih = index_of_min + dh as usize;
            if self.rows[ih] + tw > self.width as u64 || self.rows[ih] != self.rows[index_of_min] {
                return false;
            }
        }
        return true;
    }

    pub fn clone(&self) -> Space {
        Space {
            rows: self.rows.clone(),
            width: self.width,
        }
    }

    #[warn(dead_code)]
    pub fn to_string(&self) -> String {
        let mut str = String::new();
        for i in 0..self.height() {
            str.push_str(&self.rows[i].to_string());
            str.push_str(", ");
        }
        format!("rows=[{}],width={}",str, self.width)
    }
}

タイルを埋め込む「空間に直結したメソッド」があるので、そのメソッドをSpace構造体側に整理した。これだけでも、本体の処理patterns()は分かりやすくなったのではないか。このくらいの規模になってくると、設計の勉強としてちょうど良い。

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

【買付代行サービス個人開発 - No.010】会社の切替画面、会社の詳細画面、会社切替後は注文画面に行くようにする

Issue
PR

概要

会社の切替画面、会社の詳細画面、会社切替後は注文画面に行くようにする
Figmaに近づける

ToDoリスト

  • 会社の切替画面修正

  • 注文画面修正

  • 会社の詳細画面修正

ToDoリスト詳細

  • ロゴ追加
    app/assets/images/svg/sample.svg追加

  • 会社の切替画面修正

app/views/orgs/index.html.slim
= render(::Layout::NavbarComponent.new(org: @org, tab: false))
.py-10.px-8
  header
    .max-w-7xl.mx-auto.px-4.sm:px-6.lg:px-8
      h1.text-3xl.font-bold.leading-tight.text-gray-900
        | 所属会社一覧
  main
    .max-w-7xl.mx-auto.sm:px-6.lg:px-8
      .px-4.py-8.sm:px-0
        .border-4.border-dashed.border-gray-200.rounded-lg.h-96
          .bg-white.shadow.sm:rounded-md.my-16.m-auto(class="w-3/4")
            ul
              - @orgs.each do |org|
                / TODO:Sassで場合分けできるようにする
                - border_t = org == @orgs.first ? '' : 'border-t border-gray-200'
                li(class=border_t)
                  = link_to orders_ordering_org_sides_path, class: 'block hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out' do
                    .flex.items-baseline.px-4.py-4.sm:px-6
                      .min-w-0.flex-1.flex.items-baseline
                        .min-w-0.flex-1.px-4.md:grid.md:grid-cols-3.md:gap-4
                          div.align-middle
                            .text-xl.font-medium.text-indigo-600.truncate.leading-10
                              = org.name
                          div
                            .text-xs.text-gray-500
                              | 会社タイプ
                            .text-lg.leading-5.font-medium.text-gray-900.truncate
                              / TODO: ENUM化する
                              | 買付会社
                          div
                            .text-xs.text-gray-500
                              | オーナー
                            .text-lg.leading-5.font-medium.text-gray-900.truncate
                              / TODO: Userモデルの紐付けをしたら、呼び出すようにすする
                              | 山田 太郎
                      div
                        i.fas.fa-sign-in-alt.fa-lg.text-gray-600
  • 注文画面修正
app/views/orders/ordering_org_sides/index.html.slim
.bg-gray-100
  = render(::Layout::NavbarComponent.new(org: @org, tab: true, active: :order_ordering_org_sides))
  / ...略
  • 会社の詳細画面修正
app/views/orgs/show.html.slim
= render(::Layout::NavbarComponent.new(org: @org, tab: true))
.bg-white.shadow.overflow-hidden.m-auto.mt-16.sm:rounded-lg(class="w-1/2")
  .px-4.py-5.border-b.border-gray-200.sm:px-6
    h3.text-lg.leading-6.font-medium.text-gray-900
      |  会社詳細
    p.mt-1.max-w-2xl.text-sm.leading-5.text-gray-500
      |  会社の詳細を説明します
  .px-4.py-5.sm:p-0
    dl
    .sm:grid.sm:grid-cols-3.sm:gap-4.sm:px-6.sm:py-5
      dt.text-sm.leading-5.font-medium.text-gray-500
        |  会社名
      dd.mt-1.text-sm.leading-5.text-gray-900.sm:mt-0.sm:col-span-2
        = @org.name
    .mt-8.sm:mt-0.sm:grid.sm:grid-cols-3.sm:gap-4.sm:border-t.sm:border-gray-200.sm:px-6.sm:py-5
      dt.text-sm.leading-5.font-medium.text-gray-500
        | 会社タイプ
      dd.mt-1.text-sm.leading-5.text-gray-900.sm:mt-0.sm:col-span-2
        / TODO:enum化する
        | 買付会社
    .mt-8.sm:mt-0.sm:grid.sm:grid-cols-3.sm:gap-4.sm:border-t.sm:border-gray-200.sm:px-6.sm:py-5
      dt.text-sm.leading-5.font-medium.text-gray-500
        | オーナー
      dd.mt-1.text-sm.leading-5.text-gray-900.sm:mt-0.sm:col-span-2
        | 山田太郎

動作確認

準備

bin/rails db:migrate:reset
bin/rails db:reset

受入基準

  • デザインがFigmaぽくなっている
  • 画面遷移が下図のようになっている。会社の切替後は注文一覧画面に行くようになっている 9ab474f7beae054adcf79949e0375703 # ドロップダウンのJSは後日実装予定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ActiveStorageを用いた画像複数枚投稿のエラー

エラー【undefined method `to_model'】

フリマアプリの開発中、以下のようなエラーが出ました。
Can't resolve image into URL: undefined methodto_model' for #<ActiveStorage::Attached::Many:0x00007fb7ffa59fb0>
Did you mean? to_yaml

スクリーンショット 2020-10-10 11.25.10.png

現状

  • ActiveStorageというGemをを用いて画像保存を可能にしている。
  • 1つの商品につき複数の画像の投稿を可能にした。←今ここ

エラーの内容

to_modelというメソッドは定義されていないよーって言われてます。

以下のように、保存した画像を表示させたい時にエラーがでました。
スクリーンショット 2020-10-10 12.00.19.png

問題があったコード

<%= image_tag @item.images, class: 'buy-item-img' %>

解決したコード

<%= image_tag @item.images[0], class: 'buy-item-img' %>

一つの商品に複数枚画像があるため、どの画像を表示させるかを記述する必要があります。
そうじゃなかったらどの画像を表示するのか判断できないですもんね?

他の方の記事などを見てみると、
@item.images.urlと記述すれば解決することもあったそうです。
参考になれば、と思います!

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

Ruby と Python で解く AtCoder ABC178 D 動的計画法

はじめに

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

今回のお題

AtCoder Beginner Contest D - Redistribution
Difficulty: 830

今回のテーマ、動的計画法

いわゆる典型的な動的計画法の問題です。
前の記事 - Qiita で取り上げました、AtCoder Beginner Contest E - Crested Ibis vs Monster では、目標値を超えてた場合もケアしないといけませんが、今回はジャストでよいので、その分少し簡単です。

Ruby

ruby.rb
s = gets.to_i
dp = Array.new(s.next, 0)
dp[0] = 1
s.times do |i|
  next if dp[i].zero?
  3.upto(s) do |x|
    if i + x <= s
      dp[i + x] += dp[i]
      dp[i + x] %= 1000000007
    else
      break
    end
  end
end
puts dp[s]
ruby.rb
    if i + x <= s
      dp[i + x] += dp[i]
      dp[i + x] %= 1000000007
    else
      break
    end

今回はジャストでよいので、i + x <= sについてのみdpを加算します。

Python

python.py
from sys import stdin

def main():
    input = stdin.readline

    s = int(input())
    dp = [0] * (s + 1)
    dp[0] = 1
    for i in range(s):
        if dp[i] == 0:
            continue
        for x in range(3, s + 1):
            if i + x <= s:
                dp[i + x] += dp[i]
                dp[i + x] %= 1000000007
            else:
                break
    print(dp[s])
main()

PyPy は凄く速いですね。

Ruby Python PyPy
コード長 (Byte) 244 405 405
実行時間 (ms) 284 509 70
メモリ (KB) 14400 9060 64596

まとめ

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

参照したサイト
Ruby と Python で解く AtCoder ABC153 E 動的計画法 - Qiita

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

Railsで本番環境(EC2, AmazonLinux)でPDF出力ができない

本番環境(EC2, AmazonLinux)でPDF出力ができない

wicked_pdfwkhtmltopdf-binaryというGemを使ってPDF出力機能を付けていますが、開発環境ではうまく行くのに本番環境では下記のエラーが発生。
Railsのバージョンは4.2です。

RuntimeError (Failed to execute:
["/var/www/~~~/shared/bundle/ruby/2.4.0/gems/wkhtmltopdf-binary-0.12.6.3/bin/wkhtmltopdf", "--encoding", "UTF-8", "--page-size", "A4", "file:////tmp/wicked_pdf20201007-11835-gppxfs.html", "/tmp/wicked_pdf_generated_file20201007-11835-j83wu8.pdf"]
Error: PDF could not be generated!
 Command Error: /var/www/~~~/shared/bundle/ruby/2.4.0/gems/wkhtmltopdf-binary-0.12.6.3/bin/wkhtmltopdf_centos_7_amd64: error while loading shared libraries: libpng15.so.15: cannot open shared object file: No such file or directory
):

libpng15.so.15というライブラリがない様ですが、同様のエラーをネットで調べて色々と試しましたが解決しませんでした。

Gemを変更し解決

2016年の記事ですが、下記が参考になりました。
https://qiita.com/s-mori/items/00aef46e6a10499f8254
https://qiita.com/yaboojp/items/526c9397070ca5d05256

wkhtmltopdf-binaryはAmazonLinuxに対応していない様で、
AmazonLinuxに対応しているwkhtmltopdf-binary-amlを使うことでうまくいきました。

修正前のGemfile

gem 'wicked_pdf'
gem 'wkhtmltopdf-binary-aml'

修正後のGemfile

gem 'wicked_pdf'
gem 'wkhtmltopdf-binary-aml', git: 'https://github.com/insphire/wkhtmltopdf-binary-aml'

修正前のwicked_pdf

config/initializers/wicked_pdf.rb
WickedPdf.config = {
  :exe_path => "#{Gem.loaded_specs['wkhtmltopdf-binary'].full_gem_path}/bin/wkhtmltopdf"
}

修正後のwicked_pdf

config/initializers/wicked_pdf.rb
WickedPdf.config = {
  :exe_path => "#{Gem.loaded_specs['wkhtmltopdf-binary-aml'].full_gem_path}/bin/wkhtmltopdf"
}

Bundlerのバージョンが変更されない様にして、bundle installしました。
$ bundle _1.16.1_ install

日本語表示に対応させる

本番環境にデプロイすると、日本語表示がされていませんでした。
なのでIPAフォントを本番サーバでインストールします。

cd /usr/share/fonts

$ yum install -y ipa-gothic-fonts ipa-mincho-fonts

フォントを変更したことでレイアウトが崩れてしまったので、CSSなどを整えて、領収書機能を完成させることができました。

開発環境でのエラー

AmazonLinux対応のGemに変更したことで、今度は開発環境でエラーが起こる様になってしまいました。

RuntimeError - PDF could not be generated!
 Command Error: /Users/~~~/vendor/bundle/ruby/2.4.0/bundler/gems/wkhtmltopdf-binary-aml-e5340ed88aa8/bin/wkhtmltopdf:15:in `exec': Bad CPU type in executable - /Users/~~~/vendor/bundle/ruby/2.4.0/bundler/gems/wkhtmltopdf-binary-aml-e5340ed88aa8/libexec/wkhtmltopdf-darwin-x86 (Errno::E086)

開発環境でも本番環境でもうまくやるには、wicked_pdf.rbif Rails.env.production?などを使ってかき分けたり、
Gemfileを以下の様にして環境別に切り替えればできます。

group :development do
 gem 'wkhtmltopdf-binary'
end
gem 'wkhtmltopdf-binary', group: :development
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

絵文字を日本語に変換する gem

Google は絵文字で検索できる……!

image.png

絵文字を日本語に訳してくれるライブラリ

日本語 -> 絵文字 は事例がありました。
日本語で絵文字入力するための IME 追加辞書を公開しました

ですがその逆が見当たらなかったので作ってみました。
https://github.com/d-mato/emoja

Emoja.translate("?食べたい")
# => "赤リンゴ食べたい"

翻訳結果を MeCab に投げれば形態素解析をしてテキストマイニングもできるし、 ElasticSearch に投げれば全文検索もできそうです。

日本語 -> 絵文字 にも一応対応

用途が思いつかないけど、日本語から絵文字のサジェストも可能です。

Emoja.search("猫")
# => ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?", "?"] 

絵文字辞書

こちらのリポジトリにある emoji_ja.json を活用させていただいております:bow:
https://github.com/yagays/emoji-ja


みなさまのお遊びプロジェクトで使ってもらえたら幸いです!

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

Dockerと仮装サーバーとコンテナについて

Docker!仮想サーバー!コンテナ!について

Dockerとは

コンテナ化を用いてアプリケーションを開発・配置・実行するためのオープンソースソフトウェアのこと!

bit201712121505387423.jpg

仮想サーバーとは

1台のサーバーで仮想的に複数のサーバーを稼働させる仕組みのこと!

コンテナとは

他のユーザーから隔離された実行環境のこと。仮想サーバーに比べて起動時間が短く、同じ性能のハードウェアであれば、より多くのコンテナを同時に動かすことができる!

デプロイの種類について

カナリアリリースとは

一部のユーザーにのみ新機能の公開を行い、新しいバージョンにバグなどがないかを検証する手法のこと。サービスに不具合があっても全体に影響しませんよ!というもの。

ブルーグリーンデプロイメントとは

仮想サーバーを用い、2つの本番環境を用意し、それぞれバージョンを設定することができるデプロイの運用方法のこと。2つ用意するからどちらかに不具合が生じた場合どちらかにリクエストの方向を変える事ができる為、その間に不具合が生じた方をロールバックする事ができる!

イミュータブルデプロイメントとは

常に変更を行わない環境を構築することで、デプロイ時に新しい環境に切り替える手法のこと。古い環境を消去しますよというもの!

Docker公式 http://docs.docker.jp/

まとめ

Dockerについて概要を知るkとができましたね。おまけでクラウドの種類もよく使われるので載せておきます。

おまけ

SaaS

「Software as a Service」の略で、「サース」または「サーズ」と読む。
クライアント側に導入せずに、サービスを提供しているサーバーに直接アクセスをしてサービスを利用する状況を指す!
(例)
Microsoft Office 365などのオフィスソフト
GmailなどのWebメール
Dropboxなどのオンラインストレージ
サイボウズなどのグループウェア

PaaS

「Platform as a Service」の略で、「パース」と読む。
作成したアプリケーションなどを、ネットワーク上に公開するためのプラットフォームを提供するサービスのこと!
(例)
Heroku

IaaS

「Infrastructure as a Service 」の略で、「イアース」や「アイアース」と読む。
サービスを利用するユーザーが、仮想化をしたCPUやメモリ、ストレージなどをインターネット経由で提供するサービスのこと!
(例)
Microsoft Azure
Google Compute Engine

現場からは以上です!

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

[Ruby on rails] いいね機能の実装

はじめに

投稿アプリで他のユーザーがいいねを出来るようにいいね機能を実装。
usersテーブルとpostsテーブルとlikesテーブルがあるものとします。

アソシエーション

まずは、各テーブルの関係性を考え、アソシエーションを定義します。
ユーザー(1):いいね(多)
投稿(1):いいね(多)
1人1投稿に1回のいいねまでにしたいのでバリデーションもかけます。

like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post

  validates_uniqueness_of :post_id, scope: :user_id

end

投稿が削除されたされた場合いいねも削除。

post.rb
has_many :likes, dependent: :destroy
user.rb
has_many :likes, dependent: :destroy

def already_liked?(post)
  self.likes.exists?(post_id: post.id)
end

コントローラー実装

likes_controller.rb
class LikesController < ApplicationController

  def create
    @like = current_user.likes.create(post_id: params[:post_id])
    redirect_back(fallback_location: root_path)
  end

  def destroy
    @post = Post.find(params[:post_id])
    @like = current_user.likes.find_by(post_id: @post.id)
    @like.destroy
    redirect_back(fallback_location: root_path)
  end

end

ルーティングの設定

routes.rb
resources :posts do
  resource :likes, only: [:create, :destroy]
end

post_likes
DELETE /posts/:post_id/likes(.:format) likes#destroy
POST /posts/:post_id/likes(.:format) likes#create

viewの実装

~.html.erb
<% if current_user.already_liked?(post) %>
  <%= link_to post_likes_path(post), method: :delete do %>
    <i class="fas fa-heart"></i>
  <% end %>
<% else %>
  <%= link_to post_likes_path(post), method: :post do %>
    <i class="far fa-heart"></i>
  <% end %>
<% end %>
<%= post.likes.count %>    //いいねの数を表示

すでにcurrent_userがいいねしているか?
trueであればいいねを解除して、falseであればいいねをする。

最後に

いいねの実装方法は他にも非同期での実装などもあります。
まだまだ勉強中ですが、色々な技術を勉強し出来ることを増やしていきたいと思います。
最後まで読んでいただきありがとうございます:grin:

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

【Rails】Nginx, Puma な環境によるデプロイ&サーバーの勉強【AWS EC2】

はじめに

個人開発アプリを
WEBサーバー:Nginx
アプリケーションサーバー:Puma
を利用し、AWS EC2にデプロイしました。

Ruby 2.5.3
Rails 5.2.4.2
MacOS:Catalina 10.15.6

AWSの設定は既出の優れた記事通りで理解を進めながらOKだったのですが、駆け出したばかりである為、特にNginxの設定(nginx.conf等)が難しく感じ、幾つものエラーを乗り越え、試行錯誤してクリアしました。

最近はPumaをアプリケーションサーバーに選ぶ方が多いかと思います。

デプロイで必要になるnginx.conf,yourapp.confの設定2つを載せている記事がなかった為、誰かのためになるかと思い作成しました。

エラーに躓き、調べることでサーバーへの理解が深まったのでよしとしますが、恐らくチャレンジする皆さまも同じところで躓きそうなので、自分の場合はこのような設定でデプロイ出来ましたよ、という体で記事にしたいと思います。

1 NginxとPumaについて整理

自分自身、復習の意味を込めて両者について整理します(相違点があれば是非ご教示願います!)。

Nginx】...WEBサーバー。ブラウザからのコンテンツリクエストを受け取り、ブラウザにレスポンスする。静的コンテンツ(画像など)のリクエストの場合、WEBサーバーが処理する。動的コンテンツはアプリケーションサーバーが担当する。WEBサーバーの代表はApache,Nginxなど

Puma】...アプリケーションサーバー。NginxがWEBサーバーで静的コンテンツ処理に対し、こちらはNginxでは処理できない動的コンテンツを捌く。WEBサーバーからリクエスト→アプリケーションサーバー受け→Railsアプリケーションの処理結果を、WEBサーバーに返す。代表例としてUnicorn,Pumaなど。現在RailsはPumaをデフォルトのアプリケーションサーバーとして採用している。

PumaとUnicornの選択ですが前者はマルチスレッド、後者はマルチプロセスの違いがあるようです。Pumaの方がより多くのリクエストを効率的に捌ける利点があるようですが、通常使用ではさほど変わりはないという意見もあります。私はRailsで推奨されているPumaを選択しました。

2 puma.rbの設定

次にpuma.rbの設定です。以下が私の設定です。

puma.rb(local)
# アプリケーションディレクトリ
app_dir = File.expand_path("../../", __FILE__)
# ソケット通信を図る為bindでURI指定
bind "unix://#{app_dir}/tmp/sockets/puma.sock"
# PIDファイル所在(プロセスID)
pidfile "#{app_dir}/tmp/pids/puma.pid"
# stateファイルはpumactlコマンドでサーバーを操作する。その所在。
state_path "#{app_dir}/tmp/pids/puma.state"
# 標準出力/標準エラーを出力するファイルの所在。
stdout_redirect "#{app_dir}/log/puma.stdout.log", "#{app_dir}/log/puma.stderr.log", true
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
#port       ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
plugin :tmp_restart

portは使用しない為、コメントアウトしました。

PumaとNginxはソケット通信させる為、2行目のbindで指定します。
bindでは下記のようにURIでパスを指定します。

bind "unix://#{app_dir}/tmp/sockets/puma.sock"

3 nginx.confの設定

Nginxは主に2つのファイルを変更します。
1 nginx.conf...Nginx自体の設定ファイル
2 yourapp.conf...アプリケーション毎のNginx設定ファイル

下記が私のnginx.conf設定です

(EC2)etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

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

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    include /etc/nginx/conf.d/*.conf;

    index   index.html index.htm;

    upstream puma {
        server unix:///var/www/rails/yourapp/shared/tmp/sockets/puma.sock;
    }
    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  Elastic IP(URL);
        root         /var/www/rails/yourapp/current/public;

        location / {
            try_files $uri $uri/index.html $uri.html @webapp;
        }

        location @webapp {
            proxy_read_timeout 300;
            proxy_connect_timeout 300;
            proxy_redirect off;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://puma;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
    server {
        listen 443 ssl;
        server_name Elastic IP(URL);
        location @webapp {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://puma;
      }
    }
}

4 yourapp.confの設定

yourapp.confとしていますが、yourappにはあなたが作っているアプリケーション名が入ります。

(EC2)/etc/nginx/conf.d/yourapp.conf
  # log directory
  error_log  /var/www/rails/yourapp/shared/log/nginx.error.log;
  access_log /var/www/rails/yourapp/shared/nginx.access.log;
  upstream app_server {
    # for UNIX domain socket setups
    server unix:/var/www/rails/yourapp/shared/tmp/sockets/yourapp-puma.sock fail_timeout=0;
  }
  server {
    listen 80;
    server_name ;
    # nginx so increasing this is generally safe...
    # path for static files
    root /var/www/rails/yourapp/current/public;
    # page cache loading
    try_files $uri/index.html $uri @app_server;
    location / {
      # HTTP headers
      proxy_pass http://app_server;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
    }
    # Rails error pages
    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /var/www/rails/yourapp/current/public;
    }
    client_max_body_size 4G;
    keepalive_timeout 5;
  }

私がデプロイする際に参考にした記事を載せます。

Rails5+Puma+Nginxな環境をCapistrano3でEC2にデプロイする(後編)

Rails5アプリケーションのAWSによるネットワーク構築 Nginx+Puma+Capistranoな環境とAWS構築(VPC EC2 RDS CloudFlont Route53 etc)

インフラ初心者がNginx+PumaのRails5アプリケーションをCapistrano3でデプロイした話

所感

サーバーの扱いに慣れていなかったため、EC2へのデプロイに当たってAWS等の設定よりも、上記NginxとPumaの設定に苦慮した次第です。最終的にCapistrano、CircleCIを利用してAWS EC2にデプロイすることができました。
CDについては先人たちの記事通りに進めていけば難なくできるかと思います。
エラーが多発している時は参りますが、新機能の実装やエラー解決できた時の嬉しさが勝るのでやめられません!

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