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

rails で特定のカラムの確認の仕方

最近オリジナルアプリを作る中で、モデル内のカラムを効率よく検索する方法を探していました。

#コンソールを開く
$ rails c

#調べたいモデル
User.columns

こっちの方が、わかりやすい。

User.column_names

これから沢山お世話になりそう。

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

ActsAsTaggableOn 日本語訳

このプラグインは、もともとJonathan VineyによるActs as Taggable on Steroidsに基づいていました。その時点から大幅に進化していますが、多くの人々が使用していた最初のタグ付け機能はすべて彼の功績です。

たとえば、ソーシャルネットワークでは、スキル、興味、スポーツなどと呼ばれるタグがユーザーにある場合があります。タグを区別する実際の方法はないため、このタイプの実装は、ステロイドでタグ付け可能として機能することはできません。

「タグ付け可能として機能」を入力します。特定のキーワード(つまりtags)に機能を結び付けるのではなく、タグ付け可能として機能し、ステロイドが使用されたのと同じ方法でローカルまたは組み合わせて使用​​できる任意の数のタグ「コンテキスト」を指定できます。

取り付け
これを使用するには、Gemfileに追加します。

gem 'acts-as-taggable-on', '~> 6.0'
and bundle:

bundle
インストール後
移行をインストールする

#最新バージョンの場合:
rake acts_as_taggable_on_engine:install:migrations
生成された移行を確認して、移行します。

rake db:migrate
MySqlユーザーの場合
初期化ファイルを設定することにより、特殊文字の問題623の問題をいつでも回避できます。

ActsAsTaggableOn.force_binary_collation = true

または、このrakeタスクを実行して:

rake acts_as_taggable_on_engine:tag_names:collate_bin
詳細については、「構成」セクションと、古いバージョンのGemに有効な一般的な注意事項を参照してください。

使用法
セットアップ

class User < ActiveRecord::Base
acts_as_taggable_on :tags
acts_as_taggable_on :skills, :interests #モデルごとに複数のタグタイプを設定することもできます

class UsersController < ApplicationController
def user_params
params.require(:user).permit(:name, :tag_list) ## Rails 4の強力なパラメータの使用
end

@user = User.new(:name => "Bobby")

単一のタグを追加および削除する

@user.tag_list.add("awesome") # add a single tag. alias for <<
@user.tag_list.remove("awesome") # remove a single tag
@user.save # save to persist tag_list

配列内の複数のタグを追加および削除する

@user.tag_list.add("awesome", "slick")
@user.tag_list.remove("awesome", "slick")
@user.save

文字列の形式でタグを追加および削除することもできます。これは、文字列内のタグ入力パラメータを処理する場合などに便利です。

この場合、オプションとしてparse:trueを追加する必要があることに注意してください。

文字列内の区切り文字を確認することもできます。デフォルトはカンマなので、ここでは何もする必要はありません。ただし、区切り文字の設定を変更した場合は、文字列が一致することを確認してください。区切り文字について詳しくは、構成を参照してください。

@user.tag_list.add("awesome, slick", parse: true)
@user.tag_list.remove("awesome, slick", parse: true)

また、直接割り当ててタグを追加および削除することもできます。これにより既存のタグが削除されるため、注意して使用してください。

