20190714のRubyに関する記事は15件です。

7/14 今日学んだ事(登録機能)

会員登録機能の実装

作業内容

1.usersテーブルにnicknameカラムを追加
2.ビューを追加
3.ストロングパラメータを追加
4.バリテーションの追加

1.usersテーブルにnicknameカラムを追加する

usersテーブルにnicknameカラムをstring型で追加する

ターミナル.
$ rails g migration Addカラム名Toテーブル名 カラム名:データ型

となるので

$ rails g migration AddNicknameToUseres nickname:string

とターミナルで実行

マイグレーションの実行

ターミナル.
$ rake db:migrate

・RailsはAdd ~~ ToUsersのように最後に記述するテーブル名でカラムを追加するテーブルを判断しているので、ファイル名は正しく打ち込む

2.ビューの追加

サインアップ画面にnicknameを入力するフォームと、アイコン画像をアップロードできるフォームを作成。

・サインアップ画面はRailsのform_forメソッドを使って記述する。
・ニックネームは『テキストフィールド』のフィールドを生成する
・アイコン画像は『ファイルフィールド』のフィールドを生成する

生成方法

テキストフィールド

new.html.erb
<%= f.text_field :カラム名 %>

ファイルフィールド

new.html.erb
<%= f.file_field :カラム名 %>

のフォームタグを利用すると生成される。
※new.html.erbは新規作成するテキスト

3.ストロングパラメータを追加

まずnicknameとimageが設定されていないため、deviseのstrong_parametersに新しく許可するパラメーターを追加する必要がある。
そこで、deviseのdevise_parameter_sanitizerメソッドを使用する。

●devise_parameter_sanitizer

・strong_parametersに対してパラメータをー追加できる。
・before_actionに設定する※
・記述するのはDeviseのコントローラを継承したコントローラかもしくはApplicationController

コード

devise_parameter_sanitizer.permit(追加したいメソッドの種類, keys: [追加したいパラメーター名])

引数の値(処理)
1. :sign_up(新規登録時)
2. :sign_in(ログイン時)
3. :account_update(レコードの更新時)

複数パラメータを追加する場合

devise_parameter_sanitizer.permit(追加したいメソッドの種類, keys: [:パラメーター1, :パラメーター2,..])

『,』カンマで区切る。

注意
※直接before_actionに設定してはいけない
devise_parameter_sanitizerを呼び出すためのメソッドを作成してそのメソッドを呼び出すようにする。

application_controller.rb
before_action :configure_permitted_parameters

  def configure_permitted_parameters
    # devise_parameter_sanitizerメソッドを呼び出す
  end

すべてのコントローラがApplicationControllerを継承しているため、この記述ではすべてのコントローラのアクションの前でdevise_parameter_sanitizerメソッドが呼び出される。
devise_parameter_sanitizerメソッドはdeviseで追加されたメソッドなので、Deviseのコントローラ以外で呼び出すことができない。よって、before_actionを適応するコントローラを指定する。

before_actionではifというオプションを指定することができる。これはbefore_actionを呼び出す条件を指定するもの。今回はコントローラの種類を指定するので以下のように記述する。

application_controller.rb
before_action :メソッド名, if: :コントローラ名?

application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?

devise_controllerが動いた時のみこのアクションを事前に動かしたいので、ifオプションを利用してそのように設定する。
※configure_permitted_parametersはdeviseで利用出来るパラメーターを設定でるdevise関連のストロングパラメーターの管理メソッド。

4.バリテーションの追加

バリテーションとは
オブジェクトがDBに保存される前に、そのデータが正しいかどうかを検証する仕組み

今回やりたい事

・nicknameが入力されていないとエラーがでるようにバリテーションを設定する。
・nicknameの入力が必須と分かるようテキストフィールドにプレイスホルダーを設定する

●validation(検証)

入力フォームを通じてビューからサーバーへ側へパラメーターが送られてきた際、正常な値か検証できる機能。

●validates :カラム名, presence: true

フォームの中身があるか検出し、無い場合は保存しない。

userのnicknameの入力を必須にする場合

user.rb
class User < ApplicationRecord
    validates :nickname, presence: true

と記述する。

● placeholder: ''

読み:(プレイスホルダー)

・フォームの値が空の時に、''で囲んだ文字を薄く表示しておくことができる。
・入力する人が、何を入力するか分かりやすくなる
今回はnicknameの入力欄に反映させたいので、text_fieldメソッドのオプションとして、以下のように利用する。

new.html.erb
<%= f.text field :nickname, placeholder:'ニックネームを入力(必須)' %>

・ニックネームを設定していないとエラーがでるようにした
・ニックネームの入力欄にプレイスホルダーを設定できた

以上

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

Rails newするときの流れ

新しくRailsプロジェクトを作る時の流れをメモとしてざっくり残しておきます。

- Railsをインストール -

$ mkdir ○◯◯
$ cd ◯◯◯
$ bundle init

bundle initによりGemfileが作成されるので、Gemfileを編集。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

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

gem "rails" #ここのコメントアウトを外す

そしたら、
$ bundle install --path vendor/bundle
--pathでパスを指定することによって、指定されたパスにRailsがインストールされる。

- Rails new する -

bundle exec rails new . -B -d mysql -T
Rails new。
Overwrite /Users/apple/Desktop/App/Gemfile? (enter "h" for help) [Ynaqdhm]と、Gemfileを上書きしていいか聞かれるので、Yes。
Rspecを使いたいのでテストはスキップ。

- DBを作成する -

usernameとpasswordを指定。

config/database.yml
  default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: #ユーザー名
  password: #パスワード
  socket: /tmp/mysql.sock

ターミナルで

$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"

この二つのコマンドを実行し、もう一度bundle installします。

そしてbin/rails db:create

上記の2つのコマンドを入力してbundle installという工程を飛ばすとエラーが出ます(自分の環境だと)。

- Rspecの設定をする -

Gemfile
・・・
group :development, :test do
 gem 'rspec-rails' #それぞれ、必要があればバージョン指定をする。
 gem 'factory_bot_rails'
 gem 'spring-commands-rspec'
end
・・・

Gemfileに上記の3つを追加し、bundle install

$ bin/rails generate rspec:install
RSpecをインストール。

.rspec
--require spec_helper
--format documentation #この行を追加するとテストを実行した際にログが読みやすくなる。

次は$ bundle exec spring binstub rspec
このコマンドを打つとbin/rspecコマンドでテストが実行できるようになる。

config/application.rb
module Test
 class Application < Rails::Application
 ...
  config.generators do |g|
   g.test_framework :rspec,
   fixtures: true,
   view_specs: false,
   helper_specs: false,
   routing_specs: false,
   request_specs: false
  end
 ...
 end
end

上記の行を追加。
これでRSpecのセットアップは完了です。

- Slimを導入する -

Gemfile
...
gem 'slim-rails'
gem 'html2slim'

bundle install でgemをインストール。
bundle exec erb2slim app/views/layouts --delete
erbファイルを削除。

- Bootstrapをインストール -

Gemfile
gem 'bootstrap;

bundle install

$ rm app/assets/stylesheets/application.css
sassを使うために、application.cssを削除。
application.scssを作成し、

application.scss
@import 'bootstrap';

追加。

- i18nで日本語化 -

$ wget https://raw.githubusercontent.com/svenfuchs/rails-18n/master/rails/locale/ja.yml --output-document=config/locales/ja.yml

rawファイルをja.ymlにダウンロード。
config/initializers下にlocale.rbファイルを作成。

config/initializers/locale.rb
 I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
 I18n.config.available_locales = :ja
 I18n.default_locale = :ja

これで完了です。

- 終わり -

こんな感じで一通り終了です。
いつも忘れてしまうので、ザックリまとめてみました。
ここをこうした方が良いよとか、アドバイスがあったら教えていただけると嬉しいです。

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

rails ページネーションの使い方

はじめに

ページネーションとは

1ページに表示する情報が多いときに、分割して複数のページを作成しアクセスをしやすくすることです。

Googleで検索したとき、検索結果が多いと複数のページに渡って検索結果があると思うのですが、あれがページネーションになります。ページ下のGoooooooooogleってなってるやつです。

実装方法

kaminari

Gemの一種である「kaminari」をインストールすることで、ページネーションを簡単に実装できます。

kaminariのインストール

以下のように、Gemfileにkaminariを記述し、ターミナルでbundle installを実行します。

Gemfile
gem 'kaminari'
$ bundle install

これでページネーションを実装するためのメソッドを使用できるようになります。

ファイルの編集

ページネーションを実装するにはコントローラーファイル、ビューファイルを編集する必要があります。

編集の例としては以下のようになります。

コントローラーファイル編集例
class TweetsController < ApplicationController
  def index
   @tweets = Tweet.all.page(params[:page]).per(5)
  end
end

ここでページネーションのために使用するメソッドはpageメソッドとperメソッドになります。

perメソッド

perメソッドの引数には1ページに表示したい要素の数を持たせます。つまり1ページに表示する検索結果を決めることができる。

pageメソッド

kaminariをインストールすることでparamsにpageのキーが追加されます。そのためpageメソッドの引数にparams[:page]を持たせることで現在のページ番号を指定できます。

ビューファイル編集例
 <%= paginate(@tweets) %>

ビューファイルには以上の記述をすることで記述をした場所にページネーションのリンクを表示させることができます。

おわりに

ページネーションが使えるようになるとサイト作ってるなぁって感じがします笑

なんでGemの名前がkaminariなのか気になります。
わかったらまた追記したいと思います。

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

RailsアプリをHerokuにデプロイ(Nginx・Unicorn使用)

目次

  1. エラーとの遭遇
  2. デプロイまでの手順
  3. Nginxの設定
  4. Unicornの設定
  5. デプロイ
  6. Nginxの確認方法
  7. まとめ

1. エラーとの遭遇

めっちゃニッチな需要だけど、簡単なBlogのCRUDを実装したRailsアプリをHerokuにデプロイする際にNginxとUnicornを使用したい状況があった。

これまではheroku-buildpack-nginxのREADMEにあるExisting AppNew App通りにやればできたハズ、、、
だったけど、この前同じ手順でデプロイをしたところ、App crushedが出てしまった...

まあ、herokuのrestartでもすればなおるだろうと試してみたが、ダメだった?‍♀️

とりあえずログを見ようと$ heroku logs -tを試したらH10の文字が...
H10てたまに出るけど、エラーを直すだけでH10自体の意味をあまり考えたことがなかった。
H10 - App crashed
なるほど...??

