20190827のRailsに関する記事は24件です。

Rails6でブログのタグのacts-as-taggable-onがエラーになったので別のタグgem(gutentag tags)を使う

Rails6にアップデートした時の問題

Rails6になってから、今まで使っていた acts-as-taggable-on がエラーになるようになった。

原因は

まあ悪い意味での原因というわけではなく

https://github.com/rails/rails/pull/35933

この部分で set_attribute_was が消されたため発生したもの

また、これの対応のブランチもあるのだが、初歩的なバグを含むため、ブランチ切り替えの対応してもあまり意味がない。

とはいえ、ライブラリへのPRも全然反映されないし、Gemfileが汚れるのも嫌なので別のやつを実験的に使ってみた。

https://github.com/pat/gutentag

というやつで、スターは315の、RubyGemダウンロード数は54,474というところなので、act-as-taggable-onのわずか6%程度のスター数となり、多少心もとない。

開発当初のブログがあるので、読んでみた

個人的には、act-as-taggable-onしか使ったことなかったので、とりあえず調べていたら、どうも2013年頃に開発されたようだ。

https://freelancing-gods.com/2013/07/11/gutentag-simple-rails-tagging.html

https://github.com/pat/gutentag/blob/master/lib/gutentag/persistence.rb

永続化周りをforwardableを使って委譲して実装してある

使ってみた

環境

  • Rails 6.0.0
  • ruby 2.6.3

Gemfile

記載したら、bundle install する

gem 'gutentag', '~> 2.5'

インストール設定

ここは、よくあるお決まりのやつをただ打っていくだけ
migrateすることで、gutentagにより作られたマイグレーションファイルが実行され、使えるようになる
使う側のモデルにカラムを追加することなく使えるところが楽で良い

bundle exec rake gutentag:install:migrations
rails db:migrate

Schema例

create_table "blogs", force: :cascade do |t|
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

Blog.rb

今回、ブログにタグ機能をもたせたいので、
ここにGutentag::ActiveRecord.call selfを置く

class Blog < ApplicationRecord
  Gutentag::ActiveRecord.call self
  include TagExtensions
end

FactoryBot.define do
  factory :blog do
    tags_as_string { "hoge,fuga,piyo" }
  end
end

TagExtensions.rb

これは、いわゆるattr_accessorの役割をincludeして使うために別途作っただけ
メソッド毎モデルに直で書いても良いが、他のモデルでも使いたかったのでこのようにサクッと作っただけ。(若干気に入らないので後で修正する気がする)

module TagExtensions
  def tags_as_string
    tag_names.join(', ')
  end

  def tags_as_string=(string)
    self.tag_names = string.split(/,\s*/)
  end
end

作成時

View

一般的なinputタグに先程TagExtensionsで作ったgetterメソッドを配置しておく

  .form
    = f.label :tags_as_string
    = f.text_field :tags_as_string, value: @blog.tags_as_string

Controller#create

Scaffoldして、必要なところだけ抜粋しておく
フォームから送られて新規作成するところの処理と、Strong Parametersの設置

class BlogsController < ApplicationController
  def create
    @blog = Blog.new(blog_params)

    respond_to do |format|
      if @blog.save
        format.html { redirect_to blog_url(@blog), notice: 'Blog was successfully created.' }
        format.json { render :show, status: :created, location: @blog }
      else
        format.html { render :new }
        format.json { render json: @blog.errors, status: :unprocessable_entity }
      end
    end
  end

  def blog_params
    params.require(:blog).permit(:tags_as_string)
  end
end

一覧表示時

View

tag_namesなどで引っ張ってこれる

- @blogs.each do |blog|
  - blog.tag_names.each do |tag|
    = tag

Controller#index

例えばincludesなどをしないとN+1で死ぬので注意

class BlogsController < ApplicationController
  def index
    @blogs = Blog.includes(:tags)
  end
end

挙動確認

実際にテストなどで挙動見てみるとこんなように使える

blog = Blog.create! # ブログ用レコード作る

blog.tag_names << 'hogehoge'
blog.tag_names #=> ['hoge']
blog.tag_names << 'fuga' << 'piyo'
blog.tag_names #=> ['hoge', 'fuga', 'piyo']
blog.tag_names -= ['fuga']
blog.tag_names #=> ['hoge', 'piyo']

blog.save # 保存する

感想

gutentag自体は、rails6にすぐ対応したcommitがあるし、issueのclose率も100%なのでけっこういいかなと思った。懸念点はpatという創造者しかほぼ保守していないところ。
acts-as-taggable-onは、使ってる人が多いので、また修正版が上がりそうなので、そのあたり様子見

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

HerokuのDB更新時のActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR: column ... does not existの対処方

PG::UndefinedColumn: ERROR

ローカル環境に置けるrails db:migrateはエラーが起きなかったが、
本番環境(Heroku)に heroku run rails db:migrateをすると出るエラー

今回起きた要因

これが発生した要因として
今までは普通にできたが、不要なカラができてしまったので削除する。
そしてHerokuも同様に反映させようとしたが今回のエラーが発生。
ゆえに今回はカラム削除のDBファイルがからんでいると推測。

エラー内容 &対処法

PG::UndefinedColumn: ERROR: column "string" for relation "users" does not exist

とでていた。
そこでusersテーブルの削除したファイルを見てみると

..._remove_delete_to_users
 class RemoveDelete2ToUsers < ActiveRecord::Migration[5.1] 
   def change 
     remove_column :users, :job, :string 
     remove_column :users, :string, :string 
     remove_column :users, :basic_overtime_pay, :string 
     remove_column :users, :integer, :string 
     remove_column :users, :uid, :string 
     remove_column :users, :string, :string 
     remove_column :users, :image, :string 
     remove_column :users, :string, :string 
     remove_column :users, :encrypted_password, :string 
     remove_column :users, :string, :string 
     remove_column :users, :reset_password_token, :string 
     remove_column :users, :string, :string 
     remove_column :users, :note, :string 
     remove_column :users, :string, :string 
   end 
 end 

このようにカラムの型がたくさんある状態になっている。
そこでここを整理整頓してみる。

..._remove_delete_to_users
 class RemoveDelete2ToUsers < ActiveRecord::Migration[5.1] 
   def change 
     remove_column :users, :job, :string 
     remove_column :users, :basic_overtime_pay, :integer 
     remove_column :users, :uid, :string 
     remove_column :users, :image, :string
     remove_column :users, :encrypted_password, :string 
     remove_column :users, :reset_password_token, :string
     remove_column :users, :note, :string 
   end 
 end 

ここで改めて heroku run rails db:migrateをすると
通った!!!
ロールバックするさいもカラムと型が合わずにうまくいかない!
みたいなことがあるみたいなのです。
今回のように同じエラーがでてるかたは参考にしてみていただけると幸いです。

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

HTTPリクエストについて

HTTPリクエストについて

1.get

get = 通常のアクセス。ページを表示。

2.post

post = フォームを使ってデータを送信。
・何か新しいデータを追加するとき
・ログインの処理をするときにデータを送る場合

3.patch / put

patch / put = データの更新を行う。
※若干ニュアンスが違う。
patch => 現在保存されているデータがあって、そのデータを更新したい場合
put => 現在データがあるかどうかわからない状態でput形式でのデータを送る。もしデータが入っていなかったら「新規作成」を行い、もしデータが入っていたらそのデータに「上書き保存」をする。

4.delete

delete = データの削除を行う。

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

rails viewの繰り返しでitemの一覧を表示する方法

railsのviewでアイテムをfor文を使って表示しています。

<%for i in 0..4 do%>
    <li><%=link_to @ranking_articles[i].title,article_path(@ranking_articles[i].id)%><span>(<%=@previews[i]%>)</span></li>
<%end%>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの勉強会に行ってきた話

こんにちは。はじめての投稿になるので、よろしくお願いします。

感想

とても楽しい時間を過ごすことができました。
技術的な話としては、MVC構造は1960年代?ごろから使われていたものらしく、結構古いもののようで、それをRailsは未だに使っていて大丈夫なの?っていうのが話題にでたときに、Railsが批判されてる理由が少しわかった気がしました。

自由時間に現役のエンジニアの方からアドバイスをもらうことができて、とても参考になりました。きつい時もあるけど、楽しくコードを書いていこうというのが励みになりました。勉強会の後に、数人で食事に行き、業界のことなどいろいろ話ができて楽しかったです。ちなみに奢ってもらっちゃいました笑

Ruby界隈は、親切な人が多いなと思いましたし、いろんな所でイベントがあり、活発なのでまた行ってみたいと思いました。

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

プログラミング学習(三日目)ProgateでRuby On Rails レッスン3

今日学んだこと

find_byメソッドで、データベースからデータを取得

モデル名.find_by(カラム名:値)で、指定したカラムの値を持つレコードを取得できる
例えば、
>a = Post.find_by(id:3)
で、変数aにpostsテーブルのidが3のレコードを代入できる。
そしてここから、
>a.カラム名
とすれば、idが3の指定したカラム名の値を出力できる。

ルーティングのURLに:idで、任意のすべての値を適用

具体的に言うと、

routes.rb
get "/home/:id" => "home#about"

とすると、URLが「/home/a」でも、「/home/xyz」でも、「/home/1234567890」でも、
homeコントローラのaboutアクションを指定されると言う訳。
ちなみに、ルーティングはファイルの上から順に該当するURLを探すので、:idを使う場合は下の方にやらないとやばい。

paramsの使い方

そして上のURLの:idに該当する値は、コントローラ内のアクション内で取得できる。
paramsにハッシュで入っているらしく、params[:id]とやると取得できる。

入力フォームを作成し、データベースに保存する方法。

これが少し複雑で難しかった。
「form_tag」と「redirect_to」と「post」を使う

まずhtml.erbファイルに入力フォームを作るのだが、
<%= form_tag("送信先のURL") do %>
<textarea name="test"></textarea>
<input type="submit" value="送信">
<% end %>

と作る。form_tagで送信先のURLを設定しつつ、入力内容の属性をname属性とし{test:"入力した内容"}というハッシュで、指定したURLに対応するコントローラのアクションへと送られる。
ちなみに、textareaにname属性を付与しないと送信できないらしい。

そしてそのアクションへ入力内容が送られるわけだが、まずそこでここではpostsテーブルを用いているので、Postインスタンスを作成し保存する。その際にparamsを使い、任意のカラムが入力された内容になるインスタンスを作成する。
こんな感じ(postsテーブルにはcontentカラムがある)
**def create**
** @inputfile = Post.new(content:params[:test])**
** @inputfile.save**
** redirect_to("送信先のURL")**
**end**

ちなみに「redirect_to」とは、アクションに対応するビューファイルが無い時にエラーにならないために、処理?データ?をほかのアクションへ送るメソッドである。

データの並べ替え

