20201115のRailsに関する記事は30件です。

【アウトプット】eachについて

eachについて
何をするものか
配列の中身などを繰り返しやってくれる働きをもつ
例)
<% @posts.each do |post| %>
postsは変数名 postはとりあえず配列の中身を入れるやつ
< div class = "posts-index-item">

< % = post.content %>
ここはpostだけでも良い状況がある。
今回はターミナルでPost.allで配列を取っている(contentというカラム名にデータがある為)
< /div>
<% end %>

例をあげたら余計にややこしくなったが繰り返しの処理を書くときにeachを書いたら
短く書けるということ。

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

【個人メモ】RailsアプリをAWSへデプロイする際につまづいたことまとめ

index

下記の記事の通りにRailsアプリをAWSへデプロイする際につまづいたことを個人的な備忘としてまとめました。

(下準備編)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまで
https://qiita.com/naoki_mochizuki/items/f795fe3e661a3349a7ce
https://qiita.com/naoki_mochizuki/items/22cfbf4bf7ec95f6ac1c
https://qiita.com/naoki_mochizuki/items/814e0979217b1a25aa3e
https://qiita.com/naoki_mochizuki/items/5a1757d222806cbe0cd1

RDSインスタンスが生成できない

RDSの設定をして、「データベースの作成」をクリックすると、こんなエラーが発生。

DB Subnet Group doesn't meet availability zone coverage requirement. Please add subnets to cover at least 2 availability zones. Current coverage: 1 (Service: AmazonRDS; Status Code: 400; Error Code: DBSubnetGroupDoesNotCoverEnoughAZs; Request ID: 3e87202c-e6b3-46dc-8396-47c64a2f0dd6)

こちらの記事を参考にしました。
https://www.wantanblog.com/entry/2019/09/24/225020
どうやらサブネットグループを1つしか作っていなかったことが問題のようなので、「VPC」→「サブネット」→「サブネットの作成」から、下記設定でサブネットをもう一つ作成したところ、RDSインスタンスが作成できるようになった。
・VPC ・・・作成したVPCを選ぶ
・アベイラビリティーゾーン ・・・既に作成したサブネットと違う場所を指定する
・IPv4 CIDR ブロック ・・・10.0.1.0/24

EC2へSSHでログインできない

*[ .ssh ] $: ssh -i mumu.pem ec2-user@54.92.121.123

でEC2へログインしようとすると、しばらく待ってから接続エラーになりました。
こちらの記事を参考にしました。
https://xn--o9j8h1c9hb5756dt0ua226amc1a.com/?p=3583
EC2が配置されているサブネットのルートテーブルを確認したところ、外部への経路(送信先0.0.0.0/0、ターゲットigw-...)が設定されていませんでした(うっかり)
「VPC」→「ルートテーブル」→「該当のルートテーブルにチェック」→「ルートテーブル」→「ルートテーブルの編集」→「ルートを追加」で下記を追加したところ、EC2へSSH接続できるようになりました。
・送信先 ・・・ 0.0.0.0/0
・ターゲット ・・・igw-...(作成したインターネットゲートウェイ)

公開鍵作成するときにssh-keygem not foundのエラー

タイポでした。
ssh-keygemではなくssh-keygenでした。

rake secretでシークレットキーが作れない

下記のエラーが発生

$ rake secret
You must use Bundler 2 or greater with this lockfile.

こちらの記事を参考にしました。
https://programming-beginner-zeroichi.jp/articles/169

$ gem install bundler
$ bundle install
$ bundle exec rake secret