確かweb dynoってHTTPのリクエストとかを捌いてんだっけ、、って感じになったので原因を探ってみることにした。

H10が出た時はとりあえず下記コマンドからconsoleを開いて、BlogをcountしてみたりするとよくみるRailsのエラーが表示されることがある。

Terminal
$ heroku run rails c

・・・エラーが出ない?

じゃあCRUD機能は問題ないと仮定して、どこがダメなのかな〜とあちこち確認を始めた。

この時はまだ余裕があったので、とりあえずProcfileとかconfig/の中とか確認してみればいっかなーとか考えてbuildpackのドキュメント通りにconfigファイル書くときにタイポでもしたかと目を皿にして確認。

・・・ちゃんとできてる?

早いけどこの辺で長期化を覚悟し始めた

  • buildpackの読み込みの順番を変えてみる
  • Gemfileの確認
  • herokuのログの再確認
  • rails newから再度やり直し

とかそれぞれ何回もやったけど、ダメだった。
herokuのログには下記の内容が毎秒吐き出されてる...???

2019-07-14T02:22:44.859554+00:00 app[web.1]: buildpack=nginx at=app-initialization
2019-07-14T02:22:45.861308+00:00 app[web.1]: buildpack=nginx at=app-initialization
2019-07-14T02:22:46.863727+00:00 app[web.1]: buildpack=nginx at=app-initialization

ここを確認してみたけど、多分buildpackに従ってNginxを使おうとしてるけど、肝心のNginxが反応ないのかな...?
とか生意気に仮定は立てるけど、どうしたら動くのかが思いつかない...
ここまでで既に数時間経っていたので、先輩に聞いてみた。

半日くらいして、眠気で目を血走らせた先輩から「できた」と言われ、マジかと思いつつ確認してみると

確かにできてる...?

よーわかんないけどを連発しながら仮定を説明してもらった。(自分も本当によーわからんかった。??)

とりあえず言われた通り、下記のようにしたらうまく動くようになりました。

2. デプロイまでの手順

  • Railsのプロジェクト内で下記を実行。
Terminal
$ heroku create # herokuにアプリ枠を作成

$ heroku buildpacks:add heroku/ruby # Rubyのbuildpackを追加

$ heroku buildpacks:add https://github.com/heroku/heroku-buildpack-nginx # HerokuでNginxを使用するbuildpackの追加


ここまでは今までやってきたことと一緒 ?‍♀️

3. Nginxのconfigurationファイルの作成

ここからが、先輩から教えてもらって追加した設定☝️

  • config/nginx.confを作成する。

そういえば今までNginxの設定は特にしてなかったな...?とか思いながら言われた通りにやる。

下記のコードはこの記事からココを参考にしたみたい。(この人の記事よくみる気がする...?)

nginx.conf
worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>;

events {
  use epoll;
  accept_mutex on;
  worker_connections 1024;
}

http {
  gzip on;
  gzip_comp_level 3;
  gzip_min_length 150;
  gzip_proxied any;
  gzip_types text/plain text/css text/json text/javascript
    application/javascript application/x-javascript application/json
    application/rss+xml application/vnd.ms-fontobject application/x-font-ttf
    application/xml font/opentype image/svg+xml text/xml;

  server_tokens off;

  log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id';
  access_log logs/nginx/access.log l2met;
  error_log logs/nginx/error.log;

  include mime.types;
  default_type application/octet-stream;
  sendfile on;

  # Must read the body in 5 seconds.
  client_body_timeout 5;

  upstream app_server {
    server unix:/tmp/nginx.socket fail_timeout=0;
  }

  server {
    listen <%= ENV["PORT"] %>;
    server_name _;
    keepalive_timeout 5;

    root /app/public; # path to your app

    location / {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass http://app_server;
    }

    location ~* ^/documentation/(.*) {
      set $s3_bucket        'my_bucket.s3-website-us-east-1.amazonaws.com';
      set $url_full         '$1';
      resolver              8.8.8.8 valid=300s;
      resolver_timeout      10s;

      index index.html;

      proxy_hide_header       x-amz-id-2;
      proxy_hide_header       x-amz-request-id;
      proxy_hide_header       Set-Cookie;
      proxy_set_header        X-Real-IP $remote_addr;
      proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header        Host $s3_bucket;
      proxy_ignore_headers    "Set-Cookie";
      proxy_buffering         off;
      proxy_intercept_errors  on;
      proxy_redirect          off;
      proxy_pass              http://$s3_bucket/$url_full;
    }

    location ~* \.(eot|oft|svg|ttf|woff)$ {
      add_header Access-Control-Allow-Origin *;
      expires max;
      log_not_found off;
      access_log off;
      add_header Cache-Control public;
    }

    location ~* ^/assets/ {
      gzip_static on;

      # Per RFC2616 - 1 year maximum expiry
      expires 1y;
      add_header Cache-Control public;

      # Some browsers still send conditional-GET requests if there's a
      # Last-Modified header or an ETag header even if they haven't
      # reached the expiry date sent in the Expires header.
      add_header Last-Modified "";
      add_header ETag "";
      break;
    }
  }
}

先輩曰く必要ない記述もありそうとのこと。とりあえず動かすだけだから今は気にしないでおく。

4. Unicornの設定

  • Gemfileからpumaを削除し、unicornを追記
Gemfile
gem 'unicorn'
  • pumaは使用しないのでconfig/puma.rbを削除する。

  • Gemfile.lock更新のため、$ bundleしておく

ここも元々やってたことと一緒だった ?‍♀️

  • config/unicorn.rbを作成する。

ここもこれまでと一緒 ?‍♀️...と思っていたらちょっと違った。

これまではbuildpackドキュメントのUpdate Unicorn Configを参考にしてたけど、下記のようにするみたい。

unicorn.rb
worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
timeout 15
preload_app true
worker_processes 4
listen 'unix:///tmp/nginx.socket', backlog: 1024

before_fork do |server,worker|
    FileUtils.touch('/tmp/app-initialized')
end

FileUtils.touchについては、さっきの記事Configuring Pumaを読む限り、それぞれの順番を守もらうための設定らしい。?

こんな感じ?

The start-nginx script from the Nginx Buildpack will wait until the /tmp/app-initialized file is present before Nginx binds to the public port.

アプリケーションサーバの起動を待ってからNginxと連携するみたい。

  • 次に、Procfileを作成する。

config/unicorn.rbを読み込む設定。

Procfile
web: bin/start-nginx bundle exec unicorn -c config/unicorn.rb

5.デプロイ

やっとデプロイ??

ここはいつも通り、addしてcommitしてpushすればOK?‍♀️

  • herokuでDBを作成する。
Terminal
$ heroku run rake db:migrate RAILS_ENV=production

これもいつも通りなはず。

ここまでできたら、あとは$ heroku openなどでアプリを開いて実際に動作確認。
ここでエラーが出ている場合は、Railsアプリ内で何か間違っているか、ここまでの手順に何か抜け漏れがあるハズ?

6. Nginxの確認方法

しっかりとheroku上でアプリが動くことを確認できたら、いよいよNginxがしっかり動いているか確認してみる。

確認方法は複数あると覆いますが、ここではwgetコマンドを使用して確認していきます。

下記を実行してみると、下記のような結果が返ってくるハズ。

Terminal
$ wget -S --spider <HerokuアプリのURL>

Spider mode enabled. Check if remote file exists.
--2019-07-14 17:29:09-- <HerokuアプリのURL>
Resolving  <HerokuアプリのURL>...
Connecting to  <HerokuアプリのURL>|... connected.
HTTP request sent, awaiting response...
  HTTP/1.1 200 OK
  Connection: keep-alive
  Server: nginx    # ここにNginxが表示される。
  Date: Sun, 14 Jul 2019 08:29:09 GMT
  Content-Type: text/html; charset=utf-8
  X-Frame-Options: SAMEORIGIN
  X-Xss-Protection: 1; mode=block
  X-Content-Type-Options: nosniff
  X-Download-Options: noopen
  X-Permitted-Cross-Domain-Policies: none
  Referrer-Policy: strict-origin-when-cross-origin
  Cache-Control: max-age=0, private, must-revalidate
  X-Runtime: 0.090119
  Via: 1.1 vegur
Length: unspecified [text/html]
Remote file exists and could contain further links,
but recursion is disabled -- not retrieving.

Spiderモードだって?
カッコいいけどよく分かんないから調べてみると、こんな感じらしい。
--spiderはwgetコマンドのオプションみたい。これまで何も考えずに使ってた?

ファイルをダウンロードせず、URLの存在だけチェックする(“Web spider”として動作する)。例えば、ブックマークをチェックするなら「wget --spider --force-html -i bookmarks.html」のように指定する

  • wgetコマンドが使用できない場合は、brewからインストールしましょう。

7. まとめ

長くなりましたが、これでHerokuにデプロイしたアプリでNginxが動いていることがしっかりと確認できました!

先輩のおかげで助かった...
自分だけだと多分ここまでくるのに何日もかかったハズ?

※今回herokuにデプロイするために作成したRailsプロジェクト
READMEにも簡潔に一連の内容を書いてみました。
https://github.com/kawaaa26/herokuapp_nginx

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

ReactでHTMLを作成し、Clickした際にそれぞれの情報をモーダルに表示・非表示させる(Railsで)

はじめに

ReactでHTMLを作成するまでは案外すんなりと行ったのですが、クリックした際に「それぞれの情報を持たせてモーダルを表示する」ことに苦労したので忘備録としてまとめておきます。
最終的にはこのような感じになります。
たくさんの方法を試したので不必要なものもあるかもしれません。
その点は指摘していただけると幸いです。
下の画像をクリックした際にその情報を表示させています。
表示させる内容は個人で変更してください。
react modal.jpg
876747d1b80a09753cfdb7b53b817923.jpg

1.gemの導入とインストール

この2つのgemをGemfileに加え、bundle installしてください

Gemfile
gem 'react-rails'
gem 'webpacker'

その後、それぞれインストールしてください

Terminal
$ bundle install
$ rails webpacker:install  
$ rails webpacker:install:react 
$ rails generate react:install

するとapp以下にapp/javascript/componentsフォルダが作成されます。
私自身、app/assets以下にJavascriptフォルダがあるのに大丈夫なのかと思いましたが、問題ありません。