orderメソッドを用いて、「order(カラム名:並び順)」と指定するとその指定した並び順になる。昇順が「:asc」、降順が「:desc」
@posts=Post.new.order(id: :asc)とすると、idが1から並ぶ。

結構抽象的に書いた分、早く書けたが少しわかりにくい可能性が、、、

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

#Ruby #Rails で 日付同士の差分が 何週間ぐらいかを 計算しようとしてみた記録 ( ActionView::Helpers::DateHelper が使えない ))

Ruby #Rails record to calculate how many weeks the difference between dates is (ActionView :: Helpers :: DateHelper cannot be used))

ActionView::Helpers::DateHelper が週に対応してないようなので

require 'action_view'
require 'action_view/helpers'

include ActionView::Helpers::DateHelper

distance = if some_date.between?(1.weeks.after, 1.month.after)
             distance_weeks = (some_date - Date.current).to_i / 7
             "#{distance_weeks}週間"
            else
              distance_of_time_in_words(Time.zone.now, some_date)
            end

https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2356

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

#Rails の ActionView::Helpers を直接 include して i18n チックに xヶ月 x日 x年 みたいなのを表示する例

Rails' ActionView :: Helpers is included directly to display i18n ticks like x month x day x year

rails console で動作確認した

ja.yml

なぜかトップレベルに設定する必要がありそう?

ja:
  about_x_hours:
    one: 約1時間
    other: 約%{count}時間
  about_x_months:
    one: 約1ヶ月
    other: 約%{count}ヶ月
  about_x_years:
    one: 約1年
    other: 約%{count}年
  almost_x_years:
    one: 1年弱
    other: "%{count}年弱"
  half_a_minute: 30秒前後
  less_than_x_seconds:
    one: 1秒以内
    other: "%{count}秒未満"
  less_than_x_minutes:
    one: 1分以内
    other: "%{count}分未満"
  over_x_years:
    one: 1年以上
    other: "%{count}年以上"
  x_seconds:
    one: 1秒
    other: "%{count}秒"
  x_minutes:
    one: 1分
    other: "%{count}分"
  x_days:
    one: 1日
    other: "%{count}日"
  x_months:
    one: 1ヶ月
    other: "%{count}ヶ月"
  x_years:
    one: 1年
    other: "%{count}年"

rails console

require 'action_view'
require 'action_view/helpers'

include ActionView::Helpers

distance_of_time_in_words(Time.zone.now, 3.months.after)
# => "3ヶ月"

Ref

https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml

ja.yml 設定例

https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2355

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

Rails & Nuxt.jsのDocker環境をalpineイメージで構築

Ruby on Rails、Nuxt.js、MySQLのDocker環境を作成します。
Rails、Nuxtのalpine環境の構築手順はそれぞれだと多くあるですが、まとまったものがあまり ない 見つからなかったので、復習を兼ねてポストを作成します。

準備

ディレクトリ作成

作業ディレクトリは任意です。

$ NEW_APP=rails-nuxt-app #任意のアプリ名
$ mkdir ${NEW_APP}
$ cd ${NEW_APP}
$ mkdir ./backend ./frontend

backend はRails用、frontend はNuxt用のディレクトリです。
まずは下記のファイルを修正していきます。

.
├ backend
│   ├ Dockerfile
│   ├ Gemfile
│   └ Gemfile.lock
│
├ frontend
│   └ Dockerfile
│
├ docker-compose.yml
└ .env

docker-compose.yml

.env

docker-compose.ymlで参照する環境変数を記載します。
ここではMySQLのrootパスワード、RailsおよびNuxt環境のホスト、ポート番号のみ定義します。
RailsとNuxtは、共にデフォルトのポートが 3000 番なので、後の利便性のためにいずれかを変えておきます。
(本記事ではNuxt側を 8080 に変更)

MYSQL_ROOT_PASSWORD=password

BACKEND_HOST=0.0.0.0
BACKEND_PORT=3000

FRONTEND_HOST=0.0.0.0
FRONTEND_PORT=8080

./docker-compose.yml

.envで定義した変数を参照しています。
docker-compose.ymlの environment、 Dockerfileの ENV で同じ環境変数が定義されていた場合は、前者が使用されます。
(本記事ではDockerfile単体でもイメージ作成できるように環境変数の記載を残していますが、docker-compose.ymlに定義されていれば問題ありません)

下記はDockerボリュームを作成します。

  • mysqlのdatadir
  • rubyのgem_home
  • nodeのnode_modules
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7.27
    restart: always
    volumes:
      - db-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
  backend:
    build: ./backend
    ports:
      - ${BACKEND_PORT}:3000
    command: /bin/sh -c "rm -f /app/tmp/pids/server.pid && bundle exec rails s -b ${BACKEND_HOST}"
    volumes:
      - ./backend:/app
      - backend-bundle:/usr/local/bundle
    environment:
      - HOST=${BACKEND_HOST}
      - PORT=${BACKEND_PORT}
    depends_on:
      - db
    tty: true
    stdin_open: true
  frontend:
    build: ./frontend
    ports:
      - ${FRONTEND_PORT}:8080
    command: /bin/sh -c "yarn dev"
    volumes:
      - ./frontend:/app
      - frontend-node_modules:/app/node_modules
    environment:
      - HOST=${FRONTEND_HOST}
      - PORT=${FRONTEND_PORT}
    tty: true
volumes:
  db-data:
  backend-bundle:
  frontend-node_modules:

backend

Rails環境構築のためのファイルを準備します。

./backend/Gemfile

Railsのバージョンのみ指定しておきます。

Gemfile
source 'https://rubygems.org'

gem 'rails', '5.2.3'

./backend/Gemfile.lock

空ファイルをtouchしておけばOKです。

./backend/Dockerfile

Alpine Linuxのパッケージは最低限のものだけインストールします。
開発を進めるうちにgemのインストールで依存エラーが発生した場合には、不足パッケージを都度追加しましょう。

Dockerfile
FROM ruby:2.6.3-alpine

ENV RUNTIME_PACKAGES "mysql-client mysql-dev tzdata nodejs"
ENV DEV_PACKAGES "build-base curl-dev"
ENV APP_HOME /app
ENV TZ Asia/Tokyo

ENV HOST 0.0.0.0
ENV PORT 3000

WORKDIR ${APP_HOME}
ADD Gemfile ${APP_HOME}/Gemfile
ADD Gemfile.lock ${APP_HOME}/Gemfile.lock

