20200323のRailsに関する記事は19件です。

チャットアプリにAWSのS3(ストレージサービス)を導入

チャットアプリにストレージサービスを導入します。

進める上での注意点

手順を間違えると悪用され、高額請求される可能性があります。

S3を使用する上でどうなると被害が出るのかというと
悪意を持ったユーザーにAWSのIDとパスワードが漏れてしまうことで被害が発生!

AWSは従量制のサービスのため、他人がなりすましでログインして膨大な処理を行うと、それに対する支払いが発生してしまう(^◇^;)

今回は下記の3つの手段を取ります。

二段階認証

IAMユーザーの利用

git-secrets

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

Railsにユーザーフォロー機能を実装する

何をしたいのか.

  • twitterに見られるような,あるユーザーが,あるユーザーをフォローするという機能を実装する時,DBのテーブルはどういった構成にしたらいいのか
  • それをrailsで実現するにはどうしたらいいのか

を,このページ(https://umaweb1.hatenablog.com/entry/2019/10/26/120623) を参考にやってみて, 詳しく手順を書いたのが, 本記事である.

それでは早速, 実装開始

  • あるユーザーが, あるユーザーをフォローするのだから,userAテーブルとuserBテーブルが必要...なんだけど
  • userテーブルは2つ作ると複雑になりそう...
  • なので, userテーブルは1つなんだけど, その1つのテーブルにフォローする側のユーザーと, フォローされる側のユーザーの役割を持たせる!
    • 一見難しそうだけど, 以下を読んでいけば大丈夫だと思います.

テーブル構成を考える

  • テーブルは2つ
    • users テーブルと
    • relationships テーブルを作成する.
  • relationshipsテーブルのカラムを user_id, follow_idにするが, followsテーブルは作成しない. あくまでusersテーブルとrelationshipsテーブルの2つで完結させる!

modelの作成

  • $ rails generate model user name:string
  • $ rails generate model relationship

  • この時,relationshipの migrationファイルには, timestampしか書かれていない.(何も指定せずgenerate modelしたので)

image.png

  • relationshipの migrationに以下を追記
def change
  create_table :relationships do |t|
    t.references :user, foreign_key: true
    t.references :follow, foreign_key: { to_table: :users }

    t.timestamps

    t.index [:user_id, :follow_id], unique: true
  end
end

それぞれの意味

  • t.references :user, foreign_key: true

    • => 外部キーを設定する(true)といえば, idに対して設定する(多分). なので,(仮想的)カラム名は user_id になる.
  • t.references :follow, foreign_key: { to_table: :users }

    • => 外部キーをusersテーブルに求め, followカラムを設定する.(かつ 外部キーを設定するので. (仮想的)カラム名は follow_id になる(多分))
  • ※外部キーを設定する = 外部キー側のテーブルに存在しない値, を入れようとすると, エラーになるという事.

migration実行

  • $ rails db:migrate
  • この状態で, relationshipsのテーブル内はどういったカラムができているかを確認
  • user_id, follow_id のカラムができている.

image.png

  • この状態で, user テーブルにデータを入れてみる(modelへの記述を反映する為, sqlじかにではなく, rails上で)
    • $ rails console
    • $ User.create(name: 'name_1')

image.png

  • relationshipsテーブル(モデル)の中を見てみる
    • 何も入っていない

image.png

  • relationshipテーブルにデータを入れてみる
  • $ Relationship.create(user_id: 1)
  • user_id 1のデータが入った

image.png

  • もう一度relationshipテーブルにデータを入れてみる
  • $ Relationship.create(user_id: 2)
  • エラー. user_id 2などないから,(userテーブルにid 2 のデータがないから) insertできないと言っている.
  • userテーブルとrelationshipテーブルが, 連動している(relationshipテーブルが, userテーブルを参照している)ことが確認できた

image.png

  • 続いてfollow_idも確認してみる
  • 予想: relationshipテーブルのfollow_idには, 2は入れられない.(userテーブルにid 2 のデータがないので)
  • まず, $ Relationship.create(follow_id: 1)
  • 入った.

image.png

  • 続いて, $ Relationship.create(follow_id: 2)
  • エラー. 望んでいる挙動なので問題なし!

image.png

  • この時, userテーブルのid 1 のデータをdeleteすると, relationshipテーブル側の, user_id 1のデータは, 連動して消えるか.
  • $ User.destroy.find(1)
  • そもそも, 外部キーを設定しているので, deleteできない, という挙動になった.

image.png

  • user modelに, 以下を追記してみる
  • has_many :relationships, dependent: :destroy

image.png

  • もう一度, $ User.destroy.find(1)
  • 同じく, 外部キーを設定しているので, deleteできないとエラー

image.png

  • relationshipsテーブルのデータを消してから
    • usersテーブルのデータは消せるか.
  • $ Relationship.find(1).destroy
  • $ Relationship.find(2).destroy
  • $ User.find(1).destroy
  • deleteできた.
  • 外部キーを設定されているテーブルは, 該当データが他テーブルに使用されていると, そのデータを勝手に削除できない様子.

image.png

アソシエーションを設定する

  • アソシエーションとは : 連動してdelete などする挙動のこと(だいぶざっくり).
  • それぞれの関係を定義
    • user : relationshipの関係 = 1 : n
    • relationship belongs_to user
  • follow : relationshipの関係 = 1 : n
    • relationship belongs_to follow
  • 同時にvalidateも記述しておく
    • user_idは必須
    • follow_idは必須
  • 以上をrelationshipモデルに, 以下のように書いてみる.(followは架空のテーブルなので, 実際はuserテーブル(モデル)を使う)
class Relationship < ApplicationRecord
  belongs_to :user
  belongs_to :follow, class_name: 'User'

  validates :user_id, presence: true
  validates :follow_id, presence: true
end
  • この状態で, Relationshipに, 片方だけ(user_idだけ)データを入れてみる.
  • $ Relationship.create(user_id: 2)
  • おかしい, insertできてしまった.

image.png

  • irbを再起動してみる
  • 先程, user_id: 2が入ってしまったので, follow_idでやってみる
  • $ Relationship.create(follow_id: 2)
  • エラーが出た.(望んでいる挙動)(思ったのと違う挙動になるときは, 再起動してみよう)

image.png

  • user_id, follow_id どちらも指定して入れてみる
  • $ Relationship.create(user_id: 2, follow_id: 2)
  • 入った.
  • validatesの presenceが効いているのが確認できた.

image.png

  • userテーブルのmodelにアソシエーションを書いてみる
  • has_many :relationships, dependent: :destroy を追記
  • そして, userテーブルのデータをdeleteしてみる
  • $ User.find(2).destroy
  • usersテーブルのレコードが消え, 連動してrelationshipsテーブルの該当レコードも消えた.
  • relationshipsテーブルは, usersテーブルに外部キーを貼っているのに, なぜ消せたのか
    • => 外部キーを貼っていても, アソシエーションで主従の関係を作り(has_many, belong_to), かつ, dependent: :destroy しているなら, 連動して消える様子.
    • (普通は, 外部キーを設定されているテーブルは, 該当データが他テーブルに使用されていると, そのデータを勝手に削除できない(FOREIGN KEY constraint failedエラー: 外部キー制約))

image.png

userテーブルのmodelに, さらにアソシエーションを書いてみる

  • has_many :followings, through: :relationships, source: :follow を追記

image.png

  • $ User.find(4).followings で, 以下が得られた(usersテーブルのデータが返ってくる)
[#<User id: 4, name: "name_1", created_at: "2020-03-18 11:33:05", updated_at: "2020-03-18 11:33:05">,
#<User id: 5, name: "name_2", created_at: "2020-03-18 12:00:39", updated_at: "2020-03-18 12:00:39">]
  • userを特定し, followingsを指定すると
  • relationshipsテーブルを通して
  • そのテーブル(relationshipsテーブル)のfollow_idに該当する
  • userテーブルのレコードを, 配列で取得できる

image.png

  • userテーブルのmodelに, さらにアソシエーションを書いてみる. 以下を追記.
has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: 'follow_id', dependent: :destroy
has_many :followers, through: :reverse_of_relationships, source: :user
  • 挙動を確認する為, has_many :relationships, dependent: :destroy は一旦コメントアウト

image.png

  • この状態で usersテーブルのデータをdelete
  • $ User.find(5).destroy
  • エラー(FOREIGN KEY constraint failedエラー: 外部キー制約))
    • has_many :relationships, dependent: :destroy をコメントアウトしている為, エラーになる.

image.png

  • => ではなぜ, 先程は has_many :relationships, dependent: :destroy だけを書いて,
  • has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: 'follow_id', dependent: :destroy は書かなくても, 外部キー制約のエラーが出なかったのか
  • => relationshipsテーブル に関しては, migrationファイルの時点で, すでに外部キー設定をしている為

image.png

  • => reverse_of_relationships に関しては, user modelのファイルの5行目で初めて出てきたので, 外部キー設定などまだ何もされていない. その為エラーが出なかった.

image.png

  • has_many :relationships, dependent: :destroy のコメントアウトを外し, 再度userテーブルのデータを消してみる
  • $ User.find(5).destroy
  • user テーブルからid 5 が消えた
  • プラス, user modelに書いた内容
has_many :relationships, dependent: :destroy
has_many :reverse_of_relationships, class_name: 'Relationship', foreign_key: 'follow_id', dependent: :destroy
  • に従って, relationshipsテーブルから, user_idが5, または, follow_idが5 のレコードが, 連動して消えた.

image.png

image.png

user modelにさらに追記

  • follow, unfollow, following? のメソッドを追記.
  • controllerではなく, modelに書く(DB関連はmodelに書くのがいい. controllerに書いても動くが)
def follow(other_user)
  unless self == other_user
    self.relationships.find_or_create_by(follow_id: other_user.id)
  end
end

def unfollow(other_user)
  relationship = self.relationships.find_by(follow_id: other_user.id)
  relationship.destroy if relationship
end

def following?(other_user)
  self.followings.include?(other_user)
end

それぞれのdefを解釈する(ざっくりと)

def follow(other_user)
  unless self == other_user
    self.relationships.find_or_create_by(follow_id: other_user.id)
  end
end
  • unless self == other_user: 自身が, 他のuser idと同じでなければ
    • (followアクションを読んだ時, 投げた引数が自身のuser idでなければ)
  • self.relationships.find_or_create_by(follow_id: other_user.id) :
    • users経由でrelationshipsテーブルの, follow_idカラムに, 投げた引数の値でfind_or_create_byする(見つけに行き, あればそのrelationのレコードを返し, なかったらcreateする)
def unfollow(other_user)
  relationship = self.relationships.find_by(follow_id: other_user.id)
  relationship.destroy if relationship
end
  • relationship = self.relationships.find_by(follow_id: other_user.id) :
    • users経由でrelationshipテーブルの, follow_idカラムに, 投げた引数の値が見つかれば relationshipに代入する
  • relationship.destroy if relationship :
    • relationshipに値が入っていれば, destroy. user_idが自分で, follow_idが他人. その「レコード」を消す.
def following?(other_user)
  self.followings.include?(other_user)
end
  • self.followings.include?(other_user) :
    • 自分が属するレコード群のfollow_idカラムに, 指定した引数の値が入っているか.

controllerを作成する

  • $ rails generate controller relationships create destroy
    • (helperやviewが一気にできるので, 手動でcontrollerだけ作った方がいいかもしれない)
  • controllerに以下を追記する
def index
end

def create
    user = User.find(params[:follow_id])
    following = current_user.follow(user)
    if following.save
      flash[:success] = 'ユーザーをフォローしました'
      redirect_to user
    else
      flash.now[:alert] = 'ユーザーのフォローに失敗しました'
      redirect_to user
    end
end

def destroy
    user = User.find(params[:follow_id])
    following = current_user.unfollow(user)
    if following.destroy
      flash[:success] = 'ユーザーのフォローを解除しました'
      redirect_to user
    else
      flash.now[:alert] = 'ユーザーのフォロー解除に失敗しました'
      redirect_to user
    end
end
  • 上記sampleには current_user と出てくる箇所があるが, これは多分deviseを使っているから. deviseが入っていないと反応しない.
  • formボタンをクリックして, follow, unfollowするテストを. 一回indexでやってみる. viewページには, 以下を記述.
<% unless current_user == @user %>
  <% if current_user.following?(@user) %>
    <%= form_for(current_user.relationships.find_by(follow_id: @user.id), html: { method: :delete }) do |f| %>
      <%= hidden_field_tag :follow_id, @user.id %>
      <%= f.submit 'Unfollow', class: 'btn btn-danger btn-block' %>
    <% end %>
  <% else %>
    <%= form_for(current_user.relationships.build) do |f| %>
      <%= hidden_field_tag :follow_id, @user.id %>
      <%= f.submit 'Follow', class: 'btn btn-primary btn-block' %>
    <% end %>
  <% end %>
<% end %>
  • しかし, current_user は反応しないので, 一時的に User.find(4) などに, ベタ書きしておく.(こういうのをハードコーディングという?)
  • current_userを user id: 4
  • follow対象を user id: 6 ということにして, 以下話を進める
<% if User.find(4).following?(User.find(6)) %>
  <%= form_for(User.find(4).relationships.find_by(follow_id: User.find(6).id), html: { method: :delete }) do |f| %>
    <%= hidden_field_tag :follow_id, User.find(6).id %>
    <%= f.submit 'Unfollow', class: 'btn btn-danger btn-block' %>
  <% end %>
<% else %>
  <%= form_for(User.find(4).relationships.build) do |f| %>
    <%= hidden_field_tag :follow_id, User.find(6).id %>
    <%= f.submit 'Follow', class: 'btn btn-primary btn-block' %>
  <% end %>
<% end %>
  • この状態で, indexのwebページにアクセス
  • relationship_path などないとエラー

image.png

  • routesに resources :relationships を一旦設定
  • ページが表示された

image.png

  • follow ボタンをクリックしてみる
  • 4 が 6 をfollowしている, というレコードができた.

image.png

  • unfollowボタンをクリックしてみる
  • レコードが消えた. 正常に動く事を確認.

image.png

再確認

  • viewに書いてある以下のコードの流れを追う.
<% if User.find(4).following?(User.find(6)) %>
  <%= form_for(User.find(4).relationships.find_by(follow_id: User.find(6).id), html: { method: :delete }) do |f| %>
    <%= hidden_field_tag :follow_id, User.find(6).id %>
    <%= f.submit 'Unfollow', class: 'btn btn-danger btn-block' %>
  <% end %>
<% else %>
  <%= form_for(User.find(4).relationships.build) do |f| %>
    <%= hidden_field_tag :follow_id, User.find(6).id %>
    <%= f.submit 'Follow', class: 'btn btn-primary btn-block' %>
  <% end %>
<% end %>
  • <% if User.find(4).following?(User.find(6)) %>
    • User.find(4)が, userのmodelに書かれたメソッド, following? ↓ を呼んでいる.
def following?(other_user)
  self.followings.include?(other_user)
end
  • self.followings.include?(other_user) には, アソシエーションが使われている.
  • followingsの部分は, user modelに書かれたhas_many :followings, through: :relationships, source: :follow が使われている.
  • user テーブルがhas_manyしている仮想テーブル, 「followings」(relationshipsテーブルのfollow_idのカラムの集合)の中に, User.find(6) が includeされているか. をbooleanで返す.

  • <%= form_for(User.find(4).relationships.find_by(follow_id: User.find(6).id), html: { method: :delete }) do |f| %> の部分について.

    • User.find(4).relationships という書き方ができるのは, usersとrelationsが関連付けされているから
    • relationshipsテーブル上の, User.find(4)でfollow_idがUser.find(6).id のレコードに対して form_forする.
    • methodはdeleteでpostする
    • form_forの飛び先は localhost:3000/relationships/1

image.png

  • このroutes(relationships/:id) にdeleteで飛ばしたら, controllerのdestroyが動く

image.png

  • destroyメソッドの中では, unfollowメソッド(user modelに書いてある) が動く.

  • <%= hidden_field_tag :follow_id, User.find(6).id %>

    • follow_idをUser.find(6).idにして hiddenで設置
  • <%= f.submit 'Unfollow', class: 'btn btn-danger btn-block' %>

    • Unfollowというテキストで, class(この場合は見た目) は 〜〜 にしてsubmit.
  • <%= form_for(User.find(4).relationships.build) do |f| %>

    • build = new の事.
    • User.find(4)が, relationshipsテーブルで新規にレコード作る, という事.

以上で

  • railsにフォロー機能を実装できた事になります.
  • ベタ書きしている部分(User.find(4)) などは, deviseと連携してcurrent_userとかにして,変数的にすればOKです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails備忘録]配列を平坦化させる Array#flatten