2. application.html.hamlにtagを追加

application.html.hamlに以下の記述を加えてください

application.html.haml
= javascript_pack_tag 'application'

この際にTerminalでyarnがどうこうというエラーが出るかもしれません。
その場合$ yarn install等、各自で調べて解決してください。

これらでRails上でReactを使う準備ができましたので、いよいよjsファイルに記述していきます。

3. Components以下にjsファイルの作成

Components以下にApp.jsGraduate.jsを作成します。
これらの名前は自分で決めてくださって結構です。
ではこちらのコードをコピーしてください

App.js
import React from 'react';
import Graduate from './Graduate';

const lessonList = [
  {
    name: '開成太郎(2017)',
    school: '一条高校',
    image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg',
    introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました',
  },
  {
    name: '開成花子(2016)',
    school: '奈良高校',
    image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg',
    introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました',
  },
  {
    name: '開成三郎(2019)',
    school: '郡山高校',
    image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg',
    introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。',
  },
  {
    name: '開成四郎(2015)',
    school: '登美ケ丘高校',
    image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg',
    introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。',
  }
];

class App extends React.Component {
  render() {
    return (
        <div className="performance">
        {lessonList.map((lessonItem) => {
      return (
          <Graduate
            name={lessonItem.name}
            image={lessonItem.image}
            school={lessonItem.school}
            introduction={lessonItem.introduction}
            />
      );
    })}
      </div>
    );
  }
}

export default App;

それでは解説していきます。

import React from 'react';
import Graduate from './Graduate';

まずimportとは輸入という意味です。
その名の通り、一行目ではReactを二行目ではGraduateファイルを読み込んでいます。
このおかげでReactを使うことができ、またGraduateにパラメーター(props)を渡すことができます。

const lessonList = [
  {
    name: '開成太郎(2017)',
    school: '一条高校',
    image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg',
    introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました',
  },
  {
    name: '開成花子(2016)',
    school: '奈良高校',
    image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg',
    introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました',
  },
  {
    name: '開成三郎(2019)',
    school: '郡山高校',
    image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg',
    introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。',
  },
  {
    name: '開成四郎(2015)',
    school: '登美ケ丘高校',
    image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg',
    introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。',
  }
];

この部分ではそれぞれのハッシュを配列に入れています。これをあとでmapメソッドで一つずづ表示させていきます。

class App extends React.Component {
  render() {
    return (
        <div className="performance">
        {lessonList.map((lessonItem) => {
      return (
          <Graduate
            name={lessonItem.name}
            image={lessonItem.image}
            school={lessonItem.school}
            introduction={lessonItem.introduction}
            />
      );
    })}
      </div>
    );
  }
}

この部分でComponentを作成しています。(extendsは広げるという意味)
ConmonentはJavascriptの関数のようなものです。
その中のreturnでHTMLを返し、表示させています。またこのような記法をJSXと言います。
また、JSXには約束事があり、複数の要素を返すことができません。なので図の場合、performanceクラスを親要素としその中に色々と要素を追加しています。

{lessonList.map((lessonItem)ではlessonListの数だけ繰り返し、returnを読み込んでいます。
return内でとすることでGraduateコンポーネントを呼び出しています。これが可能なのはimport Graduate from './Graduate';のおかげです。
sosite
呼び出す際にはname,school,image,introductionのパラメーターを渡しています。(props)
このパラメーターをGraduate.js側で使うわけです。ちなみにJSX内でjavascriptの記法を用いる際は中括弧が必要なため、中括弧内に書いてあります。

export default App;

App.jsの最後の行ですが、exportとは輸出という意味です。
これのおかげでHTMLファイルでApp.jsを呼び出すことができ、returnとして要素をHTMLに追加することができます。App.jsがGraduateを二行目でimportできているのもGraduate.jsで最後にexport default Graduate;としているからです。

Graduate.js
import React from 'react';

class Graduate extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isModalOpen: false};
  }

  handleClickLesson() {
    this.setState({isModalOpen: true});
  }

  handleClickClose() {
    this.setState({isModalOpen: false});
  }
  render() {
    let modal;
    if (this.state.isModalOpen) {
      modal = (
        <div className='modal-area'>
        <div className='modal-inner'>
          <div className='modal-header'></div>
          <div className='modal-introduction'>
            <h2>{this.props.name}</h2>
            <p>{this.props.introduction}</p>
          </div>
          <button
            className='modal-close-btn'
            onClick={() => this.handleClickClose()}
          >
            とじる
          </button>
        </div>
      </div>
      )
    };

    return (
      <div className="graduate">
          <img className="graduate__image" src={this.props.image}
               onClick={() => {this.handleClickLesson()}}
          />
          <div className="graduate__school">{this.props.school} 合格!</div>
          <div className="graduate__student">{this.props.name}</div>
          {modal}
      </div>
    );
  }
}

export default Graduate;

この部分で、実際に表示されるHTMLを作成しています。App.jsから渡されたパラメータ(props)を使って書いていきます。

constructor(props) {
    super(props);
    this.state = {isModalOpen: false};
  }

新しくstate(状態)というものが出てきましたが、propsとstateは少し異なり、stateはそのComponent内で保持されるものであって、propsみたいにComponentからComponentに渡すことはできません。詳しくはこちら
この部分でモーダルの表示・非表示を管理しています。

  handleClickLesson() {
    this.setState({isModalOpen: true});
  }

  handleClickClose() {
    this.setState({isModalOpen: false});
  }

ここで先ほどのstateを関数(handleClickLesson or handleClickClose)が呼ばれた時に更新しています。この時に注意して欲しいのが、更新する際はsetStateとしないといけないことです。

let modal;
    if (this.state.isModalOpen) {
      modal = (
        <div className='modal-area'>
        <div className='modal-inner'>
          <div className='modal-header'></div>
          <div className='modal-introduction'>
            <h2>{this.props.name}</h2>
            <p>{this.props.introduction}</p>
          </div>
          <button
            className='modal-close-btn'
            onClick={() => this.handleClickClose()}
          >
            とじる
          </button>
        </div>
      </div>
      )
    };

ここでisModalOpenがtrueの場合のみ、変数modalに値を代入し、表示させます。
そしてモーダルの中にonClick={() => this.handleClickClose()}があると思いますが、このボタンを押すことでisModalOpenがfalseになり再び非表示になります。
またApp.jsからもらってきたパラメータ(props)を{this.props.name}とすることで代入することができます。
またReactではclassをclassNameと記載します。

return (
      <div className="graduate">
          <img className="graduate__image" src={this.props.image}
               onClick={() => {this.handleClickLesson()}}
          />
          <div className="graduate__school">{this.props.school} 合格!</div>
          <div className="graduate__student">{this.props.name}</div>
          {modal}
      </div>
    );

最後ですね。
この部分で常時表示させるHTMLをApp.jsからもらったpropsを使って作成しています。
そして{modal}の部分で先ほどの変数を代入しているわけですね。imgクラスにonClickが設定されているため、画像をクリックするとisModalOpenがtrueになり、値が代入されたmodalが表示されるわけです。
では最後にHTML側でApp.jsを呼んであげましよう。

hamlの場合は

index.haml.haml
= react_component("App")

htmlの場合は

index.html.erb
<%= react_component("App") %>

これで完成です。
CSSだけ記載しておきます。
お好みで変更してください。

stylesheet.css
.performance {
    .graduate {
      padding: 30px 0;
      display: inline-block;
      width: 25%;
      text-align: center;
      &__student {
        font-size: 15px;
        padding-top: 10px;
        text-align: right;
        padding-right: 20px;
      }
      &__school {
        font-size: 20px;
        padding-top: 10px;
      }
      &__image {
        cursor: pointer;
        height: 160px;
        width: 160px;
        border-radius: 50%;
      }
      .modal-area {
        z-index: 2;
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        background-color: rgba(0, 0, 0, 0.6);
        .modal-inner {
          position: absolute;
          top: 8%;
          right: 0;
          left: 0;
          width: 480px;
          padding-bottom: 60px;
          margin: auto;
          background-color: rgb(255, 255, 255);
          .modal-header {
            margin-bottom: 60px;
          }
          .modal-introduction p {
            color: #5876a3;
            width: 384px;
            line-height: 32px;
            text-align: left;
            margin: 36px auto 40px;
          }
          .modal-close-btn {
            font-size: 13px;
            color: #8491a5;
            width: 200px;
            padding: 16px 0;
            border: 0;
            background-color: #f0f4f9;
            cursor: pointer;
          }
          .modal-close-btn:hover {
            color: #8491a5;
            background-color: #ccd9ea;
            transition: .3s ease-in-out;
          }
        }
      }
    }
  }

番外編

Click時にモーダルが表示されるが非表示にならない場合

モーダル実装時にモーダルが閉じないというバグが起こりました。
原因を調べてみると閉じるボタンを押した際に一回閉じてから再度開いています。
これがその時のコードです。

<div className="graduate"
     onClick={() => {this.handleClickLesson()}}>
          <img className="graduate__image" src={this.props.image}/>
          <div className="graduate__school">{this.props.school} 合格!</div>
          <div className="graduate__student">{this.props.name}</div>
          {modal}
      </div>

何が問題かというと一番の親要素であるgraduateにopenmodalを設定しているせいで、その子要素であるモーダル内のclossmodalを押した際に同時に親要素のopenmodalも呼ばれてしまうからです。
なのでそれぞれのClick機能は親要素、子要素の関係に注意しましょう。

参考資料

React公式HP
Progate
https://qiita.com/k-penguin-sato/items/e3cc04f787cf3254cfae
https://qiita.com/kyrieleison/items/78b3295ff3f37969ab50

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

RailsとElasticsearchで検索機能をつくり色々試してみる - サジェスト機能の追加

はじめに

elasticsearch-railsを使うことが前提の記事になります。記事の中で出てくるサンプルや環境は、RailsとElasticsearchで検索機能をつくり色々試してみる - その1:サンプルアプリケーションの作成をもとにしています。

サジェストを実装する上でのElasticsearchのデータスキーマやアナライザー等の設計部分はElasticsearch キーワードサジェスト日本語のための設計の記事を参考にさせてもらっています。

本記事では、サジェスト機能をelasticsearch-railsを使ってRailsアプリケーションに追加していく方法に焦点を当てて紹介していきます。最終的なソースコードはGitHubに上げておきます。

完成イメージ

suggest.mov.gif