@user.tag_list = "awesome, slick, hefty"
@user.save
@user.reload
@user.tags
=> [#,
#,
#]

モデルでコンテキストを定義すると、コンテキスト内のタグを管理および表示するための複数の新しいメソッドを自由に使用できます。たとえば、:skillコンテキストでは、次のメソッドがモデルに追加されます:skill_list(およびskill_list.add、skill_list.remove skill_list =)、skills(plural)、skill_counts。

@user.skill_list = "joking, clowning, boxing"
@user.save
@user.reload
@user.skills
=> [#,
#,
#]

@user.skill_list.add("coding")

@user.skill_list

=> ["joking", "clowning", "boxing", "coding"]

@another_user = User.new(:name => "Alice")
@another_user.skill_list.add("clowning")
@another_user.save

User.skill_counts
=> [#,
#,
#]

タグが作成される順序を維持するには、acts_as_ordered_taggableを使用します。

class User < ActiveRecord::Base
# Alias for acts_as_ordered_taggable_on :tags
acts_as_ordered_taggable
acts_as_ordered_taggable_on :skills, :interests
end

@user = User.new(:name => "Bobby")
@user.tag_list = "east, south"
@user.save

@user.tag_list = "north, east, south, west"
@user.save

@user.reload
@user.tag_list # => ["north", "east", "south", "west"]

使用頻度の高いまたは最も少ないタグを見つける
以下を使用して、最も使用されているタグまたは最も使用されていないタグを見つけることができます。

ActsAsTaggableOn::Tag.most_used
ActsAsTaggableOn::Tag.least_used

メソッドに制限を渡すことで結果をフィルタリングすることもできますが、デフォルトの制限は20です。

ActsAsTaggableOn::Tag.most_used(10)
ActsAsTaggableOn::Tag.least_used(10)

タグ付きオブジェクトの検索
タグ付け可能として機能は、スコープを使用してタグの関連付けを作成します。このようにして、結果をフィルタリングして組み合わせることができます。

class User < ActiveRecord::Base
acts_as_taggable_on :tags, :skills
scope :by_join_date, order("created_at DESC")
end

User.tagged_with("awesome").by_join_date
User.tagged_with("awesome").by_join_date.paginate(:page => params[:page], :per_page => 20)

#指定されたすべてのタグに一致するユーザーを検索します。
#注:これは、指定されたタグの正確なセットを持つユーザーにのみ一致します。ユーザーが追加のタグを持っている場合、それらは返されません。
User.tagged_with(["awesome", "cool"], :match_all => true)

#指定されたタグのいずれかを持つユーザーを検索します。
User.tagged_with(["awesome", "cool"], :any => true)

#awesomeまたはcoolでタグ付けされていないユーザーを検索します。
User.tagged_with(["awesome", "cool"], :exclude => true)

#コンテキストに基づいて任意のタグを持つユーザーを検索します。
User.tagged_with(['awesome', 'cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)

:wild => trueオプションを:anyまたは:excludeオプションとともに使用することもできます。 SQLで%awesome%と%cool%を探します。

ヒント:User.tagged_with([])またはUser.tagged_with( '')は、空のレコードセットである[]を返します。

関係
特定のコンテキストの類似したタグに基づいて、同じタイプのオブジェクトを見つけることができます。また、オブジェクトは、一致したタグの総数に基づいて降順で返されます。

@bobby = User.find_by_name("Bobby")
@bobby.skill_list # => ["jogging", "diving"]

@frankie = User.find_by_name("Frankie")
@frankie.skill_list # => ["hacking"]

@tom = User.find_by_name("Tom")
@tom.skill_list # => ["hacking", "jogging", "diving"]

@tom.find_related_skills # => [, ]
@bobby.find_related_skills # => []
@frankie.find_related_skills # => []


ダイナミックタグコンテキスト
定義で生成されたタグコンテキストに加えて、動的タグコンテキストを許可することもできます(これはユーザー生成のタグコンテキストである可能性があります!)

@user = User.new(:name => "Bobby" )
@user 。set_tag_list_on(:customs 、 "same、as、tag、list" )
@user 。tag_list_on(:customs ) #=> ["same"、 "as"、 "tag"、 "list"]
@user.save
ます。tags_on(:customs ) #=> [、...]
@user 。tag_counts_on(:customs )
User.tagged_with("same", :on => :customs) # => [@user]

タグパーサー
タグの解析方法を変更したい場合は、独自の実装を定義できます。

class MyParser < ActsAsTaggableOn::GenericParser
def parse
ActsAsTaggableOn::TagList.new.tap do |tag_list|
tag_list.add @tag_list.split('|')
end
end
end
これで、このパーサーを使用して、パラメーターとして渡すことができます。

@user = User.new(:name => "Bobby")
@user.tag_list = "east, south"
@user.tag_list.add("north|west", parser: MyParser)
@user.tag_list # => ["north", "east", "south", "west"]

または

@user.tag_list.parser = MyParser
@user.tag_list.add("north|west")
@user.tag_list # => ["north", "east", "south", "west"]
または、グローバルに変更します。

ActsAsTaggableOn.default_parser = MyParser
@user = User.new(:name => "Bobby")
@user.tag_list = "east|south"
@user.tag_list # => ["east", "south"]

タグの所有権
タグは所有者を持つことができます:

class User < ActiveRecord::Base
acts_as_tagger
end

class Photo < ActiveRecord::Base
acts_as_taggable_on :locations
end

@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
@some_user.owned_taggings
@some_user.owned_tags
Photo.tagged_with("paris", :on => :locations, :owned_by => @some_user)
@some_photo.locations_from(@some_user) # => ["paris", "normandy"]
@some_photo.owner_tags_on(@some_user, :locations) # => [#...]
@some_photo.owner_tags_on(nil, :locations) # => Ownerships equivalent to saying @some_photo.locations
@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations, :skip_save => true) #won't save @some_photo object

オブジェクトを保存しない
所有されているタグの操作
tag_listタグ付けに所有者がいないタグのみを返すことに注意してください。上記の例の続き:

@some_photo 。tag_list #=> []
オブジェクトのすべてのタグを取得するには(所有権に関係なく)、または1人の所有者だけがオブジェクトにタグ付けできる場合は、を使用しますall_tags_list。

所有タグを追加する
所有タグは、文字列内のコンマ区切りタグの形式で一度に追加されることに注意してください。また、所有タグを再度追加しようとすると、以前の所有タグのセットが上書きされるだけです。そのため、既存の所有タグリストにタグを追加するには、次のようにします。

def add_owned_tag
@some_item = Item.find(params[:id])
owned_tag_list = @some_item.all_tags_list - @some_item.tag_list
owned_tag_list += [(params[:tag])]
@tag_owner.tag(@some_item, :with => stringify(owned_tag_list), :on => :tags)
@some_item.save
end

def stringify(tag_list)
tag_list.inject('') { |memo, tag| memo += (tag + ',') }[0..-1]
end

所有するタグを削除する
上記と同様に、削除は次のようになります。

def remove_owned_tag
@some_item = Item.find(params[:id])
owned_tag_list = @some_item.all_tags_list - @some_item.tag_list
owned_tag_list -= [(params[:tag])]
@tag_owner.tag(@some_item, :with => stringify(owned_tag_list), :on => :tags)
@some_item.save
end

汚れたオブジェクト
@bobby = User.find_by_name("Bobby")
@bobby.skill_list # => ["jogging", "diving"]

@bobby.skill_list_changed? #=> false
@bobby.changes #=> {}

@bobby.skill_list = "swimming"
@bobby.changes.should == {"skill_list"=>["jogging, diving", ["swimming"]]}
@bobby.skill_list_changed? #=> true

@bobby.skill_list_change.should == ["jogging, diving", ["swimming"]]

タグクラウドの計算
タグクラウドを構築するには、各タグの頻度を計算する必要があります。クラスで指定acts_as_taggable_onしたためUser、を使用してすべてのタグ数の計算を取得できますUser.tag_counts_on(:customs)。しかし、1人のユーザーの投稿のタグ数が必要な場合はどうでしょうか。これを実現するには、関連付けでtag_countsを呼び出します。

User.find(:first).posts.tag_counts_on(:tags)

タグクラウドの生成を支援するヘルパーが含まれています。

タグクラウドを生成する例を次に示します。

ヘルパー:

module PostsHelper
include ActsAsTaggableOn::TagsHelper
end

コントローラ:

class PostController < ApplicationController
def tag_cloud
@tags = Post.tag_counts_on(:tags)
end
end

見る:

<% tag_cloud(@tags、%w(css1 css2 css3 css4))do | tag、css_class | %>
<%= link_to tag.name、{:action =>:tag、:id => tag.name}、:class => css_class %>
<% end %>
CSS:

。css1 { font-size:1.0 em ; }
。css2 { font-size:1.2 em ; }
。css3 { font-size:1.4 em ; }
。css4 { font-size:1.6 em ; }
構成
タグ付けを削除した後で未使用のタグオブジェクトを削除する場合は、次を追加します。

ActsAsTaggableOn 。remove_unused_tags = true
強制的にタグを小文字で保存したい場合:

ActsAsTaggableOn 。force_lowercase = true
タグをパラメータ化して保存したい場合(to_paramも再定義できます):

ActsAsTaggableOn 。force_parameterize = true
タグで大文字と小文字を区別し、作成にLIKEクエリを使用しない場合:

ActsAsTaggableOn 。strict_case_match = true
MySqlで特殊文字をカバーする完全一致が必要な場合:

ActsAsTaggableOn 。force_binary_collat​​ion = true
テーブル名を指定したい場合:

ActsAsTaggableOn 。tags_table = 'aato_tags'
ActsAsTaggableOn 。taggings_table = 'aato_taggings'
デフォルトの区切り文字を変更する場合(デフォルトは「、」)。(['、'、 '|'])などの区切り文字の配列を渡すこともできます。

ActsAsTaggableOn 。区切り文字 = '、'

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

ReactとRailsを同一サーバーで動かす方法

経緯

Railsの学習を始めるに当たって、 ERBを利用したVIEWでフロント側を構築していたが、
最近はSPAというものが流行っていると小耳に挟んだ。

とりあえずチャレンジ!
ということでReactの学習を並行して進めていたが、、、
これどうやってデプロイするんだ。。。

となってしまいました笑

とりあえず別サーバーで動かすことにしてCORS制約などもなんとなくクリアして無事にRailsとReactのやりとりが可能になった。

しかし、ReactとRailsを利用したアプリ関係の記事を漁っていると、同一サーバーで動かしているものを多数発見!!!
何回かチャレンジしたものの上手く行かず断念。。。

しばらく時間が過ぎ、この土日に絶対動かしてやる!!! と執念で参考記事を探しまくった結果、
Heroku公式のBlogにわかりやすい記事があり、やっと構築ができました笑

同じような人がいたら参考になれば良いなと思い投稿するに至りました!

前提条件

  • Heroku blogの記事から必要最小限の部分だけ引用したものになります
  • Ruby、Rails、Node、npm OR yarnなどの基本的なインストールが済んでいること
  • Railsアプリを作成したことがある
  • Reactアプリをcreate-react-appで作成したことがある
  • Herokuでホスティングをしたことがある(gitでデプロイ)

参考記事:A Rock Solid, Modern Web Stack—Rails 5 API + ActiveAdmin + Create React App on Heroku

Railsアプリの作成

rails new コマンドでrails アプリを作成します。

--apiコマンドでapiモードにするのをお忘れなく

rails new rails_app --api

作成したアプリが起動するか確かめましょう。

cd rails_app
rails s

Reactアプリの作成

Railsアプリのルートディレクトリ上でclientという名前でReactアプリを構築します。

npm create-react-app client

Railsと同じように、Reactが動くか確かめましょう。

cd client
npm start 

このままだと開発中にReactとRailsで別ポートとなるので
package.jsonに設定を追加します。

package.json
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:3001", #ここを追加
  ...
}

この設定によりRails側には、Reactアプリがポート3001で起動しているように認識させます。

React - Rails ローカル編

ローカル環境立ち上げ用のタスクを作成します。

ProcfileにReactとRailsを立ち上げるコマンドを書き、Railsのタスクで呼び出す方法にします。

Procfileはheroku上で利用されるファイルで、プロセスのタイプ:コマンドで記述できます。

<process type>: <command>

今回は、ルートディレクトリにProcfile.devというファイル名で、RailsアプリとReactアプリの起動コマンドを記述します。

Procfile.dev
web: cd client && PORT=3000 npm start
api: PORT=3001 && bundle exec rails s

呼び出しコマンドを簡潔にしたいため、taskにします。

start.rake
namespace :start do
    desc 'Start development server'
    task :dev do
      exec 'heroku local -f Procfile.dev'
    end
end

taskでprocfileの起動コマンドをキックします。

rake start:dev

ReactとRailsがどちらも立ち上がったと思います。

ターミナル上のログでわかりづらい場合はRails側で何かしらJsonを返すようにしておき、React側からfetchできるようにしておくとわかりやすいと思います。

React - Rails Heroku編

メインテーマであるHeroku上へのデプロイです。

まずはルートディレクトリにpackage.jsonファイルを作成します。

RailsのpublicディレクトリにReactの静的ファイルを作成するコマンドを記述します。

package.json
{
    "name": "list-of-ingredients",
    "license": "MIT",
    "engines": {
      "node": "10.15.3",
      "yarn": "1.15.2"
    },
    "scripts": {
      "build": "yarn --cwd client install && yarn --cwd client build",
      "deploy": "cp -a client/build/. public/",
      "heroku-postbuild": "yarn build && yarn deploy"
    }
}

production用のProcfileを作成し、railsの起動コマンドを記述します。

必要に応じてrelease後のmigrateコマンドも記述します。

Procfile
web: bundle exec rails s
release: bin/rake db:migrate



CLIを利用してheroku上のdynoの作成とビルドパックの設定を行います。

アプリの作成
heroku apps:create
ビルドパックの設定
heroku buildpacks:add heroku/nodejs --index 1
heroku buildpacks:add heroku/ruby --index 2

heroku上の準備ができたのでgitでpushします。

git add .
git commit -m "React - Rails"
git push heroku master

アプリを開きます。

heroku open

あとがき

みなさん無事に動きましたでしょうか?

もっと良い方法があれば是非教えていただけるとありがたいです。

もし動かない場合は下記の参考元の記事を読んでみてください!

参考記事:A Rock Solid, Modern Web Stack—Rails 5 API + ActiveAdmin + Create React App on Heroku

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

100日後に1人前になる新人エンジニア(3日目)

100日後に1人前になる新人エンジニア(3日目)

どうもこんばんは。
土曜日でもめげずに学び更新していきます。

今日は本屋にいって書籍を買ってきました。
「Ruby on Rails 速習実践ガイド」 です
コツコツとやっていきたいと思います。

本日のお題は

・ REST
・Ruby製のテンプレートエンジンslim

以上の二つです。全然違うテーマだけど自分がよく分かっていないと
思っていたところなので書いていきます。

REST

Railsをやるようになってから「この設計はRESTfulだとか」,「RESTらしい」
という言葉を書籍等でよく見るようになった。
でもよく考えたらRESTについてしっかり分かっていないなと思ったので
土曜日を使って学んでみた。

RESTとは

次の6つを組み合わせたWebのアーキテクチャスタイルのこと

  • クライアント/サーバ
    • クライアントがサーバーにリクエストを送り、サーバーはそれに対してレスポンスを返す
  • ステートレスサーバ
    • クライアントのアプリケーションの状態をサーバーで管理しない
  • キャッシュ
    • 一度取得したリソースをクライアント側で使い回す。
  • 統一インターフェース -リソースに対する操作を統一したインターフェースで行う(GET POST PUT DELETE HEAD OPTIONS TRACE CONNECT の8つ)
  • 階層化システム
    • システムがいくつかの階層に分離すること
  • コードオンデマンド
    • プログラムコードをサーバからダウンロードし、クライアント側で実行するスタイル。JavaScriptとかがこれに当てはまる

これら6つを合わせたアーキテクチャスタイルをRESTって言うんだって。
ちなみにステートレスサーバに関してだけどCookieとかは
RESTの観点で言うとHTTPの間違った拡張の方法みたいです。
状態を保存しているし、ステートレスではないもんね。

でもそれは必ずしも間違いではないから
あくまでRESTを意識して必要最低限の拡張でアプリケーションを作っていくことが大事なんだって。

Railsの本を読んでてRESTに関して言っていたのは,
統一インターフェースの事だったのね。適切なインターフェースで処理ができているのか。と言うところ。それぞれのインターフェースには役割があるからそれをしっかり理解して、使いましょうって事でした。

RESTに関しては以上!!(結構長くなっちゃった)
アプリケーションを作りながら意識するともっと分かってくると思った。

slim

slimとは, Ruby製のテンプレートエンジンのこと、
htmlをより簡潔でスマートに書くことができるんだって。

erbのファイル形式しか知らなかったからまとめてみます。

slimの特徴

・<>がいらない
・<%= %> →  =
・<% %> → -
・コメント → /
・id指定 → #
・class指定 → .

|以降にある文字は全てテキストと認識される

p
  | テキストテキストテキスト
    テキストテキストテキスト
p
  | 
    テキストテキストテキスト
    テキストテキストテキスト

条件式なども省略できる

- if cuser.nil?
  li 新規登録
- else
  li ログイン

基本的な部分だけまとめてみた。
調べて分かったけど覚えることは意外と少ないみたい
はじめて見た時はなんじゃこりゃって感じだったけど、
慣れたら大丈夫になってきそうな気がしてきた。

では今日はこの辺で。

1人前のエンジニアになるまであと97日

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

ElementClickInterceptedErrorについて

前提

Selenium利用時のトラブルシューティングのごく一部について書きます。

本題

フラッシュメッセージのテストを行うとエラーが発生

エラー内容

1) Dishes 料理詳細ページ 料理の削除 削除成功のフラッシュが表示されること
     Failure/Error: click_on '削除'

     Selenium::WebDriver::Error::ElementClickInterceptedError:
       element click intercepted: Element <a class="delete-dish" data-confirm="本当に...しますか?" rel="nofollow" data-method="delete" href="/dishes/1232">削除</a> is not clickable at point (393, 91). Other element would receive the click: <div class="container">...</div>
         (Session info: headless chrome=81.0.4044.138)

     [Screenshot]: tmp/screenshots/failures_r_spec_example_groups_dishes_nested_2_nested_2_削除成功のフラッシュが表示されること_759.png


     # 0   chromedriver                        0x000000010f4e9269 chromedriver + 4674153
     # 1   chromedriver                        0x000000010f483593 chromedriver + 4257171
     # 2   chromedriver                        0x000000010f12cfcf chromedriver + 757711
     # 3   chromedriver                        0x000000010f09df9c chromedriver + 171932
     # 4   chromedriver                        0x000000010f09c9c8 chromedriver + 166344
     # 5   chromedriver                        0x000000010f09ac73 chromedriver + 158835
     # 6   chromedriver                        0x000000010f099ed4 chromedriver + 155348
     # 7   chromedriver                        0x000000010f0916f6 chromedriver + 120566
     # 8   chromedriver                        0x000000010f0b54b2 chromedriver + 267442
     # 9   chromedriver                        0x000000010f091476 chromedriver + 119926
     # 10  chromedriver                        0x000000010f0b572e chromedriver + 268078
     # 11  chromedriver                        0x000000010f0c2288 chromedriver + 320136
     # 12  chromedriver                        0x000000010f0b56d3 chromedriver + 267987
     # 13  chromedriver                        0x000000010f08f1dd chromedriver + 111069
     # 14  chromedriver                        0x000000010f090185 chromedriver + 115077
     # 15  chromedriver                        0x000000010f4aafff chromedriver + 4419583
     # 16  chromedriver                        0x000000010f4b87ba chromedriver + 4474810
     # 17  chromedriver                        0x000000010f4b8557 chromedriver + 4474199
     # 18  chromedriver                        0x000000010f48f149 chromedriver + 4305225
     # 19  chromedriver                        0x000000010f4b9067 chromedriver + 4477031
     # 20  chromedriver                        0x000000010f4a0d87 chromedriver + 4377991
     # 21  chromedriver                        0x000000010f4cf2d4 chromedriver + 4567764
     # 22  chromedriver                        0x000000010f4ef1f7 chromedriver + 4698615
     # 23  libsystem_pthread.dylib             0x00007fff684e52eb _pthread_body + 126
     # 24  libsystem_pthread.dylib             0x00007fff684e8249 _pthread_start + 66
     # 25  libsystem_pthread.dylib             0x00007fff684e440d thread_start + 13
     # ./spec/system/dishes_spec.rb:88:in `block (5 levels) in <top (required)>'
     # ./spec/system/dishes_spec.rb:87:in `block (4 levels) in <top (required)>'
     # -e:1:in `<main>'

原因

1、クリックしようとしている要素が表示されれいる領域に表示されていない
2、クリックしようとしている要素のSeleniumがクリックするポイントが他の要素に隠れている

解決

自分は2が原因でしたので被らないようにHTMLを修正すると解決しました

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

Railsでとにかくログイン機能を作る

とにかくログイン機能を作りましょう

Railsを用いたWEBアプリケーションでログイン機能を作るためだけの記事です。
これを体に覚え込ませて爆速で作れるようになっちゃいましょう!

アプリの雛形を作る

これを作らないと何も始まりません!ターミナルから以下のコマンドを実行しましょう。

% rails _5.2.3_ new アプリ名 -d mysql

上記のコマンドによりアプリの雛形ができたらアプリのディレクトリに移動しましょう。

% cd アプリ名

deviseのインストール

エディタでGemfileを開き、deviseを追加します。
deviseとは、ユーザー認証に必要な機能を簡単に作れるようになるgemのことです。
また、今回は私の好みでerbではなくhamlを使うのでerbファイルをhamlに変換するgemもインストールします。

gem 'devise'
gem "haml-rails"

Gemfileにdeviseを追記したらインストールします!

% bundle install

インストールが終了したらerbファイルをhamlに変換しておきます。

% rails haml:erb2haml

上記の変換作業が終わったらdeviseの設定ファイルを作成します。

% rails g devise:install

で、以下の設定ファイルが出来ます。
config/initializers/devise.rb
config/locales/devise.en.yml

データベース周りの設定

続いてユーザーモデルの作成するため以下のコマンドを実行します。

% rails g devise user

これでルーティングファイルにもdeviseのルーティングが記載されます。

Rails.application.routes.draw do
  devise_for :users
end

このアプリで使用するデータベースが未作成の状態なので作りましょう。

% rails db:create 

モデルを作成した時点でマイグレーションファイルが作成されていると思うので、マイグレーションしましょう
xxxxxxxxxxxxxx_devise_create_users.rb

% rails db:migrate

これでデータベース周りの設定は完了です。

トップページを表示させる

続いて、トップページのビューファイルを作ります。
ここにログイン、新規登録、ログアウトへの導線を作っておきしょう。
今回は何らかの投稿をユーザーで投稿できるアプリ、と仮定しapp/views/postsにindex.html.hamlを作ります。
deviseをインストールすると、user_signed_inというヘルパーメソッドが使えるようになるので、これで未ログイン時とログイン時で表示を分けましょう。

%header
  - if user_signed_in?
    %div
      = link_to "ログアウト", destroy_user_session_path, method: :delete
  - else
    %div
      = link_to "ログイン", new_user_session_path
      = link_to "新規登録", new_user_registration_path

トップページができたらroutes.rbにルーティングを追記します。

Rails.application.routes.draw do
  devise_for :users
  root to: 'posts#index'
end

現時点でトップページを開こうとすると下記のようなエラーになると思うので、コントローラを作ります。

スクリーンショット 2020-05-23 19.56.32.png

% rails g controller posts

これでコントローラができました!
作成されたコントローラにindexアクションを追加すればトップページが開けるはずです。

class PostsController < ApplicationController
  def index
  end
end

何とも味気ない画面ですが、うまくいっていれば下記のような画面になるはずです。
スクリーンショット 2020-05-23 20.03.48.png

ログインと新規登録

続いて、ログインと新規登録画面の作成を行います。
下記のコマンドを実行すると、新規登録とログイン用のビューが作成されます。

% rails g devise:views

.新規登録画面
app/views/devise/registrations/new.html.erb

・ログイン画面
app/views/devise/sessions/new.html.erb

今回はhamlで作成するので、下記のコマンドを再度実行してerbをhamlに変換しておきましょう。

% rails haml:erb2haml

ここまで出来れば新規登録、ログイン、ログアウトができるはずなので早速実践してみます。
・新規登録をクリック
スクリーンショット 2020-05-23 20.03.48.png

・メールアドレスとパスワードを入力して"sign up"をクリック
スクリーンショット 2020-05-23 20.11.41.png

・トップページに遷移し、ログアウトが表示されるのでログアウトをクリック
スクリーンショット 2020-05-23 20.15.26.png

・ログアウトできたので、続いてログインをクリック
スクリーンショット 2020-05-23 20.17.52.png

・メールアドレスとパスワードを入力してログインをクリック
スクリーンショット 2020-05-23 20.19.00.png

・トップページに遷移してログアウトが表示される。
スクリーンショット 2020-05-23 20.22.42.png

スタイルが当たってないので見た目はしょぼいですが、とりあえずサーバーサイドの実装は完了です!
今回の記事を参考にして、ぜひ爆速実装を目指してみてください!

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

【備忘録】Devise + cancancan の使い方

はじめに

deviseを使用してUserモデル(認証)を作成後して
cancancanを追加して認可をできるようにしました。

その過程を備忘録としてログします。

deviseのgemを追記します。

・
・
・
gem 'devise'

バンドルインストールします。

$bundle install

デバイスをインストールします。

$rails generate devise:install

設定があるか確認します。

config/environments/development.rb


config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

page#indexを追記します。

config/routes.rb
Rails.application.routes.draw do
root to: "page#index"


end

以下を追記します。

app/views/layouts/application.html.erb
<body>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
・
・
</body>

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

$ rails g devise:views

以下が作られます。

app/views/devise/unlocks/new.html.erb
app/views/devise/shared/links.html.erb
app/views/devise/shared/_error_messages.html.erb
app/views/devise/sessions/new.html.erb
app/views/devise/registrations/new.html.erb
app/views/devise/registrations/edit.html.erb
app/views/devise/passwords/new.html.erb
app/views/devise/passwords/edit.html.erb
app/views/devise/mailer/unlock
instructions.html.erb
app/views/devise/mailer/reset_password_instructions.html.erb
app/views/devise/mailer/password_change.html.erb
app/views/devise/mailer/email_changed.html.erb
app/views/devise/mailer/confirmation_instructions.html.erb
app/views/devise/confirmations/new.html.erb

モデルを作成します。
今回はUserモデルを作ります。

$ rails g devise user

以下モデルが作られます。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

以下が同時に作られます。

config/application.rb
config.i18n.default_locale = :ja

以下も作られます。

config/routes.rb
ails.application.routes.draw do
devise_for :users


end

モデル作成後に、DBに反映します。

rails db:migrate

まだpage#indexのViewがないので作ります。

$ rails g controller Pages index

余談ですが、routes.rbにスコープを作ることでPathを変更できます。

routes.rb
  devise_scope :user do
    get 'login', to: 'devise/sessions#new'
    post 'login', to: 'devise/sessions/#create'
    delete 'logout', to: 'devise/sessions#destroy' 
  end

これにより、
http://localhost:3000/login
にアクセスしたときに、ログインページが表示されます。

もちろんデフォルトの/users/sign_inでもログインページが表示されるので必要であれば削除しておきましょう。

routes.rb
 devise_for :users, skip: [:sessions]

デバイスの設定は完了です。

cancancanのインストール

次にcancancanをインストールしていきます。
これは認可と言って、ユーザによって、
アクセス権を付与するという意味になります。
システム管理者と一般ユーザの違いです。

gem 'cancancan'

バンドルインストールします。

$bundle install

Abilityクラスが作成されます。

rails g cancan:ability

Abilityクラスのデフォルトです。

class Ability
  include CanCan::Ability

  def initialize(user)
    # Define abilities for the passed in user here. For example:
    #
    #   user ||= User.new # guest user (not logged in)
    #   if user.admin?
    #     can :manage, :all
    #   else
    #     can :read, :all
    #   end
    #
    # The first argument to `can` is the action you are giving the user
    # permission to do.
    # If you pass :manage it will apply to every action. Other common actions
    # here are :read, :create, :update and :destroy.
    #
    # The second argument is the resource the user can perform the action on.
    # If you pass :all it will apply to every resource. Otherwise pass a Ruby
    # class of the resource.
    #
    # The third argument is an optional hash of conditions to further filter the
    # objects.
    # For example, here the user can only update published articles.
    #
    #   can :update, Article, :published => true
    #
    # See the wiki for details:
    # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities
  end
end

以下に変更します。

# frozen_string_literal: true

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new
    can :read,  :all

    if user.admin? 
      can :manage,  :all

    end



    # Define abilities for the passed in user here. For example:
    #
    #   user ||= User.new # guest user (not logged in)
    #   if user.admin?
    #     can :manage, :all
    #   else
    #     can :read, :all
    #   end
    #
    # The first argument to `can` is the action you are giving the user
    # permission to do.
    # If you pass :manage it will apply to every action. Other common actions
    # here are :read, :create, :update and :destroy.
    #
    # The second argument is the resource the user can perform the action on.
    # If you pass :all it will apply to every resource. Otherwise pass a Ruby
    # class of the resource.
    #
    # The third argument is an optional hash of conditions to further filter the
    # objects.
    # For example, here the user can only update published articles.
    #
    #   can :update, Article, :published => true
    #
    # See the wiki for details:
    # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities
  end
end

user ||= User.new
上記がないとゲストユーザで操作しているときに、エラーになりました。

if user.admin? ←adminって何?と聞かれました。

def initialize(user)
user ||= User.new
can :read, :all
Abilityモデルが呼ばれたときに
初期値としてログインしていれば、そのユーザを変数に入れ、ログインしていなければゲストユーザが変数に代入されます。
そして、ユーザ変数に対して、read権限を付与するという意味となる。全てのユーザがread権限があるということです。

read権限というのは、CRUDに紐づいている訳ではないので注意しましょう。
ページが読めるということであり、getができるという意味ではないし、createメソッドとかも別にプログラムすればできます。

if user.admin? 
  can :manage,  :all

こちらはUserモデルに作成したadminカラムにtrueが入っていれば実行するプログラムです。
つまりは管理者権限を持つユーザということになります。

Page#indexのビューページに以下を追加します。

app/views/pages/index.html.erb
<h1>Pages#index</h1>
<p>Find me in app/views/pages/index.html.erb</p>
  <%= link_to "削除", login_path, method: :delete %>

<% if can?  :update, current_user %>
  <h1>update</h1>

<% end %>

<% if can? :read, current_user %>
  <h1>read</h1>
<% end %>

<% if can? :update, current_user %>
今ログインしているユーザがupdateの権限を持っていれば中身を表示します。

<% if can? :read, current_user %>
もしログインしているユーザがreadの権限を持っていれば中身を表示します。

という意味になります。

db/schema.rb
  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.boolean "admin", default: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

上記でadminってUserモデルにないけどどうするの?ってなるかと思うが、
追加することとなります。

$rails g migration AddAdminToUser admin:boolean

デフォルトで以下が作成されるので
default: "false"を追加します。

ビューページでは、adminを選択するビューは作成せず、デフォルトでユーザを作成したらadmin=falseがつくようにします。
ここでは、adminは管理側で意図的に作成することとします。

db/migrate/20200522114428_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :admin, :boolean, default: "false"
  end
end

マイグレートします。

$rails g migrate

DBのスキーマの確認をします。

db/schema.rb
 create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.boolean "admin", default: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

http://localhost:3000/
で一般ユーザを作成しておきます。

adminユーザは、
以下の通り作成します。

$user =User.new(id: xx, email: "xxx@yyy", password: "xxxx", admin: true)

これで手続きは完了なので、
実際に一般ユーザとAdminユーザそれぞれで、
Page#indexにアクセスします。

一般ユーザはread権限のみなのでreadを表示可能です。

Adminユーザは,manage権限(全ての権限を持つ)なのでread,updateそれぞれ表示可能です。

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

【備忘録】Rails6 Devise + cancancan の使い方

はじめに

deviseを使用してUserモデル(認証)を作成後して
cancancanを追加して認可をできるようにしました。

その過程を備忘録としてログします。

deviseのgemを追記します。

・
・
・
gem 'devise'

バンドルインストールします。

$bundle install

デバイスをインストールします。

$rails generate devise:install

設定があるか確認します。

config/environments/development.rb


config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

page#indexを追記します。

config/routes.rb
Rails.application.routes.draw do
root to: "page#index"


end

以下を追記します。

app/views/layouts/application.html.erb
<body>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
・
・
</body>

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

$ rails g devise:views

以下が作られます。

app/views/devise/unlocks/new.html.erb
app/views/devise/shared/links.html.erb
app/views/devise/shared/_error_messages.html.erb
app/views/devise/sessions/new.html.erb
app/views/devise/registrations/new.html.erb
app/views/devise/registrations/edit.html.erb
app/views/devise/passwords/new.html.erb
app/views/devise/passwords/edit.html.erb
app/views/devise/mailer/unlock
instructions.html.erb
app/views/devise/mailer/reset_password_instructions.html.erb
app/views/devise/mailer/password_change.html.erb
app/views/devise/mailer/email_changed.html.erb
app/views/devise/mailer/confirmation_instructions.html.erb
app/views/devise/confirmations/new.html.erb

モデルを作成します。
今回はUserモデルを作ります。

$ rails g devise user

以下モデルが作られます。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

以下が同時に作られます。

config/application.rb
config.i18n.default_locale = :ja

以下も作られます。

config/routes.rb
ails.application.routes.draw do
devise_for :users


end

モデル作成後に、DBに反映します。

rails db:migrate

まだpage#indexのViewがないので作ります。

$ rails g controller Pages index

余談ですが、routes.rbにスコープを作ることでPathを変更できます。

routes.rb
  devise_scope :user do
    get 'login', to: 'devise/sessions#new'
    post 'login', to: 'devise/sessions/#create'
    delete 'logout', to: 'devise/sessions#destroy' 
  end

これにより、
http://localhost:3000/login
にアクセスしたときに、ログインページが表示されます。

もちろんデフォルトの/users/sign_inでもログインページが表示されるので必要であれば削除しておきましょう。

routes.rb
 devise_for :users, skip: [:sessions]

デバイスの設定は完了です。

cancancanのインストール

次にcancancanをインストールしていきます。
これは認可と言って、ユーザによって、
アクセス権を付与するという意味になります。
システム管理者と一般ユーザの違いです。

gem 'cancancan'

バンドルインストールします。

$bundle install

Abilityクラスが作成されます。

rails g cancan:ability

Abilityクラスのデフォルトです。

class Ability
  include CanCan::Ability

  def initialize(user)
    # Define abilities for the passed in user here. For example:
    #
    #   user ||= User.new # guest user (not logged in)
    #   if user.admin?
    #     can :manage, :all
    #   else
    #     can :read, :all
    #   end
    #
    # The first argument to `can` is the action you are giving the user
    # permission to do.
    # If you pass :manage it will apply to every action. Other common actions
    # here are :read, :create, :update and :destroy.
    #
    # The second argument is the resource the user can perform the action on.
    # If you pass :all it will apply to every resource. Otherwise pass a Ruby
    # class of the resource.
    #
    # The third argument is an optional hash of conditions to further filter the
    # objects.
    # For example, here the user can only update published articles.
    #
    #   can :update, Article, :published => true
    #
    # See the wiki for details:
    # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities
  end
end

以下に変更します。

# frozen_string_literal: true

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new
    can :read,  :all

    if user.admin? 
      can :manage,  :all

    end



    # Define abilities for the passed in user here. For example:
    #
    #   user ||= User.new # guest user (not logged in)
    #   if user.admin?
    #     can :manage, :all
    #   else
    #     can :read, :all
    #   end
    #
    # The first argument to `can` is the action you are giving the user
    # permission to do.
    # If you pass :manage it will apply to every action. Other common actions
    # here are :read, :create, :update and :destroy.
    #
    # The second argument is the resource the user can perform the action on.
    # If you pass :all it will apply to every resource. Otherwise pass a Ruby
    # class of the resource.
    #
    # The third argument is an optional hash of conditions to further filter the
    # objects.
    # For example, here the user can only update published articles.
    #
    #   can :update, Article, :published => true
    #
    # See the wiki for details:
    # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities
  end
end

user ||= User.new
上記がないとゲストユーザで操作しているときに、エラーになりました。

if user.admin? ←adminって何?と聞かれました。

def initialize(user)
user ||= User.new
can :read, :all
Abilityモデルが呼ばれたときに
初期値としてログインしていれば、そのユーザを変数に入れ、ログインしていなければゲストユーザが変数に代入されます。
そして、ユーザ変数に対して、read権限を付与するという意味となる。全てのユーザがread権限があるということです。

read権限というのは、CRUDに紐づいている訳ではないので注意しましょう。
ページが読めるということであり、getができるという意味ではないし、createメソッドとかも別にプログラムすればできます。

if user.admin? 
  can :manage,  :all

こちらはUserモデルに作成したadminカラムにtrueが入っていれば実行するプログラムです。
つまりは管理者権限を持つユーザということになります。

Page#indexのビューページに以下を追加します。

app/views/pages/index.html.erb
<h1>Pages#index</h1>
<p>Find me in app/views/pages/index.html.erb</p>
  <%= link_to "削除", logout_path, method: :delete %>

<% if can?  :update, current_user %>
  <h1>update</h1>

<% end %>

<% if can? :read, current_user %>
  <h1>read</h1>
<% end %>

<% if can? :update, current_user %>
今ログインしているユーザがupdateの権限を持っていれば中身を表示します。

<% if can? :read, current_user %>
もしログインしているユーザがreadの権限を持っていれば中身を表示します。

という意味になります。

db/schema.rb
  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.boolean "admin", default: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

上記でadminってUserモデルにないけどどうするの?ってなるかと思うが、
追加することとなります。

$rails g migration AddAdminToUser admin:boolean

デフォルトで以下が作成されるので
default: "false"を追加します。

ビューページでは、adminを選択するビューは作成せず、デフォルトでユーザを作成したらadmin=falseがつくようにします。
ここでは、adminは管理側で意図的に作成することとします。

db/migrate/20200522114428_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :admin, :boolean, default: "false"
  end
end

マイグレートします。

$rails g migrate

DBのスキーマの確認をします。

db/schema.rb
 create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.boolean "admin", default: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

http://localhost:3000/
で一般ユーザを作成しておきます。

adminユーザは、
以下の通り作成します。

$user =User.new(id: xx, email: "xxx@yyy", password: "xxxx", admin: true)

これで手続きは完了なので、
実際に一般ユーザとAdminユーザそれぞれで、
Page#indexにアクセスします。

一般ユーザはread権限のみなのでreadを表示可能です。

Adminユーザは,manage権限(全ての権限を持つ)なのでread,updateそれぞれ表示可能です。

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

Google Books APIから情報を格納するモデルを作り、直感的な扱いとテストを可能にする

対象読者

  • 本に関するRailsポートフォリオを作る方
  • Google Books APIを用いたRailsポートフォリオを作る方

本記事の到達物

Google Books APIのIDから情報を取得し、クラスに格納する

pry(main)> @google_book = GoogleBook.new_from_id('c1L4IzmUzicC')

pry(main)> @google_book.title
=> "Practical Rails Plugins"

pry(main)> @google_book.authors
=> ["Nick Plante", "David Berube"]

pry(main)> @google_book.image
=> "http://books.google.com/books/content?id=c1L4IzmUzicC&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE73ENsMYFOfY27vluLqgI1cO-b80lA7enoeZzzcDGEhA5NWIj3djHvd6gvP1zlKoMoC4V0_7fKVuIjWQDYVs4FrDjHvxoqtRUcxHZ9L7isRtsHc2Cs5iS6DPAQQcTT20Oseo9gq_&source=gbs_api"

キーワードから検索し、複数のオブジェクトを返す

pry(main)> @google_books = GoogleBook.search('Rails')

pry(main)> @google_books[0].title
=> "実践Rails"

pry(main)> @google_books.last.authors
=> ["Sam Ruby", "Dave Thomas", "David Heinemeier Hansson"]

pry(main)> @google_books.map { |google_book| google_book.title }
=> ["実践Rails",
 "独習Ruby on Rails",
 "Railsレシピ",
 "Ajax on Rails",
 "Ruby on Rails 4アプリケーションプログラミング",
 "Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法",
 "Ruby on Rails 5 超入門",
 "RailsによるアジャイルWebアプリケーション開発第3版",
 "JRuby on Rails 実践開発ガイド",
 "RailsによるアジャイルWebアプリケーション開発第4版"]

pry(main)> @google_books.class
=> Array

格納された情報を、複数のテーブルに保存できる

pry(main)> @google_book = GoogleBook.new_from_id('wlNHDwAAQBAJ')

pry(main)> @google_book.title                                   
=> "Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法"

pry(main)> @google_book.authors
=> ["太田 智彬", "寺下 翔太", "手塚 亮", "宗像 亜由美", "株式会社リクルートテクノロジーズ"]

pry(main)> @google_book.save
=> true


pry(main)> @book = Book.last

pry(main)> @book.title
=> "Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法"

pry(main)> @book.authors[0].name
=> "太田 智彬"

pry(main)> @book.authors.size
=> 5

pry(main)> @book.authors.class
=> Author::ActiveRecord_Associations_CollectionProxy


pry(main)> @author = Author.last

pry(main)> @author.name
=> "株式会社リクルートテクノロジーズ"

 本記事で書くこと

Google Books APIからの情報を格納する、GoogleBookモデルを作成します。
大きなメリットとしては、以下の2つがあります。

  • Google Books APIから受け取った情報をControllerやViewで直感的に扱える。
  • 情報を取得する / 情報を格納して整理する / 複数テーブルに跨いで保存をする、などのロジックを分離して書くことができ、それぞれテストを行える。

本記事ではControllerでの使用例、テストについても記載します。

DB設計

データベースには、本の以下の情報を保存します。

  • 「タイトル」
  • 「著者」
  • 「画像URL」
  • 「出版日」
  • 「Google Books APIのID」

もちろん、Google Books APIにある情報であれば、これ以外も取得し保存することができます。
本記事では説明のために、取得する情報としては少なめにしてあります。

ER図

名称未設定ファイル.png

BooksテーブルとAuthorsテーブルを用意します。
一つの本に対し、著者は複数人いることがあるので、book has_many authorsの関係にします。

ただし、「本を一覧で表示する」ようなページで、著者全員でなく代表著者を表示したい場合があると思います。
そのため、authorsテーブルにはis_representativeカラムを用意しておきます。

また、「Google Books APIから情報を取得するなら、自前のデータベースに情報を持つ必要は無いのでは?」と思われるかもしれません。
その設計を行い、失敗した話を以下に載せておきます。
GoogleBooksAPIだけで本リソースの取得をする設計を行い、失敗した話

要約すると、本の情報は自前のデータベースでも情報を持っておいた方が良い、という結論になります。

マイグレーションファイル

マイグレーションファイルを作るなら、以下のようになるかと思います。

 Booksテーブル
db/migrate/20202020202020_create_books
class CreateBooks < ActiveRecord::Migration[5.2]
  def change
    create_table :books do |t|
      t.string :google_books_api_id, null: false
      t.string :title, null: false
      t.string :image
      t.date :published_at

      t.timestamps
    end

    add_index :books, :googlebooksapi_id, unique: true
  end
end

Google Books APIのIDは必ず必要なため、null: falseを指定しておきます。
また、Google Books APIのIDが重複することは考えられないため、ユニークキーも付与します。

タイトルが存在しない本は、存在しないと考えられるため、null: falseを指定しておきます。

逆に、他の情報にnull: falseを指定するのには注意が必要です。
外部APIからの情報なわけで、本によってはその情報が無いことがあり、「DBに登録できない」という事態を発生させうるからです。

 Authorsテーブル
db/migrate/20202020202021_create_authors
class CreateAuthors < ActiveRecord::Migration[5.2]
  def change
    create_table :authors do |t|
      t.references :book, foreign_key: true
      t.string :name, null: false
      t.boolean :is_representative, null: false

      t.timestamps
    end
  end
end

Google Books APIを叩くメソッド

まずはAPIを叩くモジュールをapp/lib/配下に追加します。

app/lib/google_books_api.rb
module GoogleBooksApi
  def url_of_creating_from_id(googlebooksapi_id)
    "https://www.googleapis.com/books/v1/volumes/#{googlebooksapi_id}"
  end
  #  Google Books APIのIDから、APIのURLを取得する

  def url_of_searching_from_keyword(keyword)
    "https://www.googleapis.com/books/v1/volumes?q=#{keyword}&country=JP"
  end
  #  キーワードから、検索するAPIのURLを取得する

  def get_json_from_url(url)
    JSON.parse(Net::HTTP.get(URI.parse(Addressable::URI.encode(url))))
  end
  #  URLから、JSON文字列を取得し、JSONオブジェクトを構築する
end

本記事で使用するGoogle Books APIは2種類です。

  • IDから一つの本の情報を返してくれる

https://www.googleapis.com/books/v1/volumes/:ID
というURLによって取得できます。
以下のURLがその例です。
https://www.googleapis.com/books/v1/volumes/aB4B13xGEv4C

Google Books APIから取得できる情報がどんなものか知らない場合、上のURLを見て確認してみて下さい。
タイトル、出版日、購入リンク、ISBN、など色々取得できることが分かると思います。

  • キーワードから検索結果群を返してくれる

https://www.googleapis.com/books/v1/volumes?q=search?:キーワード
というURLによって取得できます。
以下のURLがその例です。
https://www.googleapis.com/books/v1/volumes?q=search?Rails

Google Books APIの他仕様について知りたい方は、公式ドキュメントをご参照ください。
Getting Started

URLをエスケープするために、addressableというgemを使っています。
以下gemをGemfileに追記しbundle installしてください。

Gemfile
gem 'addressable'

ちなみに、Railsでは、app/**/**.rbを自動で読み込んでくれます。
よって使いたいクラス内でinclude GoogleBooksApiincludeさえすれば、以上3つのメソッドをクラス内で使用できます。

情報を格納するモデル

Google Books APIからの情報を、オブジェクトとして格納するモデルを以下のように作ります。

app/models/google_book.rb
class GoogleBook
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

  attribute :googlebooksapi_id, :string
  attribute :authors
  attribute :image, :string
  attribute :published_at, :date
  attribute :title, :string

  validates :googlebooksapi_id, presence: true
  validates :title, presence: true

  class << self
    include GoogleBooksApi

    def new_from_item(item)
      @item = item
      @volume_info = @item['volumeInfo']
      new(
        googlebooksapi_id: @item['id'],
        authors: @volume_info['authors'],
        image: image_url,
        published_at: @volume_info['publishedDate'],
        title: @volume_info['title'],
      )
    end

    def new_from_id(googlebooksapi_id)
      url = url_of_creating_from_id(googlebooksapi_id)
      item = get_json_from_url(url)
      new_from_item(item)
    end

    def search(keyword)
      url = url_of_searching_from_keyword(keyword)
      json = get_json_from_url(url)
      items = json['items']
      return [] unless items

      items.map do |item|
        GoogleBook.new_from_item(item)
      end
    end

    private

    def image_url
      @volume_info['imageLinks']['smallThumbnail'] if @volume_info['imageLinks'].present?
    end
  end

  def save
    return false unless valid?

    book = build_book
    return false unless book.valid?

    ActiveRecord::Base.transaction do
      book.remote_image_url = image if image.present?
      book.save
      authors.each.with_index do |author, index|
        author = book.authors.build(name: author)
        author.is_representation = index.zero?
        author.save
      end
    end
    true
  end

  def find_book_or_save
    if Book.find_by(googlebooksapi_id: googlebooksapi_id) || save
      Book.find_by(googlebooksapi_id: googlebooksapi_id)
    else
      false
    end
  end

  private

  def build_book
    Book.new(
      googlebooksapi_id: googlebooksapi_id,
      published_at: published_at,
      title: title,
    )
  end
end

長くなりました笑
一つ一つ解説していきます。

ActiveModel

  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

ActiveModelとは「データベースと連携しないActiveRecord」みたいなものです。

Active Model の基礎 - Railsガイド

ActiveModelのattribute

  attribute :googlebooksapi_id, :string
  attribute :authors
  (以下略)

ActiveModel::Attributesincludeしたので使えます。
authorsには配列を入れることを想定していますが、配列に対応するattributeの型は無いようなので、この書き方にしています。

ActiveModel::Attributes が最高すぎるんだよな。

ActiveModelのvalidates

  validates :googlebooksapi_id, presence: true
  validates :title, presence: true

Google Books APIのIDが無い本、タイトルの存在しない本は存在しない(と思われる)ので、validetesを入れておきます。
仮にGoogle Books APIのIDが無い本がオブジェクトとして格納された場合、valid?メソッドを使ったときにfalseを返すことができます。(後述しますが、saveメソッドにて使用します)

クラスメソッドの定義方法

  class << self

new_from_idGoogleBook.new_from_id('c1L4IzmUzicC')のようにクラスメソッドとして使いたいメソッドです。クラスメソッドを定義する方法は他にもありますが、class << slefによるやり方が推奨なようです。

Rubyのクラスメソッドをclass << selfで定義している理由(翻訳)

IDからインスタンスを生成する

    def new_from_id(googlebooksapi_id)
      url = url_of_creating_from_id(googlebooksapi_id)
      item = get_json_from_url(url)
      new_from_item(item)
    end

順番がちょっと前後しますが、new_from_itemより先にnew_from_idを説明します。

前述のGoogleBooksApiモジュールのurl_of_creating_from_idおよびget_json_from_urlを使用することで、一つの本の情報(JSON)をitemとして取得します。
そのitemをnew_from_itemに渡します。

itemからインスタンスを生成する

    def new_from_item(item)
      @item = item
      @volume_info = @item['volumeInfo']
      new(
        googlebooksapi_id: @item['id'],
        authors: @volume_info['authors'],
        image: image_url,
        published_at: @volume_info['publishedDate'],
        title: @volume_info['title'],
      )
    end

Google Books APIの中身を観察すると分かりますが、多くの情報はitem['volumeInfo']に入っています。
取り出したい情報を上記のように適宜定義し、new()を使ってインスタンスを生成する、というロジックにします。
image_urlだけ下記のやり方で実装しています。

image_urlの実装

    private

    def image_url
      @volume_info['imageLinks']['smallThumbnail'] if @volume_info['imageLinks'].present?
    end

本によってはvolume_info['imageLinks']が入っていない本が存在するため、そのままvolume_info['imageLinks']['smallThumbnail']だけで使おうとするとundefindのエラーが出てしまうことがあります。
undefindを出さないようにするため、上記のように実装します。

また、このimage_urlメソッドはクラス外で使用することは考えられないので、private下で定義します。

検索結果をインスタンス群として返すクラスメソッド

    def search(keyword)
      url = url_of_searching_from_keyword(keyword)
      json = get_json_from_url(url)
      items = json['items']
      return [] unless items

      items.map do |item|
        GoogleBook.new_from_item(item)
      end
    end

Google Books APIのうち、検索結果群を返してくれるAPIを使った場合、返ってくるのは複数のitemsとなります。
itemsは配列として[item1, item2, item3, ...]という形になっています。
mapメソッド内で一つ一つnew_from_item(item)をすることで、[googlebook1, googlebook2, googlebook3, ...]という配列の形で返すことができます。

不適切なキーワードだと検索結果群を返してくれるAPIでitemsが返ってこないため、その場合もundefindのエラーが発生してしまいます。
そこでreturn [] unless itemsの一行を入れることで、itemsが無い場合は空の配列を返すようにします。

まだsaveメソッド、find_book_or_saveメソッドの説明が残っていますが、先にControllerでの使い方を見るほうが分かりやすいと思うので、Controllerの説明に移りたいと思います。

Controllerでの使用例

GoogleBookクラスを使うのは、「検索画面」と「リソース登録(create)」の2つかと思います。

ルーティング

config/routes.rb
  resources :books, only: %i[create show], shallow: true do
    collection do
      get :search
    end
  end

あらかじめ、上記のようなルーティングは設定しておきます。
本筋とズレてしまうので説明は省きます。

検索画面のController

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def search
    @search_form = SearchBooksForm.new(search_books_params)
    books = GoogleBook.search(@search_form.keyword)
    @books = Kaminari.paginate_array(books).page(params[:page])
  end

  private

  def search_books_params
    params.fetch(:q, keyword: '').permit(:keyword)
  end
end

弊記事で恐縮ですが、検索フォームは以下の記事のやり方で実装します。
【Ruby on Rails】フォームオブジェクトを使って検索キーワードをcontrollerに送る

受け取ることのできた検索キーワード(@search_form.keyword)をGoogleBook.searchに渡して、検索結果をGoogleBookクラスから生成したインスタンス群の配列として受け取ります。

以降は任意ですが、私は検索結果をkaminariのページネーションで表示したかったため、Kaminari.paginate_arrayに渡して使用しました。

リソース登録のController ver.1(今までのメソッドのみで書くNGな例)

やりたいこととしては以下の通りです。

  • GoogleBookモデルに持っていた情報をBookモデルやAuthorモデルに格納し直す
  • ヴァリデーションをかける
  • ヴァリデーション成功時にはDBに保存する
app/models/google_book.rb
class BooksController < ApplicationController
  def create
    google_book = GoogleBook.new_from_id(create_book_params[:googlebooksapi_id])
    @book = Book.build(
              googlebooksapi_id: google_book.googlebooksapi_id,
              published_at: google_book.published_at,
              title: google_book.title,
            )
    if @book.valid?
      @book.remote_image_url = google_book.image if google_book.image.present?
      @book.save
      google_book.authors.each.with_index do |author, index|
        @author = @book.authors.build(name: author)
        @author.is_representation = index.zero?
        @author.save
      end
      redirect_to @book
    else
      redirect_to search_books_path, danger: 'ページの表示に失敗しました'
    end
  end

  private

  def create_book_params
    params.permit(:googlebooksapi_id)
  end
end

しかし、上記の実装には色々な問題が発生しています。

  • Fat Controllerになってしまっている
  • 重複した本を弾けていない
  • 複数リソースへのデータ登録なのに、ActiveRecord::Base.transactionを使えていない

特にFat Controllerの問題はなんとかしなくてはいけないです。
そもそも、このDBへの保存に関する処理はGoogleBookモデルが責務を持つ処理かと思います。
よって、保存に関するロジックはGoogleBookのインスタンスメソッドとして定義することにします。

複数テーブルへの保存を実現する、saveメソッド

app/models/google_book.rb
  def save
    return false unless valid?
    book = build_book

    ActiveRecord::Base.transaction do
      book.remote_image_url = image if image.present?
      book.save
      if authors.present?
        authors.each.with_index do |author, index|
          author = book.authors.build(name: author)
          author.is_representative = index.zero?
          author.save
        end
      end
    end
    true
  end

  private

  def build_book
    Book.new(
      googlebooksapi_id: googlebooksapi_id,
      published_at: published_at,
      title: title,
    )
  end

ActiveRecordsaveメソッド風に仕上げるため、保存に成功すればtrue、失敗すればfalseを返すようにしたいです。
そこで、return false unless valid?の一行で、valid?で失敗したときにはfalseを返すようにしました。
また、成功時の最後にtrueの一行を入れることで、成功時にはtrueを返すことができます。

ActiveRecord::Base.transactionで囲うことで、複数リソース登録時で途中で失敗したときにロールバックを走らせることができます。
この場合では、何かの不具合で後半のauthor.saveで失敗したとしても、前半のbook.saveを取り止めにすることができます。

book.remote_image_url = image if image.present?はCarrierwaveで画像のアップロードを行うロジックになります。本筋とズレるので、今回は説明は省きます。

author.is_representative = index.zero?は、authors配列のうち、最初のインデックスにある著者を「代表著者」とするための一行です。
each.with_indexを使って配列を回してるのもこれが理由です。

リソース登録のController ver.2(重複の登録をしてしまう)

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def create
    google_book = GoogleBook.new_from_id(create_book_params[:googlebooksapi_id])
    if google_book.save
      @book = Book.find_by(googlebooksapi_id: google_book.googlebooksapi_id)
      redirect_to @book
    else
      redirect_to search_books_path, danger: 'ページの表示に失敗しました'
    end
  end

  private

  def create_book_params
    params.permit(:googlebooksapi_id)
  end
end

かなりスッキリしましたが、まだ問題が残っています。
Google Books APIのIDが重複した、すなわち同じ本を登録してしまう可能性があります。

find_or_create_by風味な実装

実現したいのは以下のことです。

  • すでにbooksテーブルにその本があるなら、そのレコードに対応したモデルを返す
  • なければ、saveメソッドを実行して、新しく作ったレコードに対応したモデルを返す
  • booksテーブルにその本が無く、saveメソッドが失敗するなら、falseを返す

ActiveRecordで言えば、find_or_create_byに近い挙動を実現したいことになります。
GoogleBookのインスタンスメソッドとして、以下のようにfind_book_or_saveメソッドとして実装します。

app/models/google_book.rb
  def find_book_or_save
    if Book.find_by(googlebooksapi_id: googlebooksapi_id) || save
      Book.find_by(googlebooksapi_id: googlebooksapi_id)
    else
      false
    end
  end

リソース登録のController ver.3(完成)

app/controllers/books_controller.rb
class BooksController < ApplicationController
  def create
    google_book = GoogleBook.new_from_id(create_book_params[:googlebooksapi_id])
    if (@book = google_book.find_book_or_save)
      redirect_to @book
    else
      redirect_to search_books_path, danger: 'ページの表示に失敗しました'
    end
  end

  private

  def create_book_params
    params.permit(:googlebooksapi_id)
  end
end

RSpecでテストを書く

今回扱うテストは3種類とします。

  • Google Books APIを叩くメソッドのテスト
  • GoogleBookのモデルテスト
  • 本の登録時のリクエストスペック

テストの書き方については若干自信が無いため、ご指摘お待ちしております笑

Google Books APIを叩くメソッドのテスト

spec/lib/google_books_api_spec.rb
require 'rails_helper'

describe GoogleBooksApi do
  let(:test_class) { Struct.new(:google_books_api) { include GoogleBooksApi } }
  let(:google_books_api) { test_class.new }

  it '検索するAPIを叩き、複数のデータを返すkindが取得できること' do
    url = google_books_api.url_of_searching_from_keyword('Rails')
    expect(google_books_api.get_json_from_url(url)['kind']).to eq 'books#volumes'
  end

  it 'IDから本の情報を取得するAPIを叩き、特定データを返すkindが取得できること' do
    GOOGLE_BOOKS_API_ID_SAMPLE = 'aB4B13xGEv4C'.freeze
    url = google_books_api.url_of_creating_from_id(GOOGLE_BOOKS_API_ID_SAMPLE)
    expect(google_books_api.get_json_from_url(url)['kind']).to eq 'books#volume'
    expect(google_books_api.get_json_from_url(url)['id']).to eq GOOGLE_BOOKS_API_ID_SAMPLE
  end
end

モジュールのテストをするやり方に関しては以下サイトを参考にさせて頂きました。

【Ruby on Rails】初心者でもメソッドの単体テストがしたい!!

GoogleBookのモデルテスト

まずはFactoryBotを定義しておきます。

spec/factories/google_book.rb
FactoryBot.define do
  factory :google_book do
    googlebooksapi_id { 'wlNHDwAAQBAJ' }
    authors do
      [
        '太田 智彬',
        '寺下 翔太',
        '手塚 亮',
        '宗像 亜由美',
        '株式会社リクルートテクノロジーズ'
      ]
    end
    image { 'http://books.google.com/books/content?id=wlNHDwAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE70j5lrdzOYN-iUu8w-G_JJKpEhnpUGAgqyZd7rj4jHu59NcAU48eQ75T4fkdyyZD6dMlwjjw0sAdQSKY_HiEdNBMMeyDn4DUmOcY-oLHFRAnxPXocc_T_PA7NYdSlZdwKckhCMy&source=gbs_api' }
    published_at { '2018-01-24' }
    title { 'Ruby on Rails 5の上手な使い方 現場のエンジニアが教えるRailsアプリケーション開発の実践手法' }
  end
end

現場Railsです。
次にモデルテストの本体です。

app/models/google_book_spec.rb
require 'rails_helper'

RSpec.describe GoogleBook, type: :model do
  it '有効なファクトリを持つこと' do
    google_book = build(:google_book)
    expect(google_book).to be_valid
  end

  it 'Google Books APIのIDが存在しないときに無効なこと' do
    google_book = build(:google_book, googlebooksapi_id: nil)
    google_book.valid?
    expect(google_book.errors.messages[:googlebooksapi_id]).to include('を入力してください')
  end

  it 'タイトルが存在しないときに無効なこと' do
    google_book = build(:google_book, title: nil)
    google_book.valid?
    expect(google_book.errors.messages[:title]).to include('を入力してください')
  end

  it 'Google Books APIのIDから目的のインスタンスを生成できること' do
    googlebooksapi_id = 'YEfUBgAAQBAJ'
    google_book = GoogleBook.new_from_id(googlebooksapi_id)
    expect(google_book.title).to eq 'SpriteKitではじめる2Dゲームプログラミング Swift対応'
    expect(google_book.googlebooksapi_id).to eq googlebooksapi_id
    expect(google_book.authors).to eq %w[山下佳隆 村田知常 原知愛 近藤秀彦]
    expect(google_book.author).to eq '山下佳隆'
  end

  it '適切なキーワードから複数の検索結果を返し、そのタイトルにキーワードが含まれていること' do
    keyword = 'Ruby'
    keyword_count = 0
    google_books = GoogleBook.search(keyword)
    expect(google_books.size).to be >= 5 #  検索結果を5個以上は返せる
    google_books.each do |google_book|
      if google_book.title.include?(keyword)
        keyword_count += 1
      end
    end
    expect(keyword_count).to be >= 5 #  キーワードのRubyを含むタイトルが5個以上は返せる
  end

  it '不適切なキーワードからは検索結果を返さないこと' do
    keyword = 'bbvjnaovnaov' #  適当
    google_books = GoogleBook.search(keyword)
    expect(google_books.size).to be 0
  end

  describe '保存時に' do
    context '不適切な情報しか持たないときは' do
      let(:google_book) { build(:google_book, googlebooksapi_id: nil) }
      it '保存に失敗すること' do
        expect { google_book.save }.to change { Book.count }.by(0).and change { Author.count }.by(0)
      end
      it 'falseを返すこと' do
        expect(google_book.save).not_to be_truthy
      end
    end

    context '適切な情報を持っているときは' do
      let(:google_book) { build(:google_book, authors: [
                                  '太田 智彬',
                                  '寺下 翔太',
                                  '手塚 亮',
                                  '宗像 亜由美',
                                  '株式会社リクルートテクノロジーズ'
                                ])
      }
      it '保存できること' do
        expect { google_book.save }.to change { Book.count }.by(1).and change { Author.count }.by(5)
      end
      it 'trueを返すこと' do
        expect(google_book.save).to be_truthy
      end
    end

    context '著者の情報だけを持っていないときにも' do
      let(:google_book) { build(:google_book, authors: nil) }
      it '保存できること' do
        expect { google_book.save }.to change { Book.count }.by(1).and change { Author.count }.by(0)
      end
      it 'trueを返すこと' do
        expect(google_book.save).to be_truthy
      end
    end
  end
end

ヴァリデーションと、それぞれのメソッドをテストしただけなので、あまり説明は要らないかと思います。

本の登録時のリクエストスペック

spec/requests/books_spec.rb
require 'rails_helper'

RSpec.describe 'Books', type: :request do
  it '適切なGoogle Books APIのIDである本が登録できること' do
    expect {
      post '/books', params: { googlebooksapi_id: 'xPbRxgEACAAJ' }
    }.to change { Book.count }.by(1)
    expect(response).to redirect_to book_path(Book.last.id)
  end

  it '既に登録されている本の登録に失敗し、その詳細画面に遷移すること' do
    same_google_books_api_id = 'xPbRxgEACAAJ'
    create(:book, googlebooksapi_id: same_google_books_api_id)
    expect {
      post '/books', params: { googlebooksapi_id: same_google_books_api_id }
    }.to change { Book.count }.by(0)
    expect(response).to redirect_to book_path(Book.find_by(googlebooksapi_id: same_google_books_api_id))
  end
end

対応するControllerは「リソース登録のController ver.3(完成)」のものです。
テストは説明することが少なくて楽で良いですね

以上になります。
改善していければと思っておりますので、間違ってる箇所や改善すべき箇所があればご指摘いただけると嬉しいです。

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

[Ruby]getとpostの違い

Rubyのルーティングの中にgetとpostがあります。
その違いは何なのかを説明していきます。

get:
データベースを変更しないアクション

post:
データベースを変更するアクション

非常にシンプルです

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

Rails+Webスクレイピング+DockerのアプリをEC2で立てようとしたら、デフォルト設定だとt2.small(2GB)とかが必要になりそう

※情弱なのでデフォルトのメモリ設定をしてしまってますが、もしかしたらDockerのメモリ割り当てをうまくやればもっと小さいサイズでいけるかもしれません。

概要

dockerでWebサイト(Rails + Webスクレイピング)をAWSのEC2で立てたいと思っていたんですが、自分しか使わないような遊びサイトだったので、できるだけ安い金額で立てられたらと思っていたんですが、見事に撃沈した話です。

結論

デフォルト設定(Docker、Rails)だとt2.smallを使わないと、bundle installnokogiriを入れるときにメモリエラーになります。

この記事のようにDockerを動かすだけならt2.nanoでも動きます。
※t2.smallならスクレイピング(docker-selenium)も動きました

詳細

どんな環境?

Docker: 19.03.6-ce
docker-compose: 1.25.5
ruby:2.5.3 (Dockerhubのruby:2.5.3のイメージを使用)
Rails: 5.2.3

AWSにDockerを入れるのにはこちらを参考にさせてもらいました。

どんなことが起きたの?

t2.nanoとt2.microのdocker環境内でbundle installをしたら以下エラーが起きました。
(どうでもいいですが、ローカル環境で動いていたDockerfileでbundle installしたときに以下エラーが起き、まだイメージが出来上がる前だったので、わざわざDockerfileのbundle installやdocker-compose.ymlのコマンドからbundle installやそれに依存したrails sをコメントアウトしてdocker-compose.ymlでtail -f Gemfileをしてコンテナ延命した状態で以下のように独立してbundle installを実行しました。docker-composeに慣れてdocker runが打てない人間の悲しい性です)

# bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from http://rubygems.org/............
Fetching rake 12.3.3
Installing rake 12.3.3
Fetching concurrent-ruby 1.1.5
Installing concurrent-ruby 1.1.5
Fetching i18n 1.6.0
Installing i18n 1.6.0
Fetching minitest 5.11.3
Installing minitest 5.11.3
Fetching thread_safe 0.3.6
Installing thread_safe 0.3.6
Fetching tzinfo 1.2.5
Installing tzinfo 1.2.5
Fetching activesupport 5.2.3
Installing activesupport 5.2.3
Fetching builder 3.2.3
Installing builder 3.2.3
Fetching erubi 1.8.0
Installing erubi 1.8.0
Using mini_portile2 2.4.0
Fetching nokogiri 1.10.3
Installing nokogiri 1.10.3 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

    current directory: /usr/local/bundle/gems/nokogiri-1.10.3/ext/nokogiri
/usr/local/bin/ruby -I /usr/local/lib/ruby/site_ruby/2.5.0 -r
./siteconf20200523-12-t3a56p.rb extconf.rb
checking if the C compiler accepts ... yes
Building nokogiri using packaged libraries.
Using mini_portile version 2.4.0
checking for gzdopen() in -lz... yes
checking for iconv... yes
************************************************************************
IMPORTANT NOTICE:

Building Nokogiri with a packaged version of libxml2-2.9.9
with the following patches applied:
        - 0001-Revert-Do-not-URI-escape-in-server-side-includes.patch
        - 0002-Remove-script-macro-support.patch
        - 0003-Update-entities-to-remove-handling-of-ssi.patch

Team Nokogiri will keep on doing their best to provide security
updates in a timely manner, but if this is a concern for you and want
to use the system library instead; abort this installation process and
reinstall nokogiri as follows:

    gem install nokogiri -- --use-system-libraries
        [--with-xml2-config=/path/to/xml2-config]
        [--with-xslt-config=/path/to/xslt-config]

If you are using Bundler, tell it to use the option:

    bundle config build.nokogiri --use-system-libraries
    bundle install

Note, however, that nokogiri is not fully compatible with arbitrary
versions of libxml2 provided by OS/package vendors.
************************************************************************
Extracting libxml2-2.9.9.tar.gz into
tmp/x86_64-pc-linux-gnu/ports/libxml2/2.9.9... OK
Running git apply with
/usr/local/bundle/gems/nokogiri-1.10.3/patches/libxml2/0001-Revert-Do-not-URI-escape-in-server-side-includes.patch...
OK
Running git apply with
/usr/local/bundle/gems/nokogiri-1.10.3/patches/libxml2/0002-Remove-script-macro-support.patch...
OK
Running git apply with
/usr/local/bundle/gems/nokogiri-1.10.3/patches/libxml2/0003-Update-entities-to-remove-handling-of-ssi.patch...
OK
Running 'configure' for libxml2 2.9.9... OK
Running 'compile' for libxml2 2.9.9... OK
Running 'install' for libxml2 2.9.9... OK
Activating libxml2 2.9.9 (from
/usr/local/bundle/gems/nokogiri-1.10.3/ports/x86_64-pc-linux-gnu/libxml2/2.9.9)...
************************************************************************
IMPORTANT NOTICE:

Building Nokogiri with a packaged version of libxslt-1.1.33
with the following patches applied:
        - 0001-Fix-security-framework-bypass.patch

Team Nokogiri will keep on doing their best to provide security
updates in a timely manner, but if this is a concern for you and want
to use the system library instead; abort this installation process and
reinstall nokogiri as follows:

    gem install nokogiri -- --use-system-libraries
        [--with-xml2-config=/path/to/xml2-config]
        [--with-xslt-config=/path/to/xslt-config]

If you are using Bundler, tell it to use the option:

    bundle config build.nokogiri --use-system-libraries
    bundle install
************************************************************************
Extracting libxslt-1.1.33.tar.gz into
tmp/x86_64-pc-linux-gnu/ports/libxslt/1.1.33... OK
Running git apply with
/usr/local/bundle/gems/nokogiri-1.10.3/patches/libxslt/0001-Fix-security-framework-bypass.patch...
OK
Running 'configure' for libxslt 1.1.33... OK
Running 'compile' for libxslt 1.1.33... OK
Running 'install' for libxslt 1.1.33... OK
Activating libxslt 1.1.33 (from
/usr/local/bundle/gems/nokogiri-1.10.3/ports/x86_64-pc-linux-gnu/libxslt/1.1.33)...
checking for -llzma... yes
checking for xmlParseDoc() in libxml/parser.h... yes
checking for xsltParseStylesheetDoc() in libxslt/xslt.h... yes
checking for exsltFuncRegister() in libexslt/exslt.h... yes
checking for xmlHasFeature()... yes
checking for xmlFirstElementChild()... yes
checking for xmlRelaxNGSetParserStructuredErrors()... yes
checking for xmlRelaxNGSetParserStructuredErrors()... yes
checking for xmlRelaxNGSetValidStructuredErrors()... yes
checking for xmlSchemaSetValidStructuredErrors()... yes
checking for xmlSchemaSetParserStructuredErrors()... yes
creating Makefile

current directory: /usr/local/bundle/gems/nokogiri-1.10.3/ext/nokogiri
make "DESTDIR=" clean
current directory: /usr/local/bundle/gems/nokogiri-1.10.3/ext/nokogiri
make "DESTDIR="
make failedCannot allocate memory - make "DESTDIR="

Gem files will remain installed in /usr/local/bundle/gems/nokogiri-1.10.3
for inspection.
Results logged to
/usr/local/bundle/extensions/x86_64-linux/2.5.0/nokogiri-1.10.3/gem_make.out

An error occurred while installing nokogiri (1.10.3), and Bundler
cannot continue.
Make sure that `gem install nokogiri -v '1.10.3' --source
'http://rubygems.org/'` succeeds before bundling.

In Gemfile:
  rails was resolved to 5.2.3, which depends on
    actioncable was resolved to 5.2.3, which depends on
      actionpack was resolved to 5.2.3, which depends on
        actionview was resolved to 5.2.3, which depends on
          rails-dom-testing was resolved to 2.0.3, which depends on
            nokogiri

長すぎて何がエラーかわからないので、見ろと言われたログを見てみます

# cat /usr/local/bundle/extensions/x86_64-linux/2.5.0/nokogiri-1.10.3/gem_make.out
current directory: /usr/local/bundle/gems/nokogiri-1.10.3/ext/nokogiri
/usr/local/bin/ruby -I /usr/local/lib/ruby/site_ruby/2.5.0 -r ./siteconf20200523-53-7krva6.rb extconf.rb
extconf failedCannot allocate memory - /usr/local/bin/ruby

ここでメモリ不足だとわかりましたorz
そりゃ500MBでdockerだもんな

まとめ

月2000円は痛いんですが、仕方ないかぁといった感じです。
誰か同じ状況の人やAWSに関しての意思決定で同じ問題意識を持った人の一助になれば嬉しいです。

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

Ruby on rails を初心者向けに解説⑤ ~データベースの編集と削除~

はじめに

今回は以前の記事の続きになります。

よろしければ、以前の記事も御覧ください。

Ruby on rails を初心者向けに解説①

Ruby on rails を初心者向けに解説② ~リンクの作成~

Ruby on rails を初心者向けに解説③ ~データベースの作成~

Ruby on rails を初心者向けに解説④ ~命名規則とform_Tagの使い方について~

データベースのデータ一覧の表示

以下のコードでusersテーブルのデータを取得して表示しましょう。

index.html.erb
 <% @users.each do |user| %>
   <p>name</p>
   <div class="ruby-name"><%= user.name %></div>
 <% end %>

インスタンス変数@usersを使用するために、コントローラ内で渡しています。

users_controller.rb
def index
  @users = User.all
end

以下のような画面が表示されます。

image.png

また、以下のようにcssを指定しています。

users.scss
.ruby-name {
    color: purple;
    display: inline-block;
    font-weight: bold;
}

データベースの削除

それではデータベースの削除を実装してみましょう。

以下のようにコードを書き換えてください。

index.html.erb
 <% @users.each do |user| %>
   <p>name</p>
   <div class="ruby-name">
     <%= user.name %>
     <%= link_to("削除", "/users/#{user.id}/destroy", {method: "post"})%>
   </div>
 <% end %>

image.png

削除ボタンをクリックすると、要素を削除することができました。

image.png

具体的な処理を見ていきましょう。index.html.erbに、以下のコードが追記されています。

<%= link_to("削除", "/users/#{user.id}/destroy", {method: "post"})%>

これにより、削除をクリックすると、第二引数の処理が実行されます。

第二引数の"/users/#{user.id}/destroy"は、usersコントローラーのdestroyアクションへのルーティングに、データベースのidを含めたものになっています。このようにURLを指定することで、コントローラーは削除したいデータベースのidを受け取ることができます。

第三引数は、これがgetリクエストではなく、postリクエストであることを指定しています。

以下のようにルーティングしてください。

routes.rb
post "users/:id/destroy" => "users#destroy"

:id の部分は、任意の数字を受け取ることができます。受け取った数字は、usersコントローラーにおいて、params[:id]の中に格納されます。

以下のようにコントローラーをコーディングしてください。

users_controller.rb
def destroy
  user = User.find_by(id: params[:id])
  user.destroy
  redirect_to("/users/index")
end

user = User.find_by(id: params[:id])の部分で、データベースからモデルを使ってデータを抜き出します。index.html.erbから送られてきたidと同じidのデータをデータベースから抜き出し、userに格納します。

user.destroyの部分でそのデータを削除しています。

redirect_to("/users/index")の部分で、index.html.erbにリダイレクトしています。今回は、index.html.erbから削除リンクをクリックしたときの動作であるので、リロードになります。

ここまでで、データベースからデータを削除することができました。

データベースの編集

次はデータベースの編集を行っていきましょう。

次のようにindex.html.erbを編集してください。

index.html.erb
 <% @users.each do |user| %>
   <p>name</p>
   <div class="ruby-name">
     <%= user.name %>
     <%= link_to("削除", "/users/#{user.id}/destroy", {method: "post"})%>
     <%= link_to("編集", "/users/#{user.id}/edit") %>
   </div>
 <% end %>

今回新たに追加されたのは<%= link_to("編集", "/users/#{user.id}/edit") %>の部分です。

これにより、新たにusers/editというviewファイルへ移動します。

そのときに、そのviewファイルに編集したいデータベースのidを渡すことになります。

次のようにルーティングしてください。

routes.rb
get "users/:id/edit" => "users#edit"

次のパスに以下のviewファイルを準備してください。
image.png

edit.html.erb
<%= form_tag("/users/#{@user.id}/update") do  %>
  <input type="text" value="<%=@user.name%>" name="new_name">
  <input type="submit" value="送信">
<% end %>

試しに以下のindex.html.erbファイルの編集と書いてある部分をクリックしましょう。
image.png

以下のようになります。
image.png

ここで、edit.html.erbファイルについての解説です。

form_tag("/users/#{@user.id}/update")の部分で、どのコントローラーのどのアクションを使うかを指定しています。

今回は、usersコントローラーのupdateアクションを使用します。また、データベースの編集を行うため、編集したいデータベースのidも送ります。form_tagはpostリクエストになるのでしたね。

<input type="text" value="<%=@user.name%>" name="new_name">の部分で、inputタグの初期値と名前を設定しています。@userはusersコントローラーのeditアクションから送られてきたものです。

users_controller.rb
def edit
  @user = User.find_by(id: params[:id])
end

@userには、データベースからidに応じて探してきた値が格納されています。

それでは、下のように値を変更して送信してみましょう。

image.png

そうすると、以下のルーティングにより、usersコントローラーのupdateアクションが実行されます。

routes.rb
post "users/:id/update" => "users#update"
users_controller.rb
def update
  user = User.find_by(id: params[:id])
  user.name = params[:new_name]
  user.save
  redirect_to("/users/index")
end

updateアクション内では、送られてきたidに応じてデータベースからデータを探索し、ローカル変数userに格納した後、nameカラムのデータを送られてきた新しい名前に書き換えて、user.saveでセーブした後に、/users/indexにリダイレクトしています。

そのため、以下のように変化します。

image.png

終わりに

ここまでで今回の記事は終了です。

お付き合い頂きありがとうございました。

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

Fontawsomeのアイコンが表示されなかった件

はじめに

railsでアプリケーションを作成していました。
アイコンが欲しいと思いFontawsomeを使用することにしました。
ですが表示されないということが起きました。

準備

まずはFontawsomeのアイコンが使えるようにする為に準備しました。
①Gemfileに

gem 'font-awesome-sass', '~> 5.12.0'

を記載し、bundle installしサーバーを一度きり、再起動させました。
②application.scssに

@import "font-awesome-sprockets";
@import "font-awesome";

を記載しました。

③該当のHamlファイルに

= icon('fas','star')

と記載してスターのアイコンを表示させました。
chromeを更新してみると、、、、表示がされていませんでした。

やったこと

①GitHubの公式サイトを見て最新バージョンの確認。
→最新バージョンでした。
②gemfile.lockを確認してインストールされているか確認。
→ちゃんと記載がありました。
③準備の②の記載する順番があっているかの確認。
→公式サイトを見たが合っている。
④hamlの記載方法が間違っていたのかと思い、確認。
→複数サイトを巡って確認したが合っている。
⑤検証ツールにて確認。
→0×0となって画面上にあることが確認できた。
→CSSでfont-sizeやwidthやheightを付けてみた。
→検証ツールのelement上ではCSSがかかっていることになっているが、画面上では表示されない。
⑥他の要素が上に被って表示されないのでは?
→他の要素と被らない全く関係ないところにhamlを記載していたが、表示されず。
⑦fontawsomeのサイトで調べてはいるがが、実はstarというアイコンはないのでは?
→他のアイコンも表示されなかったです。

解決方法

メンターさんに質問して解決した方法は、
stylesheetsディレクトリのapplication.cssを消したら見事表示されました!
ありがとうございます!!

考察

https://www.amazon.co.jp/%E7%8F%BE%E5%A0%B4%E3%81%A7%E4%BD%BF%E3%81%88%E3%82%8B-Ruby-Rails-5%E9%80%9F%E7%BF%92%E5%AE%9F%E8%B7%B5%E3%82%AC%E3%82%A4%E3%83%89-
「Ruby on rails 速習実践ガイド」(上記url)を夜な夜な学習しています。
こちらの本のbootstrapのcssの読み込まれる順番に、
①application.scssにてbootstrapのcssを読み込む定義をする
②application.scssを通じて作成されるapplication.cssでbootstrapのcssが読み込まれる。
③共通レイアウトであるapplication.html.slimでapplication.cssを読み込む。
④各画面がapplication.cssを読み込む。
という感じで記載されています。要約が間違っていたら申し訳ありません。

上記の②でapplication.scssを通じて作成されるはずのapplication.cssを読み込むはずが、私はapplication.cssを削除していなかったので、こちらを読みこまれてしまった。
しかし削除していないapplication.cssにはfontawsomeが記載されていないので、画面上で表示されなかったのではないかと考えました。

まとめ

作成している途中で他のcssは読み込めているので、問題ないからapplication.cssは最後に消せば良いやと考えていましたが、最初に消さないと影響を受けてしまう場合があると学びました。

考察はあくまで私が考えた考察なので合ってるとは限りません。
なので正解を知っている方がいらっしゃればご教授いただけると有難いです。

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

Passing Variable to Partial - undefined local variable or method error 2つの解決法

1: render partial 'my_partial' と書いているか、render 'my_partial' と書いているか確認する

呼び出し方に違いがあります

<%= render 'my_partial', :locals => {:class_Name => "Science", :y => 36} %>
<% render partial: 'my_partial', :locals => {:class_Name => 'Science', :y => 36 } %>

2: 定義されているか確認する

# Rails4以降
<% if defined?(local_var) do %>
# Rails3
<% if local_assigns[:local_var].present? do %>

自分の場合、partialに書いた変数を使わない場合があり、 if defined?(local_var)を書いたら解決した。defined?で確認して、local_varをそのまま使わないようにした。

参考

自分はqiitaの記事のおかげで解決しました
https://qiita.com/shinichinomura/items/1921027fc28279ce54e0
https://stackoverflow.com/questions/16229470/passing-variable-to-partial-undefined-local-variable-or-method-error/21651848#21651848

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

Rails DM機能と非同期でメッセージの送受信を実装する方法

なにこれ

DM機能とメッセージを非同期で送受信するための実装手順をまとめました。
備忘録と言語化するために書いてます!

前提条件

・Usersテーブルが作成済みであること


##全体の流れ
1,テーブル作成
2,モデルでアソシエーションを組む
3,usersコントローラーを編集

テーブル設計

今回は合計4つのテーブルが必要になります。
ユーザー情報を保存するUsersテーブル
誰がDMのどのルームに参加したかuser_idとroom_idを管理するEntriesテーブル
ルーム自体を管理するroomsテーブル
どのルームで誰がメッセージを送ったのか管理するmessagesテーブル
文字だけの説明じゃなくて、
ご自身でテーブルを紙に書き出して見ると分かりやすいと思います!

Usersテーブル

今回の場合はnicknameカラムとavatarカラムを追加してますが、
ここは個人で編集して下さい。
avatarは必要なさそうならコメントアウトすることをオススメします。

マイグレーションファイル
  def change
    add_column :users, :nickname, :string
    add_column :users, :avatar, :string
  end

Entriesテーブル

どのユーザーがどのルームに入ったかを管理するのがEntriesテーブルの役割です!
user_idとroom_idに外部キー制約をかけます。

マイグレーションファイル
class CreateEntries < ActiveRecord::Migration[5.2]
  def change
    create_table :entries do |t|
      t.references :user, foreign_key: true
      t.references :room, foreign_key: true
      t.timestamps
    end
  end
end

Roomsテーブル

どのルームか管理するのがRoomsテーブルの役割です。
このテーブルにカラムは必要ありません!

マイグレーションファイル
class CreateRooms < ActiveRecord::Migration[5.2]
  def change
    create_table :rooms do |t|
      t.timestamps
    end
  end
end

Messagesテーブル

誰がどのルームでどんなメッセージを送ったのか管理するのがMessagesテーブルの役割!
Entriesテーブルと同じくuser_idとroom_idに外部キー制約をかけます。

マイグレーションファイル
class CreateMessages < ActiveRecord::Migration[5.2]
  def change
    create_table :messages do |t|
      t.references :user, foreign_key: true
      t.references :room, foreign_key: true
      t.text :message, null: false
      t.timestamps
    end
  end
end

アソシエーションを組む

userモデル

1人のユーザーは複数のメッセージ、エントリー、ルームに加入してるので
全てhas_manyになります。
has_many :rooms, through: :entriesは、DMしてるユーザー一覧を作るために必要です。
entriesテーブルを経由してroomsの情報をかき集めてきます!

user.rb
  has_many :messages
  has_many :entries
  has_many :rooms, through: :entries

entryモデル

ユーザーの情報とルームはレコードごとに被ることはないように一意の制約をかけます。

Entriesテーブル使用中のレコードの一例)
Entry_id[0] user_id[1] room_id[0]
Entry_id[1] user_id[3] room_id[0]

entry.rb
    belongs_to :user
    belongs_to :room

    validates :room_id, uniqueness: { scope: :user_id }

roomモデル

Roomsは複数のメッセージとエントリー情報を持つのでhas_manyになります!
has_many :users, through: :entriesはuserモデルにもあった、一覧表示をするのに必要です。

room.rb
    has_many :messages
    has_many :entries
    has_many :users, through: :entries

messageモデル

message.rb
    belongs_to :user
    belongs_to :room

    validates :message, presence: true


#ルーティングの設定
全体の流れ
1,ユーザーの詳細ページからDMルームに飛ぶ
2,ルームは参加と一覧表示ができる
3,メッセージは投稿、編集、削除ができる

routes.rb
  resources :users, only: [:show]
  resources :rooms, only: [:index, :create:, :show]
  resources :messages, only: [:create, :edit, :update, :destroy]

usersコントローラーの編集

以下の記述になります。上から順番に区切って説明します。

users_controller.rb
  def show
@user = User.find(params[:id])
    if user_signed_in?
      @currentUserEntry = Entry.where(user_id: current_user.id)
      @userEntry = Entry.where(user_id: @user.id)
      unless @user.id == current_user.id
        @currentUserEntry.each do |cu|
          @userEntry.each do |u|
            if cu.room_id == u.room_id
              @haveRoom = true
              @roomId = cu.room_id
            end
          end
        end
        unless @haveRoom
          @room = Room.new
          @entry = Entry.new
        end
      end
    end
  end
users_controller.rb
@user = User.find(params[:id])
    if user_signed_in?
      @currentUserEntry = Entry.where(user_id: current_user.id)
      @userEntry = Entry.where(user_id: @user.id)
      unless @user.id == current_user.id

@user = User.find(params[:id])でパラメーターから送られたuser_idを@userに代入
if user_signed_in?でサインイン済みか判断。
@currentUserEntry=Entry.where(user_id: current_user.id)でcurrent_userが
既にルームに参加してるか判断。
@userEntry=Entry.where(user_id: @user.id)でユーザー詳細ページの表示しているユーザーがルームに参加してるか判断。
unless @user.id == current_user.id@userとcurrent_userが違うユーザーであることを確認。

ここまで理解できてますか?
やってる内容は至ってシンプルです。
current_userと@userが部屋に入ってるかの情報を出してます。
次の内容が少し複雑ですが、シンプルに考えてみてほしいです!

        @currentUserEntry.each do |cu|
          @userEntry.each do |u|
            if cu.room_id == u.room_id
              @haveRoom = true
              @roomId = cu.room_id
            end
          end
        end

@currentUserEntry.each do |cu|でcurrent_userが参加してる全てのルームidを出力します。
@userEntry.each do |u|@userが参加してる全てのルームidを出力します!
if cu.room_id == u.room_idでcurrent_userの参加してるroom_idと@userのroom_idで一致するものがあるのか判断します。

画像の例を出しておきます!

Entriesテーブル.png

このテーブルの中に一致する内容があったらそこで処理が終わります。
既にルームが作成されてるので。
もし作成されてないなら、新規にルームを作成する処理に分岐します!

もし部屋が作成されてなかったら、以下の処理をします。
@haveRoom = trueを代入
@roomId = cu.room_idを代入します。
@haveRoomは既にルームがあるよ。という意味の変数です。
@roomIdは後ほど記述しますが、ルームにアクセスするための変数です。

users_controller.rb
        unless @haveRoom
          @room = Room.new
          @entry = Entry.new
        end

@habeRoomに値がなかったら、新規作成するための変数を作成します。
form_forで@room@entryのの情報を送るためです。

ビューの編集

次はビューの編集をします。ここから必要な部分の一部抜粋になります
ユーザー詳細ページからDMルームに飛ぶための部分テンプレートを用意してrenderします

/users/show.html.haml
= render "contact"

DMルームに飛ぶための記述です。
こちらも区切って解説します!

/users/_contact.html.haml
- if user_signed_in?
    - unless @user.id == current_user.id
        - if @haveRoom == true
            = link_to "ダイレクトメッセージ", room_path(@roomId)
        - else
            = form_with(model:@room, local: true) do |f|
                = fields_for @entry do |e|
                    = e.hidden_field :user_id, value: @user.id
                = f.submit "ダイレクトメッセージ"
/users/_contact.html.haml
- if user_signed_in?
    - unless @user.id == current_user.id
        - if @haveRoom == true
            = link_to "ダイレクトメッセージ", room_path(@roomId)

1行目でサインインしてるか確認
2行目で@userとcurrent_userのidが違うことを確認
3行目でusers_controllerのshowアクションで作成した変数@haveRoomがtrueであることを確認。
4行目にルームに参加するためのリンクを表示してます!

/users/_contact.html.haml
     - if @haveRoom == true
            = link_to "ダイレクトメッセージ", room_path(@roomId)
        - else
            = form_with(model:@room, local: true) do |f|
                = fields_for @entry do |e|
                    = e.hidden_field :user_id, value: @user.id
                = f.submit "ダイレクトメッセージ"

@haveRoomがfalseだった場合(値が入ってない場合)の流れです。
form_withでデータを送信します。モデルは@roomで、local: trueで画面遷移するように命令します。
rooms_controllerのcreateアクションを呼び出すためにこの記述をしてます!
rooms_controllerは次に解説します。

fields_forという珍しいメソッドが出てきました!
簡単に言うと、
form_with内で異なるモデルを編集することができるようになるもの。
今回はroomテーブルとentryテーブルが1対多の関係の時に
entryテーブルに値を保存できるようにするために使用した。

つまり、ここでやりたいことはEntriesテーブルにuser_idとroom_idを追加したい!!
そういうことです。

roomsコントローラーの編集

全体の記述は以下のようになります。上から順番に区切って解説します。

rooms_controller.rb
class RoomsController < ApplicationController
    before_action :authenticate_user!

    def index
        @rooms = current_user.rooms.includes(:messages).order("messages.created_at desc")
    end

    def create
        @room = Room.create
        @joinCurrentUser = Entry.create(user_id: current_user.id, room_id: @room.id)
        @joinUser = Entry.create(join_room_params)
        @first_message = @room.messages.create(user_id: current_user.id, message: "初めまして!")
        redirect_to room_path(@room.id)
    end

    def show
        @room = Room.find(params[:id])
        if Entry.where(user_id: current_user.id, room_id: @room.id).present?
            @messages = @room.messages.includes(:user).order("created_at asc")
            @message = Message.new
            @entries = @room.entries
        else
            redirect_back(fallback_location: root_path)
        end
    end

    private
    def join_room_params
        params.require(:entry).permit(:user_id, :room_id).merge(room_id: @room.id)
    end

end

def indexは参加中のDMルームの一覧表示をするための記述です。

rooms_controller.rb
    def create
        @room = Room.create
        @joinCurrentUser = Entry.create(user_id: current_user.id, room_id: @room.id)
        @joinUser = Entry.create(join_room_params)
        @first_message = @room.messages.create(user_id: current_user.id, message: "初めまして!")
        redirect_to room_path(@room.id)
    end

変数@roomを作成。
変数@joinCurrentUserでEntriesテーブルにuser_id(current_user.id)と参加するroom_idの情報を入れます。
変数@joinUserで参加するユーザー(今回の場合だと/users/showで表示していたuser_id)の情報を入れます。
細かい記述は省略します!
@first_messageは初めの一言を自動的に入力させてます。
作成したroom_idにリダイレクトさせます。

rooms_controller.rb
    def show
        @room = Room.find(params[:id])
        if Entry.where(user_id: current_user.id, room_id: @room.id).present?
            @messages = @room.messages.includes(:user).order("created_at asc")
            @message = Message.new
            @entries = @room.entries
        else
            redirect_back(fallback_location: root_path)
        end
    end

@roomにパラメーターから送られたroom_idを代入
3行目でEntriesテーブルに情報があるか確認
4行目で@roomの全てのメッセージを取得
5行目で新しくメッセージを作成できるようにする@messageを定義
@entries@roomに参加してる(Entriesテーブルでヒットした情報、またはエントリーしてる)ユーザーを取得

次はルームのビューを作成します!

/rooms/show/1/.html.haml
.div#chat_area
  = render "chat_area"
.div#chat_form
  = render "chat_form"

チャット全体表示と投稿フォームの部分テンプレートを表示してます!

/rooms/_chat_area.html.haml
.left-button
%h4.rooms-title 気になる同士
- @entries.each do |e|
  .user-name
    %strong
      = image_tag "#{e.user.avatar}", class:"avatar"
      %a.rooms-user-link{href: "/users/#{e.user.id}"}
        = e.user.nickname
        さん
%hr
.chats
  .chat__scroll
    - if @messages.present?
      - @messages.each do |m|
        %div{id: "message_#{m.id}"}
          .chatbox
            .chat-face1
              = image_tag "#{m.user.avatar}", class:"avatar"
              .chat-hukidashi
              = m.user.nickname
              %br
              = m.message
              %br
              = m.created_at.strftime("%Y-%m-%d %H:%M")
              = form_with(model: @message, url: edit_message_path(m.id), remote: true, method: :get) do |f|
                = f.hidden_field :room_id, value: @room.id
                = f.submit "編集"
              = form_with(model: @message, url: message_path(m.id), remote: true, method: :delete) do |f|
                = f.hidden_field :room_id, value: @room.id
                = f.submit "削除"

上から区切って説明します!

/rooms/_chat_area.html.haml
.left-button
%h4.rooms-title 気になる同士
- @entries.each do |e|
  .user-name
    %strong
      = image_tag "#{e.user.avatar}", class:"avatar"
      %a.rooms-user-link{href: "/users/#{e.user.id}"}
        = e.user.nickname
        さん

先程@entriesに代入したユーザー情報を表示させてます。

/rooms/_chat_area.html.haml
.chats
  .chat__scroll
    - if @messages.present?
      - @messages.each do |m|
        %div{id: "message_#{m.id}"}
          .chatbox
            .chat-face1
              = image_tag "#{m.user.avatar}", class:"avatar"
              .chat-hukidashi
              = m.user.nickname
              %br
              = m.message
              %br
              = m.created_at.strftime("%Y-%m-%d %H:%M")
              = form_with(model: @message, url: edit_message_path(m.id), remote: true, method: :get) do |f|
                = f.hidden_field :room_id, value: @room.id
                = f.submit "編集"
              = form_with(model: @message, url: message_path(m.id), remote: true, method: :delete) do |f|
                = f.hidden_field :room_id, value: @room.id
                = f.submit "削除"

全てのメッセージを表示してます。
form_withの編集と削除の部分だけ解説します!
メッセージを作成したいのでmessages_controllerに情報を送る必要があります。
よって、modelは@messageになります。
editアクションを発火させるパスを記述。
remote: trueでJS形式
モデルとurlを指定してるのでメソッドを記述
= f.hidden_field :room_id, value: @room.idで現在表示してるroom_idを送ってあげます。この情報がないと、どのルームでやり取りしてるのかRailsは理解してくれません!

残り3割ぐらいです!

messages_controller.rb
class MessagesController < ApplicationController
    before_action :set_room, only: [:create, :edit, :update, :destroy]
    before_action :set_message, only: [:edit, :update, :destroy]

    def create
        if Entry.where(user_id: current_user.id, room_id: @room.id)
            @message = Message.create(message_params)
                if @message.save
                    @message = Message.new
                    gets_entries_all_messages
                end
        else
            flash[:alert] = "メッセージの送信に失敗しました"
        end
    end

    def edit
    end

    def update
        if @message.update(message_params)
            gets_entries_all_messages
        end
    end

    def destroy
        if @message.destroy
            gets_entries_all_messages
        end
    end

    private


    def set_room
        @room = Room.find(params[:message][:room_id])
    end

    def set_message
        @message = Message.find(params[:id])
    end

    def gets_entries_all_messages
        @messages = @room.messages.includes(:user).order("created_at asc")
        @entries = @room.entries
    end

    def message_params
        params.require(:message).permit(:user_id, :message, :room_id).merge(user_id: current_user.id)
    end
end

先にprivate以下から解説します。
set_roomメソッドは先程form_withで送られたroom_idの情報を取得します。
set_messageメソッドも上と同様です
before_actionでset_roomメソッドとset_messageメソッドを適用してます。

gets_all_messagesメソッドでメッセージを全件取得するのと、
@entriesでルームに参加してる人たちを改めて表示してます。
非同期通信する場合はこの記述が必ず必要です!
毎回データの情報を送ってあげないと、一覧表示できないので。
message_paramsメソッドは新しく送られたメッセージを取得してます。

次はアクション4つを簡単に説明します。

messages_controller.rb
   def create
        if Entry.where(user_id: current_user.id, room_id: @room.id)
            @message = Message.create(message_params)
                if @message.save
                    @message = Message.new
                    gets_entries_all_messages
                end
        else
            flash[:alert] = "メッセージの送信に失敗しました"
        end
    end

    def edit
    end

    def update
        if @message.update(message_params)
            gets_entries_all_messages
        end
    end

    def destroy
        if @message.destroy
            gets_entries_all_messages
        end
    end

createアクションを主に解説します。
createの中のif文はセキュリティのために書いてます。でもぶっちゃけ無くてもいいです笑
@messageをmessage_paramsメソッドを使用してcreateします
メッセージが保存できたら、新しいメッセージを作るための@messageを作成するのと、
gets_entries_all_messagesメソッドでルーム参加者を表示する@entries
メッセージ一覧を持ってきます。

updateとdestroyアクションは特に書くこともないので省略します。
コメント投稿機能と処理の流れは同じです。
違いは@entriesを取得する必要があるぐらいですね

後はcreate,edit,update,destroyの各アクションのjs.hamlファイルを作成すれば完成です!
今回はあえて内容は載せません。
ヒントはコメントの非同期投稿機能と全く同じ仕組みです。
自分の[過去の記事][https://qiita.com/kaito_program/items/ef348e170c64d224dd06]
の終盤で詳しく解説してるので、そちらをご参考にして、
ご自身で考えていただければと思います!

ここまでご覧いただきありがとうございます!

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

jQueryでパスワードの表示と非表示を切り替える

はじめに

railsでアプリケーションを行っている初学者のいりふねです。
今回、ユーザー登録画面のパスワード入力欄の表示と非表示を切り替える実装を行いました。ネット上に「カンタン」と書かれた同記事が多くありましたが、自分の環境で実装するのは少し時間がかかりました。
そこで、アウトプット兼自分用メモとして私の環境でうまく動作した手順を書いていきます。

開発環境

  • Rails 5.2.4.3
  • Haml 5.1.2
  • jquery-rails (4.4.0)
  • form_forを使用して実装しています

実現すること

ユーザー登録画面などでデフォルトで非表示になっているパスワードをcheck_boxを使用して表示と非表示を切り替えます。具体的には、以下Gifのような状態を目指します。
e177a528d227549c89344e4620d8b441.gif

手順

  1. check_boxの設置
  2. password_fieldにidを付与
  3. jsファイルにjQueryを記述

check_boxの設置

hamlファイルにcheck_boxを記述します。今回は、説明に不要なclass名などは予め外した状態にしています。cssなどをつけるときは別途設定して下さい。

example.html.haml
= form_for @user, url: user_registration_path do |f|

〜中略、password_fieldの記述などがあります〜

    %p
      = check_box_tag :passcheck
      = f.label "パスワードを表示する"

順番に説明します。
まず、pタグで囲ったのは、チェックボックスや「パスワードを表示する」という文字が、直上のpassword_fieldの右に回り込んでしまうためです。cssを編集することでカバーも可能でしょうが、pタグだけで解決するので、今回はこの方法を採用しました。

次に「check_box_tag」の部分です。これは、関連するモデルがない時のチェックボックスの書き方になります。他のフォームの記述に揃えて「= f.check_box」を使用すると、引数として保存先モデルのカラム名の記述が必須となってしまいます。今回のチェックボックスはレコードの保存が目的ではないので、「check_box_tag」を使用しました。

参考にさせていただいた記事【初心者向け】チェックボックスの書き方あれこれ[Ruby][Rails]

同行後半の「:passcheck」は、nameとidを指定しています。検証ツールで確認すると反映されていることが分かります。idはこの後、jQueryで要素を指定するための使用します。
スクリーンショット 2020-05-23 8.39.00.png

password_fieldにidを付与

password_fieldにjQueryで要素を指定するためのidを付与します。コードの後半部分がidを付与している箇所となります。

example.html.haml
〜省略〜
= f.password_field :password, autocomplete: "off", placeholder: "7文字以上の半角英数字", id: "js-password"
〜省略〜
= f.password_field :password_confirmation, autocomplete: "off", placeholder: "もう一度入力して下さい", id: "js-password_confirmation"
〜省略〜

ここで、idを同じものにしてしまうと、検証ツールのコンソール上で「idが重複しているよ!!」と黄色で警告が出てきます。ページが表示されないほどのエラーにはなりませんが、気持ち良いものではないので、今回はid名を分けて書きました。

jsファイルにjQueryを記述

jsファイルを作成し、コードを記述します。

example.js
$(function() {
  $('#passcheck').change(function(){
    if ($(this).prop('checked')) {
      $('#js-password').attr('type','text');
      $('#js-password_confirmation').attr('type','text');
    } else {
      $('#js-password').attr('type','password');
      $('#js-password_confirmation').attr('type','password');
    }
  });
});

実は、jQueryはまだ勉強中なので、十分説明ができません。汗
参考にされていただいた記事パスワード表示時にマスキング有無を選択できるようにする方法

とはいえ、自分なりに言語化してみます。
まずは、先程チェックボックスに指定したid名#passcheckで要素を取得し、changeイベントを設定します。ちなみにイベントはclickに変えても動きました。

次にif文を記述し、取得した要素からcheckedプロパティの有無をpropメソッドで確認します。チェックされていれば真、それ以外なら偽という扱いになります。

if結果が真の場合、つまりチェックが入った状態であれば、フォームに入力されたパスワードの値に対して、attrメソッドを使用して属性を取り出します。チェック済みならtype属性のtextを表示、そうでなければtype属性のpasswordを表示で隠すという具合です。

今日の積み上げ

改めて、jQueryの復習が必要であると感じました。
と同時に未熟な私でも機能実装できるようにわかりやすくまとめられた記事の多さに感謝しました。今後もアウトプットを続けて同じ初学者の方のためになればと思います。

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

Ubuntu18.04でrmagickをインストールしようとしたらできなかった

備忘がてら・・・。

発生した問題

Gemfileに以下を追記して実行したらエラーが発生した。

gem 'rmagick'
No package 'MagickCore' found

ERROR: Can't install RMagick 4.1.2.
Can't find the ImageMagick library or one of the dependent libraries.
Check the mkmf.log file for more detailed information.

MagickCore が見つからないらしい。

ImageMagick自体はインストール済みだった。

 % convert --version
Version: ImageMagick 6.9.7-4 Q16 x86_64 20170114 http://www.imagemagick.org

解決策

ここに解決策があった

sudo apt-get install libmagickwand-dev

その後、 bundle install を実行したら無事にインストールができた。

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

rails s で Renderedから動かなくなった。原因と解決までについて (grid-template)

エラーの原因

初めに、今回のエラーの原因と、どうやって解決したかについて書きます。
原因は cssファイルの grid-templateの記述ミスでした。
NGなcss
d3acc8a81392e422606a5e5337afa2b0.png
ここでgrid-templateの「;」の位置が最後の記述の下に来ているのが原因でした。

正しくは、
67796e390fb12e755bf217acc3a5aee2.png
のように、 grid-templateの記述を"footer"; と変更してアクセスできるようになりました。

grid-templateでエラーが出ているような記事などなかったためこれだけのエラーに半日使ってしまったので、
同じようなエラーで困った人の参考になれば幸いです

エラーの内容

rails sでサーバーを起動後 localhost:3000 にアクセスすると読み込み状態のままになった。
renderd ~~/~.html.erb within layouts/application
から動かず、ctrl+cも効かない状態になった。

どこが原因と考えたか

htmlファイルやコントローラーにエラーがある状態なら、エラー画面が表示された。
->rubyなどの環境が原因ではない。pcの環境ではない。と考えました。
->変更したファイルの中身が原因になる。

何を行ったのか

githubで新しくブランチを作成して、commitを一つずつファイルを確認した。
(git cherry-pick commit_id でcommitを参照)
cssファイルをコメントアウトした際、画面が表示されたのでcssの中身が原因だと判明。
順にコメントアウトを消して、grid-templateでエラーが出ていることが判明。
;の位置を変えて画面の表示を確認。

迷走の記録

初めのうちは、エラーの原因究明の方法がわからず迷走してました。
ちなみに、以下の記事を試しましたので記録として
https://qiita.com/sakuraniumarete/items/ac07d9d56c876601748c
https://teratail.com/questions/77742
https://teratail.com/questions/97670
https://b0npu.hatenablog.com/entry/2016/04/11/032826

また、今回よく使ったコマンドとしてkillコマンドを使いましたがその参考記事も
lsof -wni tcp:3000 から kill -9 killしたいPID
https://qiita.com/motty93/items/d22c1eb8f5128f8cd7f8

まとめ

grid-templateの「;」は誤った位置にあってもエラーが出ない時がある。

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

【学習メモ】tempusdominus turbolink form_with fields_for cron migration

0.目次

1.本日の学習内容
2.次週までの学習内容

1.本日の学習内容

①ナビゲーションバーが実行されないエラーの解決

【実現したいこと】
ナビゲーションバー(ハンバーガーメニュー)でボタンをクリックして、メニューを開いたり閉じたりができる状態にすること

【起こったこと・試したこと】
画面遷移すると、一度開くと閉じられなくなってしまった。

【原因】
teratailの似た質問から、turbolinkが邪魔している?

【解決策】

部分のturbolinkを消去したところ、解決。
app/views/layouts/application.html
※本当はerbだがわかりやすいようhtmlで閉じている
  <head>
    <title>Testapp</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all'%>
    <%= javascript_include_tag 'application' %>
  </head>

※参照:元々のナビゲーションバー

app/views/layouts/application.html
※本当はerbだがわかりやすいようhtmlで閉じている
<nav class="navbar navbar-expand-lg navbar-light bg-light">
      <a class="navbar-brand" href="#">TTManager</a>
      <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="ナビゲーションの切替">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
        <div class="navbar-nav">
          <a class="nav-item nav-link" href="#">ホーム</a>
          <a class="nav-item nav-link" href="#">リマインダー設定</a>
          <a class="nav-item nav-link" href="#">レポートを見る</a>

②フォームをクリックしてもカレンダーが表示されないエラーの解決

【実現したいこと】
Bootstrap4で、フォームにカレンダーを表示させて日付をクリックし、フォームに表示させる

【起こったこと・試したこと】
「tempusdominus-bootstrap-4」を実装し、カレンダーのマークは表示はされるが実行できなかった

以下は下記3つを実装

app/assets/stylesheets/application.scss
@import "tempusdominus-bootstrap-4.css";
app/assets/javascripts/application.js
//= require moment
//= require moment/ja.js
//= require tempusdominus-bootstrap-4.js
app/views/layouts/application.html
※本当はerbだがわかりやすいようhtmlで閉じている
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>

【調査方法】
デベロッパーツールのConsole・Sourcesで確認。
※既に修正済で、別のエラーが出ていますがみているものはこれです
スクリーンショット 2020-05-22 23.37.06.png

【調査結果・原因】
tempusdominus実行する前にjQueryを導入する必要があるため、導入してね
→jQueryがインストールされていなかったことが原因

【解決策】
https://www.sejuku.net/blog/57790
を参考にjQueryをGemifile、app/assets/javascripts/application.js、app/assets/stylesheets/application.scssに追記

gem 'jquery-rails'
app/assets/javascripts/application.js
//= require moment
//= require moment/ja.js
//= require jquery #ここを追記
//= require jquery_ujs #ここを追記
//= require tempusdominus-bootstrap-4.js

③1つの画面(URL)で複数のテーブルのカラムをDBに格納させる方法

【実現したいこと】
複数の親・子関係のテーブルのDBに格納するフォーム入力事項について、どうやってそれぞれ正しいテーブルに正しいカラムを格納したら良いか?

【起こったこと・試したこと】
「= fields_for」があるらしいが、どうやって使えば良いのか調べても理解仕切れず、質問。slimで実装はしていたが、、、

= form_with model:record, local:true do |f|
= f.submit

【解決策】
下記が基本

1.Model,DBの状態を確認し、ちゃんと反映されているか確認する(rails db:migrate:statusなど)
2.モデルで、親子関係・リレーションが正しいか確認する
→複数形をつけるのは、今見ているマイグレーションファイルから見て、
複数の関係性があるから(逆を言えば、単一の関係であれば、単数形にすること)

user.rb
has_many :records
task.rb
has_many :records

3.コントローラを確認し、「.build」を活用しながら実装

controller.rb
  def new
    @record = Record.new #newメソッドでインスタンス変数#record入れ込む
    output = @record.outputs.build #入れ込んだ#recordと結合させるために「.(モデル).build」を入れ込む
    practice = @record.practices.build #上記と同じ要領で
    task = @record.tasks.build #上記と同じ要領で
  end
recordモデルは、ooutput,practice,taskモデルとは親・子関係

4.viewを実装

new.html.slim
= form_with model:record, local:true do |f|
 = fields_for :output
  .form-group
   = f.text_field :output_name, class: 'form-control mb-4', id: 'output_name'
[中略]
= f.submit 'この内容で登録する', new_record_path, class: 'btn btn-primary'

※f.submitは1番下じゃなくてもOK。ちゃんと「form_with」と「f.submit」の関係性ができていればOK
※コードはこれから実装

④マイグレーションの注意事項

【状況】
モデルに新しくカラムを追加して、db:migrateしたら、「既にできています」と表示された

【原因】
マイグレートは、テーブルを作成するために実行するもの(マイグレーションファイルは、その実行内容を示したファイル)であって、モデルに変更を加えただけでは反映されない!!

【解決策】
①db:reset
DBの内容を全てリセットし、もう一度マイグレーションする処理

②変更用のマイグレーションファイルを作成すること
・UPしている状態を、DOWNにするためのマイグレーションの処理
・変更用のマイグレーションファイルを別途作成
※ここ理解が弱いので調べて追記予定

⑤その他

・デベロッパーツールのエラーの「$」は「jQuery」を指す
・リマインダーメールの実装について
→Web上で動くものではない(=クライアントの操作によるトリガーがない)ため、サーバー(インフラ)でプログラムを実行させる必要あり!つまり、最後の実装
→方法の1つが、cron
・日付のみ示すデータ型:date

2.次週までの学習内容

▼マイグレーションのUP、DOWNさせる事例・対応方法のインプット・実践
▼マイグレーションの変更方法のインプット・実践(Rails速習に載ってる?user_idをTaskテーブルに追加だったかな)
▼AWSのudemy動画学習
▼レポート表示のJS実装
▼Bootstrapのpadding設定の事例・方法のインプット・実践
▼cronのインプット

参照

【BOOTSTRAP4でフォーム入力の際にカレンダーから日付入力する方法】
https://its-office.jp/blog/bootstrap/2019/02/18/bs4calendar.html
【bootstrapのNavbarのハンバーガーメニューが閉じない[原因はTurbolinks]】
https://qiita.com/kota-shidara/items/964f9fe92a467cd28583
【【Rails】turbolinksを無効化する】
https://qiita.com/kota-shidara/items/964f9fe92a467cd28583
【accepts_nested_attributes_forで親子孫関係のレコードを同時作成する方法【実装例・rspecあり】】
https://shinmedia20.com/rails-relation-create
【【Rails】form_withの使い方を徹底解説!】
https://pikawaka.com/rails/form_with
【fields_forの上手な使い方】
https://qiita.com/kouuuki/items/5daf2b5f34273d8457f7
【fields_forを使った子モデルへの複数レコード保存【cocoonが便利】】
https://www.y-hakopro.com/entry/2019/08/03/234126
【cronの設定 - プログラムを指定した時間に定期的に自動的に実行する方法】
https://memorva.jp/memo/linux/cron.php
【5.5. 日付/時刻データ型 ※DB:Postgresql】
https://www.postgresql.jp/document/7.3/user/datatype-datetime.html

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