Railsの備忘録です。

Array#flatten

# 自身を再帰的に平坦化する例。
a = [1, [2, 3, [4], 5]]
p a.flatten                     #=> [1, 2, 3, 4, 5]
p a                             #=> [1, [2, 3, [4], 5]]

参考ページ

Ruby 2.7.0 リファレンスマニュアル instance method Array#flatten

[初心者向け] RubyやRailsでリファクタリングに使えそうなイディオムとか便利メソッドとか

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

3/23 最終課題五日目

3/23
最終課題五日目

メモです

できている商品詳細ページをgithubに
出してコードレビューをする

コンフリクトが起こっていたので
修正した後にコードレビューをしてもらう

aタグはlink_toで書きましょう。  
と指摘を頂いたので
修正
修正後無事にLGTMともらう

その後マージしてマスターにプッシュする
そして今まで作ったとこに変なところが無いかや修正があるか探す

とりあえず簡単な修正をしてコミットし
マスターにプッシュしました

これで商品詳細ページ(マークアップ)が終わったので次に進む

次はサーバーサイドに進むが
みんなで話し合った結果
先に商品出品のサーバーサイドを進めることに決定
2人で進めることになりました
画像のところとその他で別れることに決定

Git hubでブランチを作った後さらに
ブランチを作ることで二人で進めれるようにする
(孫ブランチ?)
子ブランチの状態で新しいブランチを作る
そうするとマスターブランチの小ブランチを作るか
小ブランチの孫ブランチを作るかと選択が出るので
孫の方を選択
作る際のブランチの名前は小ブランチの名前と一緒もしくは似ているものは
エラーの原因になる

