20200703のRubyに関する記事は10件です。

メッセージ送信の非同期化

フォームが送信されたら、イベントが発火するようにしよう

スクリーンショット 2020-07-03 22.12.39.png
この記述の解説をします。
$(**)には、formのクラス名を記述します。
.on(
**,にはイベント名を記述します。
e.preventDefaulでは、非同期通信を行う為にデフォルトのイベントを止めています。

イベントが発火したときにAjaxを使用して、messages#createが動くようしましょう

スクリーンショット 2020-07-03 22.17.55.png
この記述の中のthisは、イベントの発火元であるFormの情報が入っています。
$(this).attr('action');は、Form情報のパスを取得しています。

messagesコントローラーの#createアクションでメッセージを保存し、respond_toを使用してJSON形式のリクエストに対してのレスポンスを返せるようにしましょう

スクリーンショット 2020-07-03 22.21.45.png
if @message.save
リクエストで送られてきた情報を保存している
respond_to do |format|
format.json
json方式で返している

その他アウトプット

スクリーンショット 2020-07-03 22.25.20.png
クラス名MessageFieldにappend(html)でHTMLを追加している

$(".submit-btn").prop('disabled', false);

送信ボタンを一度押すとリロードしないと押せなくなるが
prop('disabled', false);を送信ボタンクラスに記述する事によりロードせずに投稿ができる様になる

非同期に失敗した場合の処理

スクリーンショット 2020-07-03 22.31.17.png
失敗した場合エラーをアラートで知らせてくれます。
Doneの後に使います

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

Kinx ライブラリ - パーサ・コンビネータ(その1)

Kinx ライブラリ - パーサ・コンビネータ(その1)

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。今回はパーサ・コンビネータです。

前回 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 が実際にパースが成功した文字列です。見て分かる通り、失敗すると valuenull です。

しかしよく見ると 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)

factornumberlbr 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

次に、termexpression です。ここでは、後で解析するために 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;
});

firstfactor の結果で、rest$.seq(muldiv, factor).many() の結果です。なので、rest は各要素が [演算子, 右辺値] の形の配列です(空配列の場合もある)。それを AST の形に整形しています。結果、例えば "2 * 3 * 4" みたいなものは以下のように整形されます。

  • コールバック時
    • まず、first2
    • rest[['*', 3], ['*', 4]]
  • expr2 が入る
  • ループに入り、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 ライブラリも使いますよ!

ではまた次回!

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

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 fumizuki

Gemfileに以下を追加

# ログイン機能
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:install

Model作成

DBの構成は下図の通りです。
管理者サイトもgemを使わず、自作したいと思います。
gemを使う場合は、active_adminが便利でした。
association.jpg
(地道な作業)

$ 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:migrate

Routing

Routingも仮のものを用意しておきます。

config/routes.rb
Rails.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で最速で管理画面を作る!

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

Ruby on Rails 住所自動入力実装方法

はじめに

現在のバージョン:
macOS Catalina 10.15.3

Ruby 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.rb
  include 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.rb
  private
  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分はかかりました。
ご参考になれば幸いです。

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

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| %> ←ここがエラー

タイトル


4 <%= list.title %>
5 <% end %>

このようなエラーが出て解決方法がわかりません。 

余りにもわからないのでテキストをコピペしましたが未可決です。

どこを見直せば大丈夫ですか?

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

rails capybara使用時に出たエラー

今回の目的として、以下の参考記事のように、
rspecテスト実行時にsign_inメソッドを利用できるようになることであった。
https://qiita.com/jnchito/items/a8360e5e7a829d1e19b2

そのために、railsでcapybaraの設定を試みたところ以下のエラーが出た
それぞれのエラーに参考になったリンクを貼っておく。
また、大まかな設定は次の記事を参考にさせていただいた。
https://qiita.com/morrr/items/0e24251c049180218db4

undefined 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 disabled

https://qiita.com/pooooon/items/4fbc429d07e4b65ed928
私の場合、下記のように変更したところエラーがでなくなった。
fill_in 'user[email]', with: user.email
fill_in 'user[password]', with: 'password'
記事通り、'session[email]'ではないパターンもあるので、きちんとブラウザで確認した方がよいだろう。

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

勤怠管理

- ruby 2.6.5
- rails 6.0.3
- devise
- rails_admin
- cancan
- rails_admin_import
- bootstrap
- slim-rails

group :test do
  - rspec
  - factory_bot_rails
end
参考サイト
サンプルサイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails 5.x】フリーフォントの導入方法

フリーフォントをRailsに導入する方法

自作のポートフォリオにフリーフォントを導入したいと思い、Qiita記事を参考に導入しました。
今更感はありますが・・・備忘録かつ、
初学者の参考になればと思い、記事を作成しました。