こんな感じで検索窓に2文字以上入力すると候補となるキーワードをサジェストする機能を追加します。

全体像

検索履歴を保存してサジェストワードとして使用

実際のアプリケーションを想定してユーザーの検索したキーワードをサジェストワードとして使用するようにします。
要件によっては検索してほしいキーワードをサジェストワードに登録していく方法などいろんな方法がが考えられますが今回はこの方法でいきます。

キーワードの登録.png

Railsアプリを経由してサジェストワードを取得

ユーザーの環境からElasticsearchが公開されているような場合は、ブラウザから直接Elasticsearchにリクエストを送ることも考えられますが、今回はRailsアプリケーションのみからElasticsearchが公開されている場合を想定して、Railsアプリケーションを経由してサジェストワードを返却していきます。

サジェスト_リクエストの流れ.png

検索ワードの保存

ここから実装に入っていきます。まずは検索ワードを保存するテーブルを追加し保存していきます。
後ほどElasticsearchにキーワードを登録する際に、検索にひっかからないワードはサジェストのワードからは除外できるように検索されたワードだけでなくhitした件数も保存するようにします。

保存用のテーブル追加

$ bundle exec rails g migration create_search_word_log

キーワードとhitした件数を保存するカラムを追加します。

db/migrate/20190601132134_create_search_word_log.rb
class CreateSearchWordLog < ActiveRecord::Migration[5.2]
  def change
    create_table :search_word_logs do |t|
      t.string :word
      t.integer :hit_number

      t.timestamps
    end
  end
end
$ bundle exec rails db:migrate

対応するモデルを追加します。

app/models/search_word_log.rb
class SearchWordLog < ApplicationRecord
end

保存処理追加

コントローラに検索履歴をためるための処理を追加していきます。

app/controllers/mangas_controller.rb
class MangasController < ApplicationController
  before_action :set_manga, only: [:show, :edit, :update, :destroy]
  + after_action :save_search_log, only: [:index]

  def index
    @mangas = if search_word.present?
                Manga.es_search(search_word).page(params[:page] || 1).per(5).records
              else
                Manga.page(params[:page] || 1).per(5)
              end
  end

  private

 +  def save_search_log
 +    return if search_word.blank?

 +    SearchWordLog.create(word: search_word, hit_number: @mangas.size)
 +  end

サジェスト用のindexの定義と検索クエリの作成

ここまででサジェストに登録するワードの準備できたので、Elasticsearchにどうのように登録していくかの定義と同時に検索のクエリを追加していきます。
検索機能で追加したものと同様にconcernを作成します。

app/models/concerns/search_word_log_searchable.rb
module SearchWordLogSearchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model

    # a. サジェスト用のindex
    index_name "es_search_log_#{Rails.env}"

    # b. self.analyzer_settingsで下のほうに定義したanalyzerを使えるようにする。
    settings analysis: self.analyzer_settings do
      mappings dynamic: 'false' do
        indexes :id, type: 'text', analyzer: 'kuromoji'
        # c. マルチフィールドを定義する
        indexes :word, type: 'text', fielddata: true, analyzer: 'keyword_analyzer' do
          indexes :autocomplete, type: 'text', analyzer: 'autocomplete_index_analyzer', search_analyzer: 'autocomplete_search_analyzer'
          indexes :readingform,  type: 'text', analyzer: 'readingform_index_analyzer',  search_analyzer: 'readingform_search_analyzer'
        end
        indexes :hit_number,     type: 'integer'
        indexes :created_at,     type: 'date', format: 'YYYY-MM-dd kk:mm:ss'
      end
    end

    def as_indexed_json(*)
      attributes
        .symbolize_keys
        .slice(:id, :word, :hit_number)
        .merge(
          created_at: created_at.strftime("%Y-%m-%d %H:%M:%S")
        )
    end
  end

  class_methods do
    def create_index!
      client = __elasticsearch__.client
      client.indices.delete index: self.index_name rescue nil
      client.indices.create(index: self.index_name,
                            body: {
                                settings: self.settings.to_hash,
                                mappings: self.mappings.to_hash
                            })
    end

    # d. 検索クエリの定義
    def es_search(query)
      __elasticsearch__.search({
        size: 0,
        query: {
          bool: {
            should: [{
              match: {
                "word.autocomplete" => {
                  query: query
                }
              }
            }, {
              match: {
                "word.readingform" => {
                  query: query,
                  fuzziness: "AUTO",
                  operator: "and"
                }
              }
            }]
          }
        },
        aggs: {
          keywords: {
            terms: {
              field: "word",
              order: {
                _count: "desc"
              },
              size: "10"
            }
          }
        }
      })
    end

   # e. analyzerの定義
    def analyzer_settings
      {
        analyzer: {
          keyword_analyzer: {
            type: "custom",
            char_filter: ["normalize", "whitespaces"],
            tokenizer: "keyword",
            filter: ["lowercase", "trim", "maxlength"]
          },
          autocomplete_index_analyzer: {
            type: "custom",
            char_filter: ["normalize", "whitespaces"],
            tokenizer: "keyword",
            filter: ["lowercase", "trim", "maxlength", "engram"]
          },
          autocomplete_search_analyzer: {
            type: "custom",
            char_filter: ["normalize", "whitespaces"],
            tokenizer: "keyword",
            filter: ["lowercase", "trim", "maxlength"]
          },
          readingform_index_analyzer: {
            type: "custom",
            char_filter: ["normalize", "whitespaces"],
            tokenizer: "japanese_normal",
            filter: ["lowercase", "trim", "readingform", "asciifolding", "maxlength", "engram"]
          },
          readingform_search_analyzer:  {
            type: "custom",
            char_filter: ["normalize", "whitespaces", "katakana", "romaji"],
            tokenizer: "japanese_normal",
            filter: ["lowercase", "trim", "maxlength", "readingform", "asciifolding"]
          },
        },
        filter: {
          readingform: {
            type: "kuromoji_readingform",
            use_romaji: true
          },
          engram: {
            type: "edgeNGram",
            min_gram: 1,
            max_gram: 36
          },
          maxlength: {
            type: "length",
            max: 36
          }
        },
        char_filter: {
          normalize: {
            type: "icu_normalizer",
            name: "nfkc_cf",
            mode: "compose",
          },
          katakana: {
            type: "mapping",
            mappings: [
              "ぁ=>ァ", "ぃ=>ィ", "ぅ=>ゥ", "ぇ=>ェ", "ぉ=>ォ",
              "っ=>ッ", "ゃ=>ャ", "ゅ=>ュ", "ょ=>ョ",
              "が=>ガ", "ぎ=>ギ", "ぐ=>グ", "げ=>ゲ", "ご=>ゴ",
              "ざ=>ザ", "じ=>ジ", "ず=>ズ", "ぜ=>ゼ", "ぞ=>ゾ",
              "だ=>ダ", "ぢ=>ヂ", "づ=>ヅ", "で=>デ", "ど=>ド",
              "ば=>バ", "び=>ビ", "ぶ=>ブ", "べ=>ベ", "ぼ=>ボ",
              "ぱ=>パ", "ぴ=>ピ", "ぷ=>プ", "ぺ=>ペ", "ぽ=>ポ",
              "ゔ=>ヴ",
              "あ=>ア", "い=>イ", "う=>ウ", "え=>エ", "お=>オ",
              "か=>カ", "き=>キ", "く=>ク", "け=>ケ", "こ=>コ",
              "さ=>サ", "し=>シ", "す=>ス", "せ=>セ", "そ=>ソ",
              "た=>タ", "ち=>チ", "つ=>ツ", "て=>テ", "と=>ト",
              "な=>ナ", "に=>ニ", "ぬ=>ヌ", "ね=>ネ", "の=>ノ",
              "は=>ハ", "ひ=>ヒ", "ふ=>フ", "へ=>ヘ", "ほ=>ホ",
              "ま=>マ", "み=>ミ", "む=>ム", "め=>メ", "も=>モ",
              "や=>ヤ", "ゆ=>ユ", "よ=>ヨ",
              "ら=>ラ", "り=>リ", "る=>ル", "れ=>レ", "ろ=>ロ",
              "わ=>ワ", "を=>ヲ", "ん=>ン"
            ]
          },
          romaji: {
            type: "mapping",
            mappings: [
              "キャ=>kya", "キュ=>kyu", "キョ=>kyo",
              "シャ=>sha", "シュ=>shu", "ショ=>sho",
              "チャ=>cha", "チュ=>chu", "チョ=>cho",
              "ニャ=>nya", "ニュ=>nyu", "ニョ=>nyo",
              "ヒャ=>hya", "ヒュ=>hyu", "ヒョ=>hyo",
              "ミャ=>mya", "ミュ=>myu", "ミョ=>myo",
              "リャ=>rya", "リュ=>ryu", "リョ=>ryo",
              "ファ=>fa", "フィ=>fi", "フェ=>fe", "フォ=>fo",
              "ギャ=>gya", "ギュ=>gyu", "ギョ=>gyo",
              "ジャ=>ja", "ジュ=>ju", "ジョ=>jo",
              "ヂャ=>ja", "ヂュ=>ju", "ヂョ=>jo",
              "ビャ=>bya", "ビュ=>byu", "ビョ=>byo",
              "ヴァ=>va", "ヴィ=>vi", "ヴ=>v", "ヴェ=>ve", "ヴォ=>vo",
              "ァ=>a", "ィ=>i", "ゥ=>u", "ェ=>e", "ォ=>o",
              "ッ=>t",
              "ャ=>ya", "ュ=>yu", "ョ=>yo",
              "ガ=>ga", "ギ=>gi", "グ=>gu", "ゲ=>ge", "ゴ=>go",
              "ザ=>za", "ジ=>ji", "ズ=>zu", "ゼ=>ze", "ゾ=>zo",
              "ダ=>da", "ヂ=>ji", "ヅ=>zu", "デ=>de", "ド=>do",
              "バ=>ba", "ビ=>bi", "ブ=>bu", "ベ=>be", "ボ=>bo",
              "パ=>pa", "ピ=>pi", "プ=>pu", "ペ=>pe", "ポ=>po",
              "ア=>a", "イ=>i", "ウ=>u", "エ=>e", "オ=>o",
              "カ=>ka", "キ=>ki", "ク=>ku", "ケ=>ke", "コ=>ko",
              "サ=>sa", "シ=>shi", "ス=>su", "セ=>se", "ソ=>so",
              "タ=>ta", "チ=>chi", "ツ=>tsu", "テ=>te", "ト=>to",
              "ナ=>na", "ニ=>ni", "ヌ=>nu", "ネ=>ne", "ノ=>no",
              "ハ=>ha", "ヒ=>hi", "フ=>fu", "ヘ=>he", "ホ=>ho",
              "マ=>ma", "ミ=>mi", "ム=>mu", "メ=>me", "モ=>mo",
              "ヤ=>ya", "ユ=>yu", "ヨ=>yo",
              "ラ=>ra", "リ=>ri", "ル=>ru", "レ=>re", "ロ=>ro",
              "ワ=>wa", "ヲ=>o", "ン=>n"
            ]
          },
          whitespaces: {
            type: "pattern_replace",
            pattern: "\\s{2,}",
            replacement: "\u0020"
          },
        },
        tokenizer: {
          japanese_normal: {
            mode: "normal",
            type: "kuromoji_tokenizer"
          },
          engram: {
            type: "edgeNGram",
            min_gram: 1,
            max_gram: 36
          }
        },
      }
    end
  end