プルリクエスするときは気をつける
間違ってもマスターにプルリクエストを送ることはしないこと!!!

やること
* 商品モデルと商品の画像モデルを作成し、1つの投稿フォームで記事とそれに紐付く複数の画像を投稿できる
* 画像は送信前にプレビューを表示できる
* 上記要件を満たし、1度出品した商品の編集ができる
* 編集画面から、ひもづく画像の変更、削除、追加ができる
* 商品を削除する機能がある
* 商品を削除する際は、削除した商品に紐づく画像が同時に削除される

データベースを作らないとできないので
データベースを作る
Itemsテーブルを作る際外部キーが原因でエラー
Itemテーブルと関連づけされているテーブルを作っていく

調べたこと

Webスクレイピングとは、ウェブサイトのHTMLから必要なデータを取得する事を言い、
それを行うプログラムをスクレイパとも呼びます。

Ancestryとは
AncestryはRuby on RailsのActiveRecordモデルのレコードを
ツリー構造(階層)として編成することを可能にするGemです。
カテゴリーを作る際に必要になるgemです
https://qiita.com/Rubyist_SOTA/items/49383aa7f60c42141871
参考ページ

orderとは
取得した値に対して、条件を指定して並び替えることができる機能
並びの順番を変えることができる
降順にする場合は”DESC”、昇順は”ASC”
https://techacademy.jp/magazine/7727
参考ページ

カテゴリーにpathカラムがあったので
Pathカラムがわからなかったので調べた
https://kyabatalian.hatenablog.com/entry/2016/12/19/193430
参考ページ
path列に対してパターン比較すれば先祖を取得できます。
メソッドとかもあるので必要みたいです

マイグレーションをしてエラーが出たので
結局は原因はわからなかったけど
メモで
Gemfile
gem 'ancestry'

ターミナル
$ bundle install
$ rails g migration add_ancestry_to_category ancestry:string:index
$ rake db:migrate
ここでエラー
エラーの原因を探して解決方法を見る
テーブルがおかしなことになっているのでリセットする
$ rake db:migrate reset
またエラー
というのを繰り返した結果
途中でモデルに記述するとうまくいった
記述していなかったのが原因?
初歩ミスっぽいです

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

【Rails】ancestryで簡単に多階層型データの作成し呼び出す

概要

・多階層型DB作成のためのgem "ancestry" の導入
・seeds.rbでのデータインプット効率化
・viewファイルへの表示

階層型DB作成のためのgem "ancestry" の導入

gemの導入

Gemfile
gem 'ancestry'
ターミナル
$ bundle install
$ rails g migration add_ancestry_to_category ancestry:string:index
$ rake db:migrate

モデルの設定

categories_controller.rb
class Category < ApplicationRecord
    has_many :items
    has_ancestry
end
items_controller.rb
class Item < ApplicationRecord
    belongs_to :category
end

データの追加

seeds.rb
drink = Category.create(name: "飲み物")

drink_juice = juice.children.create(name: "ジュース")

drink_juice.children.create([{name: "オレンジ"}, {name: "アップル"}, {name: "グレープ"}])
ターミナル
% rails db:seed

seeds.rbでのデータインプット効率化

数件、数十件レベルのデータ量であれば、上記「データの追加」でも問題ありませんが、
それ以上の膨大なデータを作成するのであれば、以下の方法で効率的に作成しましょう。

seeds.rb
@category1 = Category.create(name:"飲み物")

category1s = [
             {level2:"ジュース",level2_children:["オレンジジュース","コーラ","サイダー"]},
             {level2:"お酒",level2_children:["ビール","ハイボール","焼酎","ワイン"]}, 
             {level2:"お茶",level2_children:["緑茶","烏龍茶","ほうじ茶","はとむぎ茶"]},
            ]
category1s.each.with_index(1) do |category1,i|
  level2_var="@category1_#{i}"
  level2_val= @category1.children.create(name:"#{category1[:level2]}")
  eval("#{level2_var} = level2_val")
  category1[:level2_children].each do |level2_children_val|
    eval("#{level2_var}.children.create(name:level2_children_val)")
  end
end
ターミナル
% rails db:seed

これでデータベースに反映されるはずです。

子要素の呼び出し

親要素.children

親要素の呼び出し

子要素.parent

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

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

redisで特定のデータを一括で削除、操作をするスクリプト

redisで有効期限が設定されていないセッションデータを一括削除するスクリプト

今回は有効期限が設定されていないデータを削除するscriptを作りました。

シンプルに検索して、eachして、ifでチェックして該当すれば削除を行ってます。

session-delete.lua
redis.call('select', '1') --[[ DBの数字を選んでください ]]
local expresion = 'session*' --[[ 精査対象を正規表現で捉えてください ]]

local delete_count = 0
for _,session_key_id in ipairs(redis.call('keys', expresion))
do
  if redis.call('TTL', session_key_id) == -1 then --[[ 有効期限が -1 (未設定)のものを削除します ]]
    redis.call('DEL', session_key_id)
    delete_count = delete_count + 1
  end
end
return 'deleted:'..delete_count

実行方法は簡単

bash
redis-cli eval "$(cat search.lua)" 0

docker環境ならこんな感じで

bash
dc exec imdb redis-cli eval "$(cat search.lua)" 0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【rails】seedデータからファイルを読み込んで、ファイル内のimageを一括で保存する

railsのseedデータからファイルにあるイメージを一括で保存する方法を、説明します。

準備)
①carrierwaveを使います。
②db/imagesを作成します。

以下のコードでイメージを保存できます。

seeds.rb
Image.create!([
  (1..20).map do |n|
    [ 
      id: n,
      image: open("#{Rails.root}/db/images/#{n}.jpg")
    ]
  end
])

しかし、この場合、イメージ名を1.jpgとかにしないといけないから、めんどくさい。。。

なので、このコードで楽にいけます!

seeds.rb
images = Dir.open("#{Rails.root}/db/images")
images.each.with_index(-1) do |image, i|
  next if image =~ /^\.+$/
  Image.create!(
    id: i,
    name: image,
    image: open("#{Rails.root}/db/images/#{image}")
  )
end

説明します。

images = Dir.open("#{Rails.root}/db/images")
ここで、ディレクトリー内の全ての画像を並べることができます。

もし、以下のコードで計算すると、、

images.each do |image|
  puts image
end

(結果)
.
..
XXX.jpg
XXX.png
XXX.jpg

こんな感じになり、「.」[..]が邪魔です。

なので、next if image =~ /^\.+$/を入れることで、「.」[..]がスキップされるようになります。

今回は、idも入れたかったので、each_with_indexメゾットを使いましたが、「0」から始まるし、「.」[..]をスキップするので、逆に2から始まってしまいます。

なので、「1」から始めるために、each.with_index(-1)を使っいました。

これで、簡単にseedで画像を保存できるようになりました。めでたし。

ご指摘の点があれば、ぜひツッコンでください!
よろしくお願いします!

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