RUN apk update \
    && apk upgrade \
    && apk add --update --no-cache ${RUNTIME_PACKAGES} \
    && apk add --update --no-cache --virtual=.build-dependencies ${DEV_PACKAGES} \
    && bundle install -j4 \
    && rm -rf /usr/local/bundle/cache/*.gem \
        && find /usr/local/bundle/gems/ -name "*.c" -delete \
        && find /usr/local/bundle/gems/ -name "*.o" -delete \
    && apk del --purge .build-dependencies \
    && rm -rf /var/cache/apk/*

COPY . ${APP_HOME}

EXPOSE ${PORT}

CMD ["rails", "server", "-b", ${HOST}]

frontend

./frontend/Dockerfile

Dockerfile
FROM node:12.9.0-alpine

ENV APP_HOME /app
ENV PATH ${APP_HOME}/node_modules/.bin:$PATH
ENV TZ Asia/Tokyo

ENV HOST 0.0.0.0
ENV PORT 8080

WORKDIR ${APP_HOME}
ADD . ${APP_HOME}

RUN apk update \
    && apk upgrade \
    && yarn install \
    && rm -rf /var/cache/apk/*

EXPOSE ${PORT}

CMD ["yarn", "dev"]

アプリケーション作成

Rails、Nuxt環境にプロジェクトを作成します。
docker-compose run を実行したタイミングで、それぞれのDockerイメージがbuildされ、さらにコンテナが立ち上がります。
--no-deps ... docker-compose.ymlで depends_on or links 指定するサービスは起動しない。
--rm ... 処理を終えたコンテナを自動的に削除。

backend

アプリケーション作成

rails new でRailsアプリケーションを作成します。
--api オプションでAPIモードにしていますが、不要な方は外してください。

$ docker-compose run --no-deps --rm backend rails new . --force --api --database=mysql --skip-bundle

DB接続のため、下記のファイルを修正します。

.
└ backend
    ├ config
    │  └ database.yml
    ├ Gemfile
    └ .env

./backend/.env

docker-compose.ymlで参照している MYSQL_ROOT_PASSWORD と同じもの設定します。

MYSQL_ROOT_PASSWORD=password

./backend/Gemfile

.envから環境変数を読み込むdotenv-railsというgemを追加します。

Gemfile
~~省略~~

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]

  gem "dotenv-rails" #追加
end

~~省略~~

./backend/config/database.yml

DBへのアクセスに使用するパスワードを、環境変数から取得します。

database.yml
~~省略~~

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: <%= ENV.fetch("MYSQL_ROOT_PASSWORD") { '' } %> #環境変数を参照するように修正
  host: db #localhostからdocker-compose.ymlのサービス名に修正

~~省略~~

frontend

アプリケーション作成

npx でNuxtアプリケーションを作成します。

後から追加/変更できるので、このタイミングではEnterキー連打でOKです。

$ docker-compose run --rm frontend npx create-nuxt-app .

~~省略~~

create-nuxt-app v2.10.0
✨  Generating Nuxt.js project in .
? Project name #アプリ名 <Enter>
? Project description #任意 <Enter>
? Author name #任意 <Enter>
? Choose the package manager #Yarn <Enter>
? Choose UI framework #None <Enter>
? Choose custom server framework #None <Enter>
? Choose Nuxt.js modules #(Nothing) <Enter>
? Choose linting tools #(Nothing) <Enter>
? Choose test framework #None <Enter>
? Choose rendering mode #Universal (SSR) <Enter>
? Choose development tools #(Nothing) <Enter>

Dockerイメージ作成

アプリケーション作成時にできたDockerボリュームは削除しておきます。

$ docker-compose down --volume
# もしくは docker volume rm ボリューム名

各ファイルを修正した状態で、Dockerイメージをビルドします。

$ docker-compose build

docker-compose.ymlで build を指定しているbackendとfrontendのイメージが作成されたことを確認します。

$ docker images --format "{{.Repository}}\t{{.CreatedSince}}" ${NEW_APP}*
rails-nuxt-app_frontend     About a minutes ago
rails-nuxt-app_backend      About a minutes ago

Hello World

最後にDockerコンテナを起動し、Rails、NuxtアプリケーションのHelloWorldを確認します、

docker-compose.ymlで定義したサービスを -d オプション(デタッチモード)でバックグラウンド起動します。

$ docker-compose up -d

プロセスが立ち上がっていることを確認します。

$ docker-compose ps
        Name                       Command               State           Ports         
---------------------------------------------------------------------------------------
rails-nuxt-app_backend_1    /bin/sh -c rm -f /app/tmp/ ...   Up      0.0.0.0:3000->3000/tcp
rails-nuxt-app_db_1         docker-entrypoint.sh mysqld      Up      3306/tcp, 33060/tcp   
rails-nuxt-app_frontend_1   docker-entrypoint.sh /bin/ ...   Up      0.0.0.0:8080->8080/tcp

失敗している場合は docker-compose logs などで原因を探りましょう。

backend

RailsアプリケーションのDBを作成します。

$ docker-compose exec backend rails db:create
Created database 'app_development'
Created database 'app_test'

ブラウザで http://localhost:3000/ を開きます。
rails-helloworld.png

frontend

ブラウザで http://localhost:8080/ を開きます。
nuxt-helloworld.png


お疲れさまでした。
次回は GraphQL の導入について投稿したいと思います。

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

【Rails jquery】jbuilderを使ってインクリメンタルサーチを実装する

前置き

映画情報が保存されたproductテーブルをインクリメンタルサーチする。
映画情報は次の画像のような形式で保存されているとする。
image.png

インクリメンタルサーチ実装のステップ

  1. ルーティングなどAPI側の準備をする
  2. テキストフィールドを作成する
  3. テキストフィールドに入力されるたびにイベントが発火するようにする
  4. イベント時に非同期通信できるようにする
  5. 非同期通信の結果を得て、HTMLを作成する
  6. エラー時の処理を行う

まずjbuilderについて

入力データをJSON形式で出力するテンプレートエンジン。gemfileにデフォルトで記述されているgem。
コントローラ内でrender json:と書かなくてもよい。

jbuilderはview同様に、該当するアクションと同じ命名規則を用いる。

ex. view/comment/create.html.erb とある場合。
commentのcreateアクションに対応するjbuilderのファイルになるので
→views/comments/create.json.jbuilder とする。

1. ルーティングなどAPI側の準備をする

respond_toを利用してフォーマット毎に処理を分ける

app/controlers/products_controller.rb
#省略
 def search
   @products = Product.where('title LIKE(?)', "%#{params[:keyword]}%").limit(20)
   respond_to do |format|
     format.html
     format.json
   end
 end

app/viws/products配下に「search.json.jbuilder」というファイルを作成する。

jbuilderファイルを作成したら次の通り編集する。

search.json.jbuilder
##配列の形でjsonを取得できる
# # [{"id":1, "tile":"オオカミ少女と黒王子",  "image":"http://image~.jpeg", "detail": "八田鮎子の同名人気コミック~"}]

json.array! @products do |product|
  json.id product.id
  json.title product.title
  json.image product.image_url
  json.detail product.detail
end

2. テキストフィールドを作成する

todo: 検索フォームのスクショ

search.html.erb
#省略
<div class="container">
              <header class="header header--section">
                <h2 class="text-middle">
                  <i class="icon-movie color-gray-light"></i>投稿する作品を検索
                </h2>
              </header>
              <%= form_tag('/products/search', method: :get) do %>
                <input class="search__query" name="keyword" placeholder="作品名で検索" type="text" value=""><button class="btn-search" title="検索" type="submit"><i class="icon-search"></i></button></input>
              <% end %>
              <form accept-charset="utf-8" action="/" class="js-search-submit" method="get">
              </form>
            </div>

3. テキストフィールドに入力されるたびにイベントが発火するようにする

テキストフィールドに文字が入力されるたびにイベントが呼び出されるようにする。
文字打ち込み終わった場合、つまりキーを離したら処理実行させるためkeyupメソッドを使用する。

app/assets/javascripts/search.js
$(function() {
  $(".search__query").on("keyup", function() {
    var input = $(".search__query").val();
    console.log(input);
  });
});


検索フォームのテキストフィールドのclass名はsearch__queryとなっている。

よって、「class="search__query”の部分のテキストフィールドがkeyupしたら、テキストフィールドの文字を取得して変数inputに代入する」処理となる。

search.html.erb
//省略
<%= form_tag('/products/search', method: :get) do %>
  <input class="search__query" name="keyword" placeholder="作品名で検索" type="text" value=""><button class="btn-search" title="検索" type="submit"><i class="icon-search"></i></button></input>
<% end %>

4. イベント時に非同期通信できるようにする

キーが入力される度に非同期通信で映画タイトルを検索できるようにする。
1で設定したルーティングにアクセスし、コントローラで該当する映画タイトルを検索できるようにする。

Ajax通信を実現するため、$.ajaxメソッドを使う。
HTTPメソッドはGETで、/products/searchのURLに{ keyword: input }を送信する。サーバから値を返す際は、jsonになる

app/assets/javascripts/search.js
(function() {
  $(".search__query").on("keyup", function() {
    var input = $(".search__query").val();

    $.ajax({
      type: 'GET',
      url: '/products/search',
      data: { keyword: input },
      dataType: 'json'
    })
  });
});

前述のapp/controlers/products_controller.rbを確認しておく。

app/controlers/products_controller.rb
def search
   @products = Product.where('title LIKE(?)', "%#{params[:keyword]}%").limit(20)
   respond_to do |format|
     format.html
     format.json
   end
 end

$.ajaxのdataTypeでjsonを指定しているので、サーバはjson形式で値を返却する。

app/views/products/search.html.erbが読まれるが、json形式の場合は、app/views/products/search.json.jbuilderが読まれることになる。

処理成功の場合は該当する映画情報がjbuilderによってJSONに変換され、javascriptのファイルに返される。

5. 非同期通信の結果を得て、HTMLを作成する

非同期通信の結果をdoneの関数の引数から受取り、ビューに追加するためのHTMLを作成する。

app/assets/javascripts/search.js
//省略

   .done(function(products) {
     //emptyメソッドで指定したDOM要素の子要素のみを削除する
     $(".listview.js-lazy-load-images").empty();
     if (products.length !== 0) {
    //foreachメソッドで配列に含まれる各要素に対して一度ずつ呼びだす。
       products.forEach(function(product){
         appendProduct(product);
       });
     }
     else {
       appendErrMsgToHTML("一致する映画はありません");
     }
   })
  });
});

上記結果をビューに反映できればok。

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

Rails ターミナルコマンド

・rake db:create 新しくデータベースを作成する方法

・rails g controller  コントローラの作成
 「rails g controller コントローラ名」 
作成したいコントローラの名前を後ろに付けて実行する

・rails d controller   コントローラの削除
 「rails g controller コントローラ名」
コントローラ名を間違えて作成してしまった場合

・rails g model モデルの作成
 マイグレーションファイルはテーブルの設計図
  カラム名を追加できる

・rake db:migrate
未実行のマイグレーションファイルの実行をできる
マイグレーションファイルを編集した時などに使う

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

予約システム pickadate.jsで実装 ~ヘルスケアwebサービスを自分で作る医者の日記~

クリニックの予約サイトを作るために、勉強中

https://qiita.com/Yuta_Fujiwara/items/66af40dcc5ce206933eb
こちらの記事を参考にさせていただいた。

datepicker

公式のドキュメントとにらめっこしていると、
なんとか、実装はできるようなった。

作りたいのは、予約システム

jsからwebサーバーに送る処理の理解が必要
上の記事はajax使っているのね、
なんとか、ググってajaxの基本を1日で習得
vagrantでphpウェブサーバーを立ち上げる方法で実装

$(function() {

    // datepicker表示イベント
    $('#date').pickadate();

    // timepicker表示イベント
    $('#time').pickatime({
        format: 'HH:i', // 24時間表記
        interval: 30,   // 表示間隔
        min: [10,00],   // 予約開始時間
        max: [20,00]    // 予約終了時間
    });

    // 予約ボタン押下イベント
    $('#submit').click(onClickSubmit);

    //予約ボタン押下処理
    function onClickSubmit(){
        $('#submit_result').remove();
        var date = $('#date').val();
        var time = $('#time').val();

        if(date!='' && time !=''){

          $.ajax({
            async: true,
            url: 'http://192.168.33.10:8000/yoyaku_datepicker.php',
            type: "POST",
            dataType: 'json',
            data: {
              'hiduke': date,
              'jikan': time
            }
          }).done(function(data) {
            console.log(data.kekka + "を取得しました。");
          }).fail(function() {
            console.log("処理を失敗しました。");
          });

            //予約完了メッセージ
            $('#result').after('<div id="submit_result" class="section__block section__block--notification"><p><strong>'+date+time+'〜</strong><br>予約受け付けました。</p></div>');

        }else{
            //予約失敗メッセージ
            $('#result').after('<div id="submit_result" class="section__block section__block--notification-red"><p>予約日・予約時間を選択してください。</p></div>');
        }
    }

});
<?php

$hiduke = $_POST['hiduke'];
$jikan = $_POST['jikan'];



if ($hiduke  && $jikan) {
  $datepick['kekka'] = "ご指定の日時";
} else {
  echo false;
}

echo json_encode($datepick);

最終的にはrailsでやりたいのだが、
データベースへの格納を理解できなきゃならない
railsのacctive recordか

ていうか、vagrant のwebサーバーでデータベースの取り扱いはできるのか??

道は長い、、、

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

【Rails】turbolinksやAjaxの影響でjQueryが正常に動作しないとき

はじめに

Rails app内で、カルーセルスライダーやsubmitの自動送信といったjQueryで実装した機能が正常に動作しない現象に何度か遭遇しました。
本記事ではその対処法を紹介します。

注意

Rails学習中の初学者が備忘録を兼ねて書いています。
内容に誤りを含む可能性・さらに良い手法がある可能性が多分にありますので、参考にする際はその点ご留意ください。

RubyとRailsのバージョン

下記のバージョンにて動作確認しています。
- Ruby 2.5.1
- Rails 5.2.1

現象と原因、解決策

僕が遭遇したエラーは下記の2点でした。

  • 初回読み込み時にjQuery(スライダー)が動かない。リロードしたら動き出す。 →(原因)Turbolinksが発動し、ページ読み込みを起点としたjQueryが発火しない
  • AjaxでHTMLの一部を差し替えた後、差し替え部に埋め込んだjQuery(submit送信)が動かない。 →(原因)Ajaxで差し替えられた部分ではページ読み込みを起点とするjQueryが発火しない

(解決策)
どちらの問題も、イベントの発火タイミングを設定し直すことで修正できました。

修正前のコード

custom.jsに下記の記載をしていました。

custom.js
// carousel-slider(bxslider)
jQuery(document).ready(function() {
  $('.bxslider').bxSlider({
  });
});

// submit自動送信
jQuery(document).ready(function() {
  $('.select-box').change(function() {
      $(this).parent().submit();
    });
});

上記の jQuery(document).readyや、 $(function()の記述では、ページの読み込みを起点として発火します。
つまり、画面の一部が切り替わった場合はイベントが発生しないことになります。
そこで、Turbolinksを無効化させる、Ajax後にも発火するように設定する、ということで対処していきます。
Turbolinksについては こちらの記事 で詳しく解説されています。

修正後のコード

修正後のコードがこちらです。

custom.js
// carousel-slider(bxslider)
jQuery(document).on('turbolinks:load', function(){
  $('.bxslider').bxSlider({
  });
});

// submit自動送信
jQuery(document).bind('ready ajaxComplete', function() {
  $('.select-drop').change(function() {
      $(this).parent().submit();
    });
});
  • turbolinks: load’とすることで、初回読み込み時、リロードどちらでも発火するようになります。
  • ajaxCompleteとすることでAjaxが完了したのちに発火するように設定できます。今回はbind(‘ready ajaxComplete’)とすることで初回読み込み時、Ajax後の両方で動作するようにしています。

完成

綺麗な解決法ではないかもしれませんが、なんとか無事に狙い通りの機能を実装できました。
jQueryについてはもっと勉強が必要そうです。。。

参考

Qiita

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

fields_for内のf.collection_selectとactive_hashを表示させる

こんにちは!
ビューのマークアップ中に学んだ内容を備忘録として

実装方法

開発環境

Ruby 2.5.1
Rails 5.2.3

参考

Railsウィザード形式フォームで新規登録~Devise+session~
https://qiita.com/ATORA1992/items/40fc543742a6df5a17c1
Railsのgem 'active_hash'で都道府県データを作成したみた
https://kossy-web-engineer.hatenablog.com/entry/2019/01/08/205702
Github active_hash
https://kossy-web-engineer.hatenablog.com/entry/2019/01/08/205702

やりたい事

  • active_hashで実装した都道府県データをセレクトボックスを使ってビューに表示する
  • セレクトボックスのデフォルト矢印をFontAwesomeでナイスな矢印に変える

事前準備

  • field_forを扱えるようにmodel/controller/routesの実装を完了させる
  • 都道府県データをモデルを介さず扱えるようにする為にgem 'active_hash'を導入する
  • modelを自作し都道府県の元データを作成する

↑↑チームメンバーが実装してくれたのでこの記事では割愛

セレクトボックスをビューに表示させる

完成

住所の都道府県選択するボックスがあって、
スクリーンショット 2019-08-27 16.05.06.png

プルダウンさせると、
スクリーンショット 2019-08-27 16.06.43.png

って感じです

コード

:prefecture.html.haml
%body.new-member-registration-body
  %main.new-member-registration-body__main
    %section
      .new-member-registration__title 発送元・お届け先入力
      = form_for @user, url: signup_index_path, method: :post, html: { class: 'registration-form' } do |f|
        = f.fields_for :deliver_adress do |d|
         .new-member-registration-form-content
            .new-member-registration-form-content__group
              = f.label :都道府県, { class: 'new-registration-label' }
              = f.label :必須, { class: 'form-require' }
              .prefecture-select-wrap
                = d.collection_select :prefecture, Prefecture.all, :id, :name

colleciton_selectはform_for,form_tagにて使用可能
引数については、
(プロパティ名,オブジェクトの配列,value属性の項目,テキストの項目[,オプション])
value属性にはid: 1 ~ id: 47、テキストの項目にはname: '北海道 ~ name: 沖縄'キー: バリュー形式にてmodels/prefecture.rbには実装されているのでPrefecture.allで都道府県データを全て引っ張ってきます

cssでレイアウトを整える

prefecture.scss
#user_deliver_adress_attributes_prefecture {
  height: 32px;//箱の高さ
  width: 300px;//箱の横幅
  padding: 10px 16px 8px;//select_box内の文字を適切な位置へ
  margin-top: 8px;//箱の上部に余白を与える
  border-radius: 4px;//箱の角を滑らかに
  border: 1px solid #ccc;//箱の線のスタイル
  background-color: #fff;//箱の背景色
  font-size: 16px;//文字の大きさ
}

このidはcollection_selectに自動で付与されるidです
最初、クラス名を与えていたのですが、
= d.collection_select :prefecture, Prefecture.all, :id, :name, class: 'クラス名'って書いてもcssが反映されないんですよね...なぜだろう?と調べてみると、第三引数にclass名は持ってこれないみたいですね。Railsドキュメントのソースコードに書いています

メソッドの定義
def select(method, choices = nil, options = {}, html_options = {}, &block)

第三引数は空ハッシュでoptionをスキップしてあげる必要があります。= d.collection_select :prefecture, Prefecture.all, :id, :name, {}, class: 'クラス名'これでクラスを与える事ができます。
が、id持っているのにわざわざクラスを付与しなくても良いと思い、id = user_deliver_adress_attributes_prefectureへcssを当てました

FontAwesomeで矢印をナイスに

このデフォルト矢印を、
スクリーンショット 2019-08-27 17.11.08.png
FontAwesomeを使って、
スクリーンショット 2019-08-27 17.10.07.png
へ変更します

prefecture.html.haml
.prefecture-select-wrap
  = d.collection_select :prefecture, Prefecture.all, :id, :name
  %i.prefecture.fas.fa-chevron-down

hamlさんは親要素.prefecture-select-wrapに対して= d.collection_select :prefecture, Prefecture.all, :id, :name%i.prefecture.fas.fa-chevron-downを子要素として並列にしてあげないとうまくいかなかったです

prefecture.scss
#user_deliver_adress_attributes_prefecture {
  height: 32px;
  width: 300px;
  padding: 10px 16px 8px;
  margin-top: 8px;
  border-radius: 4px;
  border: 1px solid #ccc;
  background-color: #fff;
  line-height: 1.5;
  font-size: 16px;
  -webkit-appearance: none;//追加:デフォルト矢印を消す
}
.prefecture-select-wrap {
  position: relative;//親のポジション指定
}
.prefecture.fas.fa-chevron-down {
  position: absolute;//子のポジション指定
  right: 25px;//右25px空ける
  top: 60%;//上から60%の位置へ
  z-index: 2;//親の下へ潜り揉まないようにする
  color: #888;
  transform: translate(0, -50%);//translate(X方向の移動距離, Y方向の移動距離)
  font-size: 18px;
}

transform: translate(0, 50%)はHTMLリファレンスを参考にしましたがあまり理解できていないです。

transform …… 要素に2D変形、または、3D変形を適用する
http://www.htmq.com/css3/transform.shtml

このプロパティ設定がないと矢印が意図したレイアウトになってくれなさそう

まとめ

今回はフロントのマークアップで自分が躓いた部分を記事にまとめて見ました。
本当はactive_hashfields_forの実装部分もアウトプットできれば良いのだが、いかんせんちゃんと理解ができていないです。やはり自分の力で実装しないと理解はできないと痛感しています。個人アプリを作る時は自力で実装するのでその時にまたQittaに記事書きます。

最後に

筆者について

TEXH::EXPERT渋谷校の夜間クラスで4月からRuby・Railsの学習をしています。
記載内容に不備・不足があればご指摘いただけると幸いです。
至らぬ点ばかりですので、改善点がありましたらどんどんご指摘下さい!

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

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #12 ActionMailer, アクティベーション編

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#11 プロフィール編集編
次回:準備中

こんなことが分かる

  • ユーザ新規登録時のメールによるアクティベーションの方法
  • メーラーの使い方
  • URLによるトークンとダイジェストの認証方法
  • 本番/テスト環境ごとによるメールの設定方法
  • 文字列よりシンボルが推奨される理由

一緒に勉強していきまっせ:bow:

今回の流れ

  1. アクティベーションのイメージを掴む
  2. コントローラ、属性、トークン/ダイジェストを用意する
  3. メールを生成し、メール内のURLにトークンを仕込む
  4. トークン/ダイジェストを照らし合わせ、アクティベートする
  5. ログイン時にもアクティベーションを確認する
  6. アクティベーションに関するテストを書く

アクティベーションのイメージ

今の仕様だとどんなメールアドレスでも登録が完了してしまう。
だから登録時にメールを送ってアクティベーションしたい...
どうすればよい??

というわけでどんな風に行えば実装できるのか考えてみよう。
これまでは登録に成功すると/signupから/users/:idにリダイレクトするだけだった。
lantern_lantern_signup_sitemap.png

この間にアクティベーション用のメールを挟みたい。
アプリはホーム画面にリダイレクトする。
lantern_lantern_signup_sitemap_2.png

先述しちゃったけど、メール用に必要なパスは1つ。
その他必要なビューなどは存在しない。
よってAcountActivationsコントローラと対応するリソースを生成しよう。

bash
$ rails g controller AccountActivations
config/routes.rb
Rails.application.routes.draw do
# 中略
  resources :account_activations, only: [:edit]
end

RESTに従うと、情報を更新するためにはpatchを使用する。
でもユーザにURLをクリックしてもらう以上それはgetだ。
よってupdateアクションではなくeditアクションを使用する。

アクティベーションの手順

  1. 新規作成時にトークンとダイジェストを生成する
  2. メールを生成する
  3. メール内のURLにトークンを忍ばせる
  4. URLをクリックしたらトークンとダイジェストと照らし合わせる
  5. 正しければアクティベーション済みにする

以上の動作に必要なものを列挙する。

  • アクティベーション用トークン/ダイジェスト
  • アクティベーション済みかどうかを確認する属性
  • アクティベーション用メール
  • アクティベーションするための動作コード

ポートフォリオ#9をご覧いただきたい。
アクティベーションに必要なトークンとダイジェストは同じような手順で生成できる。
対してメールを生成するメーラーは新たに使用する。
混乱する前に頭の中で整理しておくことをおすすめする。

それでは1つずつ用意していこう。

AccountActivationの属性を用意する

属性として必要なのはこの2つ。

  • activation_digest → アクティベーション用ダイジェスト
  • activated → アクティベーション済みかどうかの真偽値
bash
$ rails g migration add_activation_to_users activation_digest:string activated:boolean

真偽値はデフォルトでfalseにしておく。

db/migrate/[timestamp]_add_activation_to_users.rb
class AddActivationToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :activation_digest, :string
    add_column :users, :activated, :boolean, default: false
  end
end

マイグレーションもお忘れなく。

bash
$ rails db:migrate

トークンとダイジェストを生成する

トークンとダイジェストの生成に関しては、以前作成したUser.new_tokenとUser.digestでまかなえる。
アクティベーション用にcreate_activation_digestメソッドを作り、これらをまとめておこう。

加えてactivation_tokenは仮属性なのでattr_accessorに追加する。

/app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  # 中略
  private

    def create_activation_digest
      self.activation_token = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end
# 中略

どうやって新規作成時に生成させるか

以前emailを入力させる際before_saveを使用し、save前にアドレスをdowncaseメソッドで小文字化した。

今回はsave前ではなくcreate前に呼び出したい。
お察しのとおりRailsにはbefore_createというメソッドが用意されている。
...使おう:relaxed:

ついでにリファクタリングとして小文字化の処理もメソッド化しておく。
最終的にはこうなる。

/app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token, :activation_token
  before_save :downcase_email
  before_create :create_activation_digest
  # 中略
  private

    def downcase_email
      email.downcase!
    end

    def create_activation_digest
      self.activation_token = User.new_token
      self.activation_digest = User.digest(activation_token)
    end
end

アクティベーション用メールを生成する

メールを生成するにはメーラーというものを使う。
早速生成してみよう。
ついでに次の章で紹介するパスワードリセット用のpassword_resetメソッドも生成しておく。

bash
$ rails g mailer UserMailer account_activation password_reset

生成されたメーラーはコントローラのアクションと似ており、メーラーの各メソッドが同名のビューと対応する形。
Tutorialを参考に改変。

app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: 'noreply@example.com'
  layout 'mailer'
end
app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer

  def account_activation(user)
    @user = user
    mail to: user.email, subject: "【重要】Lantern Lanternよりアカウント有効化のためのメールを届けました"
  end

  # この部分は次章
  def password_reset
    @greeting = "Hi"
    mail to: "to@example.org"
  end
end

「なんでaccount_activationに引数与えられるの?」
とちょっと疑問に思った。

答えはシンプルでコントローラのアクションと違い、メーラーのメソッドはコントローラ内で自発的に使っていくから(っぽい)。
まあ要は普通に定義したメソッドだから使えるよねって話。

メール内のURLにトークンを仕込む

ここからはビューでメール本文を作成しよう。
必要なのはトークンとキーにするメールアドレスを仕込んだURL。
以上を踏まえるとこんな感じ。

app/views/user_mailer/account_activation.text.erb
<%= @user.name %>さんへ

Lantern Lanternにいらして下さりありがとうございます。下記のリンクをクリックして認証を済ませてください。
<%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
app/views/user_mailer/account_activation.html.erb
<h1>Lantern Lantern</h1>
<p><%= @user.name %>さんへ</p>
<p>Lantern Lanternにいらして下さりありがとうございます。下記の『認証する』をクリックして認証を済ませてください。</p>
<%= link_to "認証する", edit_account_activation_url(@user.activation_token, email: @user.email) %>

さてこの部分。

edit_account_activation_url(@user.activation_token, email: @user.email)

ルートを確認するとedit_account_activationのURLパターンを教えてくれる。

bash
$ rails routes | grep edit_account_activation 
edit_account_activation GET /account_activations/:id/edit(.:format)

:id部分には引数が入る。でも今回は第2引数まである。
実はこの第2引数にハッシュを使用するとクエリパラメータを付与することができる。
結果的にこんな感じのURLを生み出す。

http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com

これでURLにトークンとメールアドレスを仕込むことができた。

メールのプレビュー

メールができたとはいえプレビューを確認したい。
そのためには設定を変える必要がある。

まずdevelopment.rbのこの箇所を、

config/environments/development.rb
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  config.action_mailer.perform_caching = false

こうする。

config/environments/development.rb
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = '『〜自分の環境に合わせる〜』'
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  config.action_mailer.perform_caching = false

ここに注目。

host = '〇〇'

この箇所には自分の環境によって変更する必要がある。
具体的にはdevelopment環境のURLを挿入する。
本ポートフォリオではこの部分。
lantern_lantern_development_url.png

参考にさせていただきました↓
rails チュートリアルの11章の2・2にてクラウドIDEのホスト名がわからない
Railsのconfig/enviroments配下を読んでみる

あとはプレビュー用のメソッドもいじる必要がある。

spec/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview

# https://〇〇/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    UserMailer.account_activation(user)
  end

  def password_reset
    UserMailer.password_reset
  end
end

これで下記のリンクでプレビューを確認できるようになった。

https://〇〇/rails/mailers/user_mailer/account_activation

lantern_lantern_mail_preview.png

新規作成時にメールを送る

それではメールを送ろう。
手順としては、処理を書いた後、本番環境を整える。
しかし本番環境については、完成後に整える方がスムーズなので後回しにする。

メールを送る処理を書く

ユーザ新規作成時にメールを送るようにしよう。
メーラーで作成したメールを送るにはdeliver_nowメソッドを使用する。

UserMailer.account_activation(@user).deliver_now

ここは慣習的にメソッド化しておこう。

app/models/user.rb
class User < ApplicationRecord
# 中略
  def send_activation_email
    UserMailer.account_activation(self).deliver_now
  end

あとはcreateアクションに書き込む。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  # 中略
  def create
    @user = User.new(user_params)
    if @user.save
      UserMailer.account_activation(@user).deliver_now
      flash[:info] = "認証用メールを送信しました。登録時のメールアドレスから認証を済ませてください"
      redirect_to root_url
    else
      render 'new'
    end
  end

その他、flashメッセージやリダイレクト先などの変更がある。
これによりいくつかのテストが通らなくなる。
記事後半にテスト関係をまとめたので、この問題はあとで修正する。

知識を助けていただきました↓
【Rails入門】Action Mailerのメール送信を初心者向けに基礎から解説

アクティベーションを完了させる

メール内のURLをクリックしたらアクティベーション完了にしたい。
やることは2つ。

  1. トークンとダイジェストと照らし合わせるauthenticated?を編集する
  2. editアクションに処理を書く

authenticated?を編集する

以前remember_me機能を追加する際、authenticated?メソッドでトークンとダイジェストを照らし合わせていた。
アクティベーションでも同じメソッドが使えるよう、カスタマイズする。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  # 以前のauthenticated?を書き換え
  def authenticated?(attribute, token)
    digest = send("#{attribute}_digest")
    return false if digest.nil?
    BCrypt::Password.new(digest).is_password?(token)
  end

第1引数 → rememberに使うかactivationに使うかなど
第2引数 → 指定したトークン
これで汎用性の高いメソッドに生まれ変わった。

sendを使うことで文字列をメソッドとして認識してくれる。
そこに#{}を使うことで、引数によってメソッド名を変化させるという仕組み。

再び以前書いたテストが失敗する。
これも記事後半に。

それよりもauthenticated?を使用していたcurrent_userを編集しよう。

app/helpers/sessions_helper.rb
module SessionsHelper
  # 中略
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(:remember, cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

authenticated?では第1引数を指定する際にシンボルを使用している。
ん?なんで?

文字列よりシンボルが推奨される理由

第1引数に指定する際はシンボルを推奨する。
理由はざっくりいうとこんな感じ。

  • Rubyの内部実装は、速度面で名前を整数で管理している
  • シンボルはソース上文字列、内部上整数という性質を持つ
  • よって文字列ではなくシンボルを使用する

より詳しい解説↓
Rubyの文字列とシンボルの違いをキッチリ説明できる人になりたい

editアクションに処理を書く

ようやくAccountActivationsコントローラに処理を書ける。
以下の記述でactivatedをtrueにするが、

user.update_attribute(:activated, true)

ここも慣習にならってメソッド化する。

app/models/user.rb
class User < ApplicationRecord
  # 中略
  def activate
    update_attribute(:activated, true)
  end

終わったらeditアクションに処理を書こう。

app/controllers/account_activations_controller.rb
class AccountActivationsController < ApplicationController

  def edit
    user = User.find_by(email: params[:email])
    if user && !user.activated? && user.authenticated?(:activation, params[:id])
      user.activate
      log_in user
      flash[:success] = "Lantern Lanternへようこそ!"
      redirect_to user
    else
      flash[:danger] = "アクティベーションに失敗しました"
      redirect_to root_url
    end
  end
end

ここに注目。

!user.activated?

わざわざこうしているのは、アクティベーション済みにも関わらずtrueが可能になると、リンクを盗み出すだけで攻撃でアクティベーションが成功してしまうから。

ログイン時にアクティベーションを確認する

新規登録時のアクティベーションは完了した。
でもまだログイン時にアクティベーション済みかを確認する処理が書かれていない。
Sessionsコントローラを編集しよう。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  # 中略
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      if user.activated?
        log_in user
        params[:session][:remember_me] == '1' ? remember(user) : forget(user)
        redirect_back_or user
      else
        flash[:danger] = "メールを確認してアクティベーションを済ませてください"
        redirect_to root_url
      end
    else
      flash.now[:danger] = 'メールアドレスかパスワードが正しくありません'
      render 'new'
    end
  end

これでアクティベーション関係は終了となる。

本番環境でメールが届くようにする

実際のアプリでメールを送信するにはいくつかやることがある。

  • Herokuのアドオンを追加する
  • production.rbを編集する

Herokuのアドオンを追加する

Herokuからメールを送信できるようにしよう。
そのためにはアドオンを追加する。

bash
heroku addons:create sendgrid:starter

production.rbを編集する

production.rbにこういう記述があると思う。

config.action_mailer.raise_delivery_errors = false

このあたりをこんな感じに編集する。

config/environments/production.rb
# 中略
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :smtp
  host = '〇〇.herokuapp.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com',
    :enable_starttls_auto => true
  }

〇〇の部分はHerokuで自分のアプリを開いたときのURLを挿入しよう。
(https除く)

host = '〇〇.herokuapp.com'

これで無事動作する。

メーラーのテストを書く

最後にメーラーのテストだ。
ただメーラーをテストするまでにいくつかやることがある。

  1. FactoryBotにactivated属性を与える
  2. ホスト側にドメイン名を与える

これが終わったら実際にテストといこう。

FactoryBotにactivated属性を与える

activated属性が新たに追加されたので、FactotyBotに追記しよう。
acitvatedがfalseのユーザも加えてみる。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Michael Example" }
    email { "michael@example.com" }
    password { "password" }
    password_confirmation { "password" }
    activated { true }
  end

  factory :other_user, class: User do
    name { "Sterling Archer" }
    email { "duchess@example.gov" }
    password { "foobar" }
    password_confirmation { "foobar" }
    activated { true }
  end

  factory :no_activation_user, class: User do
    name { "No Activation" }
    email { "no@activation.co.jp" }
    password { "foobar" }
    password_confirmation { "foobar" }
    activated { false }
  end
end

ホスト側にドメイン名を与える

今のままではメールの送信元のアドレスがテスト環境に存在しないことになっている。
設定を変更しよう。

config/environments/test.rb
# 中略
  config.action_mailer.delivery_method = :test
  config.action_mailer.default_url_options = { host: 'example.com' }
# 中略

メーラーテストを書く

意外と面倒くさいメーラーテスト。
自動生成されたpassword_resetに関してはコメントアウト。

spec/mailers/user_mailer_spec.rb
require "rails_helper"

RSpec.describe UserMailer, type: :mailer do

  let(:user) { create(:user) }

  describe "account_activation" do
    it "renders mails" do
      user.activation_token = User.new_token
      mail = UserMailer.account_activation(user)
      expect(mail.subject).to eq("【重要】Lantern Lanternよりアカウント有効化のためのメールを届けました")
      expect(mail.to).to eq(["michael@example.com"])
      expect(mail.from).to eq(["noreply@example.com"])
      expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include("Michael Example")
    end
  end

  # describe "password_reset" do
  #   let(:mail) { UserMailer.password_reset }
  # end
end

これが気になる。

expect(mail.body.encoded.split(/\r\n/).map{|i| Base64.decode64(i)}.join).to include("Michael Example")

もちろんこうしたいのは山々だけれど、

expect(mail.body.encoded).to include("Michael Example")

これだと失敗する。理由はエンコードの関係。
このままだと訳がわからない文字列を参照することになる。
それをデコードする必要があるからこうなった訳だ。

参考にさせていただきました↓
Rails つまづき駆動投稿(TDP)
ActionMailerのメール送信テストをRSpecで行う

その他のテストを書く

テスト追加と修正を行う。

  • アクティベートしていないユーザをログイン失敗にするテストを追加と修正
  • 新規作成時のリダイレクト先をルートに修正
  • authenticated?のモデルテストを修正

アクティベートしていないユーザをログイン失敗にするテスト

ユーザをログインさせる際、アクティベート済みとそうでないユーザを分けたい。
以前テストでユーザ情報をpostする際にはメソッドを使用した。
それの修正とリファクタリングを行い、テストを完成させる。

spec/requests/users_logins_spec.rb
require 'rails_helper'

RSpec.describe "UsersLogins", type: :request do
  include SessionsHelper

  let(:user) { create(:user) }
  let(:no_activation_user) { create(:no_activation_user) }

  def post_invalid_information
    post login_path, params: {
      session: {
        email: "",
        password: ""
      }
    }
  end

  def post_valid_information(login_user, remember_me = 0)
    post login_path, params: {
      session: {
        email: login_user.email,
        password: login_user.password,
        remember_me: remember_me
      }
    }
  end

  describe "GET /login" do
    it "fails having a danger flash message" do
      get login_path
      post_invalid_information
      expect(flash[:danger]).to be_truthy
      expect(is_logged_in?).to be_falsey
      expect(request.fullpath).to eq '/login'
    end

    it "fails because they have not activated account" do
      get login_path
      post_valid_information(no_activation_user)
      expect(flash[:danger]).to be_truthy
      expect(is_logged_in?).to be_falsey
      follow_redirect!
      expect(request.fullpath).to eq '/'
    end

    it "succeeds having no danger flash message" do
      get login_path
      post_valid_information(user)
      expect(flash[:danger]).to be_falsey
      expect(is_logged_in?).to be_truthy
      follow_redirect!
      expect(request.fullpath).to eq '/users/1'
    end
# 中略

post_valid_informationに引数を定義したので、使用する際は引数を与えよう。
(中略している部分にも忘れずに:thumbsup:

新規作成時のリダイレクト先をルートに修正する

あとは修正だけ。

spec/requests/users_signups_spec.rb
# 中略
it "is valid signup information" do
  get signup_path
  expect { post_valid_information }.to change(User, :count).by(1)
  expect(is_logged_in?).to be_falsey
  follow_redirect!
  expect(request.fullpath).to eq '/'
  expect(flash[:info]).to be_truthy
end
spec/systems/signup_spec.rb
# 中略
it "is valid because it fulfils form information" do
  visit signup_path
  submit_with_valid_information
  expect(current_path).to eq root_path
  expect(page).to have_selector '.alert-info'
end

authenticated?のテストを修正

spec/models/user_spec.rb
# 中略
  describe "User model methods" do
    describe "authenticated?" do
      it "return false for a user with nil digest" do
        expect(user.authenticated?(:remember, '')).to be_falsey
      end
    end
  end

最後にテストを走らせておこう。

$ rails spec

これがグリーンならアクティベーションは完了。
お疲れ様ー。:relaxed:

前回:#11 プロフィール編集編
次回:準備中

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

Rails6でknockを導入してload関連のエラーが発生したときの対処法

Ruby on Rails 6.0.0 で作成したアプリケーションに、JWTトークンの発行・認証を行う目的で knock をインストールしようとしたところ、loadに関するエラーが発生したので対処法をまとめます。

発生したエラー

現象1

rails generate knock:install を実行するとwarningとerrorが出力される。
config/initializers/knock.rb の生成自体はできている。

$ rails generate knock:install                                                                                                                                                               
Running via Spring preloader in process 14337
[WARNING] Could not load generator "generators/knock/install_generator". Error: expected file /home/user/.rbenv/versions

現象2

ApplicationController にて Knock::Authenticable をincludeさせると、railsが起動できない。

$ rails c                                                                                                                                                                                     
/home/user/Documents/develop/knock_test/app/controllers/application_controller.rb:2:in `<class:ApplicationController>': uninitialized constant Knock::Authenticable (NameError)

原因

Rails6でのautoloadがzeitwerkモードに変更となったことで、autoloadの挙動がRails5以前と変わったことに起因しています。

Rails 6 zeitwerk autoload problem with gem

対処法

対処法1. 明示的にrequireする[推奨]

上記issueにある通り、明示的にソースをrequireすることでエラーを回避できます。

config/initializers/eager_load_knock.rb
require 'knock/version'
require 'knock/authenticable'

対処法2. autoloadをclassicモードに戻す[非推奨]

Rails5以前と同じ方法でautoloadさせることでもエラーを回避できます。
基本的には新しいバージョンに追従すべきなので非推奨ですが、他のgemで同様のエラーが発生する場合などはこちらの方が手っ取り早いかもしれません。

config/application.rb
require_relative 'boot'

require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
require "rails/test_unit/railtie"

Bundler.require(*Rails.groups)

module KnockTest
  class Application < Rails::Application
    config.load_defaults 6.0
    config.api_only = true
    config.autoloader = :classic # ★追記
  end
end

まとめ

こちらの issueで何かしら修正が入ることに期待しつつ、まずは上記のような対策でエラーを回避するしかなさそうです。

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

Rails6 のちょい足しな新機能を試す70(ActiveModel::Errors#slice! 編)

はじめに

(多分)Rails 6 に追加された新機能を試す第70段。 今回は、 ActiveModel::Errors#slice! 編です。
Rails 6 では、 ActiveModel::Errors に インスタンスメソッド slice! が追加されました。

Ruby 2.6.3, Rails 6.0.0.rc2 で確認しました。Rails 6.0.0.rc2 は gem install rails -v 6.0.0rc2 --prerelease でインストールできます。
(Rails 6.0.0 がリリースされましたが、動作確認当時の最新版は、Rails 6.0.0.rc2 でした。悪しからず :bow:)

$ rails --version
Rails 6.0.0.rc2

今回は適切な例を思いつかなかったので、 rails console を使って確認します。

プロジェクトを作る

rails new rails_sandbox
cd rails_sandbox

User モデルを作る

4つの属性 name, email, country, city を持つ User モデルを作ります。

bin/rails g model User name email country city

ヴァリデーションを追加する

単純に必須入力のヴァリデーションを4つの項目に追加します。

app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true
  validates :country, presence: true
  validates :city, presence: true
end

マイグレーションを実行する

bin/rails db:create db:migrate

rails console で確認する

rails console で確認します。
User モデルのインスタンスを作って、 validate メソッドを呼び出します。

irb(main):001:0> user = User.new
=> #<User id: nil, name: nil, email: nil, country: nil, city: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.validate
=> false

slice! メソッドで、 countrycity のメッセージだけ残します。

irb(main):003:0> errors = user.errors.slice!(:country, :city)
=> {:name=>["can't be blank"], :email=>["can't be blank"]}

full_messages でエラーメッセージを確認すると countrycity のエラーメッセージだけになっていることがわかります。

irb(main):004:0> user.errors.full_messages
=> ["Country can't be blank", "City can't be blank"]

なお、 ActiveModel::Errors#slice! の戻り値は Hash オブジェクトのため、 戻り値に対して full_messages は使えません。

irb(main):005:0> errors.full_messages
Traceback (most recent call last):
        1: from (irb):5
        NoMethodError (undefined method `full_messages' for {:name=>["can't be blank"], :email=>["can't be blank"]}:Hash)`)

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try070_active_model_errors_slice

参考情報

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

主キー(primary key)の選択、has_one,belongs_toについて某アニメを具体例にまとめてみた

はじめに

Railsにおけるアソシエーション(belongs_to,has_many,has_one)について、それから主キーの設定について分かったようで分かっていない部分があったので、自分の理解のためにも簡単な具体例を作ってみた。

取り上げる相関

今回使用する相関図はコチラ
qiita.png
大抵、商品の管理システムや従業員データなど例が取り上げられるので、イメージしやすそうな某冒険友情活劇を例にうまいこと当てはめてみた。(逆に分かりづらくなっているかも)

どれを主キーにするか問題

意外と悩んだのがどれを主キーにしたらよいのかという部分。
そもそも主キーとはなんのために設定するのかというところから考えてみる

主キーとは

  • テーブル内でレコードを一意に識別することができるように指定される項目
  • 主キーに選ばれた列はすべてのレコードが異なる値を持たなければならず、NULL値とすることもできない。

主キーの選択

主キーの定義には次のように書かれている。

候補キーが複数あるとき、組を識別するという機能においてそれらの間に差異はないから、主キーにどれを選んでも論理的には問題がない。
しかし実用性を考えると以下に注意して選択するとよい。

  • 主キーは検索のキーとして利用されたり、他の関係に参照のために格納されたりする確率が高いため、できる限りデータ量の小さい方がよい
  • 他の関係で主キーを使用していた場合、主キーを更新すると他の関係の値(外部キー)も同時に更新しなければならなくなるため、更新がかからない項目がよい

主キーを用いた例

生徒名簿(生徒番号, 生徒名, クラス)という関係

生徒番号が主キーになり得る。
同姓同名を考慮すると、生徒番号は唯一の候補キーであるから、代理キーはない。

町村(町村ID, 町村名, 郡名, 都道府県名)という関係

町村ID と {都道府県名, 郡名, 町村名} が候補キーであり、いずれかが主キーになり得る。
例えば、町村ID を主キーにした場合、{都道府県名, 郡名, 町村名} は代理キーとなる。

以上のことを踏まえて選択

  • キャラクターは中身の情報更新が多そうな気もする、データ量も多い
  • インデックスとして機能する

という観点から「海賊団」を主キーに選択
「麦わらの一味」と検索をかけたら、ズラーーっと船員が表示されると便利だと思うので。
なお、所属がないキャラクターは「無所属」にする。(NULL回避)

データベース

あまりこの漫画について知らないのですが、それっぽいデータベースにしました。

[海賊団]

id (主キー) team_name ship
1 無所属 なし
2 麦わらの一味 サウザンド・サニー号
3 バギー海賊団 ビックトップ号
4 アーロン一味 シャーク・サバーブ号

[キャラクター]※prize_money=懸賞金 (金額はテキトー)

id team_id(外部キー) ch_name prize_money
1 2 ルフィ 2億
2 2 ナミ 100万
3 3 バギー 3億
4 2 チョッパー 1万

[悪魔の実]

id ch_id(外部キー) skill_name genre
1 1 ゴムゴム 超人(パラミシア)
2 23 モクモク 自然(ロギア)
3 3 バラバラ 超人(パラミシア)
4 4 ヒトヒト 動物(ゾオン)

キャラクターに関しては、身長、体重、性別、出身などなど色々情報を入れられそうですね。
外部キーをどこに作るか問題もあったのでそれについては後述します。
(悪魔の実のテーブルにch_idを作るか、キャラクターのテーブルにskill_idを作るかみたいな問題)

アソシエーション

相関図をもう一度載せます。

qiita.png

海賊団とキャラクターの関係は分かり易いと思います。
海賊団は複数のメンバーが所属し、キャラクターはそこに所属している。
いわゆる「多:1」の関係です。

問題はキャラクターと悪魔の実の能力の関係です。

has_oneとbelongs_toの使い分け

この関係は「1:1」です。
たしか複数の実の能力を持つことができないみたいな設定でしたよね?
(黒ひげは能力2つもってる的なことは聞いたような、、無視!笑)

ところで、どちらをhas_oneにするの?と引っかかったのでまとめます。

has_oneとbelongs_toの条件

まず、「主従関係」を考えてみると良い。
主となる方が has_one で従となる方が belongs_to になる。

今回の例では悪魔の実の能力を持たないキャラクターは存在するが、悪魔の実自体は必ず誰かの所有物であるという関係である。
例えば「ナミ」は悪魔の実の能力を持っていない。
一方で「バラバラの実」は誰かが持っている能力である。

ゆえに、キャラクターがで悪魔の実の能力がである

また、先ほど後述するといった外部キーをどちらに書くか問題について

自身が他のテーブルをたどるキーを所持している場合は、belongs_to
自身が他のテーブルからたどるキーで示されている場合は、has_one

というルールがある。

したがって、悪魔の実の能力にch_idを持たせている。

まとめ

正直、初め何も考えずにキャラクターを主キーにしようとしていた。(普段Userを主キーに設定されていることが多いので、、)
しかし、主キーはインデックスや更新頻度など目的を考えて設定する必要がある。

また、主キーについて、それからhas_onebelongs_to についてを上手くまとめようとしていく際に、
そもそもまず何を基準にしてテーブルを分けていくのかがなかなか難しいと感じた。
データベースは初めに作成するので、ここで時間を割かないようしっかり理解しておく必要がある。

どこを主キーにしたら良いのか、主従関係はどうなっているのかというのは慣れていないと時間がかかりそうなので、今後は主キーの設定箇所や主従関係に注目しながらデータベースを見ていこうと感じた。

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

主キー(primary key)の選択、has_one,belongs_toについて某アニメを例にまとめてみた

はじめに

Railsにおけるアソシエーション(belongs_to,has_many,has_one)について、それから主キーの設定について分かったようで分かっていない部分があったので、自分の理解のためにも簡単な具体例を作ってみた。

取り上げる相関

今回使用する相関図はコチラ
qiita.png
大抵、商品の管理システムや従業員データなど例が取り上げられるので、イメージしやすそうな某冒険友情活劇を例にうまいこと当てはめてみた。(逆に分かりづらくなっているかも)

どれを主キーにするか問題

意外と悩んだのがどれを主キーにしたらよいのかという部分。
そもそも主キーとはなんのために設定するのかというところから考えてみる。

主キーに適するのは

  • テーブル内でレコードを一意に識別することができるように指定される項目
  • 主キーに選ばれた列はすべてのレコードが異なる値を持たなければならず、NULL値とすることもできない

主キーの選択

主キーの定義には次のように書かれている。

候補キーが複数あるとき、組を識別するという機能においてそれらの間に差異はないため、主キーにどれを選んでも論理的には問題がない。
しかし実用性を考えると以下に注意して選択するとよい。

  • 主キーは検索のキーとして利用されたり、他の関係に参照のために格納されたりする確率が高いため、できる限りデータ量の小さい方がよい
  • 他の関係で主キーを使用していた場合、主キーを更新すると他の関係の値(外部キー)も同時に更新しなければならなくなるため、更新がかからない項目がよい

主キーを用いた例

生徒名簿(生徒番号, 生徒名, クラス)という関係

生徒番号が主キーになり得る。
同姓同名を考慮すると、生徒番号は唯一の候補キーであるから、代理キーはない。

町村(町村ID, 町村名, 郡名, 都道府県名)という関係

町村ID と {都道府県名, 郡名, 町村名} が候補キーであり、いずれかが主キーになり得る。
例えば、町村ID を主キーにした場合、{都道府県名, 郡名, 町村名} は代理キーとなる。

以上のことを踏まえて選択

  • キャラクターは中身の情報更新が多そうな気もする、データ量も多い
  • インデックスとして機能するものがよい

という観点から「海賊団」を主キーに選択。
「麦わらの一味」と検索をかけたら、ズラーーっと船員が表示されると便利だと思うので。
なお、所属がないキャラクターは「無所属」とする。(NULL回避)

データベース

あまりこの漫画について知らないのですが、それっぽいのを設定した。

[海賊団]

id (主キー) team_name ship
1 無所属 なし
2 麦わらの一味 サウザンド・サニー号
3 バギー海賊団 ビックトップ号
4 アーロン一味 シャーク・サバーブ号

[キャラクター]※prize_money=懸賞金 (金額はテキトー)

id team_id(外部キー) ch_name prize_money
1 2 ルフィ 2億
2 2 ナミ 100万
3 3 バギー 3億
4 2 チョッパー 1万

[悪魔の実]

id ch_id(外部キー) skill_name genre
1 1 ゴムゴム 超人(パラミシア)
2 23 モクモク 自然(ロギア)
3 3 バラバラ 超人(パラミシア)
4 4 ヒトヒト 動物(ゾオン)

キャラクターに関しては、身長、体重、性別、出身などなど色々情報を入れられそうですね。
また、外部キーをどこに作るか問題もあったのでそれについては後述します。
(悪魔の実のテーブルにch_idを作るか、キャラクターのテーブルにskill_idを作るかみたいな問題)

アソシエーション

相関図をもう一度載せます。

qiita.png

海賊団とキャラクターの関係は分かり易いと思います。
海賊団は複数のメンバーが所属し、キャラクターはそこに所属している。
いわゆる「多:1」の関係です。

問題はキャラクターと悪魔の実の能力の関係です。

has_oneとbelongs_toの使い分け

この関係は「1:1」です。
たしか複数の実の能力を持つことができないみたいな設定でしたよね?
(黒ひげは能力2つもってる的なことを聞いたような、、ややこしいので無視!笑)

ところで、どちらをhas_oneにするの?と引っかかったのでまとめます。

has_oneとbelongs_toの条件

まず、「主従関係」を考えてみると良い。
主となる方が has_one で従となる方が belongs_to になる。

今回の例では悪魔の実の能力を持たないキャラクターは存在するが、悪魔の実自体は必ず誰かの所有物であるという関係である。
例えば「ナミ」は悪魔の実の能力を持っていない。
一方で「バラバラの実」は誰かが持っている能力である。

ゆえに、キャラクターがで悪魔の実の能力がとなる。

また、先ほど後述するといった外部キーをどちらに書くか問題については

自身が他のテーブルをたどるキーを所持している場合は、belongs_to
自身が他のテーブルからたどるキーで示されている場合は、has_one

というルールがある。

したがって、悪魔の実の能力にch_idを持たせている。

まとめ

正直、初め何も考えずにキャラクターを主キーにしようとしていた。(普段Userを主キーに設定されていることが多いので、、)
しかし、主キーはインデックスや更新頻度など目的を考えて設定する必要がある。

また、主キーについて、それからhas_onebelongs_to についてを上手くまとめようとしていく際に、
そもそもまず何を基準にしてテーブルを分けていくのかがなかなか難しいと感じた。
データベースは初めに作成するので、ここで時間を割かないようしっかり理解しておく必要がある。

どこを主キーにしたら良いのか、主従関係はどうなっているのかというのは慣れていないと時間がかかりそうなので、今後は主キーの設定箇所や主従関係に注目しながらデータベースを見ていこうと感じた。

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

個人開発「positiveな改善案の口コミサイト」

はじめに

未経験からエンジニアを目指しており、勉強中に開発をした個人アプリについて作成理由や機能の解説になります。現在も開発を続けておりますので、途中経過を含めて内容については開発次第、随時更新をしていきます。

http://13.114.69.119/

※何故やりたいのか、というのは個人的に結構重要視をしているので、若干サービス開発にかける想いの部分が若干長めかと思います。

作成アプリケーションの内容

開発環境

サーバーサイド言語:Ruby2.5.1
フレームワーク:Rails:5.2.2.1
フロントエンド:HTML
CSS (scss)
JavaScript (jQuery)
CSSフレームワーク:bootstrap
データベース:MySQL
サーバー:AWS(EC2 + S3)

開発のきっかけ(興味が薄ければ飛ばしていただければと思います)

食べログやオープンワーク等に代表をされる、口コミサイトは今までサービスに対しての情報が全くなかったのを見える化したことで、すごく生活を便利にしてくれたと思います。
個人的には食べログは社会人になってからずっと有料会員で、一定以上に評価をされているかを基準にすることで失敗をしない店の選び方もできるようになりました。
すごく存在価値のあることだと思っています。ただ、段々と既存の口コミサイトを利用するにつれて、問題意識を感じるようになったため、今回開発をしてみました。
特に課題に感じたのが2点あります。

①影響力の大きさを要因とした、評価の不確実性

私は前職が人材紹介の法人営業だったのですごく感じるのですが、クチコミサイトは1つの投稿が持つ、影響力がすごく大きいです。選考途中に辞退になるケースの中で、口コミサイトの評判をみて不安になってしまった、という辞退理由は実は結構多いです。
本来はこういった評価は真摯に受け止めて、しっかりと改善をしていくことが重要だと思っています。しかし、現実にはそうではない企業もあり、自社で評価を操作をしてしまっている企業が多いことも事実です。
※正規分布という確率分布があるので、一定の投稿数があれば通常は評価の点数はなだらかな評価の割合になります。
 例えば5点満点で、十分なサンプルがありながら1と5の評価が2〜4と比べて飛び抜けて多い口コミの企業とかは要注意です。また、不自然にならないように自然を装って、釣り上げる業者も存在をしています。
こういった企業が多くなると必然的に本質的な改善を目指している企業がますます評価をされない、という状況になってしまいます。本来、口コミサイトが目指す世界観としては「ユーザー側からの評価をして、透明度を増すことで誰にとってもいいサービスを作っていくこと」だと思っていますが、完全にずれてしまっていると思います。

②いつまでも残り続ける低評価が、改善をしても企業につきまとってしまう

悪い口コミがいつまでも残って影響を与え続けてしまうのは個人的には企業がすごく可愛そうだと思っています。これもまた、前職での個人的な経験からすごく感じていることです。

こんな経験がありました。

ある上場企業様から、すごく高い評価での内定が出ていた求職者の方がいたのですが、急に辞退をしたい、との連絡をいただいてしまいました。理由は案の定、クチコミサイトを見て評価が低かったのと(3年前の投稿)、過去に不正行為があったからとおっしゃっていました。
企業側の評価も元々かなり高く、何もせずに辞退になってしまうことも企業側にすごく申し訳なかったので、求職者の方に交渉をして、カウンセラーも何とか次の日の20時開始の時間で企業との面談の時間を確保しました。
企業担当の私も正直に相談をしたところ、企業側の回答は「正直に話してもらいありがとうございました、何とかします!」とのことでした。翌日、面談後に求職者の方から連絡があったのですが、なんと辞退は取り消します、とのことでした。
面談の際に何があったのかを聞くと翌日の調整依頼だったのにも関わらず、人事担当の方が急いで社長の面談をセッティングしていただき、面談では「何故不正が当時起きてしまったのか」、「経営陣を一掃した今の経営体制の強固さ」を真摯に話してくれたそうです。それに心打たれて、是非とも入社をしたい、という考えに変わったとのことでした。こちらの企業様のこの姿勢には私自身すごく好感を持ち、それから何名も入社をしていただくことができました。

この時にすごく感じたのが、「改善をしていくことの大切さ」や「真摯に対応をしていくことが人との結びつきを強くする」ということです。
この企業様にはすごくよくしていただいていたのですが、反対に応募段階で過去のクチコミサイトの情報だけで判断をされるケースもすごく多く、個人的にはかなりもったいないなと感じました。
この例では上場企業でしたが、特に創業間もない企業は課題は山積みです。そんな中でその時の評価だけを元にして判断をされていては育つ企業も育たないと思います。

本アプリの目指す世界観

上記の個人的に感じた課題感を解決して、いいサービスを提供をする意志のある企業が報われる、そんな口コミサイトを作っていければと考えております。

①改善をしていくことがいいサービス

ユーザーが現時点での企業側の評価をされるのではなく、改善をしていくことが評価をされるサービスにします。
ITの進歩で変化の早い世界になっており、現状維持はもはや退化と言われるような中では、いかにして変わっていくことができるかが重要です。
飲食店のアンケートでは、改善案の欄がよくあると思います。ただ、積極的に書いている人をほとんど見たことがありません。
中々面と向かって渡すアンケートではやりづらさもあると思いますが、企業側はすごく意見がほしいと思います。
アプリであれば心理的なハードルが下がると思うので、改善案を提言をしていくスタイルにしていきます。
ユーザー側からしても自分が提言をした内容が、実施をされればすごく嬉しいと思いますし、そうやって作っていったストーリーは唯一無二のものになってくると思います。

②前向きな表現を使う

改善案を言うということは悪く考えれば、現時点でのそのサービスにおける弱みの指摘です。ただ、本アプリではユーザーが一方的に評価をするのではなく、応援をしていけるようにしていきたいと思っています。
そのため、攻撃的な投稿が出ないようにすべてポジティブな言い回しにしています。
サービスを利用していてよかった点をGoodPointにしているのはそれほど違和感はない、と思いますが改善点をChance Pointにしているのはそのためです。
また、本アプリではGCP:cloud language natural APIを使うことによって、投稿をした内容がpositiveなものかnagativeなものか判定ができるようになっています。

③投稿された改善案の数によってマネタイズをする

本質的な改善を目指すためには双方向でのやり取りが必要不可欠になってきます。
ユーザーが勝手に投稿をしていくのは簡単ですが、それだと改善はまずしないと思うので、企業側のサービスの登録を必須にして、基本的に企業から金銭をもらうモデルを想定しています。
その際に、例えば投稿数3件もらうまでは企業側は無料、といった形にしていけば新規でやり取りをしやすいはずです。
無料でサービスを良くする改善案が入ってくるのなら企業側はまずやると思います。
そして、いくつか入ってきた口コミが良質であれば、続けたくなる(はず)ので、そのまま4件以上の投稿をしてもらうためには有料プラン、みたいな形式にしたいです。
個人的に感じることなのですが、改善をするのはすごく気持ちいいことだと思っています。「あのプロジェクト実は俺がやった」とか「あれは私が変えたんだ」とかのワードの言葉は誰しも聞いたことがあるかと思います。
いつまでも残っている、というのは特に企業側の導入を決めた人にとってはすごく誇らしいものでもあるので、離脱率はそこまで高くないのではないかと勝手に考えています。

実装内容

①実装済機能

・一覧表示機能
・詳細表示機能
・投稿機能
・削除機能
・編集機能
・管理ユーザ登録機能(deviseで2つのモデルを使用)
・画像ファイルアップロード機能
・DBテーブルリレーション管理
・ページネーション機能
・コメント機能
・ユーザー登録、削除機能ポジティブ・ネガティブ判定(GCP:cloud language natural API)
・検索機能(ransack)

②未実装機能(実装予定)

課金機能(payjp)
フォロー機能
単体テスト
統合テスト

まとめ

完成途中ではあるものの、スクールの課題と違ってゴールが決まっていないものを作るのはすごく楽しいなと感じました。まだまだ技術力的にできることに限りがあるので、もやもやを感じることは多いですが、色々思いついたことをすぐに実現をできるのはすごく魅力的です。

おまけ(似ている部分があると思ったアプリ)

サービスの質から言えば恐れ多いにもほどがありますが、個人的に似ている部分があるなと思ったサービスです。私のサービスに対してのイメージもしやすくなるかと思うので、いくつか挙げてみます。

①Insight Tech
不満を集めて「お客さまの声」や「レビューデータ」等をAIで分析をして改善につなげるサービスです。この解析をもとに実際にコンサルとかもしているそうです。大量のデータがある分、効果的な提案ができそうです。
最も概念が似ているサービスだと思うのですが、差別化のためにはこちらのサービスが大量のデータを元に分析をしているのに対して、一人ひとりとのストーリーをとにかく着目をしていく必要があると思いました。
②Unipos
前職の時に使っていたサービスです。社内で相手に感謝を伝えるサービスです。何かいい動きをした同僚に対してポイント付きのメッセージを送ることができます。同僚対同僚でのやり取りに使うサービスなので、ユーザー対企業であれば差別化にはつながるかな、とは勝手に思っています。

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

Ruby on Railsを使って個人開発ver1

Ruby on Rails で個人開発

【目標】
Ruby on Railsを使って簡単なWebサイトを作成する

今回は、RailsにPostgreSQLを使用するために少しつまずいた部分を書きます。

【前提】
・Vagrant上にRails、PostgreSQLをインストール済み
・rails new #{アプリケーション名}を実行し、railsサーバーを立ち上げる
・ブラウザにアクセスすると、role "vagrant" does not exist の文字

解決方法

1.sudo -u postgres createuser --createdb vagrant を実行
postgres というユーザ名でPostgreSQLにログインする。
そしてcreatedb vagrant でDBを作成する。
これはcreatedbのrole権限の登録を行っているはず。

2.rails db:create を実行
データベースを作成する。

3.ブラウザへアクセス
無事Railsサーバーが立ち上がり、エラーが出ずにアクセスすることが出来た。

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

Ruby on Railsを使って個人開発SQL編

Ruby on Rails で個人開発

【目標】
Ruby on Railsを使って簡単なWebサイトを作成する

今回は、RailsにPostgreSQLを使用するために少しつまずいた部分を書きます。

【前提】
・Vagrant上にRails、PostgreSQLをインストール済み
・rails new #{アプリケーション名}を実行し、railsサーバーを立ち上げる
・ブラウザにアクセスすると、role "vagrant" does not exist の文字

解決方法

1.sudo -u postgres createuser --createdb vagrant を実行
postgres というユーザ名でPostgreSQLにログインする。
そしてcreatedb vagrant でDBを作成する。
これはcreatedbのrole権限の登録を行っているはず。

2.rails db:create を実行
データベースを作成する。

3.ブラウザへアクセス
無事Railsサーバーが立ち上がり、エラーが出ずにアクセスすることが出来た。

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

カラムのデータ型の変更(Rails)

環境

windows(64bit)
ruby 2.6.3p62
Rails 5.2.2
Cloud9上で開発

はじめに

$ rails g model ... などのコマンドでカラムを作成して、後からカラムのデータ型を間違えてしまったことに気づくことがあると思います。
そんな場合のために、この記事ではカラムのデータ型の変更についてご紹介します。

1. 空のマイグレーションファイルの作成

以下のようにして空のマイグレーションファイルを作成します。

$ rails g migration change_data_<カラム名>_to_<テーブル名>

僕の場合はEventのテーブルdateカラムのデータ型を変更したかったので次のようにしました。

$ rails g migration change_data_date_to_events

すると、以下のようなマイグレーションファイルが作成されます。

db/migrate/〇〇_chenge_data_date_to_events.rb
class ChangeDataDateToEvents < ActiveRecord::Migration[5.2]
  def change
  end
end

2. データ型を変更する

僕の場合、dateカラムのデータ型をdatetime型からstring型へ変更したかったので、上記のマイグレーションファイルを次のように変更しました。

db/migrate/〇〇_chenge_data_date_to_events.rb
  def change
    change_column :events, :date, :string
  end

「これで変更完了!」といいたいところですが、最後に一番大事なことを忘れずに!

3. データベースに変更内容を反映

$ rails db:migrate

おしまいです。
コンソールなどで変更内容が反映されているか確認してみてください。

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

DBの型 〜生年月日はdate型〜

生年月日がDBに登録されない:frowning2:

ユーザーの新規登録の部分の生年月日の部分だけDBに登録されなかったので困ってました。
ということで備忘録メモ:writing_hand_tone1:

困ってたこと

registration_controller.rb
def create
    @user = User.new(user_params)
    if @user.save
      sign_in(@user)
      session[:user_id] = @user.id
      redirect_to phonemumber_users_path
    else
      render 'new'
    end
  end

private
    def user_params
      params.require(:user).permit(:nickname, :email, :password, :password_confirmation, :last_name, :first_name, :first_name_kana, :last_name_kana, :birthday)
    end

ニックネームとかemailはDBに入るのにbirthdayだけ登録されない:sob:

解決した方法

:point_up_tone1:birthdayのカラムがstring型だったのでdate型に直した

一番最初のマイグレーションファイルに書いていたので、書き直してrake db:migrate:resetで書き換えました

t.string :nickname,           null: false
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
      t.string :last_name,          null: false
      t.string :first_name,         null: false
      t.string :last_name_kana,     null: false
      t.string :first_name_kana,    null: false
      t.date :birthday,             null: false
      t.integer :tel,               null: false, unique: true
      t.string :avatar

これでできました:hugging:

電話番号はintegerだとダメで、stringはOKだそうです。

勉強になりました:innocent:

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