end

作成したconcernをモデルで使えるようにinclude

app/models/search_word_log.rb
class SearchWordLog < ApplicationRecord
  + include SearchWordLogSearchable
end

定義の解説

b. analyzerの登録

self.analyzer_settings の箇所を追加することで analyzer_settings メソッド内に独自に定義したアナライザーを登録しています。

c.マルチフィールドの定義

Elasticsearchへの登録、検索、読み仮名での登録、検索、検索結果の集計とキーワードの表示で別のAnalyzerを使いたいため、マルチフィールドを使いその中で登録と検索で別のAnalyzerを使うようにマッピングを定義しています。
それぞれの用途は以下の表のようになります。各Analyzerで具体的にどのようにキーワードがアナライズされるかの例を記事の最後に載せていますので詳しくはそちらを参照ください。

filed analyzer 用途
word keyword_analyzer 検索した結果の集計とサジェストとして表示する文字列を登録するためのアナライザー
word.autocomplete autocomplete_index_analyzer 検索ワードを加工して登録していくためのアナライザー
word.autocomplete autocomplete_search_analyzer 前方一致検索を行う際に使用するのアナライザー
word.readingform readingform_index_analyzer 読み仮名でhitできるように検索ワードを加工して登録していくためのアナライザー
word.readingform readingform_search_analyzer 読み仮名で前方一致検索を行う際に使用するのアナライザー

それぞれどこで使用されているかは、全体像で使った図でいうと以下のようなイメージになります。

キーワード登録時のアナライザーのイメージ

キーワードの登録.png

検索時のアナライザーのイメージ

サジェスト_リクエストの流れ.png

docker imageの修正

さきほどのanalyzerの定義でchar_filterにicu_normalizerを指定しているのでanalysis-icuのプラグインを使えるように以下を追加してイメージをビルドしておきます。

docker/es/Dockerfile
RUN bin/elasticsearch-plugin install analysis-icu

index追加

indexの定義が作成できたのでrakeタスクを追加してindexを作成します。

lib/tasks/elasticsearch.rake
namespace :elasticsearch do

 + desc 'サジェスト用のindex作成'
 + task :create_suggest_index => :environment do
 +   SearchWordLog.create_index!
 + end
end
$ bundle exec rake elasticsearch:create_suggest_index

テスト用に検索履歴を追加

画面上でぽちぽち登録していくか、スクリプトを書いて search_word_logs テーブルにデータ登録していきます。とりあえずサンプルとして動かすなら数十件ほど追加すればOKかと思います。

データがたたまったところでindexに登録していきます。

検索にhitしたキーワードのみをサジェストワードとして使いたいので、scopeを使用します。

app/models/search_word_log.rb
class SearchWordLog < ApplicationRecord
  include SearchWordLogSearchable

  + scope :searchable_word, -> {
  +   where('hit_number > 0')
  + }
end

rakeタスクを登録して実行します。

rb:lib/tasks/elasticsearch.rake
namespace :elasticsearch do


  + desc 'サジェスト用のキーワードを登録'
  + task :import_suggest_word => :environment do
  +   SearchWordLog.__elasticsearch__.import scope: 'searchable_word'
  + end
end
bundle exec rake elasticsearch:import_suggest_word

サジェストを返却するAPIの追加

GET /mangas/suggest?word={keyword} でサジェストワードを取得できるように修正していきます。

ルーティング追加

config/routes.rb
Rails.application.routes.draw do
  resources :mangas do
   + collection do
   +   get :suggest
   + end
  end
end

コントローラーにアクション追加

app/controllers/mangas_controller.rb
class MangasController < ApplicationController

  ・・・

  + def suggest
  +  # concerns に追加した `es_search` メソッドで検索
  +  suggest_words = SearchWordLog.es_search(params[:word]).aggregations["keywords"]["buckets"]
  +  render json: { suggest_words: suggest_words.map{|word| word["key"]} }
  + end

  private

  ・・・
end

ポイントとしては、今回定義したes_searchメソッドではaggregationsで集計してhitした件数が多いキーワードを取得しているため、aggregationsメソッドを使うことで結果を取り出すことができます。
es_searchで以下のような形式のjsonが返却されaggregations["keywords"]["buckets"]で対象のキーワードの配列を取得しています。

{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 6,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "keywords" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "スライム",
          "doc_count" : 3
        },
        {
          "key" : "スラムダンク",
          "doc_count" : 3
        }
      ]
    }
  }
}

View側の修正

最後にview側に修正を入れて行きます。今回はライブラリを使わずにシンプルなJavascriptを追加します。

js追加

app/assets/javascripts/suggest.js
document.addEventListener('turbolinks:load', function () { // turbolinksを使用しない場合はここは不要 
  let timeout = null;

  function selectSuggest(e) {
    document.querySelector("#search_word").value = e.target.textContent;
    document.querySelector(".SearchForm").submit();
  }

  function displayNoneSuggestList () {
    let classList = document.querySelector(".dropdown").classList;
    if (classList.contains("is-active")) {
      classList.remove("is-active")
    }
  }

  function displaySuggestList() {
    let classList = document.querySelector(".dropdown").classList;
    if (!classList.contains("is-active")) {
      classList.add("is-active")
    }
  }

  // apiのレスポンスを元にhtmlを成形してサジェストリストを表示する
  function updateSuggestWords(words) {
    if (words.length > 0) {
      const dropdownMenu = document.querySelector("#dropdown-menu");

      let wordList = "";
      words.forEach(word => {
        wordList += `<div class="dropdown-item" role="option">${word}</div>`;
      });

      const html = `<div id="sugget-list" class="dropdown-content">${wordList}</div>`;

      dropdownMenu.innerHTML = html;
      displaySuggestList();

      const suggestList = document.querySelectorAll(".dropdown-item");
      if (suggestList) {
     // サジェストワードが選択された場合にそのワードで検索を行うようにイベントリスナーに登録
        suggestList.forEach(element => {
          element.addEventListener("click", selectSuggest);
        });
      }
    } else {
      displayNoneSuggestList();
    }
  }

  function getSuggetWords(e) {
    const value = e.target.value;
    clearTimeout(timeout);
    // 2文字以上入力された場合にサジェストワードを取得
    if (value.length > 1) {
    // setTimeoutを使ってapiが呼ばれるまでに時間を置く
      timeout = setTimeout(function () {
        fetch(`http://localhost:3003/mangas/suggest?word=${value}`)
          .then(res => {
            return res.json();
          })
          .then(resJson => {
            updateSuggestWords(resJson.suggest_words);
          })
          .catch(error => console.log(error))
      }, 300);
    } else {
      displayNoneSuggestList()
    }
  }

  // 検索ワードの入力をイベントリスナーに登録
  const inputWord = document.querySelector("#search_word");
  inputWord.addEventListener("input", getSuggetWords);
});

解説

検索ワードが入力されたらapiからサジェストワードを取得してリストを更新するメソッド(getSuggetWords)をイベントリスナーに登録します。
getSuggetWordsでは入力の度にapiが呼ばれてサジェストが何度も変わるのは使いづらいので、2文字以上入力された場合に0.3秒後にapiを呼ぶようにしています。
apiからのレスポンスをupdateSuggestWordsに渡してhtmlを成型してサジェストリストを追加しています。追加するリストにイベントリスナーを登録してキーワードが選択された場合にselectSuggestメソッドを呼び検索を実行するようにしています。
このサンプルアプリではCSSは、bulmaを使用っているので、bulmaのdropdownのクラスを使ってスタイルを整えています。

view修正

jsで使用するクラスや要素を追加し、jsを読み込みます。

app/views/mangas/index.html.erb
<div class="container" style="margin-top: 30px">
-  <%= form_tag(mangas_path, method: :get, class: "field has-addons has-addons-centered") do %>
-    <div class="control">
+  <%= form_tag(mangas_path, method: :get, class: "field has-addons has-addons-centered SearchForm", autocomplete: "off") do %>
+    <div class="control SearchInput">
      <%= text_field_tag :search_word, @search_word, class: "input", placeholder: "漫画を検索する" %>
+   <div class="dropdown">
+       <div class="dropdown-menu" id="dropdown-menu" role="menu">
+       </div>
      </div>
    </div>
    <div class="control">
      <%= submit_tag "検索", class: "button is-info" %>
    </div>
  <% end %>
</div>

   ・
   ・
   ・

+ <%= javascript_include_tag 'suggest' %>

追加したjsを読み込むためにinitializer修正

config/initializers/assets.rb
+ Rails.application.config.assets.precompile += %w(suggest.js)

以上で完成です!

おまけ:analyzerの動作確認

今回追加したanalyzerが実際にどのように動作するかを「転生」という単語を例に見ていきます。

documentの登録

まずはdocument登録する際のanalyzerの動きを確認します。
「転生」という単語を登録する場合以下のようにanalyzeされてindexされています。

analyzer_indexs.png

それぞれのAnalyzer APIでの確認結果は以下のようになります。

keyword_analyzer

request
POST /es_search_log_development/_analyze
{
  "analyzer": "keyword_analyzer",
  "text": "転生"
}
response
{
  "tokens" : [
    {
      "token" : "転生",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    }
  ]
}

autocomplete_index_analyzer

request
POST /es_search_log_development/_analyze
{
  "analyzer": "autocomplete_index_analyzer",
  "text": "転生"
}
resoponse
{
  "tokens" : [
    {
      "token" : "転",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "転生",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    }
  ]
}