日時表示を日本時間に修正

タイムゾーンの設定

application.rbに、タイムゾーンの設定を追記します。
【例】

application.rb
〜省略〜
    # Initialize configuration defaults for originally generated Rails version.
    config.time_zone = 'Tokyo'   #追記

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
〜省略〜
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ActiveRecord で unscoped を呼ぶとその前のクエリが消える

ブログ記事からの転載です。

unscoped を使うことで default_scope を取り除くことができる

さて、皆さん大好き default_scope ですが、モデルで default_scope を定義すると次のように暗黙的にクエリが追加されます。

class User < ActiveRecord::Base
  default_scope { order(:updated_at) }
end

# 暗黙的に ORDER BY のクエリが追加れる
puts User.all.to_sql
# => SELECT "users".* FROM "users" ORDER BY "users"."updated_at" ASC
puts User.where(name: "Tom").to_sql
# => SELECT "users".* FROM "users" WHERE "users"."name" = 'Tom' ORDER BY "users"."updated_at" ASC

毎回 order(:updated_at) する必要がないので便利ですね。
でも『あ〜今日は default_scope のクエリ追加してほしくないな〜〜〜』って思うときがあると思うんですよ。
そういう時に unscoped を使うと default_scope のクエリを取り除く事ができます。

# unscoped を付けると default_scope はつかなくなる
puts User.unscoped.all.to_sql
# => SELECT "users".* FROM "users"
puts User.unscoped.where(name: "Tom").to_sql
# => SELECT "users".* FROM "users" WHERE "users"."name" = 'Tom'

これで default_scope をつかっていてもシュッと取り除く事ができて便利ですね!

default_scope 以外のクエリも取り除かれる

unscoped を使うことで default_scope を取り除く事ができるようになります。
しかし unscoped はめちゃくちゃ強くて『呼び出すよりも前のリレーション』も取り除いてしまします。

# unscoped よりも前に付けた where のクエリも消してしまう
puts User.where(name: "Tom").unscoped.to_sql
# => SELECT "users".* FROM "users"

unscopeddefault_scope を消したい場合は必ず『一番最初』に unscoped を呼び出しましょう。
逆に『レシーバのクエリを全部消したい』場合は unscoped を呼び出すと一括で消すことができるので便利です。

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

Ajaxとjbuilderの参考リファレンスなど

Ajax

【jQuery日本語リファレンス 】Ajax
Ajaxのリファレンスになります。オプションも含めて説明しています。
【js STUDIO】$.ajax()
オプションや動作を含めて説明されています。
Ruby on RailsのAjax処理
RailsでのAjaxの流れを図解つきで説明されています。

jbuilder

【GitHub】jbuilder
jbuilderのドキュメントです。
Rails4でJSONを作るならto_jsonよりjbuilder
jbuilderの使い方を丁寧に説明した記事になります。

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

Herokuへのデプロイ手順|Rails + MySQL

はじめに

MySQLで作成したRailsアプリを、Herokuを使ってデプロイすることがあったので備忘録としてまとめました。同じ環境で初めてデプロイする方の参考になれば幸いです。

自分の環境

  • macOS 10.15.3
  • DB: MySQL
  • Rails 5.0.7

Railsアプリケーションの作成

・アプリ作成 [現在作成中]

・Gemfile修正

sqlite3が入っている場合は、以下のようにしてproduction環境下で反映されないようにしましょう。

Gemfile.rb
group :development, :test do
  gem 'sqlite3', '~> 1.4'
end

下記mysql2を追加。

Gemfile.rb
  gem 'mysql2'

・config/environments/production.rbの設定

Heroku上の本番環境でHTMLにCSSやJavaScriptが反映されるように、以下の記述を書き換えます。

ターミナル
# デフォルトのfalseをtrueにします。
config.assets.compile = true

作成したRailsアプリをGit管理

Railsプロジェクトに移動します。(ディレクトリへのパスは自身の物へ置き換えてください)

ターミナル
$ cd rails/MyApp

以下を実行すると、RailsアプリがGit管理されます。

ターミナル
# リポジトリを新規作成。
$ git init

# 変更があったすべてのファイルがaddされる。
$ git add .

# ファイルの変更や追加などを保存。
$ git commit -m "<ここにコミットメッセージをいれる>"

Herokuアカウントの登録

https://jp.heroku.com/ にて登録。

Heroku CLIのインストール

Heroku CLIをインストールすると、ターミナル上でHerokuのコマンド操作ができるようになります。
https://devcenter.heroku.com/articles/heroku-cli
上記サイト、もしくは下記コマンドでインストールできます。

ターミナル
brew tap heroku/brew && brew install heroku

以下のコマンドで、インストールができているか確認します。

ターミナル
$heroku -v
>>heroku/7.0.47 darwin-x64 node-v10.1.0

SSH公開鍵の作成、Herokuへ追加

・SSH公開鍵の作成

まずは公開鍵が作成されているか確認しましょう。

ターミナル
cat ~/.ssh/id_rsa.pub

作成されてない場合は以下のように表示されます。

ターミナル
No such file or directory

それでは公開鍵を作成していきましょう。(実行ディレクトリはどこでも大丈夫です)

ターミナル
$ ssh-keygen
Generating public/private rsa key pair

# 鍵の保存先を聞かれます。変更する必要はないので、そのままEnterキーで進みます。
Enter file in which to save the key (/Users/ユーザー名/.ssh/id_rsa):

# 鍵のパスワードを設定するか聞かれます。
# 設定しない場合はそのままEnterを、設定する場合はパスワードを入力します。
Enter passphrase (empty for no passphrase):

# パスワードの確認をされます。同じパスワードを入力してください。
# 設定していない場合は空欄のままEnterキーで進みます。
Enter same passphrase again:

これで公開鍵が作成されました。確認しましょう。

ターミナル
cat ~/.ssh/id_rsa.pub

#このように文字列が表示されるはずです。
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSU
GPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3
Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XA
t3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/En
mZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx
NrRFi9wrf+M7Q== schacon@mylaptop.local

・Herokuに公開鍵を追加

Herokuにログインします。

ターミナル
$ heroku login

以下のコマンドでSSH公開鍵がHerokuに追加されます。

ターミナル
$ heroku keys:add ~/.ssh/id_rsa.pub

追加されたか確認しておきましょう。

ターミナル
$ heroku keys
# SSHキーの一部が表示されます。
ssh-rsa AAAA...BBBB

作成したRailsアプリとHerokuの紐付け

Railsプロジェクトに移動します。(ディレクトリへのパスは自身の物へ置き換えてください)

ターミナル
$ cd rails/MyApp

Herokuへ新しいアプリケーションを作成しましょう。

ターミナル
$ heroku apps:create <好きなアプリ名>

以下のように表示されたらアプリ名が既に使われちゃってます。
重複は許されないので他のアプリ名へ変更しましょう。

ターミナル
Name myapp is already taken

HerokuにDBを追加

・MySQLを追加

以下のコマンドで、clearDBというMysqlを使うためのアドオンがigniteプランで追加されます。

ターミナル
$ heroku addons:create cleardb:ignite

以下のような表示が出たら、クレジットカードの登録が必要です。

ターミナル
▸    Please verify your account to install this add-on plan (please enter a credit card) For more
▸    information, see https://devcenter.heroku.com/categories/billing Verify now at
▸    https://heroku.com/verify

*アドオンを追加するためにクレジットの登録が必要ですが、iginteプランは無料で使えます。

詳しいプラン内容については以下を参照。
https://elements.heroku.com/addons/cleardb

・Herokuアプリに環境変数を設定

以下のコマンドで、環境変数に入る値を取得します。

ターミナル
$ heroku config
=== <アプリの名前> Config Vars
CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true

Herokuアプリの環境変数に、上記で取得した値をそれぞれ設定。

ターミナル
$ heroku config:add DB_NAME='<データベース名>'

$ heroku config:add DB_USERNAME='<ユーザー名>'

$ heroku config:add DB_PASSWORD='<パスワード>'

$ heroku config:add DB_HOSTNAME='<ホスト名>'

$ heroku config:add DB_PORT='3306'

# gemで「mysql2」を使用しているので、mysql://ではなく「mysql2://」とします。
$ heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'

設定した値を確認しましょう。
以下のように表示されるはずです。

$ heroku config

=== <アプリの名前> Config Vars
CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
DATABASE_URL:         mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true
DB_HOSTNAME:          <ホスト名>
DB_NAME:              <データベース名>
DB_PASSWORD:          <パスワード>
DB_PORT:              3306
DB_USERNAME:          <ユーザー名>

