- 投稿日:2020-07-03T22:34:10+09:00
メッセージ送信の非同期化
フォームが送信されたら、イベントが発火するようにしよう
この記述の解説をします。
$(**)には、formのクラス名を記述します。
.on(**,にはイベント名を記述します。
e.preventDefaulでは、非同期通信を行う為にデフォルトのイベントを止めています。イベントが発火したときにAjaxを使用して、messages#createが動くようしましょう
この記述の中のthisは、イベントの発火元であるFormの情報が入っています。
$(this).attr('action');は、Form情報のパスを取得しています。messagesコントローラーの#createアクションでメッセージを保存し、respond_toを使用してJSON形式のリクエストに対してのレスポンスを返せるようにしましょう
if @message.save
リクエストで送られてきた情報を保存している
respond_to do |format|
format.json
json方式で返しているその他アウトプット
クラス名MessageFieldにappend(html)でHTMLを追加している$(".submit-btn").prop('disabled', false);
送信ボタンを一度押すとリロードしないと押せなくなるが
prop('disabled', false);を送信ボタンクラスに記述する事によりロードせずに投稿ができる様になる非同期に失敗した場合の処理
- 投稿日:2020-07-03T22:21:57+09:00
Kinx ライブラリ - パーサ・コンビネータ(その1)
Kinx ライブラリ - パーサ・コンビネータ(その1)
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。今回はパーサ・コンビネータです。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
前回 JIT ライブラリ を紹介しましたが、最後以下の言葉で締めくくりましたね。
これでパーサ・コンビネータとか実装して組み合わせたらたら、ちょっとした JIT 付き言語処理系が作れますね。
ええ、そこで急遽作りましたよ。パーサ・コンビネータ・ライブラリ。その名も Parsek。Parsec ならぬ Parsek。
インタフェースは Parsimmon を参考にしましたが、実装は全くの独自です。API はこんなに充実してませんが、それなりに使えます。インタフェースを追加するのは簡単なので、追々必要に応じて追加しよう。
長くなりそうなので記事を 2 回に分けようかと思います。
今回はこれを使って、四則演算文法をパースして AST(Abstract Syntax Tree = 抽象構文木)を作るところまでいきましょう。次回、最後には JIT コンパイルして実行するところまでいきます。
パーサ・コンビネータとは
小さい(単純な)パーサーを組み合わせて大きなパーサーを作るためのライブラリ。詳しくは他の記事に譲るとして、以下、サンプルを見ながらわかるようにしてみましょう。
サンプル
今回は趣向を変えて、サンプルを使いながら説明してみます。サンプルは正の整数(自然数)による四則演算です。簡単のために負の数は扱いません(結果が負になることはあり得ます)。
普通はここで BNF とか PEG とかの説明に入るのでしょうが、無視します。サンプルを通してまず動かすところからスタートです。
using Parsek
パーサ・コンビネータ・ライブラリは標準組み込みではないので、using しましょう。また、ライブラリはクラスとして提供されているのでインスタンス化しておきましょう。
using Parsek; var $ = new Parsek();何気に
$
は変数名として使えます。小さな(単純な)パーサーとは?
一つずつ例を挙げてみます。
数値をパースするパーサー
まず、正の整数を定義してみましょう。これが一つ目の小さな(単純な)パーサーです。一つ目ですが、いきなり正規表現です。まぁ、それほど難しくないのでわかりやすいでしょう。
ひとつだけ落とし穴なのは、使っているエンジン(=鬼車)が POSIX NFA ではない ので、長くマッチするほうを先に書かないといけません。簡単に言うと、以下の例では
"123"
はきちんと"123"
でマッチしますが、逆(/[0-9]|[1-9][0-9]*/
)に書くと最初に書いた[0-9]
にマッチして検索をやめてしまうので"1"
となって"23"
にマッチしません。注意しましょう。var number = $.regex(/[1-9][0-9]*|[0-9]/);これでこの
number
というパーサーは数値(が書かれた文字列)をパースできるようになります。やってみましょう。実際にパースを行うのは
parseAll()
メソッドです。parse()
というのもありますが、これは途中で終了しても成功するメソッドで、通常は内部で使われます。parseAll()
の場合は全て解析し終わって後処理まで実施して結果を返します。using Parsek; var $ = new Parsek(); var number = $.regex(/[1-9][0-9]*|[0-9]/); System.println(number.parseAll("0")); // => {"position":1,"status":1,"value":"0"} System.println(number.parseAll("10")); // => {"position":2,"status":1,"value":"10"} System.println(number.parseAll("129")); // => {"position":3,"status":1,"value":"129"} System.println(number.parseAll("abc")); // => {"position":0,"status":0,"value":null} System.println(number.parseAll("0129")); // => {"position":1,"status":0,"value":null}復帰値の
position
はパースした文字列の完了位置で、status
が成功・失敗(1 が成功)、value
が実際にパースが成功した文字列です。見て分かる通り、失敗するとvalue
はnull
です。しかしよく見ると
value
は文字列ですね。文字列を解釈しているだけなので当たり前です。ここで、value
に対して変換を行うメソッドが.map()
です。以下のように変換用の関数を与えます。using Parsek; var $ = new Parsek(); var number = $.regex(/[1-9][0-9]*|[0-9]/).map(&(value) => Integer.parseInt(value)); System.println(number.parseAll("129")); // => {"position":3,"status":1,"value":129}数値になりましたね。上記の場合、単に値をパススルーしているだけなので、
Integer.parseInt
を直接渡しても同じです。var number = $.regex(/[1-9][0-9]*|[0-9]/).map(Integer.parseInt);このほうが簡潔ですね。
四則演算の演算子をパースするパーサー
演算子によって優先度が違うので 2 つに分けます。
+
または-
*
または/
または%
一文字の or を解釈するのに便利なのが
$.oneOf()
です。以下のように使います。var addsub = $.oneOf("+-"); var muldiv = $.oneOf("*/%");簡単ですねー。早速試してみましょう。
using Parsek; var $ = new Parsek(); var addsub = $.oneOf("+-"); var muldiv = $.oneOf("*/%"); System.println(addsub.parseAll("+")); // => {"position":1,"status":1,"value":"+"} System.println(addsub.parseAll("-")); // => {"position":1,"status":1,"value":"-"} System.println(addsub.parseAll("*")); // => {"position":0,"status":0,"value":null} System.println(muldiv.parseAll("*")); // => {"position":1,"status":1,"value":"*"} System.println(muldiv.parseAll("/")); // => {"position":1,"status":1,"value":"/"} System.println(muldiv.parseAll("%")); // => {"position":1,"status":1,"value":"%"} System.println(muldiv.parseAll("a")); // => {"position":0,"status":0,"value":null}期待通りです。
カッコをパースするパーサー
もう一つ、数値演算には必要なカッコを解釈させましょう。特定の文字列にマッチするパーサーは
$.string()
を使います。ここでは 1 文字ですが、何文字の文字列でも OK です。var lbr = $.string("("); var rbr = $.string(")");これも試してみるとうまく動きます。
$.string()
の効果を見るために別の文字列でも試してみましょう。using Parsek; var $ = new Parsek(); var lbr = $.string("("); var rbr = $.string(")"); var hoge = $.string("hoge"); System.println(lbr.parseAll("(")); // => {"position":1,"status":1,"value":"("} System.println(lbr.parseAll(")")); // => {"position":0,"status":0,"value":null} System.println(rbr.parseAll("(")); // => {"position":0,"status":0,"value":null} System.println(rbr.parseAll(")")); // => {"position":1,"status":1,"value":")"} System.println(hoge.parseAll("hoge")); // => {"position":4,"status":1,"value":"hoge"} System.println(hoge.parseAll("fuga")); // => {"position":0,"status":0,"value":null}正しくマッチしているのが分かります。
組み合わせるとは?(コンビネーター)
これで小さな(単純な)パーサーという道具が揃いました。
var number = $.regex(/[1-9][0-9]*|[0-9]/).map(Integer.parseInt); var addsub = $.oneOf("+-"); var muldiv = $.oneOf("*/%"); var lbr = $.string("("); var rbr = $.string(")");これらを組み合わせてみましょう。ここで PEG を出しておきます。BNF でもいいですが、PEG のほうがコンビネーターには合ってますね。文法はこうですよ、というのを示しておかないと何やってるかわからなくなりそうですので。意味は追々触れていきます。
number <- regex(/[1-9][0-9]*|[0-9]/) addsub <- '+' / '-' muldiv <- '*' / '/' / '%' lbr <- '(' rbr <- ')' expression <- term (addsub term)* term <- factor (muldiv factor)* factor <- number / (lbr expression rbr)PEG の優先度付選択の記号
/
と除算の指定'/'
が紛らわしいですが、よく見ると分かります。トップダウン、ボトムアップどちらもありですが、ここではボトムアップでパーサーを構築していきます。
factor
まずは
factor
です。factor <- number / (lbr expression rbr)
factor
はnumber
かlbr expression rbr
か、になります。プログラムにそのまま落とせます。ここで使うのは以下のメソッドです。
- ここではまだ
expression
が定義されていないので、遅延評価させるために$.lazy()
を使います。$.lazy()
を使うと実際に評価されるときにパーサーが作られます。- どちらかを選ぶ、というメソッドは
$.alt()
です。複数のものから最初に成功したパーサーの結果を返します。lbr expression rbr
というように複数のものが連続している、ということを表すのが$.seq()
です。さて書いてみましょう。
expression
は事前に宣言だけしておきます。var expression; var factor = $.lazy(&() => $.alt(number, $.seq(lbr, expression, rbr)));term
次は
term
です。term <- factor (muldiv factor)*これは、
factor
の後に(muldiv factor)
が 0 回以上続く、という意味です。0 回も許されるので、何も続かない、というのも OK です。muldiv factor
といった感じに並べるのはさっきのlbr expression rbr
と同じで連続していることを意味します。ここで使うメソッドは以下です。
- 0 回以上の繰り返し、はパーサーに対して
.many()
を指定します。では定義してみましょう。
var term = $.seq(factor, $.seq(muldiv, factor).many());これで
term
が定義できました。expression
最後に
expression
です。形はterm
と一緒ですね。expression <- term (addsub term)*そのまま書いてみましょう。
expression = $.seq(term, $.seq(addsub, term).many());これでパーサーが揃いました。試しにパースしてみましょう!
パース
一旦、ソースコードを全部載せてみます。とはいってもそんなにないですね。
using Parsek; var $ = new Parsek(); var number = $.regex(/[1-9][0-9]*|[0-9]/).map(Integer.parseInt); var addsub = $.oneOf("+-"); var muldiv = $.oneOf("*/%"); var lbr = $.string("("); var rbr = $.string(")"); var expression; var factor = $.lazy(&() => $.alt(number, $.seq(lbr, expression, rbr))); var term = $.seq(factor, $.seq(muldiv, factor).many()); expression = $.seq(term, $.seq(addsub, term).many()); // parse expression! System.println(expression.parseAll("1+2*3+2*(14-2)")); // => {"position":14,"status":1,"value":[[1,{}],[["+",[2,[["*",3]]]],["+",[2,[["*",["(",[[14,{}],[["-",[2,{}]]]],")"]]]]]]]} System.println(expression.parseAll("1+2*3+2*(14-2-)")); // => {"position":7,"status":0,"value":null}最初のは(長いですが)成功したことが分かります。結果を読むのは大変ですが、これは後で整形しましょう。そして、2 つ目は失敗していることが分かります。最後の
(14-2-)
がどの規則にもマッチしてないからですね。ではこの結果を整形していきましょう。活躍するのは
number
で使った.map()
です。カッコの式
まず、
$.seq(lbr, expression, rbr)
の部分です。$.seq()
は値として結果の配列を返します。カッコの式というのは、値としてはカッコは不要で中にある式の結果だけあればいいですね。ということで、以下のように変えます。var factor = $.lazy(&() => $.alt(number, $.seq(lbr, expression, rbr).map(&(value) => value[1])));修正すると結果は次のようになります。
System.println(expression.parseAll("1+2*3+2*(14-2)")); // => {"position":14,"status":1,"value":[[1,{}],[["+",[2,[["*",3]]]],["+",[2,[["*",[[14,{}],[["-",[2,{}]]]]]]]]]]}ちょっと短くなりましたね。
term、expression
次に、
term
とexpression
です。ここでは、後で解析するために AST(Abstract Syntax Tree = 抽象構文木)の形に整形するようにしましょう。基本的には二項演算子なので、LHS(Left Hand Side = 左辺値)と RHS(Right Hand Side = 右辺値)と演算子(Operator)の組み合わせのオブジェクトを作ります。ここで、
$.seq()
をやめて、$.seqMap()
を使うように変更します。これは$.seq()
と.map()
を一緒にしたようなもので、結果リストを引数として最後の引数に指定した関数にコールバックしてくれる便利なメソッドです。こんな風に使います。var term = $.seqMap(factor, $.seq(muldiv, factor).many(), &(first, rest) => { var expr = first; for (var i = 0, l = rest.length(); i < l; ++i) { expr = { lhs: expr, op: rest[i][0], rhs: rest[i][1] }; } return expr; });
first
はfactor
の結果で、rest
は$.seq(muldiv, factor).many()
の結果です。なので、rest
は各要素が[演算子, 右辺値]
の形の配列です(空配列の場合もある)。それを AST の形に整形しています。結果、例えば"2 * 3 * 4"
みたいなものは以下のように整形されます。
- コールバック時
- まず、
first
は2
rest
は[['*', 3], ['*', 4]]
expr
に2
が入る- ループに入り、
expr
が{ lhs: 2, op: '*', rhs: 3 }
になる。- もう一つ要素があるので、
expr
が{ lhs: { lhs: 2, op: '*', rhs: 3 }, op: '*', rhs: 4 }
になる。左側の枝が伸びていく形の AST になります(これを左結合という)。今回の演算子は全て左結合です。
expression
も一緒なので、同じように書きましょう。中身は全く同じですなので関数化して使いまわしましょう。function makeAST(first, rest) { var expr = first; for (var i = 0, l = rest.length(); i < l; ++i) { expr = { lhs: expr, op: rest[i][0], rhs: rest[i][1] }; } return expr; } var term = $.seqMap(factor, $.seq(muldiv, factor).many(), makeAST); expression = $.seqMap(term, $.seq(addsub, term).many(), makeAST);すっきりしました。
では、プログラム一式です。たったこれだけの定義で四則演算を(演算子の優先順位も考慮された形で)パースできてしまいます。素晴らしいですね!
using Parsek; function makeAST(first, rest) { var expr = first; for (var i = 0, l = rest.length(); i < l; ++i) { expr = { lhs: expr, op: rest[i][0], rhs: rest[i][1] }; } return expr; } var $ = new Parsek(); var number = $.regex(/[1-9][0-9]*|[0-9]/).map(Integer.parseInt); var addsub = $.oneOf("+-"); var muldiv = $.oneOf("*/%"); var lbr = $.string("("); var rbr = $.string(")"); var expression; var factor = $.lazy(&() => $.alt(number, $.seq(lbr, expression, rbr).map(&(value) => value[1]))); var term = $.seqMap(factor, $.seq(muldiv, factor).many(), makeAST); expression = $.seqMap(term, $.seq(addsub, term).many(), makeAST); // test System.println(expression.parseAll("1+2*3+2*(14-2)").value.toJsonString(true));結果はこうなります。うまくいってますね!
"lhs": { "lhs": 1, "op": "+", "rhs": { "lhs": 2, "op": "*", "rhs": 3 } }, "op": "+", "rhs": { "lhs": 2, "op": "*", "rhs": { "lhs": 14, "op": "-", "rhs": 2 } }おわりに
さて、目的の AST ができました。次回、これを解釈して実行させます。せっかく作った JIT ライブラリも使いますよ!
ではまた次回!
- 投稿日:2020-07-03T19:16:17+09:00
Rails5でECサイトを作る① ~アプリ構成、各種gem準備、Model・Routing作成~
はじめに
先日、プログラミングスクールにてチーム実装の課題があり、ECサイトを作りました。
自分の担当したところ以外がどのようなコードで動いているのか、復習も兼ねて作ってみたいと思います。何のお店のECサイトにするか迷いましたが、Googleフォトのアルバムにパンの写真がたくさん入っていたので、架空のパン屋さんということにします。
コードソース
https://github.com/Sn16799/bakeryFUMIZUKI
環境
Rails 5.2.4.2
Ruby 2.7.1
Centos7アプリの概要
・ECサイト(ユーザがサイト内で買い物できる)
・ユーザサイトと管理者サイトを制作
・ユーザサイトでは会員登録してログイン後、カートに商品を入れ、注文手続きをする
・管理者サイトでは注文が入った商品の注文ステータス(入金待ち、製作中、……)と製作ステータス(着手不可、製作中、……)の変更、商品や会員の管理を行うアプリ立ち上げ
$ rails new fumizuki # 7月なので店名(架空)は「ベーカリー文月」で。 $ cd fumizukiGemfileに以下を追加
# ログイン機能 gem 'devise' # view装飾 gem 'bootstrap' gem 'jquery-rails' # 画像投稿 gem 'refile', require: "refile/rails", github: 'manfe/refile' gem 'refile-mini_magick' # 環境変数の管理 gem 'dotenv-rails', require: 'dotenv/rails-now' # ページャ gem 'kaminari','~> 1.1.1' gem 'nokogiri', '1.10.9' # デバッグ gem 'pry-rails'インストール出来たら
$ rails g devise:installModel作成
DBの構成は下図の通りです。
管理者サイトもgemを使わず、自作したいと思います。
gemを使う場合は、active_adminが便利でした。
(地道な作業)$ rails g devise admin email:string $ rails g devise customer is_active:boolean first_name:string first_name_kana:string family_name:string family_name_kana:string post_code:string address:string tel:string email:string $ rails g model address customer_id:integer post_code:string addressee:string address:string $ rails g model cart_item product_id:integer customer_id:integer quantity:integer $ rails g model genre name:string validity:boolean $ rails g model order_item product_id:integer order_id:integer quantity:integer order_price:integer make_status:integer $ rails g model order customer_id:integer addressee:string post_code:string send_to_address:string how_to_pay:boolean deliver_fee:integer order_status:integer $ rails g model product genre_id:integer name:string introduction:text status:boolean image_id:string price:integer # 一通り作ったらmigrate $ rails db:migrateRouting
Routingも仮のものを用意しておきます。
config/routes.rbRails.application.routes.draw do #rootパス root 'homes#top' # 顧客用サイトのrouting devise_for :customers, controllers: { registrations: 'customers/registrations', passwords: 'customers/passwords', sessions: 'customers/sessions'} get 'homes/top' => 'homes#top', as: 'customer_top' get 'homes/about' => 'homes#about', as: 'customer_about' resources :customers, only: [:edit, :show, :update] get 'customers/:id/withdraw' => 'customers#withdraw', as: 'customer_withdraw' patch 'customers/:id/withdraw' => 'customers#withdraw_done', as: 'customer_withdraw_done' put "/customers/:id/withdraw" => "customers#withdraw_done", as: 'customers_withdraw_done' resources :orders, only: [:new, :index, :create, :show] post 'orders/confirm' => 'orders#confirm', as: 'order_confirm' get 'orders/thanks' => 'orders#thanks', as: 'order_thanks' resources :products, only: [:index, :show] resources :order_items, only: [:index, :create, :new] resources :addresses, only: [:index, :create, :edit, :update, :destroy] resources :genres, only: [:show] #カートアイテムを全て削除メソッドのために追加 resources :cart_items, only: [:index, :create, :update, :destroy] do collection do delete 'destroy_all' end end # 管理者用サイトのrouting devise_scope :admins do devise_for :admins, controllers: { registrations: 'admins/registrations', passwords: 'admins/passwords', sessions: 'admins/sessions' } end namespace :admins do get 'homes/top' => 'homes#top', as:'top' resources :customers, only: [:index, :edit, :show, :update] resources :products, only: [:index, :create, :new, :edit, :show, :update] resources :orders, only: [:index, :create, :show, :update] resources :order_items, only: [:index, :create, :show, :update] resources :genres, only: [:index, :create, :edit, :update] get 'search' => 'searches#search', as: 'search' end end後記
ひとまずModelとRoutingのみ作りました。アクションを書き込んでwebアプリらしい動きをするまで、まだまだ道のりは遠そうです。
それにしても、チーム実装の際に3人で作ったものを1人で作ろうとすると、作業量が尋常じゃないですね。でも、当時はかなり大変だと思っていた量も、今見るとそこまで高い壁でもない気がします。初めてこのアプリを作ったのは3か月前ですが、その間に私も成長したのかも知れません。この後は、ControllerとViewを揃えたいと思います。次回へ続く!
参考
active_adminの使い方(本記事では使ってないけど)
Railsで最速で管理画面を作る!
- 投稿日:2020-07-03T18:41:02+09:00
Ruby on Rails 住所自動入力実装方法
はじめに
現在のバージョン:
macOS Catalina 10.15.3Ruby on Railsで住所自動入力を実装してみました。
備忘録として記述します。もし参考になれば幸いです。手順
(1)はじめに下記からjsファイル(jQueryプラグイン)をダウンロード。
jquery.jpostal.js(2)ダウンロードしたjsファイルをapp/assets/javascriptsに配置。
(3)Gemfileに下記を追加後、ターミナルで
$ bundle install
。Gemfile# RailsでjQueryを使えるようにするため gem 'jquery-rails' # 住所機能 gem 'jp_prefecture'(4)Userモデルに下記カラムを追加後、ターミナルで
$ rails db:migrate
。ターミナルrails generate migration AddColumnsToUsers postal_code:string prefecture_code:string address_city:string address_street:string(5)Userモデルを編集する為、app/models/user.rbに下記追加
user.rbinclude JpPrefecture jp_prefecture :prefecture_code def prefecture_name JpPrefecture::Prefecture.find(code: prefecture_code).try(:name) end def prefecture_name=(prefecture_name) self.prefecture_code = JpPrefecture::Prefecture.find(name: prefecture_name).code end(6)表示をさせるビューに下記追加(本記事はdeviseを導入して新規会員登録画面で実装)app/views/devise/registrations/new.html.erbに追加記述
new.html.erb<%= f.label :郵便番号 %> <%= f.text_field :postal_code, autocomplete: "postal_code", id: "customer_postal_code" %> <%= f.label :都道府県 %> <%= f.collection_select :prefecture_code, JpPrefecture::Prefecture.all, :name, :name, autocomplete: "prefecture_code", id: "customer_prefecture_code" %> <%= f.label :市区町村 %> <%= f.text_field :address_city, autocomplete: "address_city", id: "customer_address_city" %> <%= f.label :町名番地 %> <%= f.text_field :address_street, autocomplete: "address_street", id: "customer_address_street" %>(7)app/assets/javascripts/user.coffeeにjpostalメソッドを呼び出す為記述。
user.coffee$ -> $("#user_postcode").jpostal({ postcode : [ "#user_postcode" ], address : { "#user_prefecture_code" : "%3", "#user_address_city" : "%4", "#user_address_street" : "%5%6%7" } }) # 入力項目フォーマット # %3 都道府県 # %4 市区町村 # %5 町域 # %6 大口事業所の番地 # %7 大口事業所の名称(8)app/controllers/users_controller.rbに下記追加記述
users_controller.rbprivate def user_params params.require(:user).permit(:postcode, :prefecture_code, :address_city, :address_street) #保存を許すカラム end以上で郵便番号入力後、住所が自動で入力がされるかと思います。
補足
住所自動入力を設定した画面に遷移してリロードしないと郵便番号の自動入力がしない場合、リンクの記述に data: {"turbolinks" => false} を追加すると解消できました。
<%= link_to '会員登録', '/customers/sign_up', data: {"turbolinks" => false} %>初めて実装した場合約20分はかかりました。
ご参考になれば幸いです。
- 投稿日:2020-07-03T18:34:35+09:00
ruby on rails にて 閲覧機能をつけたい
プログラミングを初めて2週間ほどです。
NoMethodError in Todolists#index
Showing /home/vagrant/work/sample_app/app/views/todolists/index.html.erb where line #2 raised:undefined method `each' for nil:NilClass
Extracted source (around line #2):
1NoMethodError in Todolists#index
Showing /home/vagrant/work/sample_app/app/views/todolists/index.html.erb where line #2 raised:undefined method `each' for nil:NilClass
Extracted source (around line #2
1投稿一覧
2 <% @lists.each do |list| %> ←ここがエラー
3タイトル
4 <%= list.title %>
5 <% end %>このようなエラーが出て解決方法がわかりません。
余りにもわからないのでテキストをコピペしましたが未可決です。
どこを見直せば大丈夫ですか?
- 投稿日:2020-07-03T17:24:34+09:00
rails capybara使用時に出たエラー
今回の目的として、以下の参考記事のように、
rspecテスト実行時にsign_inメソッドを利用できるようになることであった。
https://qiita.com/jnchito/items/a8360e5e7a829d1e19b2そのために、railsでcapybaraの設定を試みたところ以下のエラーが出た
それぞれのエラーに参考になったリンクを貼っておく。
また、大まかな設定は次の記事を参考にさせていただいた。
https://qiita.com/morrr/items/0e24251c049180218db4undefined method `visit'https://qiita.com/terufumi1122/items/aefd6c965e9e946efc3b
visitはcapybaraで使えるメソッドなので、設定したうえでないと上記のエラーになるらしいFailure/Error: fill_in 'email', with: user.email Capybara::ElementNotFound: Unable to find field "email" that is not disabledhttps://qiita.com/pooooon/items/4fbc429d07e4b65ed928
私の場合、下記のように変更したところエラーがでなくなった。
fill_in 'user[email]', with: user.email
fill_in 'user[password]', with: 'password'
記事通り、'session[email]'ではないパターンもあるので、きちんとブラウザで確認した方がよいだろう。
- 投稿日:2020-07-03T17:14:32+09:00
勤怠管理
- 投稿日:2020-07-03T16:32:53+09:00
【Rails 5.x】フリーフォントの導入方法
フリーフォントをRailsに導入する方法
自作のポートフォリオにフリーフォントを導入したいと思い、Qiita記事を参考に導入しました。
今更感はありますが・・・備忘録かつ、
初学者の参考になればと思い、記事を作成しました。筆者の環境
・Ruby 2.5.3
・Rails 5.2.21 フリーフォントのファイルをダウンロード
2 フォントファイルを app/assets/fonts 配下に置く
(app名)/app/assets/fontshogehoge.ttf ( または hoge.otf ) のようなフォントファイルを、fonts ディレクトリ内に入れます。
自分の場合はfontsディレクトリがなかったのでassets配下にmkdirで作成しました。3 SCSSファイルに記述
SCSSに、
custom.scss@font-face { font-family: 'hoge'; # font-family名は適宜決定 src: font-url('hogehoge.ttf') format('truetype'); font-weight: normal; font-style: normal; }このように記述します。
ファイル内のどこに記述してもよいです。
そして、custom.scssbody { font-family: hoge; }先ほどのfont-family名を
bodyに記述し、全体に適用させます。4 完成!
この手順でフリーフォントを適用できているかと思います。
意外に簡単でしたね。
反映されない場合は、適宜サーバーをrestartなどしてみてください!5 参考記事
- 投稿日:2020-07-03T14:27:58+09:00
fullcalendarを使ってイベント管理できるものを作ろう。①準備
fullcalendarを使ってイベント管理できるものを作成しようと思っています。
これは、RubyonRailsの勉強を兼ねてのものです。何か間違っている、こう改善した方がいい、など指摘があれば教えていただけたらと思っています。
参考書は、「現場で使えるRuby on Rails 5速習実践ガイド」です。
さあ、頑張っていこう。環境
OS:MacOS Catalina10.15.5
Ruby:2.6.3
Rails:5.2.4アプリの作成
$ rails new Mark -d postgresql
モデルの作成
$ rails g model Event
Railsのモデルは、主に2つの要素から構成
・モデルに対応するRubyのクラス
・モデルに対応するデータベースのテーブルクラス名とテーブル名には以下の命名規約がある。
・データベースのテーブル名は、モデルのクラス名を複数形にしたもの
・モデルのクラス名はキャメルケース、テーブル名はスネークケースEventモデルの属性を設定
属性の意味 属性名・カラム名 データ型 タイトル title string 始まり start datetime 終わり end datetime 終日 allday boolean カレンダー色 color string とりあえず、こんな感じで。
db/migrate/*******_create_events.rbclass CreateEvents < ActiveRecord::Migration[6.0] def change create_table :events do |t| t.string :title, null: false t.datetime :start, null: false t.datetime :end, null: false, t.boolean :allday, null: false, default: false t.string :color, null: false t.timestamps end end end
$ rails db:migrate
マイグレーションをデータベースに適用。コントローラーとビューの作成
$bin/rails g controller events index show new edit
コントローラ名は、モデルの複数形。その後ろに必要なアクション名を追記。ルーティングの設定
config/routes.rbRails.application.routes.draw do root to: 'events#index' resources :events endfullcalendarの実装
Gemfileに追加。
gem 'fullcalendar-rails' gem 'momentjs-rails'
$ bundle install
application.jsとapplication.cssに追加
assets/javascripts/application.js//= require jquery //= require moment //= require fullcalendar //= require_tree .assets/stylesheets/application.css*= require fullcalendar */application.jsにコードを記述。
assets/javascripts/application.js$(document).ready(function() { $('#calendar').fullCalendar({ events: '/events.json' }); });viewに表示できるように追加。
app/view/events/index.html.erb<div id="calendar"></div>お、出来ました。まだ、表示させただけなので、これから中身を作っていこう。。
参考
https://qiita.com/sasasoni/items/fb0bc1644ece888ae1d4
https://qiita.com/ShoutaWATANABE/items/3d0cddafadb4f275991e
https://fullcalendar.io/
- 投稿日:2020-07-03T00:17:28+09:00
Rails Tutorial MEMO#8
Rails Tutorial第8章
基本的なログイン機構
本章では、ログインの基本的な仕組みを実装していく。
8.1 セッション
HTTPはそれより前のリクエストの情報を全く利用できない、Statelessなプロトコル。故にユーザーのIDを保持しておく手段がHTTPプロトコル内「には」全く無い。
ユーザーログインの必要なWebアプリケーションでは、セッション(Session)と呼ばれる半永続的な接続をコンピュータ間(ユーザーのパソコンのWebブラウザとRailsサーバーなど)に別途設定する。
Railsでセッションを実装する方法として最も一般的なのは、cookies
を使う方法。8.1.1 Sessionsコントローラ
create
アクションにPOSTリクエストを送信すると、実際にログインする。
destroy
アクションにDELETEリクエストを送信するとログアウトする。
Sessionsコントローラを生成する
$rails generate controller Sessions new
routesにリソースを追加する。config/routes.rbRails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' #新しいセッションのページ post '/login', to: 'sessions#create' #新しいセッションの作成 delete '/logout', to: 'sessions#destroy' #セッションの削除 resources :users endSessionsコントローラのテストで名前付きルートを使うようにする。
test/controllers/sessions_controller_test.rbrequire 'test_helper' class SessionsControllerTest < ActionDispatch::IntegrationTest test "should get new" do get login_path assert_response :success end end
$rails routes
コマンドで現状のルーティングを確認できる。8.1.2 ログインフォーム
セッションフォームとユーザー登録フォームの最大の違いは、セッションにはSessionモデルというものがなく、そのため@userのようなインスタンス変数に相当するものもない点。したがって、新しいセッションフォームを作成するときには、form_forヘルパーに追加の情報を独自に渡さなければならない。
Railsでは以下のように書くだけで、「フォームのactionは/usersというURLへのPOSTである」と自動的に判定するが
form_for(@user)
↓セッションの場合は、リソースの名前とそれに対応するURLを具体的に指定する必要がある
form_for(:session, url: login_path)
ログインフォームのコードapp/views/sessions/new.html.erb<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= 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 %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>8.1.3 ユーザーの検索と認証
ログインでセッションを作成する場合の作業順番
1,入力が無効な場合の処理
2,ログインが失敗した場合に表示されるエラーメッセージの配置
3,ログイン成功した場合に使う土台部分作成
今回はパスワードとメールアドレスの組み合わせが有効かどうかの判定
createアクションを実行するとnewビューが出力されるapp/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create render 'new' end def destroy end endcerateで最初に失敗したログインのデバッグ情報 --- session: email: 'user@example.com' password: 'foobar' commit: Log in action: create controller: sessionsparamsは次のような入れ子ハッシュ(ハッシュの中にハッシュがある構造)になっている
{ session: { password: "foobar", email: "user@example.com" } }
createアクションの中では、ユーザーの認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる。app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) # ユーザーログイン後にユーザー情報のページにリダイレクトする else # エラーメッセージを作成する render 'new' end end def destroy end end
&&(論理積(and))
は、取得したユーザーが有効かどうかを決定するために使う。8.1.4 フラッシュメッセージを表示する
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end 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 def destroy end end上記のままでは一度表示されたフラッシュメッセージが消えずに残ってしまう
8.1.5 フラッシュのテスト
最初に統合テストを生成する
$ rails generate integration_test users_login
以下テストコード再現の流れ
1,ログイン用のパスを開く
2,新しいセッションのフォームが正しく表示されたことを確認する
3,わざと無効なparamsハッシュを使ってセッション用パスにPOSTする
4,新しいセッションフォームが再度表示され、フラッシュメッセージが追加されることを確認する
5,別のページに一旦移動する
6,移動先のページでフラッシュメッセージが表示されていないことを確認するtest/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest test "login with invalid information" do get login_path (1) assert_template 'sessions/new' (2) post login_path, params: { session: { email: "", password: "" } } (3) assert_template 'sessions/new' (4)新しいセッションフォームが再表示されることを確認 assert_not flash.empty? (4)フラッシュメッセージが追加されることを確認 get root_path (5) assert flash.empty? (6) end end
$ rails test test/integration/users_login_test.rb
このようにrails testの引数にテストファイルを与えると、そのテストだけを実行することができる
テストをパスさせるにはcreateアクションのflash
をflash.now
に置き換える
flash.now
のメッセージはその後のリクエストが発生したときに消滅する
これによってテストもGREENになる8.2 ログイン
ログイン中の状態での有効な値の送信をフォームで正しく扱えるようにする
セッションを実装するには様々なコントローラやビューで沢山のメソッドを定義する必要がある。そうしたメソッドを一箇所にパッケージ化できるRubyのモジュール機能を使う。
Sessionsコントローラを生成した時点で既にセッション用ヘルパーモジュールも自動生成されている。さらに、Railsのセッション用ヘルパーはビューにも自動的に読み込まれる。Railsの全コントローラの親クラスであるApplicationコントローラにこのモジュールを読み込ませれば、どのコントローラでも使えるようになる。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper end8.2.1 log_inメソッド
Railsで事前定義済みの
session
メソッドを使って、単純なログインを行えるようにする。このsession
メソッドはハッシュのように扱える。
seesion[:user_id] = user.id
上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end endsessionメソッドで作成した一時cookiesは自動的に暗号化され、上記のコードは保護される。
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy end endこれで
app/views/sessions/new.html.erb
で定義したログインフォームも正常に動作するようになった。8.2.2 現在のユーザー
current_user
メソッドを定義して、セッションIDに対応するユーザー名をデータベースから取り出せるようにする。しかし、find
を使うと例外が発生してしまう。find_by
メソッドを使うことで、IDが無効な場合(=ユーザーが存在しない場合)にもメソッドは例外を発生せず、nilを返す。
User.find_by(id: session[:user_id])
app/helpers/sessions_helper.rbdef current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end endif @current_user.nil? @current_user = User.find_by(id: session[:user_id]) else @current_user endor演算子「||」を使えば上記の「メモ化」コードが次のように変換できる
@current_user = @current_user || User.find_by(id: session[:user_id])
「Ruby的に」正しいコードではないので以下のように変換させる
この書き換えは、 x = x + 1 が x += 1 になることと同じ
@current_user ||= User.find_by(id: session[:user_id])
8.2.3 レイアウトリンクを変更する
ユーザーがログインしている時とそうでない時でレイアウトを変更する。
論理値を返すlogged_in?
メソッドを定義、ユーザーがログイン中の状態とは「sessionにユーザーidが存在している」こと、つまりcurrent_user
がnil
ではないという状態を指す。これをチェックするには!(否定演算子)
を使っていく。app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # 現在ログイン中のユーザーを返す (いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end endログイン中のユーザー用のレイアウトのリンクを変更する
app/views/layouts/_header.html.erb<header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <li><%= link_to "Users", '#' %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", '#' %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: :delete %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header>レイアウトに新しいリンクを追加したので、上記のコードにBootstrapのドロップダウンメニュー機能
dropdown
クラスやdropdown-menu
など使えるようになる。これらのドロップダウン機能を有効にするため、Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むようアセットパイプラインに指示する。app/assets/javascripts/application.js//= require rails-ujs //= require jquery //= require bootstrap //= require turbolinks //= require_tree .8.2.4 レイアウトの変更をテストする
統合テストを書いてこの動作をテストで表現し、今後の回帰バグの発生をキャッチで切るようにする。手順は
test/integration/users_login_test.rb
を元に作成する。テストの確認をするためにはテスト時に登録済みユーザーとしてログインしておく必要がある。Railsでは、このようなテスト用データを
fixture(フィクスチャ)
で作成できる。このfixtureを使って、テストに必要なデータをtestデータベースに読み込んでおくことができる。現時点のテストでは、ユーザーは一人いれば問題なので有効な名前とメールアドレスを設定しておく。テスト中にそのユーザーとして自動ログインするために、そのユーザーの有効なパスワードも用意して、
Sessions
コントローラのcreate
アクションに送信されたパスワードと比較できるようにする必要がある。app/models/user.rbclass User < ApplicationRecord before_save { self.email = email.downcase } validates :name, presence: true, length: { maximum: 50 } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false } has_secure_password validates :password, presence: true, length: { minimum: 6 } # 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end endtest/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %>test/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) #fixtureのデータを参照 end . . . test "login with valid information" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert_redirected_to @user #リダイレクト先が正しいかチェック follow_redirect! #リダイレクト先に移動、ログイン用リンクが表示されなくなったことを確認 assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) end end8.2.5 ユーザー登録時にログイン
登録の終わったユーザーがデフォルトでログインされている状態にする
Usersコントローラのcreateアクションにlog_inを追加するapp/controllers/users_controller.rbclass UsersController < ApplicationController def show @user = User.find(params[:id]) end def new @user = User.new end def create @user = User.new(user_params) if @user.save log_in @user #追加 flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end endtest/integration/users_signup_test.rbrequire 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest . . . test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! assert_template 'users/show' assert is_logged_in? #追加 end end8.3 ログアウト
ログアウト機能を追加する
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end . . . # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end endapp/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out redirect_to root_url end endユーザーログアウトのテスト
test/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest . . . test "login with valid information followed by logout" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert is_logged_in? #ログイン出来ているか確認 assert_redirected_to @user #以降ログインテスト follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) delete logout_path #以降ログアウトテスト assert_not is_logged_in? assert_redirected_to root_url follow_redirect! assert_select "a[href=?]", login_path assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 end endまとめ
- Railsのsessionメソッドを使うと、あるページから別のページに移動するときの状態を保持できる。一時的な状態の保存にはcookiesも使える
- ログインフォームでは、ユーザーがログインするための新しいセッションが作成できる
- flash.nowメソッドを使うと、描画済みのページにもフラッシュメッセージを表示できる
- テスト駆動開発は、回帰バグを防ぐときに便利
- sessionメソッドを使うと、ユーザーIDなどをブラウザに一時的に保存できる
- ログインの状態に応じて、ページ内で表示するリンクを切り替えることができる
- 統合テストでは、ルーティング、データベースの更新、レイアウトの変更が正しく行われているかを確認できる
あいも変わらずテストで理解が追いつかなかった。