readingform_index_analyzer

request
POST /es_search_log_development/_analyze
{
  "analyzer": "readingform_index_analyzer",
  "text": "転生"
}
response
{
  "tokens" : [
    {
      "token" : "t",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "te",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "ten",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "tens",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "tense",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "tensei",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    }
  ]
}

documentのsearch

続いてsearch用のanalyzerの動きを見ます。
今度は「転生」、「てんせ」、「ten」のワードで確認します。
以下のイメージのようにanalyzeされています。

analyzer_seach.png

autocomplete_search_analyzer

「転生」
request
POST /es_search_log_development/_analyze
{
  "analyzer": "autocomplete_search_analyzer",
  "text": "転生"
}
{
  "tokens" : [
    {
      "token" : "転生",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    }
  ]
}
「てんせ」
request
POST /es_search_log_development/_analyze
{
  "analyzer": "autocomplete_search_analyzer",
  "text": "てんせ"
}
{
  "tokens" : [
    {
      "token" : "てんせ",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    }
  ]
}

「ten」

request
POST /es_search_log_development/_analyze
{
  "analyzer": "autocomplete_search_analyzer",
  "text": "ten"
}
{
  "tokens" : [
    {
      "token" : "ten",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    }
  ]
}

readingform_search_analyzer

「転生」
POST /es_search_log_development/_analyze
{
  "analyzer": "readingform_search_analyzer",
  "text": "転生"
}
{
  "tokens" : [
    {
      "token" : "tensei",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "word",
      "position" : 0
    }
  ]
}
「てんせ」
POST /es_search_log_development/_analyze
{
  "analyzer": "readingform_search_analyzer",
  "text": "てんせ"
}
{
  "tokens" : [
    {
      "token" : "tense",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "word",
      "position" : 0
    }
  ]
}
「ten」
POST /es_search_log_development/_analyze
{
  "analyzer": "readingform_search_analyzer",
  "text": "ten"
}
{
  "tokens" : [
    {
      "token" : "ten",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "word",
      "position" : 0
    }
  ]
}

サジェストワード検索時のイメージ

indexとsearchのanalyzerの動きをふまえると、「転生」をindexして「転生」、「てんせ」、「ten」を入力した場合、全て「転生」がサジェストされることがわかります。

suggest_image.png

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

デバックツール(pry-rails)について binding.pryの使い方

はじめに

デバックツールとは

一般的にデバックツールとはプログラムの作業を行う際に、バグの有無、エラーの原因箇所、コード上の問題がないかなどを確認するために用いるツールのことをいいます。

pry-rails

pry-railsとは

Rails用に使われるデバックツールのことをいい、Gemの一種であるため、使用をする際は以下の様にGemfileに追加する必要があります。

Gemfile
gem 'pry-rails'

Gemの追加を行ったため、ターミナルで以下のようにbundle installを忘れずに行いましょう。

$ bundle install

これでpry-railsが使えるようになります。

binding.pryとは

pry-railsを導入したことでpry-railsの機能が使えるようになりました。pry-railsの機能の一つにbinding.pryがあります。

binding.pryはコード上にbinding.pryを記述することで
binding.pryの書かれている箇所までの処理を実行し、
binding.pryの書かれている箇所で処理を一時的に止めることができます。

使用方法

以下の様に、処理の確認をしたいコードの箇所に
binding.pryを記述します。

tweets_controller.rb
class TweetsController < ApplicationController
    def index
      @tweets = Tweet.all
      binding.pry
    end
end

記述をし、実行を行うとターミナルのローカルサーバー画面が以下のような表示になり、
binding.pryの記述している箇所で処理が止まります。

   3: def index
   4:   @tweets = Tweet.all