Herokuにデプロイ

以下コマンドでローカルリポジトリをHerokuへpushすると、自動でデプロイが進んでいきます。

ターミナル
$ git push heroku master

*master以外のブランチをpushする場合は以下を実行

ターミナル
$ git push heroku <ブランチ名>:master

最後に以下のコマンドを入力して、データベースのマイグレーションをします。

ターミナル
$ heroku rake db:migrate

以下のコマンドを実行すると、ブラウザでアプリケーションにアクセスできます。

ターミナル
$ heroku open

お疲れ様でした!

参考サイト・記事

mysqlを使ったRailsアプリをHerokuにデプロイする流れ
Deploying with Git
SSH 公開鍵の作成

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

ツイッター風Railsアプリをテストする(統合テスト編)

この記事の基本的な方針

今回は別記事、ツイッター風Railsアプリ最短復習(忙しい人の流し読みで開発シリーズ)で作ったアプリの統合テストをします。

手を動かしながら読みたいようでしたら、以下でこのツイッター風Railsアプリを手に入れてください。

Terminal
$ git clone https://github.com/annaPanda8170/cheaptweet.git
$ bundle install
$ bundle exec rake db:create
$ bundle exec rake db:migrate

基本解説はしません。手順のみ示します。

想定する読み手

既に一度Railsアプリをチュートリアルやスクール等で作ったことがある方を想定しております。
Mac使用で、パソコンの環境構築は完了していることが前提です。

具体的な手順

完成品GitHub

①準備

Gemfile
#省略
group :development, :test do
#省略
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :development do
#省略
  gem 'spring-commands-rspec'
end
#省略

※spring-commands-rspecは起動時間を速くするためのものでなくても問題はありません。

Terminal
$ bundle install
$ rails g rspec:install
$ rails g rspec:feature cheapTweet
$ rails g factory_bot:model user
$ rails g factory_bot:model tweet
$ bundle exec spring binstub rspec

control + c
$ rails s
.rspec
--format documentation

※これでRspecの出力が読みやすくなるそうです。

spec/rails_helper.rb
#省略
require 'capybara/rspec'
include Warden::Test::Helpers

ここで空っぽのまま一度起動してみます。

Terminal
$ bundle exec rspec
Output
CheapTweets
  add some scenarios (or delete) /Users/handaryouhei/Desktop/cheaptweet/spec/features/cheap_tweet_spec.rb (PENDING: Not yet implemented)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) CheapTweets add some scenarios (or delete) /Users/handaryouhei/Desktop/cheaptweet/spec/features/cheap_tweet_spec.rb
     # Not yet implemented
     # ./spec/features/cheap_tweet_spec.rb:4


Finished in 0.0014 seconds (files took 2.21 seconds to load)
1 example, 0 failures, 1 pending
spec/factories/tweets.rb
FactoryBot.define do
  factory :tweet do
    text {"hello"}
    association :user
  end
end
spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:nickname) { |n| "annaPanda#{n}" }
    sequence(:email) { |n| "a#{n}@a" }
    password {"111111"}
    password_confirmation {"111111"}
  end
end

②テスト構築

spec/features/cheap_tweet_spec.rb
require 'rails_helper'

RSpec.feature "CheapTweets", type: :feature do
  scenario "新規登録するとログアウトが表示されているTOP画面に遷移する" do
    visit root_path
    click_link "会員登録"
    fill_in "user_nickname", with: "annaPanda"
    fill_in "user_email", with: "a@a"
    fill_in "user_password", with: "111111"
    fill_in "user_password_confirmation", with: "111111"
    click_button "Sign up"
    expect(page).to have_content 'ログアウト'
  end
  scenario "ログインするとログアウトが表示されているTOP画面に遷移する" do
    user = FactoryBot.create(:user)
    visit root_path
    click_link "ログイン"
    fill_in "user_email", with: "a1@a"
    fill_in "user_password", with: "111111"
    click_button "Log in"
    expect(page).to have_content 'ログアウト'
  end
  scenario "非ログイン状態でTOP画面に遷移するとログアウトが表示されていない" do
    visit root_path
    expect(page).not_to have_content 'ログアウト'
  end
  scenario "投稿したらデータベースにtweetが一つ増える" do
    user = FactoryBot.create(:user)
    login_as(user, scope: :user)
    visit root_path
    expect{
      click_link "投稿"
      fill_in "tweet_text", with: "こんにちは"
      click_button "投稿"
    }.to  change( Tweet.all, :count ).by(1)
  end
  scenario "自分で投稿したtweetをshowして編集するとindexに反映される" do
    user = FactoryBot.create(:user)
    login_as(user, scope: :user)
    visit root_path
    click_link "投稿"
    fill_in "tweet_text", with: "こんにちは"
    click_button "投稿"
    click_link "こんにちは"
    click_link '編集'
    fill_in "tweet_text", with: "こんにちは!"
    click_button '編集'
    visit root_path
    expect(page).to have_content 'こんにちは!'
  end
  scenario "自分以外が投稿したtweetをshowすれば編集ボタンが表示されない" do
    tweet = FactoryBot.create(:tweet)
    visit root_path
    click_link "a"
    expect(page).not_to have_content '編集'
  end
  scenario "自分で投稿したtweetをshowして削除ボタンを押せばデータベースのtweetが一つ減る" do
    user = FactoryBot.create(:user)
    login_as(user, scope: :user)
    visit root_path
    click_link "投稿"
    fill_in "tweet_text", with: "こんばんは"
    click_button "投稿"
    click_link "こんばんは"
    expect{
      click_link "削除"
    }.to  change( Tweet.all, :count ).by(-1)
  end
  scenario "自分以外が投稿したtweetをshowすれば削除ボタンが表示されない" do
    tweet = FactoryBot.create(:tweet)
    visit root_path
    click_link "hello"
    expect(page).not_to have_content '削除'
  end
  scenario "ヘッダーの自分の名前をクリックすると自分の投稿一覧が表示される" do
    tweet = FactoryBot.create(:tweet)
    user = FactoryBot.create(:user)
    login_as(user, scope: :user)
    visit root_path
    click_link "投稿"
    fill_in "tweet_text", with: "おはよう"
    click_button "投稿"
    click_link "投稿"
    fill_in "tweet_text", with: "さようなら"
    click_button "投稿"
    within 'header' do
      click_link user.nickname
    end
    expect(page).to have_content 'おはよう'
    expect(page).to have_content 'さようなら'
    expect(page).not_to have_content 'hello'
  end
  scenario "TOP画面の投稿者をクリックするとそのユーザーの一覧が表示される" do
    tweet = FactoryBot.create(:tweet)
    user = FactoryBot.create(:user)
    login_as(user, scope: :user)
    visit root_path
    click_link "投稿"
    fill_in "tweet_text", with: "おはよう"
    click_button "投稿"
    click_link "投稿"
    fill_in "tweet_text", with: "さようなら"
    click_button "投稿"
    click_link tweet.user.nickname
    expect(page).not_to have_content 'おはよう'
    expect(page).not_to have_content 'さようなら'
    expect(page).to have_content 'hello'
  end
  scenario "コメントすると表示される" do
    tweet = FactoryBot.create(:tweet)
    user = FactoryBot.create(:user)
    login_as(user, scope: :user)
    visit root_path
    click_link "hello"
    fill_in "comment_text", with: "ハロー"
    click_button "コメント"
    expect(page).to have_content 'ハロー'
  end
end

click_linkclick_buttonで指定しているのはテキストです。
fill_inで指定しているのはidです。
have_contentで表示されているかを確認しています。

Terminal
$ bundle exec rspec
Output
CheapTweets
  新規登録するとログアウトが表示されているTOP画面に遷移する
  ログインするとログアウトが表示されているTOP画面に遷移する
  非ログイン状態でTOP画面に遷移するとログアウトが表示されていない
  投稿したらデータベースにtweetが一つ増える
  自分で投稿したtweetをshowして編集するとindexに反映される
  自分以外が投稿したtweetをshowすれば編集ボタンが表示されない
  自分で投稿したtweetをshowして削除ボタンを押せばデータベースのtweetが一つ減る
  自分以外が投稿したtweetをshowすれば削除ボタンが表示されない
  ヘッダーの自分の名前をクリックすると自分の投稿一覧が表示される
  TOP画面の投稿者をクリックするとそのユーザーの一覧が表示される
  コメントすると表示される