筆者の環境
・Ruby  2.5.3
・Rails 5.2.2

1 フリーフォントのファイルをダウンロード
2 フォントファイルを app/assets/fonts 配下に置く
(app名)/app/assets/fonts

hogehoge.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.scss
body {  
  font-family: hoge;
}

先ほどのfont-family名を
bodyに記述し、全体に適用させます。

4 完成!

この手順でフリーフォントを適用できているかと思います。
意外に簡単でしたね。
反映されない場合は、適宜サーバーをrestartなどしてみてください!

5 参考記事

【初学者向け】Railsアプリケーションにカスタムフォントを追加する方法

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

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.rb
class 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.rb
Rails.application.routes.draw do
  root to: 'events#index'
  resources :events
end

fullcalendarの実装

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>

rails s で起動させてみる。
スクリーンショット 2020-07-03 14.24.37.png

お、出来ました。まだ、表示させただけなので、これから中身を作っていこう。。

参考

https://qiita.com/sasasoni/items/fb0bc1644ece888ae1d4
https://qiita.com/ShoutaWATANABE/items/3d0cddafadb4f275991e
https://fullcalendar.io/

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

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.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'      #新しいセッションのページ
  post   '/login',   to: 'sessions#create'   #新しいセッションの作成
  delete '/logout',  to: 'sessions#destroy'  #セッションの削除
  resources :users
end

Sessionsコントローラのテストで名前付きルートを使うようにする。

test/controllers/sessions_controller_test.rb
require '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.rb
 class SessionsController < ApplicationController

  def new
  end

  def create
    render 'new'
  end

  def destroy
  end
end
cerateで最初に失敗したログインのデバッグ情報
---
session:
  email: 'user@example.com'
  password: 'foobar'
commit: Log in
action: create
controller: sessions

paramsは次のような入れ子ハッシュ(ハッシュの中にハッシュがある構造)になっている
{ session: { password: "foobar", email: "user@example.com" } }
createアクションの中では、ユーザーの認証に必要なあらゆる情報をparamsハッシュから簡単に取り出せる。

app/controllers/sessions_controller.rb
 class 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.rb
 class 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.rb
 require '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アクションのflashflash.nowに置き換える
flash.nowのメッセージはその後のリクエストが発生したときに消滅する
これによってテストもGREENになる

8.2 ログイン

ログイン中の状態での有効な値の送信をフォームで正しく扱えるようにする
セッションを実装するには様々なコントローラやビューで沢山のメソッドを定義する必要がある。そうしたメソッドを一箇所にパッケージ化できるRubyのモジュール機能を使う。
Sessionsコントローラを生成した時点で既にセッション用ヘルパーモジュールも自動生成されている。さらに、Railsのセッション用ヘルパーはビューにも自動的に読み込まれる。Railsの全コントローラの親クラスであるApplicationコントローラにこのモジュールを読み込ませれば、どのコントローラでも使えるようになる。

app/controllers/application_controller.rb
 class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

8.2.1 log_inメソッド

Railsで事前定義済みのsessionメソッドを使って、単純なログインを行えるようにする。このsessionメソッドはハッシュのように扱える。
seesion[:user_id] = user.id
上のコードを実行すると、ユーザーのブラウザ内の一時cookiesに暗号化済みのユーザーIDが自動で作成される。

app/helpers/sessions_helper.rb
 module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end

sessionメソッドで作成した一時cookiesは自動的に暗号化され、上記のコードは保護される。

app/controllers/sessions_controller.rb
 class 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.rb
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
end
if @current_user.nil?
  @current_user = User.find_by(id: session[:user_id])
else
  @current_user
end

or演算子「||」を使えば上記の「メモ化」コードが次のように変換できる
@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_usernilではないという状態を指す。これをチェックするには!(否定演算子)を使っていく。

app/helpers/sessions_helper.rb
 module 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.rb
 class 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
end
test/fixtures/users.yml
 michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
test/integration/users_login_test.rb
require '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
end

8.2.5 ユーザー登録時にログイン

登録の終わったユーザーがデフォルトでログインされている状態にする
Usersコントローラのcreateアクションにlog_inを追加する

app/controllers/users_controller.rb
class 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
end
test/integration/users_signup_test.rb
require '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
end

8.3 ログアウト

ログアウト機能を追加する

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end
app/controllers/sessions_controller.rb
class 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.rb
require '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などをブラウザに一時的に保存できる
  • ログインの状態に応じて、ページ内で表示するリンクを切り替えることができる
  • 統合テストでは、ルーティング、データベースの更新、レイアウトの変更が正しく行われているかを確認できる

あいも変わらずテストで理解が追いつかなかった。

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