で解決しました。(※ちなみに筆者はアプリのデータベースをsqliteに設定していたため、「bundle install」でエラーが発生し、次の記事の手順を踏んでから、「bundle install」をしました。

bundle installしたとき、sqlite3をインストールしてくださいのエラー

MySQLをデータベースに使う予定なので、railsアプリのデータベースをsqliteからMySQLへ変更します。
こちらの記事を参考にしました。
https://note.com/itoa06/n/n31fe4f9cd6b9

差分はこちら
/Gemfile

-gem 'sqlite3', '~> 1.4'
+gem 'mysql2', '>= 0.4.4'

/config/database.yml

 default: &default
-  adapter: sqlite3
+  adapter: mysql2
+  encoding: utf8mb4
   pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
-  timeout: 5000
+  username: root
+  password:
+  host: localhost

 development:
   <<: *default
-  database: db/development.sqlite3
+  database: hello_rails_development

 test:
   <<: *default
-  database: db/test.sqlite3
+  database: hello_rails_test

 production:
   <<: *default
-  database: db/production.sqlite3
+  database: hello_rails_production
+  username: hello_rails
+  password: <%= ENV['HELLO_RAILS_DATABASE_PASSWORD'] %>

Failed to start mysqld.service: Unit not found.

MySQLを立ち上げようとしたところエラーが発生しました

sudo service mysqld start
Redirecting to /bin/systemctl start mysqld.service
Failed to start mysqld.service: Unit not found.

こちらの記事を参考にしました。
https://qiita.com/hamham/items/fd77bb0bb167a150dc8e#mysql57%E3%81%AE%E5%B0%8E%E5%85%A5

@MurakamiKazutaka さんがコメントで書かれていましたが、Amazon Linux2ではyumでmysqlをインストールしようとするとmariaDBをインストールしようとするらしいです。

$ yum -y install http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm
$ yum -y install mysql mysql-community-server
$ mysqld --version
mysqld  Ver 5.7.23 for Linux on x86_64 (MySQL Community Server (GPL))
$ cd /var/www/rails/アプリ名
$ sudo service mysqld start

で無事解決しました。

と思ったら、無事ではありませんでした。この場合、MySQLが勝手にrootユーザーのパスワードを作成してしまうので、@hat_log さんがおっしゃっているように、パスワードをdatabase.ymlに記載する必要があります。
https://qiita.com/Dough/items/7493ad374a51b24abb58

$ sudo cat /var/log/mysqld.log | grep 'temporary password'
[Note] A temporary password is generated for root@localhost: XXXXXX
$ mysql -u root -p
Enter password: XXXXXX
mysql> set password for root@localhost=password('passwordPASSWORD@999');

次にdatabase.ymlにパスワードを記載

production:
  <<: *default
  database: mumu_production
  username: root
  password: passwordPASSWORD@999

NoMethodError (undefined method `deep_symbolize_keys' forのエラー

データベース作成時にエラーが発生しました。

$ rake db:create RAILS_ENV=production
...
NoMethodError (undefined method `deep_symbolize_keys' for...

.ymlファイルの書式が間違っているということで、おそらくインデントのスペースが2個になっていないのだろうと思い探しました。
そしたら、config/secrets.ymlでキー名secret_key_base:を書いていませんでした(うっかり)。

誤り↓

production:
  (生成したシークレットキー)

正しい方↓

production:
  secret_key_base: (生成したシークレットキー)

Job for nginx.service failed because the control process exited with error codeのエラー

Nginxを起動しようとしたときにエラーが発生

$ sudo service nginx start
Redirecting to /bin/systemctl start nginx.service
Job for nginx.service failed because the control process exited with error code. See "systemctl status nginx.service" and "journalctl -xe" for details.

こちらの記事を参考にしました。
https://qiita.com/shota0701nemoto/items/a6929ef6f396cf3bede4

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: [emerg] open() "/var/www/rails/[誤ったアプリ名]/log/nginx.error.log" failed (2: No such file or directory)
nginx: configuration file /etc/nginx/nginx.conf test failed

エラーの内容は人によって違うと思います。自分は"/var/www/rails/[誤ったアプリ名]/log/nginx.error.log"が無いということだったので、ファイルへ移動したところ、

$ cd /var/www/rails/[誤ったアプリ名]/log
-bash: cd: /var/www/rails/[誤ったアプリ名]/log: No such file or directory

と言われ、よく見るとアプリ名のハイフン(-)とアンダーバー(_)を書き間違えていることに気付きました。

$ vim config/unicorn.conf.rb
$ cd /etc/nginx/conf.d/
$ sudo vim mumu.conf

で誤った箇所を修正しました。

$ cd /var/www/rails/[アプリ名]/
$ sudo service nginx start

で無事に起動しました。

We're sorry, but something went wrong. パート1

Nginxを起動した後に、EC2のIPアドレスへChromeでアクセスしてみると、

We're sorry, but something went wrong.

となった。
RDSを一旦停止したせいかと思い、

$ sudo service mysqld start

MySQLを起動しましたが、変化無し。

$ less log/production.log

でRailsのログを確認したところ(一番下の方が最新の情報)、

ActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.

のエラーが出ていたので、こちらの記事を参考にconfig/envitonments/production.rbを書き換え。
https://kanoe.studio/archives/791

  # Do not fallback to assets pipeline if a precompiled asset is missed.
  config.assets.compile = true

アプリケーションサーバーを再起動して、
(こちらの記事を参考にしました
https://qiita.com/takuyanagai0213/items/259ca105e35f6eb066d6

$ ps -ef | grep unicorn | grep -v grep
takuya    2460     1  0  3月11 ?      00:00:04 unicorn_rails master -c /var/www/rails/myapp/config/unicorn.conf.rb -D -E production
takuya    2465  2460  0  3月11 ?      00:00:05 unicorn_rails worker[0] -c /var/www/rails/myapp/config/unicorn.conf.rb -D -E production
takuya    2467  2460  0  3月11 ?      00:00:04 unicorn_rails worker[1] -c /var/www/rails/myapp/config/unicorn.conf.rb -D -E production
$ kill 2460
$ unicorn_rails -c /var/www/rails/myapp(自分のアプリ名)/config/unicorn.conf.rb -D -E production

再びEC2のIPアドレスにアクセスしましたが、相変わらず「We're sorry, but something went wrong.」
次に続く(まだエラーあんのかよ。。。)

We're sorry, but something went wrong. パート2

引き続き、再度、ログを確認したところ、

$ less log/production.log

ActionView::Template::Error (Webpacker can't find application in /var/www/rails/hello-rails/public/packs/manifest.json. Possible causes:
1. You want to set webpacker.yml value of compile to true for your environment
   unless you are using the `webpack -w` or the webpack-dev-server.
2. webpack has not yet re-run to reflect updates.
3. You have misconfigured Webpacker's config/webpacker.yml file.
4. Your webpack configuration is not creating a manifest.

のエラーが発生していました。

ちょっと何言ってるかわからないです

こちらの記事を参考にしました。
https://qiita.com/natecotus/items/a2bd9f3ebd5b1866d48e

$ rm -rf bin/webpack*
$ rails webpacker:install
Webpacker requires Node.js >= 8.16.0 and you are using 6.17.1
Please upgrade Node.js https://nodejs.org/en/download/

またしてもエラーが発生。Node.jsのversionが古いみたいです。
こちらの記事を参考にしました。
https://qiita.com/paranishian/items/bddaed7c3aacedb11967

$ git clone git://github.com/creationix/nvm.git .nvm
$ . ~/.nvm/nvm.sh
$ nvm install
$ curl -o- -L https://yarnpkg.com/install.sh | bash
$ source ~/.bashrc
$ yarn -v

それでは、改めまして〜

$ rails webpacker:install
[Ynaqdhm] Y
[Ynaqdhm] Y
$ RAILS_ENV=production bundle exec rails webpacker:compile

コンパイルができたようなので、UnicornとNginxを再起動します。

$ ps -ef | grep unicorn | grep -v grep
$ kill [プロセスID]
$ unicorn_rails -c /var/www/rails/[アプリ名]/config/unicorn.conf.rb -D -E production
$ sudo nginx -s reload

これで、IPアドレスでつないでみたら。。。
やった表示された!

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

railsのサーバーが起動できない時の対処法

はじめに

初めまして。
私は未経験エンジニアとして転職するべく、1週間前にMacBookProを購入し、Progateで学習をしている者です。
早速ですが、Ruby on Railsの環境構築をする時に詰みました。

rails s が起動しない

Progateに言われるがままAtom, Homebrew, rbenv, Railsをインストールし、後はローカルでサーバーを建てるだけ。
しかし、

rails s

と入力すると

Could not find gem 'rails (~> 6.0.3, >= 6.0.3.4)' in any of the gem sources listed in your Gemfile.
Run `bundle install` to install missing gems.

何のことやら。
とりあえず

bundle install

と入力しろと書いてあるので実行してみます。

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

初学者の心を折りに来ています。
Google先生に聞くこと数時間、

sudo gem install bindex -v '0.8.1'

これでパスワードを入力してインストールすれば良いみたいです。
さて、再度bundle installを実行すると、

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

またですか。
しかし、同じように

sudo gem install msgpack -v '1.3.3'

でインストールしていきます。
このあともbundle installを実行しては足りないものをインストールする作業を繰り返していき、最終的にはrails sが実行できるようになりました。

おわりに

私のように出鼻をくじかれている方の助けになれば幸いです。

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

Railsのサーバーが起動できない時の対処法

はじめに

初めまして。
私は未経験エンジニアとして転職するべく、1週間前にMacBookProを購入し、Progateで学習をしている者です。
早速ですが、Ruby on Railsの環境構築をする時に詰みました。

rails s が起動しない

Progateに言われるがままAtom, Homebrew, rbenv, Railsをインストールし、後はローカルでサーバーを建てるだけ。
しかし、

rails s

と入力すると

Could not find gem 'rails (~> 6.0.3, >= 6.0.3.4)' in any of the gem sources listed in your Gemfile.
Run `bundle install` to install missing gems.

何のことやら。
とりあえず

bundle install

と入力しろと書いてあるので実行してみます。

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

初学者の心を折りに来ています。
Google先生に聞くこと数時間、

sudo gem install bindex -v '0.8.1'

これでパスワードを入力してインストールすれば良いみたいです。
さて、再度bundle installを実行すると、

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

またですか。
しかし、同じように

sudo gem install msgpack -v '1.3.3'

でインストールしていきます。
このあともbundle installを実行しては足りないものをインストールする作業を繰り返していき、最終的にはrails sが実行できるようになりました。

おわりに

私のように出鼻をくじかれている方の助けになれば幸いです。

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

Ruby on Railsの開発環境をDockerで構築する方法(Rails 6.x)

Docker公式手順を参考に、Rails 6系で Dockerの開発環境を構築する手順を詳解します。

各コマンドの詳しい解説は私が以前に書いた Rails5系での手順 で解説しているので、本記事では割愛します。

Rails プロジェクトを作成

$ docker-compose run web rails new . --force --no-deps --database=mysql

コンテナをビルド

$ docker-compose build

データベースファイルの修正

上記実行後、config/database.yml ファイルを下記の通りにまるっと修正します。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

development:
  <<: *default
  database: myapp_development
  host: db
  username: root
  password: password

test:
  <<: *default
  database: myapp_test
  host: db
  username: root
  password: password

データベースを作成

$ docker-compose run web rails db:create

Webpackerをインストール

Rails 6系から Webpacker が必要となっているため、webサーバのコンテナにwebpackerをインストールします。

$ docker-compose run web rails webpacker:install 

docker-compose でコンテナを起動

$ docker-compose up -d

アクセスして起動を確認

ブラウザから localhost:3000 にアクセスし、Rails の初期画面が表示されることを確認しましょう。

image.png

コンテナを停止

検証を終え、コンテナを停止させるときも docker-compose を使います。

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

TECH CAMP (エンジニア転職)6週目の学習内容の振り返り

火曜日から本格的に最終課題の学習にはいったのですが、火曜日から日曜日までで6割ぐらいは進めた(?)と思います。この調子ですすめて22日までは必要な作業すべてを終わらせたいですね。それが終われば自分のポートフォリオを進めれる、、、ぐへへへ(変人感)
復習に入っていきたいところですがその前に、最終課題とはなんなのか、どんなことをやるのか少し話していきたいと思います。最終課題ではメルカリのようなフリマアプリの作成を通してサーバーサイドの実装を行っていきます。フロントサイドに関してはすでに用意されているのでやらなくても大丈夫です。この最終課題と今までの学習内容の違いはなにかというと、目標を達成(機能実装)するための手順を自分で考えないといけないところです。今までのカリキュラムだと、テキストに沿って決められたことをこなしていけばよかったのですが、最終課題においては機能実装を行うためにどのような工程が必要か自分で道筋を立てながら学習していくことになります。
例えば、ユーザー登録機能を作るならカラムの洗い出しとDB作成、必要なアクションの定義などなどがあります。最終課題以前だとカリキュラムに書かれたそれらの項目をこなしていくことで実装できるのですが、今回だと実装のための過程が明示されていないのでプロセスを考えないといけないという難しさがあります。私は今の所は大丈夫ですが、終盤にどうなるかが心配です。(笑)
ではでは、今週の学習内容をざざっと振り返ろうかなと思います。

11/10~11/15までの進捗状況
・herokuへのデプロイ
・Basic認証の導入
・テーブル read.meの作成
・ユーザー管理(登録 ログイン ログアウト)機能の実装
・商品を出品機能の実装
・トップページに商品を表示する機能の実装

herokuへのデプロイについて
これと認証に関してはやることがそう多くないですね
heroku内にアプリケーション作成をして、mysqlを作成、アプリケーションの情報をgitからプッシュしてマイグレーションを実行することでデプロイが可能となります!

Basic認証について
Basic認証はauthenticate_or_request_with_http_basicメソッドを用いることでidとpasswordを要求することができます。ここで注意すべき点は、環境変数にidとpasswordを入れることです。コントローラーにそのまま設定してたらどちらもgithubで漏洩してしまうので。

テーブル設計 Read.meの作成
それぞれのテーブルに必要なカラムを洗い出します。難しいことはないですが、read.meのバー(|や-)を揃えるのがかなり面倒です

ユーザー管理機能の実装
deviseの導入と正規表現の導入がかなり苦戦しました。
passwordだと半角英数字混合6文字以上というのがあったり、半角カタカナ限定だったりと、、、あとちゃんと機能しているか確認のためテストコードを書くのですが、100行以上かくはめになりました。がんばったなぁ()

商品を出品機能の実装
ここでは商品の状態や発送元、送料の負担などをプルダウン機能を用いて実装していくのですが、結構調べるのに時間がかかってしまいました。商品の値段に下限と上限があり、そのバリデーションをどうかけるのか全くわからなかったですね。自分のググり力が上がる食べ物があったらそれを主食にしたいです。

トップページに商品を表示する機能の実装
トップページに登録された商品が新しいものから上に表示されるように実装を行いました。
横並びにするのに試行錯誤して2時間ほどかかってしまいました。

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

【Rails】テーブルのカラム名の変更方法

テーブルのカラム名の変更方法について

Railsでテーブルのカラム名の変更方法についてまとめました。
ステップとしては下記の通りです。

① migrationファイル作成
② migrationファイルの編集
③ データベースへ反映

今回は下記のようにカラム名を変更します。

変更前
wheather

変更後
weather

モデル名 カラム名(変更前) カラム名(変更後)
users wheather weather

① migrationファイル作成

まずはカラム名を変更するためのmigrationファイルを作成します。

$rails generate migration rename_【変更前のカラム名】_column_to_【モデル名(複数形)】

今回は
$rails generate migration rename_wheather_column_to_users
と記述する。

② migrationファイルの編集

/db/migrateに新しいファイルが作成されるので、changeメソッドを追加し、そこに変更したいカラム名を記述する。

今回作成されたファイル:20201115004326_rename_wheather_column_to_users.rb
*作成日によって数字の部分は変わります。

下記のように記述する。

/db/migrate/20201115004326_rename_wheather_column_to_users.rb
class RenameWheatherColumnToUsers < ActiveRecord::Migration[6.0]
  def change
    rename_column :モデル名, :カラム名(変更前), :カラム名(変更後)
  end
end

今回の場合は下記の通り記述。

/db/migrate/20201115004326_rename_wheather_column_to_users.rb
class RenameWheatherColumnToUsers < ActiveRecord::Migration[6.0]
  def change
    rename_column :users, :wheather, :weather
  end
end

③ データベースへ反映

最後に、データベースへ反映し、カラム名の変更は完了。
$rails db:migrate

以上です。
weatherwheatherと書き間違えたばかりに、このような作業が発生してしまいました(笑)
皆様はくれぐれもスペルミスの無いようにお気をつけください☆

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

[初心者]一気に複数行のインデント修正ができるワザ「矩形選択(ブロック選択)」について

はじめに

Railsなどで、コードを打ち終わった後に、全体のバランスを整えたいと思う時ってありますよね。
コードを考えるのに夢中になってしまい、見返してみたら、インデントがバラバラで、めちゃくちゃ見にくいバランスになってしまっていたり。
また、複数の要素をまたぐ形で、親要素を後から追加しようと思って、子要素の全ての行に対して、1スペース分のインデントを空けなければならない時もあると思います。
プログラミング駆け出しの私は、インデントを空ける為に、ひたすらTABキーを打ち続けていました。

そんな中、「矩形選択」なるワザを知ってから、世界が変わりました。
知っている方も多いと思いますが、もし使っていないor知らない方がいたら、非常にもったいないと思いますので、この便利な技を共有させてもらいます。

こんな時ってどうしてますか?

次のコードをご覧ください。
image.png
deviseを利用して、ユーザーの新規登録画面を作成したコードです。
これだけで、機能としては利用できるのですが、見た目が美しくありません。

Bootstrapを利用して、バランスを整えたいと思い、全体を
<div class="container">
<div class="row">
で囲もうと思います。

qiita.rb
<div class="container" >
  <div class="row" >
  ------------- ←ここに上のコードを入れ込みたい!!!
  </div >
</div >

そうですね。
間に入れるだけならそれで良いのですが、リーダブルコードを意識するなら、全ての行にインデントを空ける方が美しくなりそうです。

でも、面倒ですよね。
分かります。
そんな時に、利用できるのがこの「矩形選択」です!

矩形選択(ブロック選択)とは?

非常に便利な機能の使い方は簡単です。
macOSの場合、

キーボードによる矩形選択のコマンド
開始する箇所をクリックしておく
Shift]+[Option]+[Command]+矢印キー
そのままドラッグ

これだけです。
もっと分かりやすく説明すると、
image.png
このように、開始する箇所をクリックしておきます。
そして、↑のコマンドを打ち込むと、
image.png
何と便利なことに、<div class="field">全体を囲むように、ポインタが拡大されました。
ここまで出来ると、もう簡単です。
見やすいように、↑の部分だけインデントを空けると、こんな感じになります。
image.png
はい、メチャクチャ便利です。

矩形選択の他の使い方

もちろん、スペースを空けるだけじゃなく、文字を打つことも出来ます。
image.png
一度文字を打ち込むだけで、矩形選択した他の行にも同じ文字を打つことができました。
他にも色々と応用した使い方があるので、興味がある方は調べてみてください!

おわりに

プログラミングの世界には、このように作業時間を縮小できるような小ワザがたくさんあると思います。
全てを一気に覚えるのは難しいですが、一つ一つ体に馴染ませていけば、非常にスマートなプログラマーになれる日が近づくかもしれません!

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

[Rails][ jQuery]フォームの入力ならびに選択が完了するまで送信ボタンを押せないようにする。

はじめに

今回は、jQueryを使って、フォームの入力ならびに選択が完了するまで送信ボタンが押せないように設定していきます。

完成イメージ

submitButtonJs.gif

記事を書いた目的

情報の共有ならびに、自身の備忘録として執筆する。

導入した目的

誤送信を防ぎ、ユーザビリティを向上させるため。

環境

MacOS 10.15.7
ruby 2.6.5
Ruby on Rails 6.0.0

前提条件

  • jQueryが導入済みであること。
  • 画像の複数枚投稿機能を実装している。

記事執筆者の状況

テーブル

Untitled Diagram-ページ2 (1).png

Userテーブルが「投稿者」、Postテーブルが「投稿」、Imageテーブルが「投稿画像」、Prefectureテーブルが「都道府県データ」、Categoryテーブルが「投稿カテゴリー」となります。

PrefectureテーブルとCategoryテーブルはseedデータを活用しております。

コントローラー

新規投稿機能はposts_controller.rbが担当しております。

posts_controller.rb
class PostsController < ApplicationController
  def new
    @post = Post.new
    @post.build_spot
    @post.images.build()
  end

  def create
    @post = Post.new(post_params)
    if @post.save
      redirect_to root_path, notice: "投稿が完了しました"
    else
      flash.now[:alert] = "必須項目を入力してください"
      @post.images.build()
      render :new
    end
  end
  ...下記一部記述を省略
  .
  .
  .
  private
  def post_params
    params.require(:post).permit(:title, :content, :prefecture_id, :category_id, images_attributes: [:id, :image, :_destroy]).merge(user_id: current_user.id)
  end

それでは作業していきましょう。

①-1 new.html.erbを作成する

まずはフォームをhtmlで作成していきます。

new.html.erb
<%= form_with(model: @post, local: true, multipart: true) do |form| %>
  <ul class='formSpace'>
    <li class="prefecture">
      <label class="labelName" for="Prefecture">Prefecture:</label>
      <%= form.collection_select :prefecture_id, Prefecture.all, :id, :name, {include_blank: '選択してください'}, {class: "prefecture__input", id: 'input01'} %>
    </li>
    <li class="category">
      <label class="labelname" for="category">Category:</label>
      <%= form.collection_select :category_id, Category.all, :id, :name, {include_blank: '選択してください'}, {class: "category__input", id: 'input02'} %>
    </li>
    <li class="title">
      <label class="labelName" for="titleSpace">Title:</label>
      <%= form.text_field :title, class: 'title__input', id: "input03", placeholder: "タイトルを入力してください" %>
    </lil
    <li class='newImage'>
      <label class="labelName" for="imageSpace">Photo:</label>
      <div class="prevContent">
      </div>
      <div class="labelContent">
        <label class="labelBox" for="post_images_attributes_0_image">
          <div class="labelBox__text-visible">
            クリックしてファイルをアップロード(最大5枚)
          </div>
        </label>
      </div>
      <div class="hiddenContent">
        <%= form.fields_for :images do |i| %>
          <%= i.file_field :image, class: "hiddenField", id: "post_images_attributes_0_image", name: "post[images_attributes][0][image]", type: "file" %>
          <%= i.file_field :image, class: "hiddenField", id: "post_images_attributes_1_image", name: "post[images_attributes][1][image]", type: "file" %>
          <%= i.file_field :image, class: "hiddenField", id: "post_images_attributes_2_image", name: "post[images_attributes][2][image]", type: "file" %>
          <%= i.file_field :image, class: "hiddenField", id: "post_images_attributes_3_image", name: "post[images_attributes][3][image]", type: "file" %>
          <%= i.file_field :image, class: "hiddenField", id: "post_images_attributes_4_image", name: "post[images_attributes][4][image]", type: "file" %>
        <% end %>
      </div>
    </li>
    <li class='content'>
      <label class="labelName" for="contentSpace">Content:</label>
      <%= form.text_area :content, class: 'content__input', id: "input05", placeholder: "コメントを入力してください" %>
    </li>
  </ul>
  <div class='send'>
    <%# <%= form.submit "送信中", class: 'send__btn', id: 'sending', value: "投稿する" %>
    <input type='submit' id='sending' class='send__btn' value='投稿する'>
  </div>
<% end %>

続いてscssを記述します。

new.scss
.formSpace {
  height: auto;
}

.labelName {
  color: #000000;
}
// 都道府県================================================================

.prefecture {
  height: auto;
  width: auto;
  margin-top: 1vh;
  font-size: 1.5vh;
  line-height: 1.5;
  color: #fff;
  &__input {
    width: auto;
    border: 1px solid #ccc;
    background-color: #fff;
    border-radius: 5px;
    text-align: center;
    color: #000000;
  }
}

// カテゴリー==============================================
.category {
  height: auto;
  width: auto;
  margin-top: 1vh;
  font-size: 1.5vh;
  line-height: 1.5;
  color: #fff;
  &__input {
    width: auto;
    border: 1px solid #ccc;
    background-color: #fff;
    border-radius: 5px;
    color: #000000;
  }
}

//Title===================================================================
.title {
  height: auto;
  width: auto;
  margin-top: 1vh;
  font-size: 1.5vh;
  line-height: 1.5;
  color: #fff;
  &__input {
    width: 30vw;
    border-radius: 5px;
    border: 1px solid #ccc;
    background-color: #fff;
    color: #000000;
    margin-left: 25px;
  }
}

//Image======================================================================

.newImage {
  display: block;
  margin: 16px auto 0;
  display: flex;
  flex-wrap: wrap;
  cursor: pointer;
}

.imageLabelName {
  color: #fff;
  margin-right: 25px;
}

.prevContent {
  display: flex;
}

.previewBox {
  height: 162px;
  width: 112px;
  margin: 0 15px 10px 0;
}

.upperBox {
  height: 112px;
  width: 100%;
  img {
    width: 112px;
    height: 112px;
  }
}

.lowerBox {
  display: flex;
  text-align: center;
}

.deleteBox {
  color: #1e90ff;
  width: 100%;
  height: 50px;
  line-height: 50px;
  background: #f5f5f5;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
}

.imageDeleteBtn {
  background-color: #f5f5f5;
  line-height: 4vh;
  height: 4vh;
  width: 60px;
}

.imageDeleteBtn:hover {
  color: rgba($color: #1e90ff, $alpha: 0.7);
}

//投稿クリックエリアのCSS
.labelContent {
  margin-bottom: 10px;
  width: 620px;
  .labelBox {
    display: block;
    border: 1px dashed #ccc;
    position: relative;
    background: #f5f5f5;
    width: 100%;
    height: 162px;
    cursor: pointer;
    &__text-visible {
      position: absolute;
      top: 50%;
      left: 16px;
      right: 16px;
      text-align: center;
      font-size: 14px;
      line-height: 1.5;
      font-weight: bold;
      -webkit-transform: translate(0, -50%);
      transform: translate(0, -50%);
      pointer-events: none;
      white-space: pre-wrap;
      word-wrap: break-word;
    }
  }
}
//file_fieldのcss
.hiddenContent {
  .hiddenField {
    display: none;
  }
  .hidden-checkbox {
    display: none;
  }
}

//コメント====================================================================

.content {
  display: flex;
  height: auto;
  width: auto;
  margin-top: 5px;
  line-height: 1.5;
  font-size: 1.5vh;
  &__input {
    height: 15vh;
    width: 40vw;
    border-radius: 5px;
    color: #000000;
    border: 1px solid #ccc;
    background-color: #fff;
    margin-left: 0px;
    padding: 1vh;
  }
}


//SENDボタン=========================================================================
.send {
  display: flex;
  justify-content: center;
  &__btn {
    height: 5vh;
    width: 25vw;
    margin: 50px 0;
    border-radius: 20px;
    background-color: #87cefa;
    border: none;
    box-shadow: 0 0 8px gray;
    color: #ffffff;
    line-height: 1.5;
    font-size: 2vh;
    font-weight: bold;
    -webkit-transition: all 0.3s ease;
    -moz-transition: all 0.3s ease;
    -o-transition: all 0.3s ease;
    transition: all 0.3s ease;
  }
  :hover {
    background-color: #00bfff;
  }
  .send__btn[disabled] {
    background-color: #ddd;
    cursor: not-allowed;
  }
}

ここまで行うと次のような形になると思います。
newpageform1.png

ポイント

今回、JavaScriptにてフォームが入力または選択されているかどうかを状態管理するために

<%= form.collection_select :prefecture_id, Prefecture.all, :id, :name, {include_blank: '選択してください'}, {class: "prefecture__input", id: 'input01'} %>

という風に、id: 'input01'という形でidを指定しています。

このように、各フォームにidを指定していくのですが、idは同名のものを使い回しすることが不可のため、今回は
id='input02'id='input03'・・・という形でidを順番に振っています。
(クラス名を統一して、使い回すのもありですが、今回は省略します。)

SCSSについては、ボタン(class:send__btn)が有効・無効の状態別でボタンの色を変更する記述をしております。ボタンの状態管理は後述するdistabledという値を使って管理します。
無効の場合にはdistabledという値が要素に付与されることになるので、scssの方で

.send__btn[disabled] {
  background-color: #ddd;
  cursor: not-allowed;
}

という記述をして、ボタンが無効状態の場合は色を灰色にして、カーソルを無効にしています。

①-2 submit.jsに処理を記述

あとはjsファイルにフォームの入力・選択がされているかどうかを判定し、送信ボタンの有効・無効を切り替える処理を記述していきます。

今回はsubmit.jsというファイルに記述していきます。

submit.js
// フォームを入力・選択するまで送信ボタンが押せないようにする=============================================
$(function() {
  //最初に送信ボタンを無効にする
  $('#sending').prop("disabled", true);

  //idに「input」と設定している入力欄の操作時
  $("[id^= input],#post_images_attributes_0_image").change(function () {
      //入力欄が空かどうか判定を定義するために、sendという変数を使ってフォームの中身の状態管理を行う。
      let send = true;
      //id=input~と指定している入力欄をひとつずつチェック&画像(インデックス番号が0番の画像)をチェックする
      $("[id^= input],#post_images_attributes_0_image").each(function(index) {
        //フォームの中身(値)を順番に確認し、もしフォームの値が空の時はsend = false とする
        if ($("[id^= input],#post_images_attributes_0_image").eq(index).val() === "") {
          send = false;
        }
      });
      //フォームが全て埋まっていたら(send = trueの場合)
      if (send) {
          //送信ボタンを有効にする
          $('#sending').prop("disabled", false);
      }
      // フォームが一つでも空だったら(send = falseの場合)
      else {
          //送信ボタンを無効にする
          $('#sending').prop("disabled", true);
      }
  });
});

ポイント

最初に送信ボタン

<input type='submit' id='sending' class='send__btn' value='投稿する'>

に対して、prop(disabled, false)として、ボタンを無効化しています。

propメソッドは指定した属性に値を設定する役割を持っています。
distabledとは指定したHTML要素を無効化できる属性のことです。
propメソッドと組み合わせて使うことで
prop( ‘disabled’, true)」・・・要素を無効化
prop( ‘disabled’, false)」・・・要素を有効化
というふうに使用することができます。

参照: 
propメソッド・・・http://js.studio-kingdom.com/jquery/attributes/prop
distabled・・・https://persol-tech-s.co.jp/hatalabo/it_engineer/463.html#disabled

次に、

$("[id^= input],#post_images_attributes_0_image").change(function ()

と記述しています。

[id^= input]#post_images_attributes_0_imageの値が変化した時に、イベントが発火する」という記述になります。

注目いただきたいのは
javascript
[id^= input]

の部分です。
こちらはjQueryの属性を使った指定方法を採用しています。
指定方法には大まかに分けて4つあります。

  • 前方一致
  • 後方一致
  • 部分一致
  • 否定

「前方一致」は「属性 ^= 属性名」のように「^」を追加するだけで、属性名の先頭部分の文字列が一致するすべての要素を取得することができます。

今回の場合、[id^= input]とすることで、id="input01", id="input02,・・・ id="input05の要素、つまりid名にinputと命名されている要素を全て取得することができます。

なお、jQueryの属性を使った指定方法についてはこちらの記事を参考にさせていただきました。前方一致指定意外にも知りたい方はご覧いただければと思います。

続いて、

let send = true;

については、入力欄が空かどうか判定するために、sendという変数を用いてフォームの状態管理を行うために記述しています。trueの場合は、フォームが全て埋まっている状態を表します。

$("[id^= input],#post_images_attributes_0_image").each(function(index) {
  //フォームの中身(値)を順番に確認し、もしフォームの値が空の時はsend = false とする
  if ($("[id^= input],#post_images_attributes_0_image").eq(index).val() === "") {
    send = false;
  }
});

については、eachメソッドを使って、idにinputと命名している要素を、要素の個数分に応じて取り出します。
取り出す際、eachメソッドの引数にコールバック関数を定義する必要があるので、「function(index)」と指定します。こうすることでindex番号を取得することができ、取り出した要素にそれぞれindex番号を振り分けます。

今回の場合、イメージとしては

0 : id="input01"の要素
1 : id="input02"の要素
2 : id="input03"の要素
3 : id="input04"の要素
4 : id="input05"の要素

このような形になるかと思います。

加えて、#post_images_attributes_0_imageも対象のオブジェクトに加えております。正直なところ、
複数枚画像投稿する際、上手いidの設定、指定ができなくて、ここに加えております。
(他に上手い方法があれば教えていただけると助かります!)

eachメソッドでインデックス番号と一緒に取り出したあと、

if ($("[id^= input],#post_images_attributes_0_image").eq(index).val() === "") {
  send = false;
}

の処理に移ります。

ここでは、取り出した要素1つ1つのフォームの中身が空なのかを検証しています。
1つ1つ検証するにあたり、eqメソッドを使用しています。
eqメソッドとは現在マッチしている要素をインデックス番号でフィルタリングします。(eqメソッド参照サイト)

idにinputと命名されている要素は、eachメソッドでインデックス番号が0〜4が振られているので、順番にeqメソッドの引数にインデックス番号が入るイメージです。(例: eq(0),eq(1)...eq(4) )

フォームの値を取得するのはvalメソッドを使用します(valメソッド参照サイト

〜〜 === ""とは、「〜〜は空である」という意味を表します。

1つ1つ取り出した要素を検証して、1つでもフォームの値が空の要素があれば、send = falseを返します。

最後の

//フォームが全て埋まっていたら(send = trueの場合)
if (send) {
  //送信ボタンを有効にする
  $('#sending').prop("disabled", false);
}
// フォームが一つでも空だったら(send = falseの場合)
else {
  //送信ボタンを無効にする
  $('#sending').prop("disabled", true);
}

の部分については、
if (send)(if send ==trueという意味)の場合つまりフォームが全て埋まっている場合は、$('#sending').prop("disabled", false);というふうに、送信ボタンを有効にして、押せる状態にしています。

else(send == false)の場合つまりフォームが1つでも空だった場合は、$('#sending').prop("disabled", true);という形で、送信ボタンを無効にして、押せない状態にします。

①-3 完成

以上で完成です。

submitButtonJs2.gif

最後に

初学者のためまだまだ理解不足な部分も多く、今回の実装については正直、改善の予知が多くあると思いますので、もっと良い実装の方法等がありましたらご教示いただけますと幸いです。
また、この記事をご覧いただきましたら、LGTMもいただけるとすごく嬉しいです。何卒よろしくお願い致します。

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

【Ruby On Rails】ネストした状態で、link_toメソッド内Prefixの後に書く括弧内の記述

すみません。完全なる備忘録です。。笑

前提

show.html.erb
  <% @fuga_events.each do |event| %>
  ----------------------------------------
    <ul>
      <div>
        <%= "名前:#{event.hoge.name}" %>
      </div>
      <div>
        <%= "アプリ:#{event.hoge.app_name}" %>
      </div>
      <div>
        <%= "開始日:#{event.started_at}"%>
      </div>
      <div>
        <%= "終了日:#{event.finished_at}" %>
      </div>
      <div>
        <%= "todo:#{event.todo}" %>
      </div>
      <div>
        <%= "場所:#{event.place}" %>
      </div>
      <div>
        <%= "見込:#{event.expected_reward}" %>
      </div>
      <div>
        <%= "報酬:#{event.reward}" %>
      </div>
    </ul>
    <div>
      <%= link_to "編集する", edit_hoge_fuga_event_path(), method: :get %>
      <%= link_to "削除する", hoge_fuga_event_path(), method: :delete %>
    </div>
  <% end %>

今回は、()内に渡したいparameterのid(キー)に相当するものを記述したい。
上記では、次の様にhogeが親でfuga_eventsが子のネスト状態となっている。

routes.rb
(前略)

    resources :hoges, except: [:index] do
      resources :fuga_events, except: [:index]
    end

(後略)

実際に、link_toメソッド内に記述されているPrefix(パス)、今回で言うとedit_hoge_fuga_event_pathとhoge_fuga_event_pathの後の括弧内にはどの様な記述が必要となるでしょうか。

書き方

show.html.erb
<%= link_to "編集する", edit_hoge_fuga_event_path(event.papa_id, event.id), method: :get %>
<%= link_to "削除する", hoge_fuga_event_path(event.papa_id, event.id), method: :delete %>

編集(editアクション)と削除(destroyアクション)機能には、それぞれ(event.papa_id, event.id)を記述しました。

考え方

私は赤色がネストの親、青色がネストの子を表していて、それぞれに対応するキーを記述する と考えました。

<%= link_to "編集する", edit_ hoge _ fuga_event _path ( event.papa_id , event.id ), method: :get %>

<%= link_to "削除する", hoge _ fuga_event _path ( event.papa_id , event.id ), method: :delete %>

気付き

決してこの様な書き方が必ずではありません。結局、考え方としては、パスの後に書く括弧内ではどの様なidが渡されるかを指定して書いてあげることが重要であると気がつきました。

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

【Rails API + Vue】Active Storageを使って画像をアップロード・表示する

バックエンドはRails、フロントエンドはVueといった構成のときにActive Storageを使って画像をアップロード・表示する方法を、プロジェクトを1から作りながらまとめます
ソースコードはGitHubで公開しています

画像をアップロード・表示する処理の流れをざっくりと

  • Vueで画像を選択して送信するための画面を作る
  • 送信ボタンを押した時、画像をアップロードする処理を行うRails APIを呼び出す
  • Railsは受け取った画像をstorageディレクトリに保存し、保存した画像のURLを返す
  • Vueで画像のURLを受け取り、表示する

Railsプロジェクトを作成する

↓のようなディレクトリ構成で作成していきます

rails-vue-file-uploader-sample
└── backend   # Railsプロジェクト
└── frontend  # Vueプロジェクト

まずはRailsプロジェクトをAPIモードで作成します

$ mkdir rails-vue-file-uploader-sample
$ cd rails-vue-file-uploader-sample
$ rails _6.0_ new backend --api
$ cd backend
$ rails db:create

Active Storageを使えるようにする

$ rails active_storage:install
$ rails db:migrate

これらを実行するとactive_storage_blobsactive_storage_attachmentsという名前の2つのテーブルが作成されます
これらはActiveStorage::BlobActiveStorage::Attachmentの2つのモデルで扱われます

  • ActiveStorage::Blob:アップロードファイルのメタ情報を管理するためのモデル
  • ActiveStorage::Attachment:主となるモデルとActiveStorage::Blobとの中間テーブルに相当するモデル

例えばPostモデルに画像を持たせる場合は次のような関係になります
スクリーンショット 2020-11-15 16.54.33.png

モデルを作成する

titleとimageを属性に持つPostモデルを作成します
imageの型にはattachmentを指定します

$ rails g model post title:string image:attachment
$ rails db:migrate

これらを実行するとpostsテーブルが作成されます
マイグレーションファイルを見てみるとわかるのですが、postsテーブルにimageカラムは作られません
image属性の中身はActiveStorage::Blob及びActiveStorage::Attachmentに保存され、それを参照するようになります

生成されたapp/models/post.rbを見ると、has_one_attached :imageが指定されています
この指定によって画像を参照できるようになります

app/models/post.rb
class Post < ApplicationRecord
  has_one_attached :image
end

コントローラを作成する

$ rails g controller posts
app/controllers/posts.rb
class PostsController < ApplicationController
  def index
    render json: Post.all
  end

  def create
    post = Post.new(post_params)
    if post.save
      render json: post
    else
      render json: post.errors, status: 422
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy!
    render json: post
  end

  private

  def post_params
    params.permit(:title, :image)
  end
end

とりあえず普通に書きます
routesも設定します

config/routes.rb
Rails.application.routes.draw do
  scope :api do
    resources :posts, only: [:index, :create, :destroy]
  end
end

保存したファイルのURLを返すようにする

Postモデルに、紐づいている画像のURLを取得するメソッドを追加します
url_forメソッドを使うためにRails.application.routes.url_helpersをincludeする必要があります

app/models/post.rb
class Post < ApplicationRecord
  include Rails.application.routes.url_helpers

  has_one_attached :image

  def image_url
    # 紐づいている画像のURLを取得する
    image.attached? ? url_for(image) : nil
  end
end

アクションで返すJSONにimage_urlの値を追加します

app/controllers/posts.rb
class PostsController < ApplicationController
  def index
    render json: Post.all, methods: [:image_url]  # ここを変更
  end

  def create
    post = Post.new(post_params)
    if post.save
      render json: post, methods: [:image_url]  # ここを変更
    else
      render json: post.errors, status: 422
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy!
    render json: post
  end

  private

  def post_params
    params.permit(:title, :image)
  end
end

画像のURLを取得するためにconfig/environments/development.rbに次の設定を追加する必要があります

config/environments/development.rb
Rails.application.configure do
  ...

  # これを追加
  Rails.application.routes.default_url_options[:host] = 'localhost'
  Rails.application.routes.default_url_options[:port] = 3000
end

VueとのAPI通信をするためにCORSの設定をしておきます
Gemfileのgem 'rack-cors'のコメントを外してbundle installし、config/initializers/cors.rbを次のように書きます

config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8080'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

Vueプロジェクトを作成する

ここからはVueを書いていきます
まずはルートディレクトリに戻ってVueプロジェクトを作成します

$ cd rails-vue-file-uploader-sample
$ vue create frontend
$ cd frontend

vue createの設定は以下のように選択しました

? Please pick a preset: Manually select features
? Check the features needed for your project: Vuex, Linter
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

Vuexストアを作成する

Vuexを次のように書きます
axiosを使用するのでインストールしておきます

$ npm install --save axios
src/store/modules/posts.js
import axios from "axios";

const apiUrlBase = "http://localhost:3000/api/posts";
const headers = { "Content-Type": "multipart/form-data" };

const state = {
  posts: []
};

const getters = {
  posts: state => state.posts.sort((a, b) => b.id - a.id)
};

const mutations = {
  setPosts: (state, posts) => (state.posts = posts),
  appendPost: (state, post) => (state.posts = [...state.posts, post]),
  removePost: (state, id) =>
    (state.posts = state.posts.filter(post => post.id !== id))
};

const actions = {
  async fetchPosts({ commit }) {
    try {
      const response = await axios.get(`${apiUrlBase}`);
      commit("setPosts", response.data);
    } catch (e) {
      console.error(e);
    }
  },
  async createPost({ commit }, post) {
    try {
      const response = await axios.post(`${apiUrlBase}`, post, headers);
      commit("appendPost", response.data);
    } catch (e) {
      console.error(e);
    }
  },
  async deletePost({ commit }, id) {
    try {
      axios.delete(`${apiUrlBase}/${id}`);
      commit("removePost", id);
    } catch (e) {
      console.error(e);
    }
  }
};

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
};
src/store/index.js
import Vue from "vue";
import Vuex from "vuex";
import posts from "./modules/posts";

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    posts
  }
});

画像をアップロードするコンポーネントを作成する

画像を選択して送信するフォームを表示するためのsrc/components/PostForm.vueを作成します

src/components/PostForm.vue
<template>
  <div>
    <h2>PostForm</h2>
    <section>
      <label for="title">title: </label>
      <input type="text" name="title" v-model="title" placeholder="title" />
    </section>
    <section>
      <label for="image">image: </label>
      <input type="file" id="image" name="image" accept="image/png,image/jpeg" @change="setImage" />
    </section>
    <section>
      <button type="submit" @click="upload" :disabled="title === ''">upload</button>
    </section>
  </div>
</template>

<script>
import { mapActions } from "vuex";

export default {
  name: "PostForm",
  data: () => ({
    title: "",
    imageFile: null
  }),
  methods: {
    ...mapActions("posts", ["createPost"]),
    setImage(e) {
      e.preventDefault();
      this.imageFile = e.target.files[0];
    },
    async upload() {
      let formData = new FormData();
      formData.append("title", this.title);
      if (this.imageFile !== null) {
        formData.append("image", this.imageFile);
      }
      this.createPost(formData);
      this.resetForm();
    },
    resetForm() {
      this.title = "";
      this.imageFile = null;
    }
  }
};
</script>

選択された画像はe.target.filesで取り出すことができます
POSTリクエストを送信するときはFormDataに必要な値をappendしたものをパラメータとして指定します

画像を表示するコンポーネントを作成する

保存されている画像を取得して表示するためのsrc/components/PostList.vueを作成します

src/components/PostList.vue
<template>
  <div>
    <h2>PostList</h2>
    <div v-for="post in posts" :key="post.id">
      <h3>{{ post.title }}</h3>
      <img :src="post.image_url" />
      <br />
      <button type="submit" @click="del(post.id)">delete</button>
    </div>
  </div>
</template>

<script>
import { mapActions, mapGetters } from "vuex";

export default {
  name: "PostList",
  created() {
    this.fetchPosts();
  },
  computed: {
    ...mapGetters("posts", ["posts"])
  },
  methods: {
    ...mapActions("posts", ["fetchPosts", "deletePost"]),
    del(id) {
      this.deletePost(id);
    }
  }
};
</script>

<img :src="post.image_url" />でsrcに取得したURLを指定して表示させます

最後にApp.vueを編集してコンポーネントを表示します

src/App.vue
<template>
  <div id="app">
    <PostForm />
    <PostList />
  </div>
</template>

<script>
import PostForm from "./components/PostForm.vue";
import PostList from "./components/PostList.vue";

export default {
  name: "App",
  components: {
    PostForm,
    PostList
  }
};
</script>

完成

画像を選択してアップロードボタンを押すと、画像が保存されて表示されます
画像はbackend/storageディレクトリにバイナリ形式で保存されます

スクリーンショット 2020-11-15 17.43.33.png

ソースコードはGitHubで公開しています
参考になれば嬉しいです
https://github.com/youichiro/rails-vue-file-uploader-sample

参考

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

Ruby on Railsの開発環境をDockerで構築する方法

この記事では、Dockerを用いてRuby on Rails(以下、rails)の開発環境を構築する方法を紹介します。

通常、rails の初学者の方はマシンに直接インストールして開発環境を構築する人がほとんどだと思います。
しかし、環境の微妙な際(Rubyのバージョンや環境変数など)によってつまづいた場合、
そのトラブルシューティングには多くの時間を要してしまうかと思います。

そこで、Dockerで仮想環境上にrailsの環境を構築してあげることで
どのマシン上の開発環境でも等しい動作を行うことができるため、環境の差異によるエラーを避けることができます。
環境をいじりすぎた時には最悪一度リセットを出来るというのもメリットですね。

また、作成したアプリケーションを本番環境として外部へ公開しようとするときにも
開発環境と本番環境での違いを最小限に抑え、安定したアプリケーションとして稼働させることができます.

Dockerで仮想環境を作って公開するなんて難しそうと思うかもしれませんが、
最近ではAWSといったクラウドサービスでもDockerコンテナをそのまま実行できるECS等のサービスが整っているので
Dockerで作った環境を公開するのも楽になってきています。

前提条件

マシンに Docker がインストールされていること

Docker をインストールしていない人は、公式サイトから Docker のインストーラをダウンロードしてインストールしておきましょう。
Docker公式

また、任意の作業フォルダを作成しておきます。
ここでは "rails-docker" というフォルダを作成し、その中で作業していくことにします。

$ mkdir rails-docker
$ cd rails-docker

Docker関連ファイルの準備

rails-docker 内に以下4つの空ファイルを準備しておきます。
VScode を利用しているなら新規ファイルの作成で作ったり、
Mac のターミナルから touch コマンドを使ってもよいです。

.
├── Dockerfile # 新規作成
├── Gemfile # 新規作成
├── Gemfile.lock # 新規作成
└── docker-compose.yml # 新規作成

Dockerファイルの解説

Dockerでは、Dockerfileというファイルに基づいてコンテナのビルドが行われ、
Docker Image(コンテナの雛形)が作成されることになります。

まず Dockerfile の全文を記載します。
使用する Ruby のバージョンは 2.7.0 としています。

FROM ruby:2.7.0
RUN apt-get update -qq && apt-get install -y build-essential nodejs
RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install
COPY . /app

箇条書きとはなりますが、それぞれの意味を解説します。

  • FROM ruby:2.7.0
    • 公開されている ruby インストール済みコンテナのうち、Rubyのバージョンが 2.7.0 であるものを引っ張ってきます。
  • RUN apt-get update -qq && apt-get install -y build-essential nodejs
    • Rails の実行に必要なパッケージをインストールしています
  • RUN mkdir /app
    • Rails のプロジェクトファイルを作成するアプリディレクトリを作成
  • WORKDIR /app
    • 作業用のディレクトリを指定
  • COPY Gemfile /app/Gemfile
    • 自分のマシン(PC)上にある Gemfile をコンテナの作業用ディレクトリに移動させ、コンテナから利用できるようにします
    • Gemfile.lock も同様
  • RUN bundle install
    • Gemfileに記載されているgemを一括インストール
  • COPY . /app
    • Dockerファイルが置いてあるフォルダのファイルすべてをコンテナ内の app ディレクトリにコピー
    • Rails の実行に必要なファイルをコンテナに含めるためにコピーしています

Gemfile の解説

Ruby では Gemfile というファイルで環境で使いたい gem を定義することができます。

gemとはRubyのライブラリのことをいいます。
gemはRubyGemsと呼ばれるRuby用のパッケージ管理システムで管理されており、RubyGemsが提供するgemコマンドを通じてインストール等ができます。

source 'https://rubygems.org'
gem 'rails', '5.2.1'

また少し解説します。

  • source 'https://rubygems.org'
    • gem のダウンロード元を指定
  • gem 'rails', '5.2.1'
    • インストールする gem である rails というパッケージ名と、バージョンを指定
    • 今回は rails のバージョンは 5.2.1 を指定しています
    • どのバージョンを指定してもよいのですが rails 6 以降は webpacker, yarn が必要になるので注意してください
    • ここでは簡単に検証するため rails 5 系を使用

Gemfile があるディレクトリで "bundle install" コマンドを実行すると、
Gemfile の定義に従って、定義した gem をインストールすることができます。

先ほど解説した Docker ファイルでも、Gemfile をコンテナの作業用ディレクトリにコピーして
そのフォルダ内で bundle install を実行するように指定していましたね。

Gemfile.lock の解説

実は Gemfile.lock は最初の時点では何も書き込む必要がありません。
これは直接編集するようなファイルではなく、
Gemfile に基づいて bundle install を実行した後に
インストールされた gem が Gemfile.lock に一覧として記述されるようになります。

使い方としては、Gemfile.lock を参照すればインストールされている gem とバージョンが分かり、
Gemfile.lock から bundle install を実行することもできるので
完全に同じ環境をもう一度作りたいときや、多人数で開発をするときに Gemfile.lock が使われます。

Gemfile だけでも良いのでは?と思った鋭い方に補足しますと、Gemfile では実はこういう書き方もできることが理由の一つになっています。

gem 'rails', '~> 5.2.1'

この場合、bundle install でインストールされる rails のバージョンは 5.2.1 以上であるものの
実際にインストールできる(公開されている)バージョンはその時点によって変わってきます。

しかし Gemfile.lock では Gemfile に従ってインストールしようとした時に実際にインストールされた gem 本体と具体的なバージョン、
付随して必要となるためにインストールされたパッケージまで記録されます。

少し整理すると Gemfile は「アプリで必要な gem の一覧」であり、
一方の Gemfile.lock は「Gemfile に従った結果、実際にインストールした gem の情報」が記述されることになります。

docker-compose.yml の解説

この yml ファイルは Docker Compose で使用されます。

Docker Composeは、複数のコンテナで構成されるアプリケーションについて
Dockerイメージのビルドや各コンテナの起動・停止といった管理を行うためのツールです。

Docker Composeでは、Dockerビルドやコンテナ起動のオプションも含め、複数のコンテナのための定義を docker-compose.yml というファイルに記述し、
それを利用して Docker イメージのビルドやコンテナ起動をすることができます。

開発環境のように、Rails をそれ単体で動かすならば Dockerfile だけでもよいのですが
Rails を動かすためのアプリケーションサーバに加え、
実際に公開するときにはインターネットからのアクセスを受け付けるための Web サーバや、
またデータを保存・処理するためのデータベースサーバも用意することがあります。

docker-compose.yml
version: '3'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
    tty: true
    stdin_open: true
  db:
    image: mysql:5.7
    volumes:
      - db-volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
volumes:
  db-volume:

rails ではデータベースも必要になるため、WebサーバだけではなくMySQLを用いたデータベースサーバのコンテナも構築します。
ここでは詳解しませんが、web という欄の中の「depends_on」で rails が連携するデータベースサーバ(コンテナ)を名前で指定しています。

rails の立ち上げ

rails プロジェクトの作成 (rails new)

docker-compose.yml が置いてあるフォルダ(rails-dockerフォルダ)で以下の docker-compose コマンドを実行します。

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

コマンドについて、オプション含め少し読み方を解説します↓

  • docker-compose: docker-compose というツールをつかって
  • run: 以下のコマンドを実行します
  • web: web コンテナで
  • rails new: rails で新しいプロジェクトを作成する
  • . : 現在のディレクトリに対して
  • --force: 既存の file (ここでは Gemfile, Gemfile.lock) は上書きする形で
  • --database=mysql: ただし rails のデータベースには MySQL を使用します

という意味を持っているのです。

この一行のコマンドを実行すると数分単位で処理に時間がかかるので、チョコレートでも食べながら気長に待ちましょう。

build の実行

Gemfile に追記された gem のインストール、
および作成されたファイルをコンテナ内に取り込むため build を実行します。

$ docker-compose build

データベース設定ファイルの編集

build が完了したら、rails で使用するデータベースファイルの設定を編集します。
config ディレクトリ内の database.yml というファイルが対象です。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password # 追加
  host: db # 変更

password は docker-compose.yml で指定したパスワードと合わせる必要があります。

docker-compose.yml
...
    environment:
      MYSQL_ROOT_PASSWORD: password # ここと合わせる
...

また、host の欄は MySQL コンテナ名を設定しています。

コンテナの起動

コンテナを起動するため、次のコマンドを実行します。

$ docker-compose up -d

docker-compose up がコンテナをdocker-compose.yml に基づいて起動するコマンドであり、
オプションの「-d」によりバックグラウンドで起動させることができます。

データベースの作成

いまはまだコンテナが起動しただけであり、データベースは作成されていないので
次のコマンドを実行してデータベースを作成します。

$ docker-compose run web bundle exec rails db:create

rails を実行している web コンテナで、rails db:create (=データベースの新規作成)を実行する処理となっています。

rails 開発用サーバの起動確認

これで無事に rails の開発用サーバが起動したことになります。
ブラウザのアドレスバーに localhost:3000 と入力し、起動を確認してみましょう。

image.png

rails ではお馴染みの画面が表示されれば完了です。

コンテナの停止

開発用サーバを止めるため、コンテナを一括で停止するには以下のコマンドを実行します。

$ docker-compose down

また立ち上げたいときには docker-comopose up -d を実行しましょう。

最後に

これによって作られるファイルの一式は Github にあげています。
フォルダ構成が分からなくなったり、自分の環境でうまくいっているか比較して確かめたい方はご参照ください。

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

Ruby on Railsの開発環境をDockerで構築する方法(Rails 5.x)

この記事では、Dockerを用いてRuby on Rails(以下、rails)の 5.x 系における開発環境を構築する方法を紹介します。
※Rails 6.x 系で Docker 環境を作りたい場合は こちら をご覧ください。

Docker で環境構築をするメリットとは?

通常、rails の初学者の方はマシンに直接インストールして開発環境を構築する人がほとんどだと思います。
しかし、環境の微妙な際(Rubyのバージョンや環境変数など)によってつまづいた場合、
そのトラブルシューティングには多くの時間を要してしまうかと思います。

そこで、Dockerで仮想環境上にrailsの環境を構築してあげることで
どのマシン上の開発環境でも等しい動作を行うことができるため、環境の差異によるエラーを避けることができます。
環境をいじりすぎた時には最悪一度リセットを出来るというのもメリットですね。

また、作成したアプリケーションを本番環境として外部へ公開しようとするときにも
開発環境と本番環境での違いを最小限に抑え、安定したアプリケーションとして稼働させることができます.

Dockerで仮想環境を作って公開するなんて難しそうと思うかもしれませんが、
最近ではAWSといったクラウドサービスでもDockerコンテナをそのまま実行できるECS等のサービスが整っているので
Dockerで作った環境を公開するのも楽になってきています。

前提条件

マシンに Docker がインストールされていること

Docker をインストールしていない人は、公式サイトから Docker のインストーラをダウンロードしてインストールしておきましょう。
Docker公式

また、任意の作業フォルダを作成しておきます。
ここでは "rails-docker" というフォルダを作成し、その中で作業していくことにします。

$ mkdir rails-docker
$ cd rails-docker

Docker関連ファイルの準備

rails-docker 内に以下4つの空ファイルを準備しておきます。
VScode を利用しているなら新規ファイルの作成で作ったり、
Mac のターミナルから touch コマンドを使ってもよいです。

.
├── Dockerfile # 新規作成
├── Gemfile # 新規作成
├── Gemfile.lock # 新規作成
└── docker-compose.yml # 新規作成

Dockerファイルの解説

Dockerでは、Dockerfileというファイルに基づいてコンテナのビルドが行われ、
Docker Image(コンテナの雛形)が作成されることになります。

まず Dockerfile の全文を記載します。
使用する Ruby のバージョンは 2.7.0 としています。

FROM ruby:2.7.0
RUN apt-get update -qq && apt-get install -y build-essential nodejs
RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install
COPY . /app

箇条書きとはなりますが、それぞれの意味を解説します。

  • FROM ruby:2.7.0
    • 公開されている ruby インストール済みコンテナのうち、Rubyのバージョンが 2.7.0 であるものを引っ張ってきます。
  • RUN apt-get update -qq && apt-get install -y build-essential nodejs
    • Rails の実行に必要なパッケージをインストールしています
  • RUN mkdir /app
    • Rails のプロジェクトファイルを作成するアプリディレクトリを作成
  • WORKDIR /app
    • 作業用のディレクトリを指定
  • COPY Gemfile /app/Gemfile
    • 自分のマシン(PC)上にある Gemfile をコンテナの作業用ディレクトリに移動させ、コンテナから利用できるようにします
    • Gemfile.lock も同様
  • RUN bundle install
    • Gemfileに記載されているgemを一括インストール
  • COPY . /app
    • Dockerファイルが置いてあるフォルダのファイルすべてをコンテナ内の app ディレクトリにコピー
    • Rails の実行に必要なファイルをコンテナに含めるためにコピーしています

Gemfile の解説

Ruby では Gemfile というファイルで環境で使いたい gem を定義することができます。

gemとはRubyのライブラリのことをいいます。
gemはRubyGemsと呼ばれるRuby用のパッケージ管理システムで管理されており、RubyGemsが提供するgemコマンドを通じてインストール等ができます。

source 'https://rubygems.org'
gem 'rails', '5.2.1'

また少し解説します。

  • source 'https://rubygems.org'
    • gem のダウンロード元を指定
  • gem 'rails', '5.2.1'
    • インストールする gem である rails というパッケージ名と、バージョンを指定
    • 今回は rails のバージョンは 5.2.1 を指定しています
    • どのバージョンを指定してもよいのですが rails 6 以降は webpacker, yarn が必要になるので注意してください
    • ここでは簡単に検証するため rails 5 系を使用

Gemfile があるディレクトリで "bundle install" コマンドを実行すると、
Gemfile の定義に従って、定義した gem をインストールすることができます。

先ほど解説した Docker ファイルでも、Gemfile をコンテナの作業用ディレクトリにコピーして
そのフォルダ内で bundle install を実行するように指定していましたね。

Gemfile.lock の解説

実は Gemfile.lock は最初の時点では何も書き込む必要がありません。
これは直接編集するようなファイルではなく、
Gemfile に基づいて bundle install を実行した後に
インストールされた gem が Gemfile.lock に一覧として記述されるようになります。

使い方としては、Gemfile.lock を参照すればインストールされている gem とバージョンが分かり、
Gemfile.lock から bundle install を実行することもできるので
完全に同じ環境をもう一度作りたいときや、多人数で開発をするときに Gemfile.lock が使われます。

Gemfile だけでも良いのでは?と思った鋭い方に補足しますと、Gemfile では実はこういう書き方もできることが理由の一つになっています。

gem 'rails', '~> 5.2.1'

この場合、bundle install でインストールされる rails のバージョンは 5.2.1 以上であるものの
実際にインストールできる(公開されている)バージョンはその時点によって変わってきます。

しかし Gemfile.lock では Gemfile に従ってインストールしようとした時に実際にインストールされた gem 本体と具体的なバージョン、
付随して必要となるためにインストールされたパッケージまで記録されます。

少し整理すると Gemfile は「アプリで必要な gem の一覧」であり、
一方の Gemfile.lock は「Gemfile に従った結果、実際にインストールした gem の情報」が記述されることになります。

docker-compose.yml の解説

この yml ファイルは Docker Compose で使用されます。

Docker Composeは、複数のコンテナで構成されるアプリケーションについて
Dockerイメージのビルドや各コンテナの起動・停止といった管理を行うためのツールです。

Docker Composeでは、Dockerビルドやコンテナ起動のオプションも含め、複数のコンテナのための定義を docker-compose.yml というファイルに記述し、
それを利用して Docker イメージのビルドやコンテナ起動をすることができます。

開発環境のように、Rails をそれ単体で動かすならば Dockerfile だけでもよいのですが
Rails を動かすためのアプリケーションサーバに加え、
実際に公開するときにはインターネットからのアクセスを受け付けるための Web サーバや、
またデータを保存・処理するためのデータベースサーバも用意することがあります。

docker-compose.yml
version: '3'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    depends_on:
      - db
    tty: true
    stdin_open: true
  db:
    image: mysql:5.7
    volumes:
      - db-volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
volumes:
  db-volume:

rails ではデータベースも必要になるため、WebサーバだけではなくMySQLを用いたデータベースサーバのコンテナも構築します。
ここでは詳解しませんが、web という欄の中の「depends_on」で rails が連携するデータベースサーバ(コンテナ)を名前で指定しています。

rails の立ち上げ

rails プロジェクトの作成 (rails new)

docker-compose.yml が置いてあるフォルダ(rails-dockerフォルダ)で以下の docker-compose コマンドを実行します。

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

コマンドについて、オプション含め少し読み方を解説します↓

  • docker-compose: docker-compose というツールをつかって
  • run: 以下のコマンドを実行します
  • web: web コンテナで
  • rails new: rails で新しいプロジェクトを作成する
  • . : 現在のディレクトリに対して
  • --force: 既存の file (ここでは Gemfile, Gemfile.lock) は上書きする形で
  • --database=mysql: ただし rails のデータベースには MySQL を使用します

という意味を持っているのです。

この一行のコマンドを実行すると数分単位で処理に時間がかかるので、チョコレートでも食べながら気長に待ちましょう。

build の実行

Gemfile に追記された gem のインストール、
および作成されたファイルをコンテナ内に取り込むため build を実行します。

$ docker-compose build

データベース設定ファイルの編集

build が完了したら、rails で使用するデータベースファイルの設定を編集します。
config ディレクトリ内の database.yml というファイルが対象です。

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password # 追加
  host: db # 変更

password は docker-compose.yml で指定したパスワードと合わせる必要があります。

docker-compose.yml
...
    environment:
      MYSQL_ROOT_PASSWORD: password # ここと合わせる
...

また、host の欄は MySQL コンテナ名を設定しています。

コンテナの起動

コンテナを起動するため、次のコマンドを実行します。

$ docker-compose up -d

docker-compose up がコンテナをdocker-compose.yml に基づいて起動するコマンドであり、
オプションの「-d」によりバックグラウンドで起動させることができます。

データベースの作成

いまはまだコンテナが起動しただけであり、データベースは作成されていないので
次のコマンドを実行してデータベースを作成します。

$ docker-compose run web bundle exec rails db:create

rails を実行している web コンテナで、rails db:create (=データベースの新規作成)を実行する処理となっています。

rails 開発用サーバの起動確認

これで無事に rails の開発用サーバが起動したことになります。
ブラウザのアドレスバーに localhost:3000 と入力し、起動を確認してみましょう。

image.png

rails ではお馴染みの画面が表示されれば完了です。

コンテナの停止

開発用サーバを止めるため、コンテナを一括で停止するには以下のコマンドを実行します。

$ docker-compose down

また立ち上げたいときには docker-comopose up -d を実行しましょう。

最後に

これによって作られるファイルの一式は Github にあげています。
フォルダ構成が分からなくなったり、自分の環境でうまくいっているか比較して確かめたい方はご参照ください。

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

Amazon S3に画像を保存する(Local/Heroku)

はじめに

自分用の備忘録

手順(Local)

Gemインストール

Gemfile
gem "aws-sdk-s3", require: false

保存先を変更

config/environments/development.rb
config.active_storage.service = :local 

#下記に変更
config.active_storage.service = :amazon

storage.ymlに追記

config/storage.yml
amazon:
 service: S3
 access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
 secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
 region: ap-northeast-1
 bucket: バケット名

環境変数を設定

ターミナル
# Catalina以降
% vim ~/.zshrc

[insert mode]
export AWS_ACCESS_KEY_ID="Access key ID"
export AWS_SECRET_ACCESS_KEY="Secret access key"
[:wqで保存]

# 反映させるコマンド
% source ~/.zshrc

手順(Heroku)

※Gemインストール済

保存先を変更

config/environments/production.rb
config.active_storage.service = :local 

#下記に変更
config.active_storage.service = :amazon

環境変数を設定

ターミナル
heroku config:set AWS_ACCESS_KEY_ID="Access key ID"
heroku config:set AWS_SECRET_ACCESS_KEY="Secret access key"

確認コマンド

ターミナル
% heroku config

pushして反映

おわりに

バケットは使いまわせるのでいちいち作らなくてよろしい。

✔︎

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

バリデーションの実行タイミングを指定するオプション 「on:」 

はじめに/エラー発生時の状況

Railsでカレンダーアプリ(旅のしおり)的なものを実装中です。
日付や詳細、メンバー(deviseのユーザー登録機能を利用)を指定し、
旅行ページを登録します。

旅行ページ(trips)の参加ユーザー(users)は、
下図のようにtrip_usersと言う中間テーブルで、多対多のアソシエーションを組んで紐付けています。

スクリーンショット 2020-11-15 16 14 29

必須項目を埋めて登録すると
tripsテーブルには
スクリーンショット 2020-11-15 16 19 20

trip_usersテーブルには
スクリーンショット 2020-11-15 16 21 03

という具合に保存されます。

しかし、実装を進めていると…何かの拍子に

スクリーンショット 2020-11-15 16 03 24

見たことのないエラーと共に、、旅行ページの登録が突然出来なくなりました。
※当方、エラー文を日本語化しています。
先ほどまでと同じようにメンバーを選択しているのに、「参加メンバーは不正な値です」とは何ぞや…
私は何をしてしまったのでしょうか…

開発環境

Ruby 2.6.5
Rails 6.0.3.4
MySQL
Visual Studio Code
(GoogleChrome)

原因の考察・検証

binding.pryでcreate処理を確認しました。

[1] pry(#<TripsController>)> trip_params
=> <ActionController::Parameters {
(中略)

 "user_ids"=>["", "2", "3", "4", "5", "6", "1"]} permitted: true>
[2] pry(#<TripsController>)> @trip = Trip.new(trip_params)
(中略)

[3] pry(#<TripsController>)> @trip.valid?
=> false
[4] pry(#<TripsController>)> @trip.errors.full_messages
=> ["参加メンバーは不正な値です"]

参加ユーザーはparamsではuser_idsにidが格納されて送られます。
これに対して、「不正な値」と言われているようです。
さっきまでは全然不正じゃなかったのに、何が不正になったのでしょうか…

何かエラーの心あたりはないものかと、最近何をやったか、記憶をたどると…

「そういえば、パスワードの英数混合バリデーション書いてなかったな〜」

と、急に思い出して書いた記述を発見しました。

user.rb
class User < ApplicationRecord
 (中略)

  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze # 英数混合の正規表現
  validates :password, format: { with: PASSWORD_REGEX, message: 'は英字と数字を両方含んでください' }
end

試しにコメントアウトしてみると…戻りました!
こちらが原因だった模様…

どうやら、ユーザー新規登録時のバスワードに設定した英数混合バリデーションが、アソシエーションしている他モデルの生成時にも発動してしまっていた?みたいです。
数字のみのデータであるuser_idsに対しても、「英数混合にしてください!」と弾いていたと考えられます。
そんな珍事件もあるの…

では、そのバリデーションをユーザー登録のみに適用できないものか…
調べた結果…

【公式】Railsドキュメント/validates

スクリーンショット 2020-11-15 16 29 30

こちらが使えそうです!

結果

:onオプションでバリデーション実行のタイミングを指定することができます。
今回はユーザー登録時にのみパスワードのバリデーションを実行したいので、on: :createを記述します。

↓修正後のUserモデル

user.rb
class User < ApplicationRecord
 (中略)

  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze # 英数混合の正規表現
  validates :password, format: { with: PASSWORD_REGEX, message: 'は英字と数字を両方含んでください' }, on: :create
end

これでパスワードのバリデーションを残しつつ、
旅行ページの登録機能も今まで通りに戻りました!

終わりに/感想

原因となる記述を探すのに時間がかかりました。
エラー時に遡れるように、マメにコミットする重要性も学びました…

初学者で拙い記事ですが、少しでもお役に立てると嬉しく思います。
最後まで読んでいただき、誠にありがとうございました。

参考記事

【公式/Railsガイド】Active Record バリデーション
【公式】Railsドキュメント/validates
【Qiita】状況によってsave時に実行するバリデーションを切り替える
【Qiita】Rails |onオプションを使ってバリデーションのタイミングを指定

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

バリデーションの実行タイミングを指定するオプション 【on:】

はじめに/エラー発生時の状況

Railsでカレンダーアプリ(旅のしおり)的なものを実装中です。
日付や詳細、メンバー(deviseのユーザー登録機能を利用)を指定し、
旅行ページを登録します。

旅行ページ(trips)の参加ユーザー(users)は、
下図のようにtrip_usersと言う中間テーブルで、多対多のアソシエーションを組んで紐付けています。

スクリーンショット 2020-11-15 16 14 29

必須項目を埋めて登録すると
tripsテーブルには
スクリーンショット 2020-11-15 16 19 20

trip_usersテーブルには
スクリーンショット 2020-11-15 16 21 03

という具合に保存されます。

しかし、実装を進めていると…何かの拍子に

スクリーンショット 2020-11-15 16 03 24

見たことのないエラーと共に、、旅行ページの登録が突然出来なくなりました。
※当方、エラー文を日本語化しています。
先ほどまでと同じようにメンバーを選択しているのに、「参加メンバーは不正な値です」とは何ぞや…
私は何をしてしまったのでしょうか…

開発環境

Ruby 2.6.5
Rails 6.0.3.4
MySQL
Visual Studio Code
(GoogleChrome)

原因の考察・検証

binding.pryでcreate処理を確認しました。

[1] pry(#<TripsController>)> trip_params
=> <ActionController::Parameters {
(中略)

 "user_ids"=>["", "2", "3", "4", "5", "6", "1"]} permitted: true>
[2] pry(#<TripsController>)> @trip = Trip.new(trip_params)
(中略)

[3] pry(#<TripsController>)> @trip.valid?
=> false
[4] pry(#<TripsController>)> @trip.errors.full_messages
=> ["参加メンバーは不正な値です"]

参加ユーザーはparamsではuser_idsにidが格納されて送られます。
これに対して、「不正な値」と言われているようです。
さっきまでは全然不正じゃなかったのに、何が不正になったのでしょうか…

何かエラーの心あたりはないものかと、最近何をやったか、記憶をたどると…

「そういえば、パスワードの英数混合バリデーション書いてなかったな〜」

と、急に思い出して書いた記述を発見しました。

user.rb
class User < ApplicationRecord
 (中略)

  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze # 英数混合の正規表現
  validates :password, format: { with: PASSWORD_REGEX, message: 'は英字と数字を両方含んでください' }
end

試しにコメントアウトしてみると…戻りました!
こちらが原因だった模様…

どうやら、ユーザー新規登録時のバスワードに設定した英数混合バリデーションが、アソシエーションしている他モデルの生成時にも発動してしまっていた?みたいです。
数字のみのデータであるuser_idsに対しても、「英数混合にしてください!」と弾いていたと考えられます。
そんな珍事件もあるの…

では、そのバリデーションをユーザー登録のみに適用できないものか…
調べた結果…

【公式】Railsドキュメント/validates

スクリーンショット 2020-11-15 16 29 30

こちらが使えそうです!

結果

:onオプションでバリデーション実行のタイミングを指定することができます。
今回はユーザー登録時にのみパスワードのバリデーションを実行したいので、on: :createを記述します。

↓修正後のUserモデル

user.rb
class User < ApplicationRecord
 (中略)

  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze # 英数混合の正規表現
  validates :password, format: { with: PASSWORD_REGEX, message: 'は英字と数字を両方含んでください' }, on: :create
end

これでパスワードのバリデーションを残しつつ、
旅行ページの登録機能も今まで通りに戻りました!

終わりに/感想

原因となる記述を探すのに時間がかかりました。
エラー時に遡れるように、マメにコミットする重要性も学びました…

初学者で拙い記事ですが、少しでもお役に立てると嬉しく思います。
最後まで読んでいただき、誠にありがとうございました。

参考記事

【公式/Railsガイド】Active Record バリデーション
【公式】Railsドキュメント/validates
【Qiita】状況によってsave時に実行するバリデーションを切り替える
【Qiita】Rails |onオプションを使ってバリデーションのタイミングを指定

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

Rails migration カラムの変更やら何やらをまとめるんやで

この記事について

初心者が初心者に対して記述した初心者のRails記事

migrationの記述をすぐ忘れてまうこいつらをまとめるんやで〜

①現在のマイグレーションのバージョンを確認

②migrationを指定したバージョンまで戻す

③直前に実行されたマイグレーションを1つ取り消す

④カラムの変更

⑤インデックスの追加

①現在のマイグレーションのバージョンを確認

rails db:migrate:status

database: first_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20201115064445  Create members

②migrationを指定したバージョンまで戻す。

ファイル名にある数字の羅列(年月日時)を指定してコマンドを実行。

スクリーンショット 2020-11-15 15.58.33.png

rails db:migrate VERSION=20201115064445

③直前に実行されたマイグレーションを1つ取り消す

「あ、さっきの書き忘れてるやつあるわ!!!」って気づいた時に使える

rails db:migrate:rollback

④カラムの変更

#migrationファイルを作成するためのコマンド
rails g migration rename_変更前のカラム名_column_to_テーブル名(複数形)

#migrationファイルの中身
class Rename変更前のカラム名ColumnToテーブル名s < ActiveRecord::Migration[5.2]
  def change
    rename_column :テーブル名(複数形), :変更前のカラム名, :変更後のカラム名
  end
end


④カラムの変更

#migrationファイルを作成するためのコマンド
rails g migration rename_変更前のカラム名_column_to_テーブル名(複数形)

#migrationファイルの中身
class Rename変更前のカラム名ColumnToテーブル名 < ActiveRecord::Migration[5.2]
  def change
    rename_column :テーブル名(複数形), :変更前のカラム名, :変更後のカラム名
  end
end


⑤インデックスの追加

#migrationファイルを作成するためのコマンド
rails generate migration add_index_テーブル名_カラム名

#migrationファイルの中身
class AddIndexToテーブル名 < ActiveRecord::Migration
  def change
    add_index :テーブル名, カラム名
  end
end

ちなみにのインデックスの説明をしておこう

特定のカラムからデータを取得する際に、検索を高速化させる便利屋さん。
例えば、あるユーザーをnameで検索したい!となった時、Usersテーブルのnameカラムにインデックスがなかったら、Userテーブルのnameカラムを上から順に1つずつ確認して、該当ユーザーのデータを取得しようとする。
もし、これが何万人のデータを1から確認していくと、流石のプログラミング様でも時間がかかってしまう。
そこでUsersテーブルのnameカラムにindexを張ることで、アルファベット順にnameを並べ替え検索しやすいようにしてくれる。

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

初心者がWebアプリを公開してから1か月間でやったことまとめ[個人開発]

初めに

僕はちょうど一か月前にWebアプリを公開しました。そこで公開してから1か月間やったこと、立ちはだかった問題などを振り返ってみようと思います。

作ったWebアプリ

code-sell3.png

URL
https://www.code-sell.net/

プログラムコードを販売できるサービスです。公開した直後にqiitaでも宣伝させていただきました。
コードを販売できるサービス「Code-sell」をリリースした!(個人開発)

やったこと

ここからが本題です。どのようなことを、いつ、どうして、やったかを振り返っていきます

1 動作確認

いつ...公開した直後
どうして...開発環境ではうごいても本番環境では動かないということがあるから

まあこれは皆さん普通にやると思います。ただ筆者の場合これが甘すぎました。
アカウント登録と有効化
コード販売
購入機能

このくらいしかしませんでした。おかげで細かいところでエラーの目撃が相次ぎました。
送金機能、検索機能、アカウント編集機能、ページが開けないなど...
こういうのはサービスの信頼にかかわるのでしっかりやることをお勧めします。焦らずに、すべてのページを開いてすべての機能を試したほうがいいです。しかもこれをしっかりしなかったせいで、後で紹介する致命的バグに3,4週間くらい気づけないことになります。

2 初期データ投稿

いつ...動作確認が終わってから
どうして...初期データがないと使ってもらえないから

これはいろんなサイトで言われていますが、だれも投稿していないあやしいサイトを使おうと思いますか?使わないでしょう。
だから初期データを作るか、トップページでは投稿されてるかどうかわからなくしたほうがいいです。

3 めっちゃ宣伝

qiita
dev.to
service safari
つくろぐ
startapp
eggineer
ロケットリリース
note
zenn
crieit

くらいですかね。宣伝できる場所があれば徹底的にやりました。特にqiitaとservice safariとnoteが効果が大きかったと思います。

4 バグ・エラー修正

いつ...qiitaなどで宣伝した直後(本当は動作確認で気づくべき)

がばがば動作確認によって発生したバグ・エラーたちを修正します。

5 機能追加

いつ...バグ・エラー修正が一通り終わったころ

公開してから追加した機能は
技術メモ...廃止済み
運営からのおしらせ...最初から実装しとくべきでした。
フォロー...なくてもいいかも
pv機能...なくてもいいかも

くらいですかね。技術メモは「このままだとオワコンになるかも!」とか言って深夜テンションで作ってしまった黒歴史です。qiitaには投稿できないようなしょっぼいメモを残すというものです。全く使われませんでした。しかもサービスの機能の統一性がなくなるのでSEOも弱くなるかもしれません。
皆さんがwebアプリを作るときは一つのサービスだけをやりましょう。

6 公式ツイッターをつくる

いつ...機能追加してる合間

これは、もっとユーザーが増えてからでもいいかもしれません。宣伝効果もないし、アップデートはお知らせでできるし。

7 致命的バグに気づく

いつ...公開してから3,4週間たってから

どうのようなバグかというと送金先がみんな同じになるというものです。
幸い、お金の取引はまだされていなかったので大丈夫でしたが本当に取り返しのつかなくなるところでした。

なぜここまで発見が遅れたか

  • 基本的な機能過ぎて逆に見直してなかった

  • 管理画面を見てもアカウント数がある程度がないとわかならい

  • testコードを書いていなかった

  • 一つや二つのアカウントだけで動作させているとわかならい(お問い合わせが来なかった理由)

みなさんも動作確認するとき1つや2つのユーザーではなくseedで5~10くらいのテストユーザーを作ってやりましょう。

8 SEO対策や速度改善(現在)

今は機能を追加するというよりSEO対策や速度改善などを中心にしています。トップページのデザインを細かく変えたり、サイトマップを作りGoogle Search Consoleに送ったり、余計なcssを消したりしています。

まとめ

ここ一か月いろいろありました。急にアカウント数が増えたり、バグに気づき冷や汗をかいたりすごい良い体験をして一か月だったなと思います。よかったら開発したアプリ使ってみてください。

https://www.code-sell.net/

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

個人開発でWebアプリを公開してからやった8つのこと

初めに

僕はちょうど一か月前にWebアプリを公開しました。そこで公開してから1か月間やったこと、立ちはだかった問題などを振り返ってみようと思います。

作ったWebアプリ

code-sell3.png

URL
https://www.code-sell.net/

プログラムコードを販売できるサービスです。公開した直後にqiitaでも宣伝させていただきました。
コードを販売できるサービス「Code-sell」をリリースした!(個人開発)

やったこと

ここからが本題です。どのようなことを、いつ、どうして、やったかを振り返っていきます

1 動作確認

いつ...公開した直後
どうして...開発環境ではうごいても本番環境では動かないということがあるから

まあこれは皆さん普通にやると思います。ただ筆者の場合これが甘すぎました。
アカウント登録と有効化
コード販売
購入機能

このくらいしかしませんでした。おかげで細かいところでエラーの目撃が相次ぎました。
送金機能、検索機能、アカウント編集機能、ページが開けないなど...
こういうのはサービスの信頼にかかわるのでしっかりやることをお勧めします。焦らずに、すべてのページを開いてすべての機能を試したほうがいいです。しかもこれをしっかりしなかったせいで、後で紹介する致命的バグに3,4週間くらい気づけないことになります。

2 初期データ投稿

いつ...動作確認が終わってから
どうして...初期データがないと使ってもらえないから

これはいろんなサイトで言われていますが、だれも投稿していないあやしいサイトを使おうと思いますか?使わないでしょう。
だから初期データを作るか、トップページでは投稿されてるかどうかわからなくしたほうがいいです。

3 めっちゃ宣伝

qiita
dev.to
service safari
つくろぐ
startapp
eggineer
ロケットリリース
note
zenn
crieit

くらいですかね。宣伝できる場所があれば徹底的にやりました。特にqiitaとservice safariとnoteが効果が大きかったと思います。

4 バグ・エラー修正

いつ...qiitaなどで宣伝した直後(本当は動作確認で気づくべき)

がばがば動作確認によって発生したバグ・エラーたちを修正します。

5 機能追加

いつ...バグ・エラー修正が一通り終わったころ

公開してから追加した機能は
技術メモ...廃止済み
運営からのおしらせ...最初から実装しとくべきでした。
フォロー...なくてもいいかも
pv機能...なくてもいいかも

くらいですかね。技術メモは「このままだとオワコンになるかも!」とか言って深夜テンションで作ってしまった黒歴史です。qiitaには投稿できないようなしょっぼいメモを残すというものです。全く使われませんでした。しかもサービスの機能の統一性がなくなるのでSEOも弱くなるかもしれません。
皆さんがwebアプリを作るときは一つのサービスだけをやりましょう。

6 公式ツイッターをつくる

いつ...機能追加してる合間

これは、もっとユーザーが増えてからでもいいかもしれません。宣伝効果もないし、アップデートはお知らせでできるし。

7 致命的バグに気づく

いつ...公開してから3,4週間たってから

どうのようなバグかというと送金先がみんな同じになるというものです。
幸い、お金の取引はまだされていなかったので大丈夫でしたが本当に取り返しのつかなくなるところでした。

なぜここまで発見が遅れたか

  • 基本的な機能過ぎて逆に見直してなかった

  • 管理画面を見てもアカウント数がある程度がないとわかならい

  • testコードを書いていなかった

  • 一つや二つのアカウントだけで動作させているとわかならい(お問い合わせが来なかった理由)

みなさんも動作確認するとき1つや2つのユーザーではなくseedで5~10くらいのテストユーザーを作ってやりましょう。

8 SEO対策や速度改善(現在)

今は機能を追加するというよりSEO対策や速度改善などを中心にしています。トップページのデザインを細かく変えたり、サイトマップを作りGoogle Search Consoleに送ったり、余計なcssを消したりしています。

まとめ

今回学んだこと

  • 動作確認は慎重に、seedを利用しテストユーザーを作ってからやる

  • 宣伝、完璧な状態になってからやる

  • 変なノリでいらない機能は作らない。

ここ一か月いろいろありました。急にアカウント数が増えたり、バグに気づき冷や汗をかいたりすごい良い体験をして一か月だったなと思います。よかったら開発したアプリ使ってみてください。

https://www.code-sell.net/

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

[個人開発]Webアプリを公開してからやった8つのこと

初めに

僕はちょうど一か月前にWebアプリを公開しました。そこで公開してから1か月間やったこと、立ちはだかった問題などを振り返ってみようと思います。

作ったWebアプリ

code-sell3.png

URL
https://www.code-sell.net/

プログラムコードを販売できるサービスです。公開した直後にqiitaでも宣伝させていただきました。
コードを販売できるサービス「Code-sell」をリリースした!(個人開発)

やったこと

ここからが本題です。どのようなことを、いつ、どうして、やったかを振り返っていきます

1 動作確認

いつ...公開した直後
どうして...開発環境ではうごいても本番環境では動かないということがあるから

まあこれは皆さん普通にやると思います。ただ筆者の場合これが甘すぎました。
アカウント登録と有効化
コード販売
購入機能

このくらいしかしませんでした。おかげで細かいところでエラーの目撃が相次ぎました。
送金機能、検索機能、アカウント編集機能、ページが開けないなど...
こういうのはサービスの信頼にかかわるのでしっかりやることをお勧めします。焦らずに、すべてのページを開いてすべての機能を試したほうがいいです。しかもこれをしっかりしなかったせいで、後で紹介する致命的バグに3,4週間くらい気づけないことになります。

2 初期データ投稿

いつ...動作確認が終わってから
どうして...初期データがないと使ってもらえないから

これはいろんなサイトで言われていますが、だれも投稿していないあやしいサイトを使おうと思いますか?使わないでしょう。
だから初期データを作るか、トップページでは投稿されてるかどうかわからなくしたほうがいいです。

3 めっちゃ宣伝

qiita
dev.to
service safari
つくろぐ
startapp
eggineer
ロケットリリース
note
zenn
crieit

くらいですかね。宣伝できる場所があれば徹底的にやりました。特にqiitaとservice safariとnoteが効果が大きかったと思います。

4 バグ・エラー修正

いつ...qiitaなどで宣伝した直後(本当は動作確認で気づくべき)

がばがば動作確認によって発生したバグ・エラーたちを修正します。

5 機能追加

いつ...バグ・エラー修正が一通り終わったころ

公開してから追加した機能は
技術メモ...廃止済み
運営からのおしらせ...最初から実装しとくべきでした。
フォロー...なくてもいいかも
pv機能...なくてもいいかも

くらいですかね。技術メモは「このままだとオワコンになるかも!」とか言って深夜テンションで作ってしまった黒歴史です。qiitaには投稿できないようなしょっぼいメモを残すというものです。全く使われませんでした。しかもサービスの機能の統一性がなくなるのでSEOも弱くなるかもしれません。
皆さんがwebアプリを作るときは一つのサービスだけをやりましょう。

6 公式ツイッターをつくる

いつ...機能追加してる合間

これは、もっとユーザーが増えてからでもいいかもしれません。宣伝効果もないし、アップデートはお知らせでできるし。

7 致命的バグに気づく

いつ...公開してから3,4週間たってから

どうのようなバグかというと送金先がみんな同じになるというものです。
幸い、お金の取引はまだされていなかったので大丈夫でしたが本当に取り返しのつかなくなるところでした。

なぜここまで発見が遅れたか

  • 基本的な機能過ぎて逆に見直してなかった

  • 管理画面を見てもアカウント数がある程度がないとわかならい

  • testコードを書いていなかった

  • 一つや二つのアカウントだけで動作させているとわかならい(お問い合わせが来なかった理由)

みなさんも動作確認するとき1つや2つのユーザーではなくseedで5~10くらいのテストユーザーを作ってやりましょう。

8 SEO対策や速度改善(現在)

今は機能を追加するというよりSEO対策や速度改善などを中心にしています。トップページのデザインを細かく変えたり、サイトマップを作りGoogle Search Consoleに送ったり、余計なcssを消したりしています。

参考サイト

まとめ

今回学んだこと

  • 動作確認は慎重に、seedを利用しテストユーザーを作ってからやる

  • 宣伝、完璧な状態になってからやる

  • 変なノリでいらない機能は作らない。

ここ一か月いろいろありました。急にアカウント数が増えたり、バグに気づき冷や汗をかいたりすごい良い体験をして一か月だったなと思います。よかったら開発したアプリ使ってみてください。

https://www.code-sell.net/

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

【Rails】投稿機能に公開・非公開機能を追加した時の実装手順

はじめに

制作しているポートフォリオに記事の公開・非公開機能を追加したので、実装手順を紹介します。
記事を投稿したけど、一旦非公開にしたい時があると思うので、実装してみました。

前提

  • kaminariのgemの機能を使用し、非公開記事一覧取得してます
  • deviseのgemの機能を使用し、他ユーザーを非公開記事からリダイレクトさせてます

バージョン情報

  • Ruby 2.6.3
  • Rails 6.0.2.1

実装した手順

postsテーブルにstatus用のカラムを追加

$ rails g migration Add_status_To_posts status:integur
db/migrate/20201111213454_add_status_to_posts.rb
class AddStatusToPosts < ActiveRecord::Migration[6.0]
  def change
    add_column :posts, :status, :integer, null: false, default: 0
  end
end

モデルを定義

app/models/post.rb
class Post < ApplicationRecord

#・・・省略

  enum status: { public: 0, private: 1 }, _prefix: true

#・・・省略

end

上記のように_prefix: trueを記述してない状態でブラウザを開いたら、下記のエラーが出た。
エラー文を確認すると、publicというメソッドが重複しているとのこと。
重複してエラーが出ていなければ_prefix: trueを記述しなくてもOK。

log/development.log
ArgumentError (You tried to define an enum named "status" on the model "Post", but this will generate a class method "public", which is already defined by Active Record.):

app/models/post.rb:19:in `<class:Post>'
app/models/post.rb:1:in `<main>'
app/controllers/posts_controller.rb:85:in `set_post'
Started GET "/posts/3" for 127.0.0.1 at 2020-11-12 06:52:36 +0900
Cannot render console from 172.22.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1
  [1m[35m (1.2ms)[0m  [1m[35mSET NAMES utf8mb4,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483[0m
Processing by PostsController#show as HTML
  Parameters: {"id"=>"3"}
Completed 500 Internal Server Error in 27ms (ActiveRecord: 0.0ms | Allocations: 5566)

投稿編集ページで投稿ステータスを選択できるようにする

app/views/posts/_form.html.erb
<%= form_with(model: post, local: true) do |form| %>

<!--・・・省略・・・-->

  <%= form.label(:public, for: nil, class:'post-status__label') do %>
      <%= form.radio_button :status, :public %>
      <%= I18n.t('activerecord.attributes.post.statuses.public') %>
  <% end %>
  <%= form.label(:private, for: nil, class:'post-status__label') do %>
      <%= form.radio_button :status, :private %>
      <%= I18n.t('activerecord.attributes.post.statuses.private') %>
  <% end %>

<!--・・・省略・・・-->

<% end %>

公開・非公開を選択するUIは下記のようにしました。
この記事でははその実装手順はメイントピックではないので、割愛。
しかし、この実装に時間かかってしまった。

post_status.gif

選択した投稿ステータスを保存できるようにする

app/controllers/posts_controller.rb
class PostsController < ApplicationController

# ・・・省略

  def create
    @post = Post.new(post_params)
    @post.user_id = current_user.id

    respond_to do |format|
      if @post.save
        format.html { redirect_to @post, notice: '新規投稿を行いました。' }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

# ・・・省略

  private

# ・・・省略

    def post_params
      params.require(:post).permit(
        :title,
        :content,
        :image,
        :status, # <= 追加:statusカラム
        {:cat_ids => []}
      )
    end

# ・・・省略

end


非公開記事一覧、詳細ページは、他のユーザーにアクセス時にはリダイレクトさせる

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  # GET /posts/1
  # GET /posts/1.json
  def show

    if @post.status_private? && @post.user != current_user
      respond_to do |format|
        format.html { redirect_to posts_path, notice: 'このページにはアクセスできません' }
      end
    end

    # ・・・省略
  end

  # ・・・省略

  private
    def set_post
      @post = Post.find(params[:id])
    end

    # ・・・省略

end


記事一覧の取得方法

# 公開記事
$ Post.status_public.order(created_at: :desc).page(params[:page])

# 非公開記事
$ Post.status_private.order(created_at: :desc).page(params[:page])

# ランキング(Likeのトップ3)
$ Post.status_public.joins(:likes).group(:post_id).order('count(likes.post_id) desc').limit(3)


所要時間

作業内容 所要時間
見積 0.75H
実装・検証:新規投稿 7H
実装・検証:ユーザー詳細ページ 2.25H
実装・検証:投稿詳細ページ(localhost/posts/:id) 1H
実装・検証:記事一覧・詳細ページ(Like Ranking) 0.5H
実装・検証:記事一覧(localhost/posts) 0.125H
実装・検証:TOP(localhost) 0.125H
合計 12H

投稿編集ページを作るのに7Hかかってしまったので反省だが、なんとか作れたので良かったかな

さいごに

記事の公開・非公開機能追加の際にこの記事が参考になれば幸いです。

参考

【Rails】enumチュートリアル
Rails5 から enum 使う時は_prefix(接頭辞)_suffix(接尾辞)を使おう

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

ローカルで Rails アプリを production モードで起動する(API サーバー編)

ローカル で Rails を production モードで起動するまでの手順を書きます。

今回は、Rails を API サーバーとして使うのに必要な手順のみです。

環境

  • macOS Catalina 10.15.7
  • Ruby 2.7.2
  • Rails 6.0.3.4
  • MySQL 5.7.32

手順

database.yml の設定

動作確認のため、DB 名以外は development モードで起動していた時と同じ設定にしてしまえば良い。rails new した時のままであれば、だいたいこんな感じになるはず。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

development:
  <<: *default
  database: my_app_development

test:
  <<: *default
  database: my_app_test

production:
  <<: *default
  database: my_app_production

DB セットアップ

% RAILS_ENV=production bundle exec rails db:setup
Created database 'my_app_production'

db:setup だけで db:create, db:migrate, db:seed してくれるはず。

サーバーを production モードで起動する

$ bundle exec rails s -e production

curl などでアクセス確認できれば OK。

動作確認したアプリケーションは sidekiq を入れていたので、 /sidekiq にアクセスしたが、問題なく動いた。

おわりに

API サーバーだけとはいえ、こんなに簡単に動いたっけ?

昔より楽になったのかな。

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

ArgumentError(wrong number of arguments (given 0, expected 1))のエラーメッセージ

はじめに

「rails s」をしたときにdestroyメソッドに関するエラーが出て、どこを直せば良いのかよくわからずに苦労したので、記録として書きます。

エラーメッセージ

persistence.rb:325:in `destroy': wrong number of arguments (given 0, expected 1) (ArgumentError)

原因

モデルの中の記述の仕方に問題がありました。

user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  attachment :profile_image
  has_many :tasks, dependent: destroy
end

解決策

すごく単純ですが、モデルの中のファイルの記述を治してあげれば解決しました。

user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  attachment :profile_image
  has_many :tasks, dependent: :destroy
end

参考記事
https://teratail.com/questions/244096

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

【Rails】Capistranoによる自動デプロイで発生したエラー(fatal: not a valid object name: master)

はじめに

現在プログラミングスクール卒業後、ポートフォリオ作成をしており、
Capistranoを使用した自動デプロイで発生したエラーを備忘録として投稿します。

■開発環境
  • Rails 5.0.7.2
  • ruby 2.5.1
  • AWS EC2
  • Nginx
  • Unicorn
  • capistrano

Capistrano導入について

下記の記事を参考に、導入しました。
導入方法が分からない方は、私と同様に参考にしてみてください。
自動デプロイツール(Capistrano)導入方法

発生したエラーについて

上記の記事を参考にCapistranoを導入し、自動デプロイを実行したところ、途中でエラーが発生しました。

ターミナル(ローカル環境)
# アプリケーションのディレクトリで、下記の自動デプロイコマンドを実行する。
$ bundle exec cap production deploy

自動デプロイコマンド実行後の、エラー内容はこちら。

ターミナル(ローカル環境)
$ bundle exec cap production deploy
[Deprecation Notice] Future versions of Capistrano will not load the Git SCM
plugin by default. To silence this deprecation warning, add the following to
your Capfile after `require "capistrano/deploy"`:

    require "capistrano/scm/git"
    install_plugin Capistrano::SCM::Git

00:00 git:wrapper
      01 mkdir -p /tmp
    ✔ 01 ec2-user@52.193.230.41 0.239s
      Uploading /tmp/git-ssh-smot-production-nakayakouyuu.sh 100.0%
      02 chmod 700 /tmp/git-ssh-smot-production-nakayakouyuu.sh
    ✔ 02 ec2-user@52.193.230.41 0.291s
00:00 git:check
      01 git ls-remote git@github.com:nakaya-kousuke/smot.git HEAD
      01 5e943870f1583d9775b045f3c40d418324d8ad8a       HEAD
    ✔ 01 ec2-user@52.193.230.41 2.098s
00:02 deploy:check:directories
      01 mkdir -p /var/www/smot/shared /var/www/smot/releases
    ✔ 01 ec2-user@52.193.230.41 0.132s
00:03 deploy:check:linked_dirs
      01 mkdir -p /var/www/smot/shared/log /var/www/smot/shared/tmp/pids /var/www/smot/shared/tmp/cache /var/www/smot/shared/tmp/sockets /var/www/smot/sha…
    ✔ 01 ec2-user@52.193.230.41 0.224s
00:03 git:clone
      The repository mirror is at /var/www/smot/repo
00:03 git:update
      01 git remote set-url origin git@github.com:nakaya-kousuke/smot.git
    ✔ 01 ec2-user@52.193.230.41 0.237s
      02 git remote update --prune
      02 Fetching origin
    ✔ 02 ec2-user@52.193.230.41 2.093s
00:06 git:create_release
      01 mkdir -p /var/www/smot/releases/20201103133334
    ✔ 01 ec2-user@52.193.230.41 0.226s
      02 git archive master | /usr/bin/env tar -x -f - -C /var/www/smot/releases/20201103133334
      02 fatal: not a valid object name: master
      02 tar:
      02 これは tar アーカイブではないようです
      02
      02 tar:
      02 前のエラーにより失敗ステータスで終了します
      02
#<Thread:0x00007f9e5507c1f0@/Users/nakaya-kousuke/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.21.0/lib/sshkit/runners/parallel.rb:10 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
        1: from /Users/nakaya-kousuke/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.21.0/lib/sshkit/runners/parallel.rb:11:in `block (2 levels) in execute'
/Users/nakaya-kousuke/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/sshkit-1.21.0/lib/sshkit/runners/parallel.rb:15:in `rescue in block (2 levels) in execute': Exception while executing as ec2-user@52.193.230.41: git exit status: 2 (SSHKit::Runner::ExecuteError)
git stdout: Nothing written
git stderr: fatal: not a valid object name: master
tar: これは tar アーカイブではないようです
tar: 前のエラーにより失敗ステータスで終了します
(Backtrace restricted to imported tasks)
cap aborted!
SSHKit::Runner::ExecuteError: Exception while executing as ec2-user@52.193.230.41: git exit status: 2
git stdout: Nothing written
git stderr: fatal: not a valid object name: master
tar: これは tar アーカイブではないようです
tar: 前のエラーにより失敗ステータスで終了します


Caused by:
SSHKit::Command::Failed: git exit status: 2
git stdout: Nothing written
git stderr: fatal: not a valid object name: master
tar: これは tar アーカイブではないようです
tar: 前のエラーにより失敗ステータスで終了します

Tasks: TOP => git:create_release
(See full trace by running task with --trace)
The deploy has failed with an error: Exception while executing as ec2-user@52.193.230.41: git exit status: 2
git stdout: Nothing written
git stderr: fatal: not a valid object name: master
tar: これは tar アーカイブではないようです
tar: 前のエラーにより失敗ステータスで終了します

上記のエラー内容を確認すると、git:create_releaseの部分でエラーが発生していることが分かります。

ターミナル(ローカル環境)※エラー文のとろこを抜粋
00:06 git:create_release
      01 mkdir -p /var/www/smot/releases/20201103133334
    ✔ 01 ec2-user@52.193.230.41 0.226s
      02 git archive master | /usr/bin/env tar -x -f - -C /var/www/smot/releases/20201103133334
      02 fatal: not a valid object name: master
      02 tar:
      02 これは tar アーカイブではないようです
      02
      02 tar:
      02 前のエラーにより失敗ステータスで終了します
      02

fatal: not a valid object name: master
こちらの内容が、今回のエラーの原因のようですので、
「masterが有効なオブジェクト名ではありません」と言われています。

fatal: not a valid object name: master の解決方法を調べてみる!

fatal: not a valid object name: masterを検索して調べると、下記のような記事がたくさん出てきました。

Git エラー「fatal: Not a valid object name: 'master'.」の対処法

【Git】fatal: Not a valid object name: 'master'.の解決方法

fatal: Not a valid object name: 'master'. て言われたときどうした?

【Git】fatal: Not a valid object name: 'master'って怒られた【大体そんなもん】

こちらの記事を読んでみると、fatal: not a valid object name: masterはGitで発生しているエラーのようです。

ほとんどの記事に「マスターブランチにコミット」するように言われているので、下記のように実行。

ターミナル(ローカル環境)
# コミットしたいファイルを全て選択する
$ git add . 

# masterブランチへコミット
$ git commit -m "fatal: not a valid object name: masterエラー解消のため"

# 自動デプロイコマンドを実行
$ bundle exec cap production deploy

しかし、これでもfatal: not a valid object name: masterエラーは解決できませんでした!

ほかに何が原因か仮説を立ててみる!

Capistranoで、fatal: not a valid object name: master のエラーが出ている記事がひとつも出てこないので、仮説を立ててみました。

「masterが有効なオブジェクト名ではありません」と言われていて、Gitのエラーであることは分かりました。
そもそもGitを使っていて、masterブランチにもpush、commit、pullもできているのになぜ??

エラー文の中にあるこれは tar アーカイブではないようですを調べてみると、下記の記事を見つけました。

capistranoエラーtar:これはtarアーカイブのようには見えません

こちらの記事のベストアンサーに、「gitから存在しないブランチを引っ張っている」と書かれていて、もしかしてmasterブランチがgitに存在していないからエラーが発生しているのか?と仮説を立てて調べてみました。

エラー解決!!

さっそく開発中のGitHubを調べてみると、デフォルトブランチが「master」ではなく、「main」になっていました!!!
git

そのため、Capistranoのgitのブランチを「main」に変更。

config/deploy.rb
# config valid only for current version of Capistrano
# capistranoのバージョンを記載。固定のバージョンを利用し続け、バージョン変更によるトラブルを防止する
lock '3.14.1'

# Capistranoのログの表示に利用する
set :application, 'smot'

# どのリポジトリからアプリをpullするかを指定する
set :repo_url,  'git@github.com:nakaya-kousuke/smot.git'

---------- 追記 ----------
# ブランチを指定する
set :branch, "main"
--------------------------

# バージョンが変わっても共通で参照するディレクトリを指定
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')

set :rbenv_type, :user
set :rbenv_ruby, '2.5.1' #カリキュラム通りに進めた場合、2.5.1か2.3.1です

# どの公開鍵を利用してデプロイするか
set :ssh_options, auth_methods: ['publickey'],
                  keys: ['~/.ssh/smot.pem'] 

# プロセス番号を記載したファイルの場所
set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }

# Unicornの設定ファイルの場所
set :unicorn_config_path, -> { "#{current_path}/config/unicorn.rb" }
set :keep_releases, 5

# secrets.yml用のシンボリックリンクを追加
set :linked_files, %w{ config/secrets.yml }

# 元々記述されていた after 「'deploy:publishing', 'deploy:restart'」以下を削除して、次のように書き換え
after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:restart'
  end

  desc 'upload secrets.yml'
  task :upload do
    on roles(:app) do |host|
      if test "[ ! -d #{shared_path}/config ]"
        execute "mkdir -p #{shared_path}/config"
      end
      upload!('config/secrets.yml', "#{shared_path}/config/secrets.yml")
    end
  end
  before :starting, 'deploy:upload'
  after :finishing, 'deploy:cleanup'
end
ターミナル(ローカル環境)
# 自動デプロイコマンドを実行
$ bundle exec cap production deploy

これで自動デプロイに成功することができました!!!

補足情報

なぜGitHubのデフォルトブランチが「main」になっていたのか???を調べてみました。

■参考記事
GitHub、これから作成するリポジトリのデフォルトブランチ名が「main」に。「master」から「main」へ変更

なんとGitHubのデフォルトブランチが「master」から「main」へ変更になっていました!

この変更には、2020年5月25日に米国ミネソタ州ミネアポリスでの事件をきっかけとした人権運動を背景にしたものとなっています。

このような事件が、IT業界にも影響されることもあると勉強になりました。

ちなみにGitHubの設定によって新規に作成するリポジトリのデフォルトブランチ名は任意に変更可能のようです。

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

Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)のエラー

エラー内容

railsのアプリでデータベースを作成しようとしたところ、

$ bundle exec rake db:create
warning ../../../package.json: No license field
Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
Couldn't create 'hello_rails_development' database. Please check your configuration.
rake aborted!
Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

のエラーが発生。

解決策

mysqlが起動できない(Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2))
https://qiita.com/carotene4035/items/e00076fe3990b9178cc0
の記事を参考にmysqlサーバーを再起動しました。

$ sudo mysql.server restart
Password:
Sorry, try again.
Password:
Sorry, try again.
Password:
sudo: 3 incorrect password attempts

しかし、Passwordが必要と言われ、MySQLのパスワードはとっくに忘れていたので、こちらの記事を参考にMySQLのパスワードを無しにしました。(本当ならrailsアプリのdatabase.ymlにパスワードを記述するべきですが。。。)
Mac ローカル環境の MySQL 8.x のrootパスワードを忘れた時のリセット方法
https://qiita.com/miriwo/items/1880e9d2ebcfd3c0e60d

$ mysql.server stop
$ mysqld_safe --skip-grant-tables &
$ mysql -u root
$ mysql.server status
$ kill [番号]
$ ps aux| grep mysqld
$ kill [番号]
$ mysql.server restart
$ bundle exec rake db:create

で無事にデータベースを作成できました。

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

Rails Tutorial 拡張機能のメッセージ機能を作ってみた(その2):表示する画面を作成

Rails Tutorialの第14章にある、メッセージ機能を作る件の続きです。

前回まででモデルができました。表示する画面を作ります。

DMを表示するViewの仕様を設計

DMを表示する方法を作ります。
tutorialの13.2 「マイクロポストを表示する」を読みます。

MicropostのようにUserの画面に合わせて表示するのではなく、独立したページで表示することにします。Twitterと同様です。

モックアップを作ります。送信者が複数いるので、送信者が表示されているモックアップとして、図 14.5を参考にします。


DM(3)
画像1  Thomas Hobbes Lorem ipsum
sent 1 day ago.
画像2  Sasha Smith Also poor,nasty,
sent 2 days ago.
画像3  John Calvin Excepteur sint
sent 3 days ago.

Previous 1 2 3 next


図 DMページのモックアップ

DMを表示するViewを作成

コントローラとビューを作成するために、コントローラを生成します。

ubuntu:~/environment/sample_app (create-dm) $ rails generate controller Dms

ビューを作ります。リスト13.22と13.24を参考にします。

app/views/dms/show.html.erb
<% provide(:title, @user.name)%>
<div class="row">
  <div class="col-md-8">
    <% if @user.send_dms.any? %>
      <h3>DMs (<%= @user.sent_dms.count %>)</h3>
      <ol class="dms">
        <li id="dm-<%= dm.id %>">
          <%= link_to gravatar_for(dm.sender, size: 50), dm.sender %>
          <span class="user"><%= link_to dm.sender.name, dm.sender%></span>
          <span class="content"><%= dm.content %></span>
          <span class="timestamp">
            Sent <%= time_ago_in_words(dm.created_at) %> ago.
          </span>
        </li>
      </ol>
    <% end %>  
  </div>
</div>

DMを表示するコントローラーを作成

新しいDMのページを表示するためのコントローラーを作ります。
tutorialの「12.1.1 PasswordResetsコントローラ」を読みます。

config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
  get  '/signup',  to: 'users#new'
  post '/signup',  to: 'users#create'
  get  '/login',   to: 'sessions#new'
  post '/login',   to: 'sessions#create'
  delete '/logout', to: 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
    resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
  resources :relationships,       only: [:create, :destroy]
  resources :dms,                 only: [:new, :create, :index, :destroy]
end
HTTPリクエスト URL Action 名前付きルート
GET /dms/new new new_dm_path
POST /dms create dms_path
GET /dms index dms_path
DELETE dms/ destroy dm_path

RESTfulルーティング

tutorialの「10.3.1 ユーザーの一覧ページ」を読みます。
リスト「 10.40: ユーザー一覧ページへのリンクを更新する 」にリンクを追加しているところがありました。同様に追加します。

views/layouts/_header.html.erb
                <ul class="dropdown-menu">
                  <li><%= link_to "Profile", current_user %></li>
                  <li><%= link_to "Settings", edit_user_path(current_user) %></li>
                  <li><%= link_to "DM", dms_path %></li>

画面を表示してリンクがメニューに追加されたことを確かめます。

dm1.png

ログインしていなかったらRedirectするテストは後で作ることにします。

コントローラーはindexなのに、viewはshowなことに気が付きました。ファイルをリネームします。
show.html.erb -> index.html.erb

app/controllers/dms_controller.rb
class DmsController < ApplicationController
  def index
  end
end

画面を試しに表示

rails serverで画面で表示してみます。
エラーになりました。メッセージは
undefined method `name' for nil:NilClass
で、エラーが起きた場所は
<% provide(:title, @user.name)%>
です。@userがnilなのだと考えます。
@userにどこでログインしたユーザーを設定するのか、userのshow画面を参考に見て同様に変更します。

app/controllers/dms_controller.rb
class DmsController < ApplicationController
  def index
      @user = current_user
  end
end

画面で表示してみます。またエラーになりました。

undefined local variable or method `dm' for #<#<Class:0x00005575f57bf2a8>:0x00005575f57deb58>

エラーが起きた場所は

  <li id="dm-<%= dm.id %>">

です。
コントローラーで@dmsにデータを入れる必要があると考えます。
micropostをhome画面に表示するところを参考に見てみます。

コントローラーで

      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])

@feed_itemsにデータを入れています。

ビューでは

  <ol class="microposts">
    <%= render @feed_items%>
  </ol>

@feed_itemsをrenderで一覧表示しています。参考にして変更します。

app/controllers/dms_controller.rb
  def index
      @user = current_user
      @dms = @user.sent_dms
  end

Micropostではfeedとfeed_itemsをうまく使っている13章を参考にするとよさそうです。読み返すなかで

render @user

が何を意味しているのかがあやふやだったので、さかのぼって読み返します。
コントローラーに

app/controllers/dms_controller.rb
  def index
      @user = current_user
      @dms = @user.sent_dms.paginate(page: params[:page])
  end

とすればよいと分かり、その場合viewの 

<span class="user"><%= link_to dm.sender.name, dm.sender%></span>

に何を書けばいいのか考えます。

app/views/dms/index.html.erb
<% provide(:title, @user.name) %>
<h1>DM</h1>

<% if @user.sent_dms.any? %>
    <h3>DMs (<%= @user.sent_dms.count %>)</h3>
    <ol class= "microposts">
      <%= render @dms %>  
    </ol>
    <%= will_paginate @dms %>
<% end %>  
app/views/dms/_dm.html.erb
<li id="dm-<%= dm.id %>">
  <%= link_to gravatar_for(dm.sender, size: 50), dm.sender %>
  <span class="user"><%= link_to dm.sender.name, dm.sender%></span>
  <span class="content"><%= dm.content %></span>
  <span class="timestamp">
    Sent <%= time_ago_in_words(dm.created_at) %> ago.
  </span>
</li>

試しにrails serverで画面を表示してみます。データが少ないため1ページしかありません。
pagnateがされているか確認するために、データを増やします。
contentのテストデータ生成に、Faker::Hipster.sentenceを使ってみます。

db/seeds.rb
# DM
users = User.order(:created_at).take(6)
receiver = users.second
50.times do
  content = Faker::Hipster.sentence
  users.each {|user| user.sent_dms.create!(content: content,
                                           receiver_id: receiver.id) }
end

画面を表示してみます。

dm3.png

receiverが出ていないことに気が付きましたので、senderから変更します。

app/views/dms/_dm.html.erb
<li id="dm-<%= dm.id %>">
  <%= link_to gravatar_for(dm.receiver, size: 50), dm.receiver %>
  <span class="user"><%= link_to dm.receiver.name, dm.receiver%></span>

dm4.png

DM表示のテスト作成

DMを表示する画面のテストを作ります。
tutorialの「13.2.3 プロフィール画面のマイクロポストをテストする」を参考にします。

test/fixtures/dms.yml
...
<% 30.times do |n| %>
dm_<%= n %>
  content: <%= Faker::Hipster.sentence %>
  created_at* <%= 42.days.ago %>
  sender: michael
  receiver: archer
<% end %>
test/integration/dms_test.rb
class DmsTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "dm display" do
    log_in_as(@user)
    get dms_path
    assert_template 'dms/index'
    assert_select 'title', full_title(@user.name)
    assert_match @user.sent_dms.count.to_s, response.body
    assert_select 'div.pagination'
    @user.sent_dms.paginate(page: 1).each do |dm| 
      assert_match CGI.escapeHTML(dm.content), response.body
    end
  end

リスト 13.28を参考に、「'」などの記号が特殊文字で出力されていたので、エスケープする方法をネットで調べて修正しました。
https://rakuda3desu.net/rakudas-rails-tutorial14-3/

コントローラーのアクセス制御のテスト作成

controllerのテストを作ります。
tutorialの「13.3.1 マイクロポストのアクセス制御」を読みます。

test/controllers/dms_controller_test.rb
  test "should redirect index when not logged in" do
    get dms_path
    assert_redirected_to login_url
  end  

REDです。コントローラーにindexアクションに対するアクセス制限を追加します。

app/controllers/dms_controller.rb
class DmsController < ApplicationController
  before_action :logged_in_user, only: [:index]

テストがGREENになりました。

所要時間

11/7から11/14までの7.0時間です。

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

Rails <%= 式 %> 中身がない時にエラーが起こらない理由

この記事について

初心者が初心者に対して記述した初心者のRails記事

ずっと何となく疑問に思っていたこと。

controllerでインスタンス変数に何かしらの値を入れたつもりだったが実は中身が空(nil)やって、その変数をviewに渡しても何のエラーも起こらないことが謎やった。

なぜエラーが起こらないのか?

理由は<%= 式 %>が出力されるときは、式に対してto_sメソッドが呼び出されているから

つまり、結果の出力はいつも自動的に<%= 式.to_s %>となっている。このメソッドが使える理由はRubyのオブジェクトは全てto_sメソッドを持ち合わせているからやねんな。

中身が入っていないインスタンスに対してto_sメソッドを呼びだす(nil.to_s)と、空文字を出力するようになっている。やからエラーが出さず、そしてそのまま何も表示されないんやな。

#controller
@name = nil

#view
<p><%= @name.to_s %>さん</p>

#出力
#=> さん

まとめ

学びたての時はエラーが出ないから気付き辛いけど、「裏側ではこうなってるんだよー」って分かるとどんな問題が起こってるか発見しやすくなりそう。
やから、このままちゃんと学習を続けていこうと改めて思えた。

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

Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)の解決まで。

原因:

helokuのMysqlとデプロイ使用しているデータベースが紐付けられていなかったため。

確認したこと

heroku info

実行後、データベースの環境変数を確認

CLEARDB_DATABASE_URL:     mysql://xxxxxxxxxxxx
CLEARDB_DATABASE_GREEN:   mysql://xxxxxxxxxxxx
LANG:                     en_US.UTF-8
RACK_ENV:                 production
RAILS_ENV:                production
RAILS_LOG_TO_STDOUT:      enabled
RAILS_SERVE_STATIC_FILES: enabled
SECRET_KEY_BASE:

あるはずのDATABASE_URL:がないし、mysql2でデータベース作ったはずなのに反映されてない・・・。

結論:

これは、ターミナルで

heroku config:set DATABASE_URL=mysql2${heroku_cleardb:5}

の入力がなく、mysql2のデータベースに環境変数を格納できていなかったため。

最後に:

環境変数を変更するため、herokuのHPのアプリのページへ遷移し、settingsをクリック。

画面中央のReveal Config Varsをクリック。Config Varsを編集して終了。

無事、

heroku run rake db:migrate

出来ました!

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

Railsにて検索機能を追加する。

概要

アプリケーション内でデータを検索できる機能があると、ユーザーがデータを探す際便利である。検索機能はSNS等でもよくある機能なので、実際に検索機能を実装してみる。
userというテーブルがあるとした場合、userを検索する機能を実装する。

1.ルーティングの設定

userを扱うUserModel、UsersControllerは作成されていることを前提とします。

/config/routes.rb
# 省略
  resources :users do
    get "search", on: :collection
  end

searchアクションはリソースの集合を表すためon: :collectionを追加します。

2.モデルにクラスメソッドsearchを追加

/app/model/user.rb
class User < ApplicationRecord
  class << self
    def search(query)
      rel = order("id")
      if query.present?
        rel = rel.where("カラム名 LIKE ?, "%#{query}%")
      end
      rel
    end
  end
end

class << self〜endでクラスメソッドを定義できます。
ローカル変数relを定義し、検索ワードが空でなければSQLのLIKEを使用し、該当カラムから対象のレコードを絞り込みます。

3.コントローラにsearchアクションを追加

app/controllers/users_controller.rb
# 省略
def search
  @users = User.search(params[:q])
  render "index"
end

Userモデルで定義したクラスメソッドsearchをここで使用します。

4.viewにフォームを記載

app/views/users/index.html.erb
# 省略
<%= form_tag :search_users, method: :get, class: "search" do %>
  <%= text_field_tag "q", params[:q] %>
  <%= submit_tag "検索" %>
<% end %>

form_tagは引数にパスを指定したフォームを作成します。デフォルトのmethodがPOSTであるため、getメソッドを指定しています。
text_field_tagでフォームを作り、検索ワード"q"は検索後もフォーム内に残しておくために第2引数にparams[:q]を指定しています。

まとめ

以上で、検索機能を実装できます。
SQLを扱う箇所もありましたので、これを機会に勉強していきたいです。

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

【rails 初心者向け】複数Deviseモデルのログイン後の遷移先指定

管理者(admin)、顧客側(customer/user等)などを作成した際に、それぞれのログイン後のリダイレクト先を指定したい

前提

・devise使用モデルとしてadminモデルとcustomerモデルがある(モデル名は置き換えて考えてください)

実装

application_controller.erb
class ApplicationController < ActionController::Base

  def after_sign_in_path_for(resource)
    case resource
    when Admin
      admin_top_path          #pathは設定したい遷移先へのpathを指定してください
    when Customer
      root_path              #ここもpathはご自由に変更してください
    end
  end

end

記述してみると簡単で、すぐ覚えられそうですね。
記述内容について、少し解説してみます!
after_sign_in_path_for(resource)の引数(resource)の情報で条件分岐します。
・resouseインスタンスの中身が、AdminなのかCustomerなのかでcase文で処理をわけています。
ここまで調べるとresourceってどこから来てて、なんでこんな命名なんだ?@userとかでもいいんじゃないか、、とか考えてしまったので調べました。(以下は興味ある方のみ見てみてください)

おまけ

registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: new_customer_registration_path(resource_name)) do |f| %>
#以下省略

上記のコードはDevise使用時に自動生成してくれるViewページの1つですね。
少し調べてみました。↓

・Deviseは、複数のModelに対する認証を同時に扱えるフレームワークなので、どのようなModelのインスタンスが来ても大丈夫なように定義されているらしいです。
Deviseの内部では、それをresourceという名前で参照できるように統一しているようですね。
同様に、どのModelを認証しようとしているかの情報は、resource_nameという名前で参照できるようです。
・urlに(resource_name)が入っていますが、new_customer_registration_pathだけでは、どのModelの登録か分からないため(resource_name)が必要になるようです。

少し物足りない説明になってしまったかもしれませんが、、普段便利すぎて何も考えずに実装してしまっていた部分についてある程度深堀り出来るいい機会になりました。
プログラミング経験数か月の新米ですので、おかしな部分等はご指摘お願いします、、!

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