Finished in 0.87206 seconds (files took 2.18 seconds to load)
11 examples, 0 failures

まとめ

読むだけなら割と直感的にいけると思います。
この統合テストさえ済ませていれば、コントローラは不要なのでは?という考えもあるようです。
モデルのテストとこの統合テストを行うことがRailsアプリのテストのスタンダードになるかもしれませんね。

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

Rails2.7 Rails6 Docker React環境でシンプルCRUD実装

streampackのminsuです。
以前の記事で Docker + Rails + React の環境構築を行いindexページの表示まで行ったのでCRUD機能を追加します。
ですが期間も空いているため、折角なので以前の環境である

  • Rails 5.1.4
  • Ruby 2.4.1
  • mysql 5.7

ではなく、新しい環境で作り直します。

最新版確認
https://rubygems.org/gems/rails
https://www.ruby-lang.org/ja/downloads/

作成環境

  • Rails 6.0.2
  • Ruby 2.7
  • mysql 5.7

ファイルの用意

Gemfile Gemfile.lock Dockerfile docker-compose.yml を作成します。

Gemfile
source "https://rubygems.org"
gem "rails", "6.0.2"
Gemfile.lock
FROM ruby:2.7.0

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

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn

RUN mkdir /app
WORKDIR /app

ADD Gemfile* /app/

RUN bundle install -j4 --retry 3

ADD . /app

WORKDIR /app

CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
    ports:
      - "4306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=root
    volumes:
      - mysql_vol:/var/lib/mysql
  app:
    build: . 
    command: /bin/sh -c "rm -f /app/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  mysql_vol:

rails app作成

rails new

rails new を行います。

$ docker-compose run app rails new . --force --database=mysql

db設定を変更します。

database.yml
  username: root
  password: root #docker-compose.ymlのMYSQL_ROOT_PASSWORD
  host: db #docker-compose.ymlのサービス名

今回も gem react-rails を利用するのでGemfileに追記します。

Gemfile
gem 'react-rails'

再度 build して

$ docker-compose build

reactを使うので下記コマンドを実行

$ docker-compose run app rails webpacker:install
$ docker-compose run app rails webpacker:install:react
$ docker-compose run app rails generate react:install

model 作成

$ docker-compose run app rails g model List title:string description:string
$ docker-compose run app rails db:create
$ docker-compose run app rails db:migrate

かなりの数の warning 出てきた。
Ruby 2.7.0に対応していないgemが存在することに起因しているようで非表示にすることもできる* が必要なwarningも見逃す可能性があるのでスルーすることにする。
*bash_profileにexport RUBYOPT='-W:no-deprecated -W:no-experimental'を追加

controller 作成

lists controller と view を作成

$ docker-compose exec app rails g controller Lists index
lists_controller.rb
class ListsController < ApplicationController
  def index
    @lists = List.all
  end
end
index.html.erb
<%= react_component 'ListsIndex', lists: @lists %>

react_component タグを用いてreactを呼び出します。

react file 作成

viewから呼び出すreact fileを実装していきます。

$ rails g react:component ListsIndex

コマンドで app/javascript/components/ListsIndex.js が作成されるので編集します。

ListsIndex.js
import React from "react"
import PropTypes from "prop-types"
export default class Lists extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      lists: []
    };
  }
  componentDidMount(){
    this.setState({
      lists: this.props.lists
    })
  }
  render () {
    return (
      <div>
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>Title</th>
              <th>Description</th>
            </tr>
          </thead>
          <tbody>
            {this.state.lists.map((list) => {
              return (
                <tr key={list.id}>
                  <td>{list.id}</td>
                  <td>{list.title}</td>
                  <td>{list.description}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    );
  }
}

動作確認

List モデルに適当な値を保存して動作確認をしてみます。
ss1.png

無事に一覧が表示されました。

simple CRUD の実装

railsにapiを追加します。
apiで行うアクションは index, create, update, destroy です。

/api/v1/xxxでアクセスできるようにrouteを設定し、controllerを追加します。

routes.rb
Rails.application.routes.draw do
  get 'lists/index'
  namespace :api do 
    namespace :v1 do 
     resources :lists, only: [:index, :create, :update, :destroy]
    end 
  end 
end
app/controllsers/api/v1/lists_controllser.rb
class Api::V1::ListsController < ApplicationController
  protect_from_forgery with: :null_session
  def index
    render json: List.all
  end
  def create
    list = List.create(list_params)
    render json: list
  end
  def update
    list = List.find(params[:id])
    list.update(list_params)
    render json: list
  end
  def destroy
    List.destroy(params[:id])
  end
  private
  def list_params
    params.require(:list).permit(:id, :title, :description)
  end
end

controllerには基本的なメソッド、そしてprotect_from_forgery with: :null_sessionを記述しました。

http://localhost:3000/api/v1/listsでindexが呼び出されリストが取得できるはずです。

index

reactからapiを利用してlists を取得します。
componentDidMountを書き換えます。

ListsIndex.js
  componentDidMount(){
    this.getIndex();
  }
  getIndex(){
    fetch('/api/v1/lists.json')
    .then((response) => {return response.json()})
    .then((data) => {this.setState({ lists: data }) });
  }

delete

delete機能を実装します。
ボタンを追加

    return (
      <div>
        <div>this is list</div>
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>Title</th>
              <th>Description</th>  
              <th>function</th>
            </tr>
          </thead>
          <tbody>
            {this.state.lists.map((list) => {
              return (
                <tr key={list.id}>
                  <td>{list.id}</td>
                  <td>{list.title}</td>
                  <td>{list.description}</td>
                  <td>
                    <button onClick={() => this.handleDelete(list.id)}>delete</button>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

    );

ボタンから呼び出されるhandleDeleteを実装します。

  handleDelete(id){
    fetch(`http://localhost:3000/api/v1/lists/${id}`, 
      {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json'
        }
    })
    .then((response) => { 
        console.log('List was deleted');
        this.deleteList(id);
    })
  }
  deleteList(id){
    let lists = this.state.lists.filter((list) => list.id != id)
    this.setState({
      lists: lists
    })
  }

apiでのdestroyだけではstateの値は変わらないので、画面は更新されません。
そのためdeleteListにてstateの値を変更しています。

constructorに下記も追記します。

  constructor(props){
...
    this.getIndex = this.getIndex.bind(this);
    this.handleDelete = this.handleDelete.bind(this);
    this.deleteList = this.deleteList.bind(this);
  }

画面を確認すると deleteボタンが追加されており、要素の削除が行えます。
ss2.png

create

要素追加のformを作成します。
stateにてformの値を管理するために下記のように追記します。

  constructor(props){
    super(props)
    this.state = {
      // lists: this.props.lists
      lists: [],
      form: {
        title: "",
        description: "",
      }
    };
...

各inpuフォームとaddボタンを追加

    return (
      <div>
        <div>this is list</div>
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>Title</th>
              <th>Description</th>  
              <th>function</th>
            </tr>
          </thead>
          <tbody>
            {this.state.lists.map((list) => {
              return (
                <tr key={list.id}>
                  <td>{list.id}</td>
                  <td>{list.title}</td>
                  <td>{list.description}</td>
                  <td>
                    <button onClick={() => this.handleDelete(list.id)}>delete</button>
                  </td>
                </tr>
              );
            })}
            <tr>
              <td></td>
              <td><input type="text" value={this.state.form.title} onChange={e=>this.handleChange(e,'title')} /></td>
              <td><input type="text" value={this.state.form.description} onChange={e=>this.handleChange(e,'description')} /></td>
              <td><button onClick={() => this.handleCreate()}>add</button></td>
            </tr>
          </tbody>
        </table>
      </div>

    );
  }

ここで利用するhandleChangehandleCreateを実装します。
handleChangeではinputフォームの入力値をstateにて管理させています。

  handleChange(e,key){
    let target = e.target;
    let value = target.value;
    let form = this.state.form;
    form[key] = value;

    this.setState({
      form: form
    });
  }

handleCreateではapiのcreateメソッドを呼び出して要素の追加を行います。
追加後はstateのlistsの更新と
inputフォームの値のリセットを行なっています。

  handleCreate(){
    let body = JSON.stringify({
      list: {
        title: this.state.form.title, 
        description: this.state.form.description
      } 
    })
    fetch('http://localhost:3000/api/v1/lists', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: body,
    })
    .then((response) => {return response.json()})
    .then((list)=>{
      this.addList(list);
      this.formReset();
    })
  }
  addList(list){
    this.setState({
      lists: this.state.lists.concat(list)
    })
  }
  formReset(){
    this.setState({
      form:{
        title: "",
        description: ""
      }
    })
  }

constructorに下記を追記

    this.handleChange = this.handleChange.bind(this);
    this.addList = this.addList.bind(this);
    this.formReset = this.formReset.bind(this);

画面を確認するとcreate用のinputフォームが追加され、addボタンのクリックにより要素の追加を行えます。
ss3.png

完成したListIndex.js

ListIndex.js
import React from "react"
import PropTypes from "prop-types"
class ListsIndex extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      // lists: this.props.lists
      lists: [],
      form: {
        title: "",
        description: "",
      }
    };
    this.getIndex = this.getIndex.bind(this);
    this.handleDelete = this.handleDelete.bind(this);
    this.deleteList = this.deleteList.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.addList = this.addList.bind(this);
    this.formReset = this.formReset.bind(this);
  }
  componentDidMount(){
    this.getIndex();
  }
  getIndex(){
    fetch('/api/v1/lists.json')
    .then((response) => {return response.json()})
    .then((data) => {this.setState({ lists: data }) });
  }
  handleDelete(id){
    fetch(`http://localhost:3000/api/v1/lists/${id}`, 
      {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json'
        }
    })
    .then((response) => { 
        console.log('List was deleted');
        this.deleteList(id);
    })
  }
  deleteList(id){
    let lists = this.state.lists.filter((list) => list.id != id)
    this.setState({
      lists: lists
    })
  }
  handleChange(e,key){
    let target = e.target;
    let value = target.value;
    let form = this.state.form;
    form[key] = value;

    this.setState({
      form: form
    });
  }
  handleCreate(){
    let body = JSON.stringify({
      list: {
        title: this.state.form.title, 
        description: this.state.form.description
      } 
    })
    fetch('http://localhost:3000/api/v1/lists', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: body,
    })
    .then((response) => {return response.json()})
    .then((list)=>{
      this.addList(list);
      this.formReset();
    })
  }
  addList(list){
    this.setState({
      lists: this.state.lists.concat(list)
    })
  }
  formReset(){
    this.setState({
      form:{
        title: "",
        description: ""
      }
    })
  }
  render () {
    return (
      <div>
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>Title</th>
              <th>Description</th>  
              <th>function</th>
            </tr>
          </thead>
          <tbody>
            {this.state.lists.map((list) => {
              return (
                <tr key={list.id}>
                  <td>{list.id}</td>
                  <td>{list.title}</td>
                  <td>{list.description}</td>
                  <td>
                    <button onClick={() => this.handleDelete(list.id)}>delete</button>
                  </td>
                </tr>
              );
            })}
            <tr>
              <td></td>
              <td><input type="text" value={this.state.form.title} onChange={e=>this.handleChange(e,'title')} /></td>
              <td><input type="text" value={this.state.form.description} onChange={e=>this.handleChange(e,'description')} /></td>
              <td><button onClick={() => this.handleCreate()}>add</button></td>
            </tr>
          </tbody>
        </table>
      </div>

    );
  }
}