=> 5:   binding.pry
   6: end

  [1] pry(#<TweetsController>)>

この状態ではコンソールと似た操作ができるので
@tweets.find(6)のようなApplicationRecordメソッドを使い、binding.pryが書かれている箇所までの処理の値を確認することができます。

処理停止状態を終了させるには以下のようにexitを入力することで停止状態が終了し、binding.pry以降の処理を実行します。

  [1] pry(#<TweetsController>)>exit

ちなみにexit!を入力するとローカルサーバーも終了します。

おわりに

以上pry-railsの説明とpry-railsでよく使うbinding.pryについてまとめました。
デバックツールはプログラミングをする上で非常にお世話になると思うので覚えておこうと思います。

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

わたしがRailsチュートリアルで学んだこと【7章】

  • 注意:プログラミング歴34日の初心者が書いています

  • 注意:間違っていたら優しく教えてください(喜びます)

「Ruby on Rails チュートリアル実例を使ってRailsを学ぼう」
https://railstutorial.jp/

素晴らしいチュートリアルに感謝します。

8.1 セッション

HTTPは、その場限り。以前の情報を全く持たないリクエストです。
そのため、ログイン情報の保持には、セッションという接続を使います。

Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。

セッションについても例に漏れず、Sessionsコントローラで操作します。

コントローラを追加したら、routes.rbに対応するルーティングを追加します。今回は、

  • new 新しいセッションのページ (ログイン画面)

  • create 新しいセッションの作成 (実際のログイン)

  • destroy セッションの削除 (ログアウト)

という3つのアクションを、/loginというURLに紐づけます。

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

わたしがRailsチュートリアルで学んだこと【8章】

  • 注意:プログラミング歴34日の初心者が書いています

  • 注意:間違っていたら優しく教えてください(喜びます)

「Ruby on Rails チュートリアル実例を使ってRailsを学ぼう」
https://railstutorial.jp/

素晴らしいチュートリアルに感謝します。

8.1 セッション

HTTPは、その場限り。以前の情報を全く持たないリクエストです。
そのため、ログイン情報の保持には、セッションという接続を使います。

Railsでセッションを実装する方法として最も一般的なのは、cookiesを使う方法です。cookiesとは、ユーザーのブラウザに保存される小さなテキストデータです。

セッションについても例に漏れず、Sessionsコントローラで操作します。

コントローラを追加したら、routes.rbに対応するルーティングを追加します。今回は、

  • new 新しいセッションのページ (ログイン画面)

  • create 新しいセッションの作成 (実際のログイン)

  • destroy セッションの削除 (ログアウト)

という3つのアクションを、/loginというURLに紐づけます。

8.1.2 ログインフォーム

フォームに入力されたログイン情報を保存したい、でもセッションにはUserのときのようなモデル、データベースはありません。

form_for(:session, url: login_path)

そのため上記のように、データを保存するリソース名(ここでは:session)とそのパスを指定ながら、form_forでformタグを生成します。

rb
<%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

送信されたデータは、

  • params[:session][:email]

  • params[:session][:password]

で受け取ることができます。こうしてみると、リソース名(ここでは:session)をシンボルで記述する意味がわかりますね。

"session" => { 
            "email" => "foo@bar.com",
            "password" => "something",  
          }

送信されるデータはこういうハッシュになってるということですね。

これが、login_pathのPOST、つまりcreateアクションに受け渡されるということです。

8.1.3 ユーザーの検索と認証

sessionコントローラのcreateアクションでparamsからデータを受け取ることができます。

renderredirect_toについて

flashで表示させるエラーメッセージは、redirect_toの場合は次のページ移動で消えるのに、rederではページ移動しても消えずに残ったままになってしまう。

 def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash[:danger] = 'Invalid email/password combination' # 本当は正しくない
      render 'new'
    end
  end

renderとは

  • renderは指定したviewを呼び出します。
  def new
  end

これも実はrenderが動いています。
GETアクション内になにも記述がない場合は、

  def new
    render 'new'
  end

に勝手に補完されています。

redirect_toとは

  • redirect_toはHTTPリクエスト(GET)をサーバーに送ります。
def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

わかりにくいですが、ここでもRailsの自動補完が効いているのでした。
redirect_toの行は、以下と同じ意味です。

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

親クラスも含めて、メソッドの定義場所を特定する

経緯

このモンキーパッチを解読していたところ、元のcolumnsがどのクラスで定義されているかを知りたくなった。

module ActiveRecordInvisibleColumn
  INVISIBLE_COLUMNS = {
    'foos' => ['bar'],
  }.freeze
  def columns(table_name)
    super.delete_if { |column| INVISIBLE_COLUMNS.fetch(table_name, []).include?(column.name) }
  end
end

ActiveSupport.on_load :active_record do
  ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(ActiveRecordInvisibleColumn)
end

ハマった罠

initializeに引数が必要らしく、面倒。

irb(main):036:0> ActiveRecord::ConnectionAdapters::Mysql2Adapter.new
ArgumentError: wrong number of arguments (given 0, expected 4)

irb(main):037:0> ActiveRecord::ConnectionAdapters::Mysql2Adapter.new(nil, nil, nil, nil)
NoMethodError: undefined method `fetch' for nil:NilClass

結論

これで解決した。
allocateはinitializeをskipしてインスタンス化できるメソッド。

irb(main):035:0> ActiveRecord::ConnectionAdapters::Mysql2Adapter.allocate.method(:columns).source_location
=> ["/Users/xxxx/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-4.2.11.1/lib/active_record/connection_adapters/abstract_mysql_adapter.rb", 469]

参考

https://qiita.com/jnchito/items/fc8a61b421d026a23ffe
https://techracho.bpsinc.jp/hachi8833/2019_07_04/75432
https://eagletmt.hateblo.jp/entry/2017/09/24/004709

共同編集者

@popmac

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

【Ruby】prependの使い道

まとめ

  1. prependしたクラスの挙動の前後に、処理を追加したい
  2. prependしたクラスの挙動を完全に上書きしたい

1. prependしたクラスの挙動の前後に、処理を追加したい

モンキーパッチではこのパターンがよく使われている。

module Hoge
  def execute
    pp 'モジュール'
    super
  end
end

class Human
  def execute
    pp 'クラス'
  end
end

Human.prepend(Hoge)

Human.ancestors
=> [Hoge, Human, ....]

Human.new.execute
#=> 
'モジュール'
'クラス'

2. prependしたクラスの挙動を完全に上書きしたい

module Hoge
  def execute
    pp 'モジュール'
  end
end

class Human
  def execute
    pp 'クラス'
  end
end

Human.prepend(Hoge)

Human.ancestors
=> [Hoge, Human, ....]


Human.new.execute
#=> 
'モジュール'

2'. prependしたクラスの挙動を完全に上書きしたい -> includeではダメ

module Hoge
  def execute
    pp 'モジュール'
  end
end

class Human
  include Hoge

  def execute
    pp 'クラス'
  end
end

Human.ancestors
=> [Human, Hoge, ....]

Human.new.execute
#=> 
'クラス'

おまけ: 継承している場合

superを呼ぶと、Humanだけでなく、Humanの親クラスであるAnimalまで探索される。

module Hoge
  def execute
    pp 'モジュール'
    super
  end
end

class Animal
  def execute
    pp '親クラス'
  end
end

class Human < Animal
end

Human.prepend(Hoge)

Human.ancestors
=> [Hoge, Human, Animal, ....]

Human.new.execute
#=> 
'モジュール'
'親クラス'

参考

https://eagletmt.hateblo.jp/entry/2017/09/24/004709

共同編集者

@popmac

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

【Ruby】methodsとinstance_methodsの違い

version

Ruby 2.5.5

違いまとめ

縦メソッド/横レシーバ Class Instance
methods クラスメソッド インスタンスメソッド
instance_methods インスタンスメソッド NoMethodError

おまけ: 返り値の公開レベル

メソッド 公開レベル
methods public、protected
instance_methods public、protected
private_methods private

共同編集者

@popmac

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

【個人開発】チャットBotのAppStoreが1週間で出来上がるまで(rails)

thumbnail.png

はじめに

チャットBotや音声アプリのダウンロードサイトを作りました。2022年にはチャットボットの市場規模は132億円規模になる!アメリカでは成人の1/4にスマートスピーカーが普及している!など、このあたりの産業は高騰してくのが目に見えていますので。

今のうちに、こういう横断的なサイトを立ち上げておけばまだ間に合うかもしれない。そもそも、それっぽいのが国内には見当たらなかったので作ってみよう!と決めて、1週間で仕上げました。今回は、コードを書き始めて、リリースするまでをオープンに書いていきたいと思います。

「個人開発に挑戦してみたいな。」と考えている方にオススメです!

Botcampとは

スクリーンショット 2019-07-13 20.27.40.png

URL:http://botcamp.jp

AppStoreや、Google Play Storeってありますよね?これらのプラットフォームでは、スマホアプリをダウンロードできますが、BotcampではチャットBotや音声アプリをサイトからダウンロードできます。

できることは、まず「Botを登録する」。運営元じゃなくても、会員登録しなくても、Bot登録はできるような仕様にします。何といっても、このBot数こそがサイトを成り立たせるために必須条件となるので、なるべく登録していただきたいところ。

次に、「Botをダウンロードする」です。といっても、各プラットフォームが提供しているURLをBotごとに設定しておくだけです。

最後に、「Botをレビューする」になります。App Storeのデザインにだいぶ寄せてます(笑)Botやアプリの品質を評価できる部分なので、Bot数を増やしていったあとに、力を入れて増やしていきたい部分です。

おまけに、Slackのワークスペースをコミュニティとして用意してみました。とりあえず、会員登録した人を全員ワークスペースに招待していくという状況を作ってみようかと思います。これに関しては、どうなるかは分からないですが、Botマニアたちが情報交換できるコミュニティ的になれれば嬉しいですね。ちょっと実験的に入れてみた感じです。

1週間で出来上がるまで

Day1

スクリーンショット 2019-07-13 0.02.51.png

Gitを確認してみたところ、2019年7月8日から開発をスタートしています。初日はRails newして、トップのデザインをコーディングしただけでした。デザインに関しては、ベースは「開発会議」や「個人開発のフリマ」で使っているものを流用しています。

MVPを作る過程において、デザインに力を入れる必要があるプロダクトではないので、毎回流用してリソースをカットしています。ただ、どうしてもこだわり的にレスポンシブサイトが好きになれなくて、pcとspを分けています。

昔はgem jpmobileというライブラリを使っていたのですが、Rails 4.1.0で追加されたActionPack Variantsを使うようになりました。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :detect_device_variant

  private

  def detect_device_variant
    request.variant = :sp if is_mobile?
  end
end
app/models/concerns/user_agent.rb
module UserAgent
  extend ActiveSupport::Concern

  def is_mobile?
    request.user_agent =~ /iPhone|iPad|Android/
  end
end

これで、HTML側は切り替わってくれるので、スタイルシートもpcはpc直下のディレクトリのみ、spはsp直下のディレクトリのみ読み込んでくれるように設定を変更しておきます。

app/views/layouts/application.html.erb
<%= stylesheet_link_tag    "application", :media => "all" %>

## pc直下のみに変更
<%= stylesheet_link_tag 'pc/style', media: 'all', "data-turbolinks-track" => true%>

初日で本番環境(Heroku)にはあげました。ドメイン購入しましたし、Amazon s3(画像サーバー)、SES(メールサーバー)も設定しました。こういう本番を意識した開発をDay1からするのは、個人開発の精神面で大切な気がしますね。

Day2

スクリーンショット 2019-07-13 0.19.40.png

Userの認証〜管理までを実装しました。gem deviseを使っていて、ライブラリとしては賛否両論ありますが、管理画面rails_adminと連携するところもありますし、初心者が爆速でローンチするんだったら、使ってokだと思います。

このあたりは、完全に今まで作ってきたサービスのコピペです。振り返ってみると、コミュニティ系のサイト作るならここまでは毎回同じなのでどっかに置いておいても良いですね。

Day3

スクリーンショット 2019-07-13 1.00.09.png

Bot関連のCRUDを作ってみました。少しいつもと違ったのが、Botを作成する際に運営元(Botを配信している会社/個人開発者)を追加するのですが、これを既存のものからか、新しく追加できるのを選べるようにするパターンがあるのです。

まず、Makerモデルを用意しました。Botモデルとは一対多の関係になります。もっと良いやり方がありそうですが、attr_accessorを使って、これがある場合には運営元を更新して、新しいものを追加するようにしました。

app/models/bot.rb
  attr_accessor :bot_maker
  attr_accessor :bot_maker_url
app/controllers/bots_controller.rb
  def create
    @bot = Bot.create(create_params)
    if @bot.bot_maker.present?
      @maker = Maker.create({
          name: @bot.bot_maker,
          website: @bot.bot_maker_url
        })
      @maker.save
      @bot.maker_id = @maker.id
    end
    if @bot.save
      flash[:success] = "新しくボットが追加されました!"
      redirect_to @bot
    else
      flash[:alert] = "ボットの追加に失敗しました。"
      redirect_to new_bot_path
    end
  end

カテゴリと、お気に入り機能はチュートリアルにあるくらいの感覚で実装しました。いつもと同じです。

Day4

この日、Mac Book Proが急に死にます。Apple Storeへ重病の我が子を看てもらうように急ぎましたが、4時間待たされた上に完治するまで7営業日かかると宣告されました。スペアPCの開発に急遽移行へ。
スクリーンショット 2019-07-13 1.09.50.png

Reviewと、Type(プラットフォーム)、BotImage(ボットの複数画像)を実装しました。プラットフォームはLINEや、MessengerなどのプラットフォームごとのダウンロードURLを入力するところです。Botを入力するときに、同時に入力できるようにしたいので、gem cocoonを使いました。

Typeは、Botと多対多。BotImageは一対多で関係を構築しました。Typeの方の例を載せておきます。

app/views/bots/new.html.erb
  <section class="feed">
    <div class="box">
      <h2>プラットフォーム</h2>
      <div class="formContainer">
        <div class="imageContainer">
          <%= f.fields_for :bot_types do |t| %>
            <div id="types">
              <%= link_to_add_association "プラットフォームを追加", f, :bot_types, partial: 'type' %>
            </div>
          <% end %>
        </div>
      </div>
    </div>
  </section>
app/views/bots/_type.html.erb
<div class="nested-fields">
  <div class="formItem flexContainer typeContainer">
    <div class="label">
      <%= f.collection_select :type_id, Type.all, :id, :name, {class: "selectType"} %>
    </div>
    <div class="formInput">
      <%= f.text_field :url, placeholder: "ホームページや、ダウンロードリンクを入力してください" %>
    </div>
  </div>
  <p><%= link_to_remove_association "プラットフォームを削除", f %></p>
</div>

Day5

夜ご飯を料理してたら、シェアメイトのオリーブオイルが棚から落ちてきて派手に割れる、右中指からまあまあ出血しました。キーを打つのが極めて困難に。
スクリーンショット 2019-07-13 1.25.25.png

この日は結構微調整が多かったですね。検索機能をつけたり、カテゴリ、プラットフォーム、運営元のURL指定がIDになっていたので、slugを作成して、それに変更しました。botcamp.jp/types/botcamp.jp/types/lineに変えた感じですね。

config/routes.rb
  resources :types, param: :slug
config/controllers/types_controller.rb
class TypesController < ApplicationController
  def show
    @type = Type.find_by_slug(params[:slug])
  end
end

Day6

スクリーンショット 2019-07-13 1.31.38.png
この日はほとんどコードを書いていません。デザインや、仕組みの微調整をしたくらいですね。

ひたすら、ボットを探していました。スクレイピングをするのもよぎったのですが、なかなか良い感じのスクレイピング先が思い浮かばなくて。とりあえず、手で探し、手で入力するような、労働集約的な作業をしました。それでも、40ちょっとしか見つからなくて泣きそうになりました。

100は欲しかったのですが、気が滅入りそうなので、とりあえず、出して、毎日ちょこちょこ入れていこうと決意しました。

Day7

リリースします。リリース時にやることはそこまでないのですが、開発会議の週一で送っているニュースレターを一日ずらして、リリースのことを含んだ文書にする。もちろん、開発会議も更新する。

そして、TwitterとこのQiita記事を投稿するくらいです。あと、落ち着いたら、Crieitにも、個人開発寄りの記事をまとめて報告しようと思っています。

さいごに

公開するつもりでやってなかったので、コミットメッセージ分かりづらくてすみません。自分がわかればそれで良いのが、個人開発の甘えなので(笑)

一週間あれば、プロダクトはリリースできます。むしろ、アイデアからリリースまでに一週間以上かかっていたら、遅すぎるのではないでしょうか。とはいえ、自分がリモートワーク/フリーランスなので、一般的なスタイルではないかもです。(もちろん、日中は仕事ありますが)

この記事を読んでいただいた方が、個人開発面白いなと思ってくれたら嬉しいです!!あと、BotcampにぜひBotやアプリを追加したり、サイトを楽しんでもられば最高です!!!

P.S.

リリース Day1から、個人開発のフリマでサイト丸ごと販売します。サービス開発の勉強として、あるいは、Botビジネスに興味ある人は入札してみてはいかがでしょうか?(笑)

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

TECH DAY 1 Ruby

勉強した項目: Ruby
時間: 4:30
内容:
【メソッド】
 メソッド:オブジェクトをメソッドと呼ばれる処理によって自身の形を変えたりすること
【lengthメソッド】
 文字列が利用できるメソッドで、文字列の文字の数を数えてくれる
 返り値として数値オブジェクトの数字で返してくれる
【to_sメソッド】
 文字列と数値は別のオブジェクトなので、連結することができないため、to_sメソッドを利用して数値を文字列に変換する

【Rubyの演算子】
+(足し算)
-(引き算)
* (かけ算)
/(割り算)
%(剰余)

比較演算子(>, <, ==)  式が正しければtrue、間違っていればfalse,等しい場合は==

比較演算子(>=, <=)  比較演算子>,<のあとに=を続けることで〜以上、〜以下を表す

not演算子(!)   !(エクスクラメーションマーク)はnot演算子と呼ばれ、否定の意味
          !と=を合わせた!=は==と反対で、値同士が等しくない場合にtrueを返す

【変数】
 変数とは、オブジェクトの入れ物、またはオブジェクトを識別する名札のようなもの
  =を挟んで右側においたオブジェクトの返り値を、左側の変数の中に入れるイメージ

 変数 = 格納するオブジェクト

 変数の名前は、原則として小文字から

 putsの返り値は「nil」となります。これは、オブジェクトが何もない、という意味

【変数の命名規則】
 ・原則、小文字から始める
  ((アンダーバー)から始めることも可能ですが、特に理由がない場合は避けましょう。)
 ・名前の1文字目でなければ大文字や数字、
(アンダーバー)を使ってもよい
 ・名前にスペースが入ってはいけない
 ・予約語(Rubyによって最初から用途を与えられた単語)と同一の名前は使ってはいけない

【プログラミングにおける=の意味】
 Rubyにおいて=が1つの式は必ず『右側の値を左の変数に代入する』という意味になる

【名札としての変数】
 変数にはオブジェクトが何であるかを識別する名札という役割があります。変数を定義することにより、ソースコード中でオブジェクトをわかりやすく使える

【定数】
 定数は、一度オブジェクトを代入したら再代入することをせずに、定義するときは名前の最初を大文字にする。
  hello world ではなく Hello World

【アプリケーションの作成】
 putsメソッドは最後に自動で改行する
 putsの改行はputs""でも可能

【バックスラッシュ記法】
 バックスラッシュの打ち方は option + \

記法 意味
\n 改行
\t タブ
\b バックスペース
\ バックスラッシュ

 バックスラッシュ記法が適応されるのは文字を"(ダブルクォテーション)で囲んだときのみ

 バックスラッシュ記法で改行は、
 
  puts "吾輩は猫である\n\n名前はまだない"  で表せる

【式展開】
 式展開の書き方は文字列中で#{式}とするだけ
  ポイントは、"(ダブルクォテーション)で囲む必要があります。'(シングルクォテーション)で囲んだ場合は式展開が行われない

【getsメソッド】
 getsメソッドはユーザーからターミナルへ入力できるようにするメソッド
  返り値はユーザーが入力した値の文字列オブジェクト
 getsメソッドが呼ばれるとターミナル画面は入力待ちの状態になる

【chompメソッド】
 chompメソッドは文字列の末尾の改行文字を取り除いた新しい文字列を返してくれるメソッド
 getsメソッドで返ってきた文字列オブジェクトは勝手に文末で改行してしまうのを防ぐ

  gets.chompのように後方につける

【ハッシュ】
 ハッシュという複数の情報を扱うことのできるオブジェクト

【ハッシュオブジェクト】
 ハッシュオブジェクトは、自身の中にデータとそれに対応するキーのセットを所持することができる

【キーバリューストア】
 ハッシュのように、保存したいデータ(バリュー)とそれに対応する標識(キー)を対応させてペアで保存する方式のこと

【ハッシュオブジェクトの変数を作る】
  hash = {}

 ハッシュは{} (中括弧)を使って記述します。また左辺のhashは変数名であり、任意の名前を定義

【シンボルオブジェクト】
 シンボルオブジェクト(略称:シンボル)とは今回のハッシュのキーのような名前を識別するためのラベル
 基本的に先頭に接頭語:をつける

 # どちらも同じ
hash1 = {:title => "時をかける少女"}
hash2 = {"title" => "時をかける少女"}

 ハッシュではシンボルを使うようにする、実行の速度がシンボルの方が早いため、ハッシュのキーにはシンボルの使用が推奨

例:変数をハッシュオブジェクトpostで生成する
  post = {} # 空のハッシュの宣言

#以下で要素の追加
post[:genre] = "マンガ"
post[:title] = "るろうに剣心"
post[:review] = "面白い!"
puts post # => {:genre=>"マンガ", :title=>"るろうに剣心", :review=>"面白い"}

【条件分岐 if文】
 
if 条件式 then
# 条件式が真(true)のときに実行する処理
end

条件式とは基本文法で出てきた(3 > 0)などのような返り値がtrueかfalseの式のことです。
if 条件式 thenとendの間にはその条件式がtrueのときに実行する処理を書く

【条件式が偽の場合の処理】
 条件式が偽のときに処理を行うには以下のようにelseを使う
 
if 条件式 then
# 条件式が真(true)のときに実行する処理
else
# 条件式が偽(false)のときに実行する処理
end

 複数の条件分岐を書くときはelsifという文法を使う
if 条件式1 then
# 条件式1が真(true)のときに実行する処理
elsif 条件式2 then
# 条件式1が偽(false)のとき、かつ
# 条件式2が真(true)のときに実行する処理
else
# 条件式1と条件式2がどちらとも偽(false)のときに実行する処理
end

【to_iメソッド】
 文字列オブジェクトに対してto_iメソッドを使うとその文字列を数値オブジェクトに変換すること

string = "30" # 文字列の30
number = string.to_i # 数値の30に変換
puts number + 20 # => 50が出力

【メソッド】
 メソッドとは、ある処理をまとめること。定義したメソッドを呼び出すことで同じ処理を何度も実行すること

【メソッドの定義方法】
 def メソッド名
# 実行する処理
end

メソッドを呼び出すときは、メソッド名を記述

def say_hello # メソッドの定義
puts "Hello World"
end

say_hello # メソッドの実行

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

herokuにアップする画像を永続的に表示させる方法【Rails】

heroku に画像をアップできるアプリケーションをデプロイしたとしても、アップした画像は数時間で消えるかデプロイするたびに消えてしまいます。
しかし、画像が消えるということを回避できる方法があります。

それは、画像をデータベースに格納するときにバイナリデータとして保存する方法です。

ですが、この記事の方法は「モックアップ用の簡易なアプリケーション」「アクセスがなく自分しか使わないアプリケーション」といったケースでやるのが好ましいです。
下記の注意点を読んでから、アプリケーションの状況に応じて実施を決断するようにしてください。

注意点

以下のサイトの回答者の方の答えが非常にわかりやすくて、画像をデータベースに保存するデメリット(とメリット)についてまとめられているので、参考にしてみてください。

データベースに画像を保存するのはありでしょうか? - teratail

テーブルを作成

今回は、既存のPostテーブルに紐付けしたPhotoテーブルのなかに画像を保存していくとします。
PostテーブルPhotoテーブルの関係は、1対多です。

まずは、画像を保存するためのテーブルの作成から行います。
画像を保存するためのカラムは、bynary型にします。
コンソールに下記のコマンドを打ち込み実行します。

$ rails g model Photo

そうすることで、マイグレーションファイルが生成されるので下記のように編集します。

xxxxxxxxxxxxxx_create_photos.rb
class CreateUsers < ActiveRecord::Migration

  def change
    create_table : photos do |t|
      t.bynary :image, nill: false
      t.references :post, foreign_key: true, null: false

      t.timestamps null: false
    end
  end
end

マイグレーションファイルを編集したら、下記のコマンドを実行してテーブルを作成しましょう。

$ rails db:migrate

作成した全てのテーブル情報は「schema.rb」ファイルでも確認することができます。

コントローラを編集

コントーラファイルには、「画像をデータベースに保存する」と「画像をデータベースから取り出す」といった2つの動きを追加していきます。

画像をデータベースに保存する

画像をデータベースに保存するには、readというメソッドが重要になります。
photos_controller.rb」ファイルを作成して、下記を追加します。

photos_controller.rb
def create
  @post = Post.new(post_params)

  # バイナリ化した画像の呼び出し
  params[:post][:photos_attributes]["0"][:image].each_with_index do |item, i|

    if i != 0
      @post.photos.build
    end

    @post.photos[i].image = item.read
  end
end

private
  def post_params
    params.require(:post).permit(photos_attributes: [:image])
  end

画像をデータベースから取り出す

画像をデータベースから取り出すには、send_dataというメソッドが重要です。

photos_controller.rb
def send_img
  tmpbin = Photo.find(params[:id])
  send_data(tmpbin.image, :type => 'image/jpeg', :disposition => 'inline')
end

ビューを編集

ビューには、「画像のアップロード」と「画像の表示」とこれまた2つの動きを追加していきます。

画像のアップロード

画像をアップできるようにフォームを設置します。

new.html.erb
<%= form_with model: @post, multipart: true do |f| %>
  <%= f.field_for :photos do |i| %>
    <%= i.file_field :image, type: :file %>
  <% end %>

  <%= f.submit "投稿する" %>
<% end %>

画像の表示

次に、データベースに保存した画像が表示されるように下記を追加します。

index.html.erb
<% @post.photos.each do |photos| %>
  <%= image_tag url_for(:controller => 'photos', :action => 'send_img', id => photos.id %>
<% end %>

ルートの設定を追加

最後の仕上げとして、「routes.rb」に下記を追加します。

routes.rb
get '/photos/send_img/:id', to: 'photos#send_img'

これでバイナリデータとした画像の保存から表示までを終わらせることができました。
heroku にデプロイするアプリケーションで画像投稿機能が付いているものでも、永続的に画像を表示させ続けられることでしょう!

参考にさせてもらった記事

rails で画像ファイルを DB に保存する

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