ノンカフェインであなたにやさしい

Ruby,Rails,HTML,CSS,Reactなど

Jiraの履歴を簡単に復元できるよう記法をMarkdownに変換するwebツールを作った

jira2md.web.app

背景

Jiraの説明を複数人が編集して気づかないうちにコンフリクト、苦労して書いた説明が消えていたのが辛かったので簡単に戻せるようJira記法からMarkdownに変換するwebツールを作りました。
Jira絶対に許さないぞ…!!

使い方

変更履歴をアクティビティから確認

f:id:kuronekopunk:20200521222518p:plain

開発者ツールで変更前のテキストをコピー

改行含むテキストを取得できます f:id:kuronekopunk:20200521222652p:plain

ツールに貼り付けて変換完了

f:id:kuronekopunk:20200521223037p:plain

使った技術など

  • Nuxt
  • Firebase Hosting
  • Jira記法をMDに変換するnpm jira2md

Nuxtの設定とnpmでJira記法をMDに変換をしただけなので2時間でリリースまでいけました✌️ OGPとか何もやっていないので少しは整えたい。。。

Rails 複数カラムのユニーク制約のバリデーションを設定する

よく忘れる複数カラムの一意のバリデーション

2カラムの場合

validates :column1, presence: true, uniqueness: { scope: :column2 }

3カラム以上の場合

validates :column1, presence: true, uniqueness: { scope: [:column2, :column3] }

Active Record バリデーション - Railsガイド

Heroku SeachBox ElasticsearchがRails Searchkickのデフォルト設定で動かない

実運用しているアプリが2020/2/13あたりからインデックスの新規作成が上手くいかず調査しました。

エラー

class Post < ApplicationRecord
  searchkick
end

動かない状態のサンプル(GitHub)

reindexでエラーが出ます。

irb(main):001:0> Post.reindex
Traceback (most recent call last):
        1: from (irb):1
Elasticsearch::Transport::Transport::Errors::BadRequest ([400] {"error":{"root_cause":[{"type":"remote_transport_exception","reason":"[oin-1][10.0.24.93:9300][indices:admin/create]"}],"type":"illegal_argument_exception","reason":"The difference between max_gram and min_gram in NGram Tokenizer must be less than or equal to: [1] but was [49]. This limit can be set by changing the [index.max_ngram_diff] index level setting."},"status":400})

create_indexでエラーが出ている模様

対応

searchkick のインデックス設定を変更

  • max_shingle_size を4にする
  • min_gram, max_gram の差を1にする

※値は各自の環境、必要要件に合わせてください

class Post < ApplicationRecord
  MIN_GRAM = 1.freeze
  searchkick settings: {
    analysis: {
      filter: {
        searchkick_suggest_shingle: {
          max_shingle_size: 4
        },
        searchkick_ngram: {
          min_gram: MIN_GRAM,
          max_gram: MIN_GRAM + 1
        }
      }
    }
  }
end

サンプル(GitHub)

調査過程

index.max_ngram_diff を1に設定

The difference between max_gram and min_gram in NGram Tokenizer must be less than or equal to: [1] but was [49]. This limit can be set by changing the [index.max_ngram_diff] index level setting.

index.max_ngram_diff は1にしないといけないということで設定する

class Post < ApplicationRecord
  searchkick settings: {
    index: { max_ngram_diff: 1 }
  }
end

結果

エラー変わらず

irb(main):001:0> Post.reindex
Traceback (most recent call last):
        1: from (irb):1
Elasticsearch::Transport::Transport::Errors::BadRequest ([400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"The difference between max_gram and min_gram in NGram Tokenizer must be less than or equal to: [1] but was [49]. This limit can be set by changing the [index.max_ngram_diff] index level setting."}],"type":"illegal_argument_exception","reason":"The difference between max_gram and min_gram in NGram Tokenizer must be less than or equal to: [1] but was [49]. This limit can be set by changing the [index.max_ngram_diff] index level setting."},"status":400})

indexの設定を確認する

irb(main):001:0> Post.searchkick_index.index_options
{:settings=>
  {:analysis=>
    {:analyzer=>{},
     :filter=>
      {:searchkick_index_shingle=>{:type=>"shingle", :token_separator=>""},
       :searchkick_search_shingle=>
        {:type=>"shingle",
         :token_separator=>"",
         :output_unigrams=>false,
         :output_unigrams_if_no_shingles=>true},
       :searchkick_suggest_shingle=>{:type=>"shingle", :max_shingle_size=>5},
       :searchkick_edge_ngram=>
        {:type=>"edge_ngram", :min_gram=>1, :max_gram=>50},
       :searchkick_ngram=>{:type=>"ngram", :min_gram=>1, :max_gram=>50},
       :searchkick_stemmer=>{:type=>"snowball", :language=>"English"}},
     :char_filter=>{:ampersand=>{:type=>"mapping", :mappings=>["&=> and "]}}},
   :index=>{:max_ngram_diff=>1, :max_shingle_diff=>4}},
# ...省略

max_ngram_diff は1になったが min と max の実際の差が問題のよう

:searchkick_ngram=>{:type=>"ngram", :min_gram=>1, :max_gram=>50},

analysis.filter.searchkick_ngram.max_gram を2に設定

class Post < ApplicationRecord
  searchkick settings: {
    analysis: {
      filter: {
        searchkick_ngram: {
          max_gram: 2
        }
      }
    }
  }
end

結果

エラーが変わった

irb(main):001:0> Post.reindex
Traceback (most recent call last):
        1: from (irb):1
Elasticsearch::Transport::Transport::Errors::BadRequest ([400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"In Shingle TokenFilter the difference between max_shingle_size and min_shingle_size (and +1 if outputting unigrams) must be less than or equal to: [3] but was [4]. This limit can be set by changing the [index.max_shingle_diff] index level setting."}],"type":"illegal_argument_exception","reason":"In Shingle TokenFilter the difference between max_shingle_size and min_shingle_size (and +1 if outputting unigrams) must be less than or equal to: [3] but was [4]. This limit can be set by changing the [index.max_shingle_diff] index level setting."},"status":400})