export default ListsIndex

まとめ

Ruby2.7, Rails6
Docker
react
での環境構築
reactからのrails api利用の実装を行いました。
自分用のまとめですが、誰かの助けとなれば幸いです。

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

headerとflashの間にできた謎の余白を消す(Devise, Bootstrap)

はじめに

先日投稿したこちら
Bootstrap + jQueryを使ってFlashなどの
<button>xボタン</button>をクリックした時にFlashを閉じる機能を実装しました。
ただ、ブラウザ場で確認すると、
Flashが消える前、後、双方で との間に余白がないようにぴったり合わしているにも関わらず
<header>flashの間に余白が発生していました。

この余白を無くすことができたので紹介します。

開発環境

  • bootstrap-sass (3.3.7)
  • devise (4.7.1)
  • jquery-rails (4.3.1)
  • rails (5.2.4.1)

前置き (layoutのデザイン)

header.html
<header class= "header-layout navbar navbar-fixed-top navbar-inverse">
  • Bootstrapのnavbar-fixed-topを使ってheaderを固定しております。
  • headerの下の<%= yield%>には画面いっぱいの背景画像を設置した表紙のようになっております。
  • headerの下にHome画面レイアウトの背景画像がぴったりになるように<body>padding: top;でしっかり合わせてあります。)

  • (余白ができていた時のパターン↓)

application.html.erb
<body>
  <%= render 'shared/header'%>
  <%= notice %> //ここ
  <%= alert %> //ここ
<body>

*おそらく、ユーザーがログイン後にflash[:notice]が呼び出された場合、flash[:alert]は、余白としてflash[:norice]の上に表示されていました。

if構文(余白消えない。)

まずこちらを参考にしてみた
https://whatsupguys.net/programming-school-dive-into-code-learning-47/]

app/views/shared/_flash_messages.html.erb
<% if notice %>
  //条件
<% elsif alert %>
 //条件
<% end %>

上記のように条件を組んでブラウザ場の検証を確認しても

ブラウザ場の検証(要素)
<header> </header>
  "       "   //謎の余白
<class= "alert">
  "ログインしました" //flash[:notice]
</>

flashは表示できているけれど<header>flashが呼ばれる<class= "alert">の間に" "と表示され、余白の原因になっています。
おそらく" "の部分は表示されなかったflash[alert]だと思います。
if構文を使って条件を書いても、、余分な余白は表示されたままでした。

解決方法

解決方法を調べてみました。
まず、deviseとBootstrapを組み合わせる祭、Bootstrapには、
deviseのkeyである[:notice],[:alert]に対応したクラスがないので、keyを置き換えることが最善です。

こちらを参考に
(https://hachy.github.io/2019/10/15/flashes-with-devise-and-bootstrap.html)

app/helpers/users_helper.rb
module UsersHelper
#Bootstrapに対応できるdevise flash[notice][alert]を変更する
  def bootstrap_alert(key)
    case key
      when "alert"
       "warning"
      when "notice"
        "success"
      when "error"
       "danger"
    end
  end
end

:alert:warning,:notice:successに変更する為にヘルパーメソッドを作成しています。(devise userモデルを使っているのでそのヘルパーを利用しています。)

Flashの変更

変更前↓ (deviseのデフォルト)

application.html.erb
<body>
  <%= render 'shared/header'%>
  <%= notice %> //ここ
  <%= alert %>  //ここ
<body>

変更後↓

app/views/shared/_flash_messages.html.erb
<% flash.each do |key, value| %>
  <div class="alert alert-<%= bootstrap_alert(key)%> close-flash">
    <%= value %>
    <button type="button" class="close close-button" data-dismiss="alert">
      &times;
    </button>
  </div>
<% end %>
  • deviseのデフォルトflashを変更し、Bootstrapに組み合わせれるように作ったヘルパーメソッドbootstrap_alert(key)を使ってdevise flash[:key]を変更。

これでFlash[:key]がしっかりBotstrapにも適用でき、余分な" "が呼び出されることなく、特定のflash[:key]だけが呼び出され余白も消えました。

参考にしたサイト

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

Devise <%= notice %> <%= alert %>による余白をなくす方法

はじめに

先日投稿したこちら
Bootstrap + jQueryを使ってFlashなどの
<button>xボタン</button>をクリックした時にFlashを閉じる機能を実装しました。
ただ、ブラウザ場で確認すると、
Flashが消える前、後、双方で
<header><%= yield%>の間に余白が発生していました。

この余白を無くす方法を紹介します。

開発環境

  • bootstrap-sass (3.3.7)
  • devise (4.7.1)
  • jquery-rails (4.3.1)
  • rails (5.2.4.1)

前置き (layoutのデザイン)

私の場合<header>

header.html
<header class= "header-layout navbar navbar-fixed-top navbar-inverse">
  • Bootstrapのnavbar-fixed-topを使ってheaderを固定しております。
  • headerの下の<%= yield%>には画面いっぱいの背景画像を設置した表紙のようになっております。

  • (余白ができていた時のパターン↓)

application.html.erb
<body>
  <%= render 'shared/header'%>
  <%= notice %> //ここ
  <%= alert %> //ここ
<body>

ユーザーがログイン後に、flash[:notice]が呼び出された場合、呼び出されなかったflash[:alert]は、余白としてflash[:norice]の上に表示されていました。

if構文(余白消えない。)

まずこちらを参考にしてみた
https://whatsupguys.net/programming-school-dive-into-code-learning-47/]

app/views/shared/_flash_messages.html.erb
<% if notice %>
  //条件
<% elsif alert %>
 //条件
<% end %>

上記のように条件を組んでブラウザ場の検証を確認しても

ブラウザ場の検証(要素)
<header> </header>
  "       "   //謎の余白
<class= "alert">
  "ログインしました" //flash[:notice]
</>

flashは表示できているけれど<header>とflashが呼ばれる<class= "alert">の間に" "と表示され、余白の原因になっております。
おそらく" "の部分は表示されなかったflash[alert]だと思います。
if構文を使って条件を書いても、、余分な余白は表示されたままでした。

解決方法

解決方法を調べてみました。
まず、deviseとBootstrapを組み合わせる祭、Bootstrapには、
deviseのkeyである[:notice],[:alert]に対応したクラスがないので、keyを置き換えることが最善です。

こちらを参考に
(https://hachy.github.io/2019/10/15/flashes-with-devise-and-bootstrap.html)

app/helpers/users_helper.rb
module UsersHelper
#Bootstrapに対応できるdevise flash[notice][alert]を変更する
  def bootstrap_alert(key)
    case key
      when "alert"
       "warning"
      when "notice"
        "success"
      when "error"
       "danger"
    end
  end
end

:alert:warning,:notice:successに変更する為にヘルパーメソッドを作成しています。(devise userモデルを使っているのでそのヘルパーを利用しています。)

Flashの変更

変更前↓ (deviseのデフォルト)

application.html.erb
<body>
  <%= render 'shared/header'%>
  <%= notice %> //
  <%= alert %>  //
<body>

変更後↓

app/views/shared/_flash_messages.html.erb
<% flash.each do |key, value| %>
  <div class="alert alert-<%= bootstrap_alert(key)%> close-flash">
    <%= value %>
    <button type="button" class="close close-button" data-dismiss="alert">
      &times;
    </button>
  </div>
<% end %>
  • deviseのデフォルトflashを変更し、Bootstrapに組み合わせれるように作ったヘルパーメソッドbootstrap_alert(key)を使ってdevise flash[:key]を変更。

これでFlash[:key]がしっかりBotstrapにも適用でき、余分な" "が呼び出されることなく、特定のflash[:key]だけが呼び出され余白も消えました。

参考にしたサイト

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

Devise <%= notice %> <%= alert %>による謎の余白をなくす方法

はじめに

先日投稿したこちら
Bootstrap + jQueryを使ってFlashなどの
<button>xボタン</button>をクリックした時にFlashを閉じる機能を実装しました。
ただ、ブラウザ場で確認すると、
Flashが消える前、後、双方で
<header><%= yield%>の間に余白が発生していました。

この余白を無くす方法を紹介します。

開発環境

  • bootstrap-sass (3.3.7)
  • devise (4.7.1)
  • jquery-rails (4.3.1)
  • rails (5.2.4.1)

前置き (layoutのデザイン)

私の場合<header>

header.html
<header class= "header-layout navbar navbar-fixed-top navbar-inverse">
  • Bootstrapのnavbar-fixed-topを使ってheaderを固定しております。
  • headerの下の<%= yield%>には画面いっぱいの背景画像を設置した表紙のようになっております。

  • (余白ができていた時のパターン↓)

application.html.erb
<body>
  <%= render 'shared/header'%>
  <%= notice %> //ここ
  <%= alert %> //ここ
<body>

ユーザーがログイン後に、flash[:notice]が呼び出された場合、呼び出されなかったflash[:alert]は、余白としてflash[:norice]の上に表示されていました。

if構文(余白消えない。)

まずこちらを参考にしてみた
https://whatsupguys.net/programming-school-dive-into-code-learning-47/]

app/views/shared/_flash_messages.html.erb
<% if notice %>
  //条件
<% elsif alert %>
 //条件
<% end %>

上記のように条件を組んでブラウザ場の検証を確認しても

ブラウザ場の検証(要素)
<header> </header>
  "       "   //謎の余白
<class= "alert">
  "ログインしました" //flash[:notice]
</>

flashは表示できているけれど<header>とflashが呼ばれる<class= "alert">の間に" "と表示され、余白の原因になっております。
おそらく" "の部分は表示されなかったflash[alert]だと思います。
if構文を使って条件を書いても、、余分な余白は表示されたままでした。

解決方法

解決方法を調べてみました。
まず、deviseとBootstrapを組み合わせる祭、Bootstrapには、
deviseのkeyである[:notice],[:alert]に対応したクラスがないので、keyを置き換えることが最善です。

こちらを参考に
(https://hachy.github.io/2019/10/15/flashes-with-devise-and-bootstrap.html)

app/helpers/users_helper.rb
module UsersHelper
#Bootstrapに対応できるdevise flash[notice][alert]を変更する
  def bootstrap_alert(key)
    case key
      when "alert"
       "warning"
      when "notice"
        "success"
      when "error"
       "danger"
    end
  end
end

:alert:warning,:notice:successに変更する為にヘルパーメソッドを作成しています。(devise userモデルを使っているのでそのヘルパーを利用しています。)

Flashの変更

変更前↓ (deviseのデフォルト)

application.html.erb
<body>
  <%= render 'shared/header'%>
  <%= notice %> //
  <%= alert %>  //
<body>

変更後↓

app/views/shared/_flash_messages.html.erb
<% flash.each do |key, value| %>
  <div class="alert alert-<%= bootstrap_alert(key)%> close-flash">
    <%= value %>
    <button type="button" class="close close-button" data-dismiss="alert">
      &times;
    </button>
  </div>
<% end %>
  • deviseのデフォルトflashを変更し、Bootstrapに組み合わせれるように作ったヘルパーメソッドbootstrap_alert(key)を使ってdevise flash[:key]を変更。

これでFlash[:key]がしっかりBotstrapにも適用でき、余分な" "が呼び出されることなく、特定のflash[:key]だけが呼び出され余白も消えました。

参考にしたサイト

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

ユーザーの状態によって表示を変更する方法 Rails

初めに

Rubyのライブラリの1つである、便利機能のdeviseを使いました


devise

ログイン機能の実装を簡単にしてくれるgemです

本題

今回は、ログインしているかしていないかで見せ方を変更したかったので、、、

  - if user_signed_in?  
    .purchase
      = link_to '購入ページ','#'
  - else
    %p.setumei 親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。なぜそんな無闇をしたと聞く人があるかも知れぬ。別段深い理由でもない。新築の二階から首を出していたら、同級生の一人が冗談に、いくら威張っても、そこから飛び降りる事は出来まい。弱虫やーい。と囃したからである。小使に負ぶさって帰って来た時、おやじが大きな眼をして二階ぐらいから飛び降りて腰

と記述しました。

ログインしている

スクリーンショット 2020-03-23 3.10.03.png

ログインしていない

スクリーンショット 2020-03-23 3.10.53.png

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

PHP サイト でも Capistrano (Ruby Gem)

環境

  • Ruby 2.6.5

設定手順

Capistrano 設定ファイルの用意

bundle add capistrano
bundle exec cap install

これで必要なファイルが作成されます。

あとは deploy.rb, config/deploy/* を変更して設定完了です。

ファイルを置けば使えるPHPサイトなら、認証設定程度を記述すればOKです。

設定ファイル例

staging.rb/production.rb
# server configuration
role :web, %w{ec2-user@123.123.123.123}
# file location
set :deploy_to, "/var/www/xxx"
deploy.rb
# application name
set :application, "xxxxxx"
# repository
set :repo_url, "sample@sample.git.jp:/sample.git"

# Default branch is :master
ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp

# revision to be kept
set :keep_releases, 2

デプロイ手順

# config/deploy/staging.rb を使ったデプロイ
bundle exec cap staging deploy
# config/deploy/production.rb を使ったデプロイ
bundle exec cap production deploy

ロールバック手順

cap deploy:rollback

このほかにも cap deploy:rollback:code などがある。

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

Rails slimの書き方分からなくなった時

slimの書き方分からなくなった時

slimって便利なんですけど、、
たまにちょっと入り組んでる参考サイトのコードをどうやってslimで書いたらいいか迷うことあるんですよね。

そんな時に便利なサイトがあったのでメモ。

http://erb2slim.com/

コチラで対象コードをコピペしてConversion Typeをslimにしてコンバートすると
1発で解決しました。
変換されたコード見て「あーそっかそう書くのね!」って感じのアハ体験ができました。

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