次は max_shingle_diff は3にしないといけないとのこと

index.max_shingle_diff を3に設定

class Post < ApplicationRecord
  searchkick settings: {
    index: { 
      max_shingle_diff: 3 
    },
    analysis: {
      filter: {
        searchkick_ngram: {
          max_gram: 2
        }
      }
    }
  }
end

結果

エラー変わらず

irb(main):001:0> Post.reindex
Traceback (most recent call last):
        1: from (irb):1
Elasticsearch::Transport::Transport::Errors::BadRequest ([400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"In Shingle TokenFilter the difference between max_shingle_size and min_shingle_size (and +1 if outputting unigrams) must be less than or equal to: [3] but was [4]. This limit can be set by changing the [index.max_shingle_diff] index level setting."}],"type":"illegal_argument_exception","reason":"In Shingle TokenFilter the difference between max_shingle_size and min_shingle_size (and +1 if outputting unigrams) must be less than or equal to: [3] but was [4]. This limit can be set by changing the [index.max_shingle_diff] index level setting."},"status":400})

max_ngram_diff と同じで実際の数値が問題のよう :searchkick_suggest_shingle=>{:type=>"shingle", :max_shingle_size=>5}, 現状5になっている

analysis.filter.searchkick_suggest_shingle.max_shingle_size を4に設定

class Post < ApplicationRecord
  searchkick settings: {
    analysis: {
      filter: {
        searchkick_suggest_shingle: {
          max_shingle_size: 4
        },
        searchkick_ngram: {
          max_gram: 2
        }
      }
    }
  }
end

結果

reindex成功

irb(main):001:0> Post.reindex
D, [2020-05-06T04:53:35.428208 #4] DEBUG -- :   Post Load (632.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT $1  [["LIMIT", 1000]]
D, [2020-05-06T04:53:35.532908 #4] DEBUG -- :   Post Import (84.4ms)  {"count":3}
=> true

Railsでモーダルの出し分けを管理する

Railsでモーダルの表示処理をまとめて衝突しないように管理する方法を紹介します。

背景

ツクリンクを運営する中でモーダルが少しづつ増え、衝突することがあったためモーダルの優先順位を付け、衝突しないよう実装をしました。

実装

前提

以下の3つのモーダルがあるとします。

  • A: 初回ログインで出すモーダル(全ページ)
  • B: 特定のユーザーにだけお知らせを出すモーダル(全ページ)
  • C: 特定のページで出すモーダル(Posts#show)

コードサンプル

全ページに出すモーダルはApplicationControllerで該当ユーザーか判断しモーダルに必要な情報をセットします。

# controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_modal_A
  before_action :set_modal_B

  def set_modal_A
    return if cookies['modal_A'].present? # 表示済みなら何もしない
    if is_first_signed_in? # 初回ログインか?
      @modal_A = { title: '表示に必要な情報など' }
    end
  end

  def set_modal_B
    return if cookies['modal_B'].present? # 表示済みなら何もしない
    if show_notice? # お知らせを出すユーザーか?
      @modal_B = true
    end
  end
end

Viewごとに出るモーダルは provide でモーダルをセット

# views/posts/show.html.erb
<% provide :modal, render('modal/C') %>

レイアウトのView(もしくはそれに準ずるパーシャル)でモーダルの出し分けを行います。 ifの上位にあるものが優先され、モーダルが複数renderされるのを防いでいます。

# views/layouts/application.html.erb
<% if yield(:modal).present? %>
  <%= yield(:modal) %>

<% elsif @modal_B.present? %>
  <%= render 'modal/B' %>
<% elsif @modal_A.present? %>
  <%= render 'modal/A' %>
<% end %>

各モーダルのViewでは表示を管理するCookieを保存するなどの処理をしています。

# views/modal/A.html.erb
<% cookies.permanent['modal_A'] = { value: true, expires: 1.day } %>
<div class="modal">お知らせだよ!</div>

FirebaseFunctionsで素のJSを返す

実現したいこと

FirebaseFunctionsを使ってリクエスト元やパラメータに合わせたJSファイルを作り返したい

こんな風に使いたい

<script src="https://asia-northeast1-fir-functions-return-js.cloudfunctions.net/hello"></script>

Functionsの実装

functions/index.js

exports.hello = functions
  .region('asia-northeast1')
  .https.onRequest((request, response) => {
    // Using query
    const name = request.query.name || 'defaultName';
    // Return alert js
    response
      .contentType('application/javascript; charset=utf-8')
      .send(`alert('Hello ${name}')`);
  });

response.send で返してあげるだけで読み込み可能でした。

サンプル

サンプルページ

fir-functions-return-js.firebaseapp.com

ソースコード

github.com