20200403のRubyに関する記事は20件です。

Rails deviceでログアウト時に【No route matches[Get]"/users/sign_out"】とエラー

エラー内容

gemのdeviseを導入してログアウトするとNo route matches[GET]"/users/sign_out"とエラー。
ログアウト先ページへ行けない。

スクリーンショット 2020-04-03 21.50.39.png

解決策

config/initializers/devise.rbにある
config.sign_out_via = :deleteを
スクリーンショット 2020-04-03 21.51.10.png
config.sign_out_via = :getに変更。
スクリーンショット 2020-04-03 21.51.24.png

これでrails s -b 0.0.0.0で再起動→ログアウト後、無事ページに推移。

[参考文献]http://gaku3601.hatenablog.com/entry/2014/08/24/204538

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

Rails deviceでログアウト時に【No route matches[GET]"/users/sign_out"】とエラー

エラー内容

gemのdeviseを導入してログアウトするとNo route matches[GET]"/users/sign_out"とエラー。
ログアウト先ページへ行けない。

スクリーンショット 2020-04-03 21.50.39.png

解決策

config/initializers/devise.rbにある
config.sign_out_via = :deleteを
スクリーンショット 2020-04-03 21.51.10.png
config.sign_out_via = :getに変更。
スクリーンショット 2020-04-03 21.51.24.png

これでrails s -b 0.0.0.0で再起動→ログアウト後、無事ページに推移。

[参考文献]http://gaku3601.hatenablog.com/entry/2014/08/24/204538

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

【Ruby on Rails】ERROR: Error installing rails: sprockets requires Ruby version >= 2.5.0.【環境構築時エラー】

railsインストール時のエラー

Rubyは「Ruby+Devit 2.4.6-1 (x64)」をインストールしました。

Rubyのバージョンや環境を切り替えることができるuru導入しました。
そしてrailsをインストールするために以下コマンドを入力しました。

gem install rails --version="5.2.2"

するとエラー発生。

ERROR: Error installing rails: sprockets requires Ruby version >= 2.5.0.

Rubyのバージョンが低いことが原因のようです。
今度は「Ruby+Devkit 2.6.5-1 (x64)」をダウンロードし、以下コマンドで登録。

uru admin add C:\Ruby26-x64\bin --tag Ruby26

Ruby26に変更してみます。

uru Ruby26

再びrailsインストールコマンドを入力します。

gem install rails --version="5.2.2"

今度はエラーが表示されなくインストールできました。

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

Ruby要点整理

Ruby要点整理

Rubyを学ぶ中で、理解が曖昧で、何度も書籍に立ち戻り確認してしまう点について、知識の整理のために記事を作成します。

アクセスメソッド

オブジェクトの外部から直接インスタンス変数を参照したり、インスタンス変数に代入したりすることができないため、オブジェクトの内部情報にアクセスするためのメソッドを定義する必要がある。

例1

example1.rb
class Menu
 def initialize(name)
   @name = name
 end

 def name
   @name
 end

 def name=(name)
   @name = name
 end
end

上の例1では、nameメソッドはインスタンス変数を参照するもので、name=メソッドは、インスタンス変数を変更するもの。
しかし、インスタンス変数をいくつも扱う場合、それぞれについてこれらのメソッドを定義する必要があり、手間やミスの可能性が増える。
そこで用意されているのがアクセスメソッド

例2

example2.rb
class Menu
  attr_accessor :name

 def initialize(name)
   @name = name
 end
end

例2のように、 「attr_accessor インスタンス変数名を示すシンボル」
とすることで、例1のnameメソッドとname=メソッドの定義を1行で書くことができる。
このように、「attr_accessor」を用いることで、Menuクラスのインスタンスに、nameという情報(=インスタンス変数)を持たせることができる。

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

Kinx ライブラリ - XML

XML

News!

Kinx - 3rd Preview Release!

これまでの修正も含め、3rd Preview Release を行いました。ここで触れている Fiber の修正 も含まれてます。

しかし未だ プレビュー。もし宜しければバグ報告等頂けると大変助かります。特に今回の XML はあまりテストできていない感満載。もうちょっとテストできてから紹介しようかとも思ったものの、せっかくのプレビュー版で使い方が分からないのもどうかと思い公開することにしました。実装自体はしてあり、サンプルは動くことを確認済です。

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。

今回は XML です。

XML もよく利用するのでスクリプト言語でサクッと扱いたい要素の一つ。

XML

DOMパース

Xml.parseFile() または Xml.parseString() を使って DOM ツリーを構築する。ファイルを読み込む場合は以下の通り。

var doc = Xml.parseFile("xmlfile.xml");

下記は文字列を直接パースする例。

var doc = Xml.parseString(%{
<?xml version="1.0" encoding="UTF-8" ?>
<artists>
  <artist country="US" id="1">
    <name>BON JOVI</name>
    <price>2400</price>
    <img file="bonjovi.jpg"/>
  </artist>
  <artist country="US" id="2">
    <name>GUNS N ROSES</name>
    <price>21000</price>
    <img file="GNR.jpg"/>
  </artist>
  <artist country="DE" id="3">
    <name>Helloween</name>
    <price>2400</price>
    <img file="helloween.jpg"/>
  </artist>
</artists>
});

返されたドキュメント・オブジェクトは以下のメソッドを持つ。

メソッド 内容
documentElement() ルートドキュメントを取得
createElement(tagname) Element ノードを作成する
createTextNode(text) Text ノードを作成する
createComment(comment) Comment ノードを作成する
createCdataSection(content) CDATA Section ノードを作成する
createProcessingInstruction(target, data) Processing Instruction ノードを作成する
createEntityReference(name) Entity Reference ノードを作成する
createElementNS(nsUri, qname) Element ノードを名前空間を指定して作成する
getElementById(id) id を指定してノードを検索する
getElementsByTagName(tagName) tagName のノードを配列にして返す
xpath(expr) expr の XPATH を評価し、結果を配列にして返す

ルートノード

ルートノードは documentElement() メソッドを使用して以下のように取得する。

var root = doc.documentElement();

XMLノード

ルートノードを含む XML ノードは以下のプロパティとメソッドを持つ。

プロパティ
プロパティ 内容
type ノード種別
name QName
tagName タグ名
localName ローカル名
namespaceURI 名前空間 URI
prefix プレフィックス
メソッド
メソッド 内容
attributes() 属性一覧を配列で返す。
setAttribute(qname, value) 属性を設定する。
setAttributeNS(nsUri, qname, value) 名前空間を指定して属性を設定する。
removeAttribute(qname) 属性を削除する。
removeAttributeNS(nsUri, localName) 名前空間を指定して属性を削除する。
parentNode() 親ノードを返す。
children() 子ノードを配列で返す。
firstChild() 最初の子ノードを返す。
lastChild() 最後の子ノードを返す。
nextSibling() 次のノードを返す。
previousSibling() 前のノードを返す。
appendChild(node) 子ノードにノードを追加する。
removeChild(node) 子ノードからノードを削除する。
replaceChild(node1, node2) 子ノードのノードを置き換える。
replaceNode(node) 自分自身のノードを別のノードでを置き換える。
insertBefore(node) 前のノードとしてノードを追加する。
insertAfter(node) 次のノードとしてノードを追加する。
remove() ノードを削除する。
textContent() テキストを取得する。
innerText() テキストを取得する。
hasChildren() 子ノードが存在すれば 1 を返す。
hasAttributes() 属性があれば 1 を返す。
getElementById(id) id を指定してノードを検索する
getElementsByTagName(tagName) tagName のノードを配列にして返す
xpath(expr) expr の XPATH を評価し、結果を配列にして返す

XPath

XPath は XPATH 式にマッチしたノードをノードセット(配列)の形で返す。ノードセットにも xpath() メソッドがあり、絞り込んだノード群に対し XPATH をチェインして使うことができる。

先ほどのサンプル XML のドキュメントに対して実行。

var nodes = doc.xpath("//artist")
               .xpath("price")
               .map(&(p) => p.innerText());

nodes.each(&(text) => {
    System.println(text);
});

結果。

2400
21000
2400

サンプル・ソース

同梱しているサンプルソースを載せておきます。説明していない Xml.Writer とかありますが、こんな感じの DOM パースができる例ということで、参考になると思い。

function displayXml(doc, node, indent) {
    System.print("  " * indent);
    if (node.type == Xml.ELEMENT_NODE) {
        System.print("ELEM %s" % node.name);
    } else if (node.type == Xml.TEXT_NODE) {
        System.print("TEXT %s" % node.value.trim());
    }

    var attr = node.attributes();
    for (var i = 0, len = attr.length(); i < len; ++i) {
        System.print("[%s=%s]" % attr[i].name % attr[i].value);
    }
    System.println("");

    var child = node.firstChild();
    while (child) {
        displayXml(doc, child, indent + 1);
        child = child.nextSibling();
    }
}

var doc = Xml.parseString(%{
<?xml version="1.0" encoding="UTF-8" ?>
<artists>
  <artist country="US" id="1">
    <name>BON JOVI</name>
    <price>2400</price>
    <img file="bonjovi.jpg"/>
  </artist>
  <artist country="US" id="2">
    <name>GUNS N ROSES</name>
    <price>21000</price>
    <img file="GNR.jpg"/>
  </artist>
  <artist country="DE" id="3">
    <name>Helloween</name>
    <price>2400</price>
    <img file="helloween.jpg"/>
  </artist>
</artists>
});
var root = doc.documentElement();
displayXml(doc, root);

var el = root.getElementById("3");
if (el) {
    el.remove();
}

System.println("");
System.println("getElementByTagName:");
var els = root.getElementsByTagName("img");
if (els.isArray) {
    els.each(&(el) => displayXml(doc, el));
}

System.println("");
System.println("XPath:");
var nodes = doc.xpath("//artist").xpath("price");
if (nodes.isArray) {
    nodes.each(&(el) => displayXml(doc, el));
}

var xmlWriter = new Xml.Writer(System);
xmlWriter.write(doc);
xmlWriter.write(root);

実行結果。

ELEM artists
  TEXT
  ELEM artist[country=US][id=1]
    TEXT
    ELEM name
      TEXT BON JOVI
    TEXT
    ELEM price
      TEXT 2400
    TEXT
    ELEM img[file=bonjovi.jpg]
    TEXT
  TEXT
  ELEM artist[country=US][id=2]
    TEXT
    ELEM name
      TEXT GUNS N ROSES
    TEXT
    ELEM price
      TEXT 21000
    TEXT
    ELEM img[file=GNR.jpg]
    TEXT
  TEXT
  ELEM artist[country=DE][id=3]
    TEXT
    ELEM name
      TEXT Helloween
    TEXT
    ELEM price
      TEXT 2400
    TEXT
    ELEM img[file=helloween.jpg]
    TEXT
  TEXT

getElementByTagName:
ELEM img[file=bonjovi.jpg]
ELEM img[file=GNR.jpg]

XPath:
ELEM price
  TEXT 2400
ELEM price
  TEXT 21000
<artists>
        <artist country="US" id="1">
                <name>BON JOVI</name>
                <price>2400</price>
                <img file="bonjovi.jpg" />
        </artist>
        <artist country="US" id="2">
                <name>GUNS N ROSES</name>
                <price>21000</price>
                <img file="GNR.jpg" />
        </artist>
</artists>
<artists>
        <artist country="US" id="1">
                <name>BON JOVI</name>
                <price>2400</price>
                <img file="bonjovi.jpg" />
        </artist>
        <artist country="US" id="2">
                <name>GUNS N ROSES</name>
                <price>21000</price>
                <img file="GNR.jpg" />
        </artist>
</artists>

おわりに

XPath が使えると便利だ。

そして、XML と Zip(以前の記事)を組み合わせると、実は Xlsx ファイル(Excel ファイル)の読み書きができたりします。Xlsx ファイルは Office Open XML という名前で標準仕様化されており(色々問題もあるが)、XML ファイルを Zip で固めたものになってるので読めたりするといった具合。

ただ、実際問題として Office Open XML の全てをサポートするってのは相当量のコードになるので、すぐにできそうなのは簡易的な読み書きくらいですね。時間があればチャレンジしよう。

ではまた次回。

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

Perl と Ruby で解く AtCoder ABC146 B - ROT N

はじめに

Pocket詳解 Perl/CGI辞典をパラパラっと眺めておりましたら、pack/unpack 関数が目に留まりましたので、投稿いたします。

次の文字を取得する場合

'A' の次の文字 'B' を取得する場合、
C言語でしたら、'A' + 1 より 'B' を得られます。
しかし Perl の場合、'A' + 1 の結果は、1 となってしまいます。

pack/unpack 関数

よって、次の文字を取得する場合、一旦バイナリ値に変換し足し算を行ってから、char 値に逆変換することにより、期待する操作が行えます。

pack char値をバイナリ値に変換
unpack バイナリ値をchar値に変換

Perl Ruby Python
pack/chr pack/chr chr
unpack/ord unpack/ord ord
unpack.pl
unpack("C*", 'A'); # => 65
pack("C*", 66);    # => B
unpack.rb
"A".ord            # => 65
66.chr             # => B

Pythonpack/unpack があるかどうかは不明 適当

B - ROT N

AtCoder ABC 146 B - ROT N

perl.pl
use v5.18;
use warnings;

chomp (my $n = <STDIN>);
chomp (my $s = <STDIN>);
my @s = split '', $s;
map {$s[$_] = chr((ord($s[$_]) + $n) % ord("A") % 26 + ord("A"))} (0..@s-1);
say join('', @s);
ruby.rb
n = gets.chomp.to_i
s = gets.chomp.split('')
(0..s.size-1).each do |i|
  s[i] = ((s[i].ord + n) % "A".ord % 26 + "A".ord).chr
end
puts s.join()

むむっ、ruby のスッキリ感は凄いですね。

まとめ

  • Perl の関数 pack/unpack を覚えた
  • Ruby のメソッド chr/ord も覚えた

参照したサイト
pack テンプレート文字列
instance method String#ord
ABC146 感想

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

【初心者向け】ピラミッド(三角形)問題

環境,前提

Ruby 2.5.1
MacOS Mojave Ver.10.14.6

本記事はRubyがインストールされた前提の記事です。
Rubyをインストールしたあと、とにかくRubyをいろいろ触ってみて慣れていくための記事です。お役に立てば幸いです。

採用試験に出ました。

採用試験で実際に出題されたのでやはり大事なのかなと思い今回記録に残します。

ピラミッド問題とは?

"#"をうまく出力して"#"だけで10段の三角形を作る問題です。

⬇︎こう言った出力を求められました。


##
###
####
#####
######
#######
########
#########
##########

私が書いたコード

実際の試験では「言語は問わない、形式も自由」だったので今回はRubyのfor文を用いてやってみます。

pyramid.rb
for x in 1..10 do 
    for y in 1..10do
        if y<=x then 
            print '#'
        end
    end
    puts ''
end


他にも逆ピラミッドなども実現可能なのでぜひやってみてください!!

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

active hashでdbに登録させる時の注意点

1 はじめに

active hashについては以下の記事に書いています。

「active hash でセレクトボックスをつくる」

https://qiita.com/DON4024/items/78edb7a309ee96766952

今回は某フリマサイトを作成中に起こった思わぬ動作について書いています。

2 生じた問題

今回のアプリケーションではactive hashで複数のモデルを作成していました。
年、月、日、都道府県、配送方法、配送負担、発送日数などです。
すべて入力必須項目なのでモデルにnull falseのバリデーションをかけていました。

しかし、ユーザー登録の動作を確認中に問題が生じました。
ミスで生年月日を何も選択せずに登録ボタンを押してしまった時です。
バリデーションをかけているので、当然エラーメッセージが出ると思っていたのですが、登録できてしまいました。

すぐに登録先のモデルとマイグレーションファイルを確認。
null falseのバリデーションは問題なく効いていました。

2 原因

1) active hashのコード

以下が使用していた、生年月日の年のactive hashのモデルです。

Birthdayy.rb
class Birthdayy < ActiveHash::Base
  self.data = [
      {id: 0, year: '---'},
      {id: 1, year: '2020'}, {id: 2, year: '2019'}, {id: 3, year: '2018'}, {id: 4, year: '2017'}, {id: 5, year: '2016'}, 
      {id: 6, year: '2015'}, {id: 7, year: '2014'}, {id: 8, year: '2013'}, {id: 9, year: '2012'}, {id: 10, year: '2011'}, 
      {id: 11, year: '2010'}, {id: 12, year: '2009'}, {id: 13, year: '2008'}, {id: 14, year: '2007'}, {id: 15, year: '2006'}, 
      {id: 16, year: '2005'}, {id: 17, year: '2004'}, {id: 18, year: '2003'}, {id: 19, year: '2002'}, {id: 20, year: '2001'}, 
      {id: 21, year: '2000'}, {id: 22, year: '1999'}, {id: 23, year: '1998'}, {id: 24, year: '1997'}, {id: 25, year: '1996'}, 
      {id: 26, year: '1995'}, {id: 27, year: '1994'}, {id: 28, year: '1993'}, {id: 29, year: '1992'}, {id: 30, year: '1991'}, 
      {id: 31, year: '1990'}, {id: 32, year: '1989'}, {id: 33, year: '1988'}, {id: 34, year: '1987'}, {id: 35, year: '1986'}, 
      {id: 36, year: '1985'}, {id: 37, year: '1984'}, {id: 38, year: '1983'}, {id: 39, year: '1982'}, {id: 40, year: '1981'}, 
      {id: 41, year: '1980'}, {id: 42, year: '1979'}, {id: 43, year: '1978'}, {id: 44, year: '1977'}, {id: 45, year: '1976'}, 
      {id: 46, year: '1975'}, {id: 47, year: '1974'}, {id: 48, year: '1973'}, {id: 49, year: '1972'}, {id: 50, year: '1971'}, 
      {id: 51, year: '1970'}, {id: 52, year: '1969'}, {id: 53, year: '1968'}, {id: 54, year: '1967'}, {id: 55, year: '1966'}, 
      {id: 56, year: '1965'}, {id: 57, year: '1964'}, {id: 58, year: '1963'}, {id: 59, year: '1962'}, {id: 60, year: '1961'}, 
      {id: 61, year: '1960'}, {id: 62, year: '1959'}, {id: 63, year: '1958'}, {id: 64, year: '1957'}, {id: 65, year: '1956'}, 
      {id: 66, year: '1955'}, {id: 67, year: '1954'}, {id: 68, year: '1953'}, {id: 69, year: '1952'}, {id: 70, year: '1951'}, 
      {id: 71, year: '1950'}, {id: 72, year: '1949'}, {id: 73, year: '1948'}, {id: 74, year: '1947'}, {id: 75, year: '1946'}, 
      {id: 76, year: '1945'}, {id: 77, year: '1944'}, {id: 78, year: '1943'}, {id: 79, year: '1942'}, {id: 80, year: '1941'}, 
      {id: 81, year: '1940'}, {id: 82, year: '1939'}, {id: 83, year: '1938'}, {id: 84, year: '1937'}, {id: 85, year: '1936'}, 
      {id: 86, year: '1935'}, {id: 87, year: '1934'}, {id: 88, year: '1933'}, {id: 89, year: '1932'}, {id: 90, year: '1931'}, 
      {id: 91, year: '1930'}, {id: 92, year: '1929'}, {id: 93, year: '1928'}, {id: 94, year: '1927'}, {id: 95, year: '1926'}, 
      {id: 96, year: '1925'}, {id: 97, year: '1924'}, {id: 98, year: '1923'}, {id: 99, year: '1922'}, {id: 100, year: '1921'}, 
  ]
end

2) registrationコントローラー new.html.haml

ユーザー登録ページのhamlの記述です。

new.html.haml
= f.collection_select :birthdayy_id, Birthdayy.all, :id, :year

3) userマイグレーションファイルのバリデーション

必須事項にバリデーションをかけています。

devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      ---省略---
      t.integer :birthdayy_id, null: false
      t.integer :birthdaym_id, null: false
      t.integer :birthdayd_id, null: false
      ---省略---
  end
end

4) userモデルのバリデーション

必須事項にバリデーションをかけています。

user.rb
class User < ApplicationRecord
  ---省略---
  extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to_active_hash :birthdayy
  belongs_to_active_hash :birthdaym
  belongs_to_active_hash :birthdayd

  ---省略---
  validates :birthdayy_id, presence: true
  validates :birthdaym_id, presence: true
  validates :birthdayd_id, presence: true
  ---省略---
end

一見問題なさそうですが、原因はこれでした。
1) Birthdayy.rb'---' のidが 「0
2) devise_create_users.rbでカラムの型をintegerにしている
3) collection_select:idをparamsで送っている

1~3により、何も選択しなかった場合、「0」がcreateアクションに送られたためバリデーションをすり抜けてしまったようでした。

3 解決策

解決方法はいくつかあると思います。

  • 保存させる数字の範囲を指定する
  • integerのうち「0」という数字以外を登録させる
  • '---'を使うのを諦める など

今回は以下の方法を教えてもらったので使用しました。

= f.collection_select :birthdayy_id, Birthdayy.all, :id, :year, prompt: '---'

最後の

prompt: '---'

を記述することで、セレクトタブでは問題なく選択肢に'---'が表示されます。



また、'---'を選択した状態でsubmitを押すと、nullとして送信されるため、null falseでバリデーションに引っ掛かることになるため設定も簡単です。

4、終わりに

チームで開発すると繋がりができて今まで話したことがない人から情報を得られる機会が増えるので大事な期間です。
積極的に他チームの人と繋がりましょう。



以上です。最後までご覧いただきありがとうございました。

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

隠キャ元美容師

自分

野本 彬央 (nomoto akihisa)
読みにくいので、カタカナで表現することが多いです。
1995.01.23生まれ。

常にhappyを目指して生きてます。:hugging:

経歴

20〜23歳まで美容室の現場で活躍してました。:haircut_tone2:
24歳で最年少店長になり、ゆるっとやってました。

美容師だとチャラいイメージあると思うんですが、隠キャ美容師でした。:man_with_turban_tone3:

休日の過ごし方

6:00〜7:00   起床・風呂・洗濯
7:00〜8:00   瞑想・読書・白湯
9:00〜10:30   トレーニング
11:00〜15:00  カフェで作業
15:30〜18:00  映画観賞・YouTube
18:30〜20:00  買い物・料理・食事
20:30〜22:00  風呂・寝る

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

active hash でセレクトボックスをつくる

1 active hashとは

某フリマサイトを作成する際に、active hashというgemを使ってセレクトタグを作成しました。
active hashは都道府県などデータベースを作成するほどではなく、かつ変更される可能性が低いデータを扱いたい時に便利なgemです。
ハッシュを使ってアクティブレコードのモデルのように扱えるファイルを作成できます。

<gemのリンク>

https://github.com/zilkey/active_hash

READMEの記述

ActiveHash is a simple base class that allows you to use a ruby hash as a readonly datasource for an ActiveRecord-like model.

2 使い方

1) gemのインストール

まずはgemfileに以下のコードを記述してbundle installします。

gem 'active_hash', '~> 2.3.0'  ←2020/04/03現在のバージョン

2) モデルの作成

app>modelsの中に直接ファイルを作成します。
注意: rails g model は使わない

今回は都道府県のactive hashを作成するためprefecture.rbという名前でファイルを作成します。

ファイルの中身の書き方は以下の通りです。

class Prefecture < ActiveHash::Base
  self.data = [
    {id: 0, name: '選択してください'},
      {id: 1, name: '北海道'}, {id: 2, name: '青森県'}, {id: 3, name: '岩手県'},
      {id: 4, name: '宮城県'}, {id: 5, name: '秋田県'}, {id: 6, name: '山形県'},
      {id: 7, name: '福島県'}, {id: 8, name: '茨城県'}, {id: 9, name: '栃木県'},
      {id: 10, name: '群馬県'}, {id: 11, name: '埼玉県'}, {id: 12, name: '千葉県'},
      {id: 13, name: '東京都'}, {id: 14, name: '神奈川県'}, {id: 15, name: '新潟県'},
      {id: 16, name: '富山県'}, {id: 17, name: '石川県'}, {id: 18, name: '福井県'},
      {id: 19, name: '山梨県'}, {id: 20, name: '長野県'}, {id: 21, name: '岐阜県'},
      {id: 22, name: '静岡県'}, {id: 23, name: '愛知県'}, {id: 24, name: '三重県'},
      {id: 25, name: '滋賀県'}, {id: 26, name: '京都府'}, {id: 27, name: '大阪府'},
      {id: 28, name: '兵庫県'}, {id: 29, name: '奈良県'}, {id: 30, name: '和歌山県'},
      {id: 31, name: '鳥取県'}, {id: 32, name: '島根県'}, {id: 33, name: '岡山県'},
      {id: 34, name: '広島県'}, {id: 35, name: '山口県'}, {id: 36, name: '徳島県'},
      {id: 37, name: '香川県'}, {id: 38, name: '愛媛県'}, {id: 39, name: '高知県'},
      {id: 40, name: '福岡県'}, {id: 41, name: '佐賀県'}, {id: 42, name: '長崎県'},
      {id: 43, name: '熊本県'}, {id: 44, name: '大分県'}, {id: 45, name: '宮崎県'},
      {id: 46, name: '鹿児島県'}, {id: 47, name: '沖縄県'}, {id: 48, name: '未定'}
  ]
end

3) アソシエーションを組む & バリデーションをかける(任意)

a) アソシエーションを組む
active_hashにはbelongs_to_active_hashメソッドが用意されているのでaddressモデルに記述します。
※active hashの方には記述する必要はありません。

b) 保存するデータベースにバリデーションをかける
今回はユーザー登録の際に必ず都道府県を入力させたいので、addressモデルにバリデーションをかけます。

address.rb
extend ActiveHash::Associations::ActiveRecordExtensions
  belongs_to_active_hash :prefecture
  belongs_to :user, optional: true
  validates :prefecture, presence: true   ←この記事に関係ないカラムは省略してます

以上です。
簡単ですね。

2 viewにactive hashからデータを持ってくる

active hashを使いたいビューページに以下の記述をします。

= f.collection_select :prefecture_id, Prefecture.all, :id, :name 

<記述内容の解説>
第一引数:登録するデータベースのカラム名
第二引数:作成したactive hashからどのようにデータを持ってくるかを指定
第三引数:paramsで送るデータを指定(:nameでも可能 ※dbのカラムはstringにしましょう。)
第四引数:ユーザーが見れるセレクトタグの内容
※今回はcollection_selectの説明は割愛します。




なお今回は、都道府県を記入させるタイミングが二回あり、二度目の発送元の記入では-未定-を選べるためactive hash内に記述しましたが、ユーザー登録では-未定-は使いたくないためそれ以外を引き出します。

= f.collection_select :prefecture_id, Prefecture.where.not(name: '未定'), :id, :name

3 完成写真

4、注意

formを使うと自動でidが割り振られるので、cssを当てる時は検証ツールを使ってidを確認しましょう。





以上です。最後までご覧いただきありがとうございました。

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

[Rails]httpからhttpsにリダイレクトする方法

はじめに

オリジナルサービスを作成した際に、httpで作成しておりパスワードを入れる時に
「このサイトは危険です!」みたいな警告が流れてきました。
これは。と思い、URLを見るとhttpのままになっていました。パスワード打つ時にこんな警告が流れるともうそのサイト使いたくならないですよね笑

結論

単純ですが、 config/environments/production.rb にこの一文を書き足す・または修正するだけでhttps化できました。

config/environments/production.rb
config.force_ssl = true

まとめ

railsってなんでもありますね。ほんとすごい。
他にも設定することがあるのかもしれませんが、特に現状は問題なく経過しています。

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

nullを条件にしたクエリでデータを取ってくる方法 in ruby and firebase

一覧表示させるためにwhereを使う

hogehoge.rb
    def self.all
        list = collection.where("format", "=", null).order(:created_at).get
    end

上記のような形でformatというフィールドにnullを持ったドキュメントを全て持ってくるようなクエリを作るようなコードを書いているつもりなのですが、null部分がundefinedのエラーが出てきてしまい、取得することができなさそうなんですね。

ここの記事には、

Firestore select where a field is null [duplicate]

it is not possible to index on a field that is NOT in the document

と書いてあるし、無理なのかとも思ったんですが、更に下の方には、

However there is a workaround which consists in querying documents that contain properties with a null value data type

とあるので、いけそうでもある。

解決策

rubyではnullってnilで表すんだわ
そう、これですね。ruy上ではnullを扱うにはnilにするのでした。

hogehoge.rb
    def self.all
        list = collection.where("format", "=", nil).order(:created_at).get
    end

とすれば、firebaseからnullを条件にしてドキュメントを取ってくることができました。

おまけ

データを取得してくる時にコレクションに対して、インデックスがないから作れという形でエラーが出ることがあります。

9: The query requires an index. You can create it here: 長ったらしいURL

こんな感じのエラーが出るので、URLをコピペすると、firebaseコンソールに飛び、インデックスを作成するためのダイアログが開くので、ポチッと押してインデックスがビルドされるのを待ってください。ビルドには数分かかります。しばらく経ってまだビルド中みたいになっていてもコンソール自体に更新かけると終わってたりします。

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

committeeを使ったOpenAPI3のバリデーション

committeeを使ってOpenAPI3のスキーマファイルからリクエスト&レスポンスをバリデーションする方法と実際の開発に組み込む際の設定についてまとめます。

OpenAPI3とcommiteeについてはota42yさんの登壇資料が参考になります。また、committeeについての記事はいっぱいあるので、ここでは実際の開発で取り入れるにあたって設定ファイルの書き方に絞って紹介したいと思います。

committeeを使って実現したいこと

  • API仕様書を作りたい
  • スキーマファーストな開発をして、常に正しい状態にメンテナンスしていきたい
  • リクエストのバリデーションなどのRailsが苦手な部分を楽に実装したい

committeeを使う理由

  • すでに運用中のサービスに途中から組み込める
  • RSpecだけで実行したり、実際のリクエストのバリデーションに使ったりと用途に合わせて組み込みやすい
  • 使い方がシンプルで導入が簡単

準備物

  • OpenAPI3で記載したスキーマファイル(今回は準備されている前提)

スキーマファイルの管理は手動で管理していく方法とコードから自動生成する方法があります。前者の場合は、Stoplightのようなツールを使えば新しい人のキャッチアップも楽です。後者だとswagger-blocksのようなgemがありますが、OpenAPI3のサポートはまだ実装まで行っていないようです。 コードから自動生成の方がスキーマファイルと実際のコードとの差分が生まれないため、メンテナンスが楽になりますが現状良いgemが見つからなかったので自分は手動で管理しています。

  • Gemfile

今回使うgemは以下です。

gem 'committee'

group :test do
  gem 'committee-rails'
end

committeeをテストにだけ利用する場合は、:test配下に書いて問題ないです。

rspecで利用する

committeeをrspecに導入するためにはcommittee-railsが便利です。

rspec_helper.rb等に以下の設定を入れれば使えるようになります。

RSpec.configure do |config|
  config.add_setting :committee_options
  config.committee_options = { schema_path: Rails.root.join('schema', 'schema.json').to_s, old_assert_behavior: false }
end

assert_schema_conformを実行すればスキーマを使って検証してくれます。

describe 'request spec' do
  include Committee::Rails::Test::Methods

  describe 'GET /' do
    it 'conform json schema' do
      get '/'
      assert_schema_conform
    end
  end
end

設定でold_assert_behaviorをtrueにした場合にはレスポンスしかチェックされずwarningが出る(参照)のでfalseで設定しておくと良いと思います。

全てのrequest specでテストを実行する

常に最新の仕様にスキーマファイルを合わせるために、全てのrequest specで強制的にassert_schema_conformを書くには以下のように設定すれば可能です。

RSpec.configure do |config|
  config.include Committee::Rails::Test::Methods
  config.add_setting :committee_options
  config.committee_options = {
    schema_path: Rails.root.join('schema', 'schema.json').to_s,
    old_assert_behavior: false
  }

  config.after(:each) do |example|
    next unless example.metadata[:type] == :request
    assert_schema_conform unless RSpec.configuration.skip_assert_schema_conform
  end

  config.before(:each) do |example|
    skip_flag = example.metadata[:skip_assert_schema_conform] || false
    config.skip_assert_schema_conform = skip_flag
  end
end

こっちの記事のようにActionDispatch::Integration::Sessionにpretendする方法もありますが、このcommitで依存関係が崩れる1ようになってpretendではエラーが出てしまうため、afterで実行するようにしています。

もし、assert_schema_conformをスキップしたいテストがあった場合には、下記のように記載してスキップ可能です。

describe 'GET /', type: :request, skip_assert_schema_conform: true do
  it 'skip conform schema' do
    get '/'
  end
end

このように設定しておけば、スキーマファイルを書き忘れた場合や、スキーマファイルの値が間違えていた場合などにCIで気づくことができ、常に最新の仕様を反映するように強制することができます。

実運用のサービスで使う

committeeではリクエスト&レスポンスのバリデーションが可能です。これを使えばRailsのController側で細かなparamsのチェックをしなくてよくなるので、テストだけでなく実際のリクエストにも適用するのがおすすめです。

設定は下記の通りで、config/initializers/committee.rbなどのファイルに記述します。

return if Rails.env.test?

file = YAML.load_file(Rails.root.join('schema', 'schema.yml'))
open_api = OpenAPIParser.parse(file)
schema = Committee::Drivers::OpenAPI3::Driver.new.parse(open_api)

Rails.application.config.middleware.insert_before(
  ActionDispatch::Executor,
  Committee::Middleware::RequestValidation,
  error_class: Custom::RequestValidationError,
  schema: schema,
  strict: !Rails.env.production?
)

Rails.application.config.middleware.insert_after(
  ActionDispatch::Callbacks,
  Committee::Middleware::ResponseValidation,
  error_class: Custom::ResponseValidationError,
  schema: schema,
  validate_success_only: true
)

rspec実行時にも読み込まれるファイルなので、 return if Rails.env.test?してますが他にいい方法があれば知りたい...
各オプションについては Committee::Middleware::RequestValidationを参考にしてください。上記ではstrictを本番だけOFFにすることで、開発環境では厳しくジャッジして本番では緩く運用する設定になっています。本番ではどのようなリクエストが来るのかわからないので、ある程度緩く運用したいケースもあるかと思います。

他にはエラー時にcommittee独自のエラーが返ってしまうので、独自のエラークラスを定義した方が良いと思います。

https://github.com/interagent/committee#validation-errors

開発中はrspecや動作確認でスキーマファイルの正当性を担保できるので、両方でcommitteeを動作させた方が良いかなと思います。また、本番で動作させることである程度サービスへの攻撃の防御や検知に使えるかなーと思いました。


  1. undefined local variable or method 'integration_session' for #<#<Class:0x000056477f0bcee0>:0x00005647831c1fd0>のエラーが出るようになる。integration_sessionはActionDispatch::Integration::Sessionを呼び出しているActionDispatch::Integration::Runner側で定義されているメソッドなので参照できない。 

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

【rspec】confirmダイアログのテスト

開発環境

ruby '2.6.3'
rails '6.0.2'

railsでconfirmダイアログのテストをする。

systemテスト
  context "削除する" do
    it "投稿消去" do
      click_button "消去する"
      expect{
        expect(page.accept_confirm).to eq "本当に削除しますか?"
        expect(page).to have_content "レビューを消去しました。"
        }. to change(@user.posts, :count).by(-1)
    end
  end

注意点

  • expectのブロック内にひとつ以上のexpectもしくはfindを入れないと、ダイアログが表示されてacceptされる前に次へ進んでしまうので注意が必要です。 つまり
systemテスト
  context "削除する" do
    it "投稿消去" do
      click_button "消去する"
      expect{
        expect(page.accept_confirm).to eq "本当に削除しますか?"
        expect(page).to have_content "レビューを消去しました。"
     # ↑この一文かsleepが必要です。
        }. to change(@user.posts, :count).by(-1)
    end
  end

その他

slim
= button_to "消去する",post_path(@post), method: :delete, data: {confirm: "本当に削除しますか?"}
コントローラー
  def destroy
    @post = current_user.posts.find(params[:id])
    flash[:notice] = "レビューを消去しました。"
    @post.destroy
    redirect_to user_path(current_user.id)
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Remember機能を実装する~Railsチュートリアル9章~

第9章も終了しました。
9章では主にRemember機能の実装方法について学びました。
ここはあまり時間をかけずに少し足早に通りすぎましたね。記憶トークンやRemember me機能についてざっくりとだけ理解した感じです。というのも今回はログイン機能の発展型、応用であるためまだまだ覚えることがたくさんある初学者の自分としては現段階では優先度が低い内容なのかなと感じました。

Remember me機能

ユーザーのログイン状態をブラウザを閉じた後でも有効にする機能を実装する。
またこの機能を使うかをユーザーに決めてもらうためチェックボックスをフォームに追加する。
Sessionの永続化を行うために、記憶トークン(要するにパスワードのようなもの)を生成しcookiesメソッドによる永続cookiesの作成や、安全性の高い記憶ダイジェストによるトークン認証に記憶トークンを活用する。

パスワードとトークンの違い

パスワードはユーザーが作成・管理する情報
トークンはコンピュータが作成・管理する情報

sessionメソッドで保存した情報は自動的に安全が保たれるが、cookiesメソッドに保存する情報はそうはなっていない。特に、cookiesを永続化するとセッションハイジャックという攻撃を受ける可能性があります。(記憶トークンを奪って、特定のユーザーになりすましてログインする)

cookiesを盗み出す有名な方法

(1) 管理の甘いネットワークを通過するネットワークパケットからパケットスニッファという特殊なソフトウェアで直接cookieを取り出す。

対策...Secure Sockets Layer (SSL) をサイト全体に適用して、ネットワークデータを暗号化で保護し、パケットスニッファから読み取られないようにしている。

(2) データベースから記憶トークンを取り出す。

記憶トークンをそのままデータベースに保存するのではなく、記憶トークンのハッシュ値を保存するようにする。

(3) クロスサイトスクリプティング (XSS) を使う。

Railsによって自動的に対策が行われます。具体的には、ビューのテンプレートで入力した内容をすべて自動的にエスケープする。

(4) ユーザーがログインしているパソコンやスマホを直接操作してアクセスを奪い取る。

ログイン中のコンピュータへの物理アクセスによる攻撃については、さすがにシステム側での根本的な防衛手段を講じることは不可能。二次被害を最小限に留めることは可能である。具体的には、ユーザーが (別端末などで) ログアウトしたときにトークンを必ず変更するようにし、セキュリティ上重要になる可能性のある情報を表示するときはデジタル署名 (digital signature) を行うようにする。

記憶トークンと暗号化

まずデータベースに種類=stringのremember_digest属性をApplicationに追加する。

記憶ダイジェスト用に生成したマイグレーション

class AddRememberDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :remember_digest, :string
  end
end

これをデータベースに追加。次は記憶トークンの作成を行う。新しいトークンを作成するためにnew_tokenメソッドを作成する。このダイジェストメソッドではユーザーオブジェクトが不要となるためUserモデルのクラスメソッドとして作成する。

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end
end

次にuser.rememberメソッドを作成する。これは
記憶トークンをユーザーと関連付け、トークンに対応する記憶ダイジェストをデータベースに保存する役割を担う。
またremember_token属性を設定しなければならない。
(要するにパスワードを生成するフェーズ)
attr_accessorを使って「仮想の」属性を作成する。

class User < ApplicationRecord
  attr_accessor :remember_token


  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

selfキーワードを与えると、この代入によってユーザーのremember_token属性が期待どおりに設定される。
update_attributeメソッドを使って記憶ダイジェストを更新することでこのメソッドはバリデーションを素通りさせる。

ログイン状態の保持

記憶トークンをユーザーと関連付け、トークンに対応する記憶ダイジェストをデータベースに保存する役割が完成。
次にユーザーの暗号化済みIDと記憶トークンをブラウザの永続cookiesに保存して、永続セッションを作成する準備ができました。これを実際に行うにはcookiesメソッドを使います。
個別のcookiesは、1つのvalue (値) と、オプションのexpires (有効期限) からできている。

ユーザーIDとcookiesに保存するにはcookies[:user_id] = user.idを使用するがこれではIDがそのままcookiesに保存されてしまう。
そのため署名付きcookiesを使用する。

cookies.signed[:user_id] = user.id

さらに有効期限20年cookiesを設定できるpermanentメソッドを追加するとこうなる。

cookies.permanent.signed[:user_id] = user.id

cookiesを設定すると以後のビューでこのようにcookiesからユーザーを取り出せる。

User.find_by(id: cookies.signed[:user_id])

cookies.signed[:user_id]では自動的にユーザーIDのcookiesの暗号が解除され、元に戻る。
続いて、bcryptを使ってcookies[:remember_token]がremember_digestと一致することを確認します

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

引数がremember_tokenとなっているが、これはattr_accessorで定義したremember_tokenとは無関係のただの引数であることに注意。ここにはログイン時にremember(user)メソッドで設定したcookieに保存されたremember_tokenが代入される。
つまり、remember_tokenを使って設定されたcookieとremember_digestカラムの値が一致するかを検証しており、一致すればtrueを返す。

cookiesメソッドでユーザーIDと記憶トークンの永続cookiesを作成する。

  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

永続セッションの場合は、session[:user_id]が存在すれば一時セッションからユーザーを取り出し、それ以外の場合はcookies[:user_id]からユーザーを取り出して、対応する永続セッションにログインする必要があります

if (user_id = session[:user_id])
@current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
user = User.find_by(id: user_id)
if user && user.authenticated?(cookies[:remember_token])
log_in user
@current_user = user
end
`

さらにcurrent_userヘルパーを定義する

# 記憶トークンcookieに対応するユーザーを返す

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

新しくログインしたユーザーは正しく記憶される。

ユーザーを忘れる

ユーザーがログアウトできるようにするために、ユーザーを記憶するためのメソッドと同様の方法で、ユーザーを忘れるためのメソッドを定義する。このuser.forgetメソッドによって、user.rememberが取り消さる。

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end

永続Sessionからログアウトする

  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end

2つの目立たないバグ

実は小さなバグが2つ残っている。
1つ目のバグ
ユーザーは場合によっては、同じサイトを複数のタブ (あるいはウィンドウ) で開いていることもあり、ログアウト用リンクはログイン中のみ表示される。
今のcurrent_userの使い方では、ユーザーが1つのタブでログアウトし、もう1つのタブで再度ログアウトしようとするとエラーになってしまう。これは、もう1つのタブでログアウトし、current_userがnilとなるため、log_outメソッド内のforget(current_user)が失敗してしまうからである。
→この問題を回避するためには、ユーザーがログイン中の場合にのみログアウトさせる必要がある。

2番目の問題は、ユーザーが複数のブラウザ (FirefoxやChromeなど) でログインしていたときに起こります。例えば、Firefoxでログアウトし、Chromeではログアウトせずにブラウザを終了させ、再度Chromeで同じページを開くと起こる。例えばユーザーがFirefoxからログアウトすると、user.forgetメソッドによってremember_digestがnilになります。この時点では、Firefoxでまだアプリケーションが正常に動作しているがlog_outメソッドによってユーザーIDが削除されるため、ハイライトされている2つの条件はfalseになります。

記憶トークンcookieに対応するユーザーを返す

def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end

結果として、current_userメソッドの最終的な評価結果は、期待どおりnilになる。

一方、Chromeを閉じたとき、session[:user_id]はnilになります (これはブラウザが閉じたときに、全てのセッション変数の有効期限が切れるためです)。しかし、cookiesはブラウザの中に残り続けているため、Chromeを再起動してサンプルアプリケーションにアクセスすると、データベースからそのユーザーを見つけることができてしまいます。

# 記憶トークンcookieに対応するユーザーを返す
def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end

結果として、次のif文の条件式が評価される。

user && user.authenticated?(cookies[:remember_token])

このとき、userがnilであれば1番目の条件式で評価は終了するのですが、実際にはnilではないので2番目の条件式まで評価が進み、そのときにエラーが発生します。原因は、Firefoxでログアウトしたときにユーザーのremember_digestが削除してしまっているにもかかわらず、Chromeでアプリケーションにアクセスしたときに次の文を実行してしまうからです。

BCrypt::Password.new(remember_digest).is_password?(remember_token)

上のremember_digestがnilになるので、bcryptライブラリ内部で例外が発生します。この問題を解決するには、remember_digestが存在しないときはfalseを返す処理をauthenticated?に追加する必要があります。

テスト駆動開発は、この種の地味なバグ修正にはうってつけです。そこで、2つのエラーをキャッチするテストから書いていくことにしましょう。まずはリスト 8.31の統合テストを元に、redになるテストを作成する。

リスト 9.14: ユーザーログアウトのテスト red
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
    # 2番目のウィンドウでログアウトをクリックするユーザーをシミュレートする
    delete logout_path
    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

current_userがないために2回目のdelete logout_pathの呼び出しでエラーが発生し、テストスイートは redになる。

次にこのテストを成功させます。具体的にはリスト 9.16のコードで、logged_in?がtrueの場合に限ってlog_outを呼び出すように変更します。

ログイン中の場合のみログアウトする green

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

2番目の問題は、統合テストで2種類のブラウザをシミュレートは困難である。その代わり、同じ問題をUserモデルで直接テストするだけなら簡単に行えます。記憶ダイジェストを持たないユーザーを用意し (setupメソッドで定義した@userインスタンス変数ではtrueになります)、続いてauthenticated?を呼び出す。この中で、記憶トークンを空欄のままにしていることにご注目ください。記憶トークンが使われる前にエラーが発生するので、記憶トークンの値は何でも構わないのです。

ダイジェストが存在しない場合のauthenticated?のテスト red

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end

上のコードではBCrypt::Password.new(nil)でエラーが発生するため、テストスイートは redになる。
このテストを greenにするためには、記憶ダイジェストがnilの場合にfalseを返すようにすれば良い

authenticated?を更新して、ダイジェストが存在しない場合に対応 green

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

# ユーザーのログイン情報を破棄する

  def forget
    update_attribute(:remember_digest, nil)
  end
end

ここでは、記憶ダイジェストがnilの場合にはreturnキーワードで即座にメソッドを終了している。処理を中途で終了する場合によく使われるテクニックである。

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

【初心者向け】FizzBuzz問題

環境,前提

Ruby 2.5.1
MacOS Mojave Ver.10.14.6

本記事はRubyがインストールされた前提の記事です。
Rubyをインストールしたあと、とにかくRubyをいろいろ触ってみて慣れていくための記事です。お役に立てば幸いです。

採用試験に出ました。

採用試験で実際に出題されたのでやはり大事なのかなと思い今回記録に残します。

FizzBuzz問題とは?

1から30まで順に数えて出力して行き、3で割り切れる数は「Fizz」5で割り切れる数は「Buzz」両方で割り切れる数は「FizzBuzz」と出力する、ようにプログラムを書く問題です。

これが書けるか書けないかでプログラマー志願者を仕分けられるようになったようです。実際今でも使われていますから押さえておいて損はないと思います。FizzBuzz詳細

私が書いたコード

実際の試験では「言語は問わない、形式も自由」だったので今回はRubyのWhile文を用いてやってみます。

FizzBuzz.rb
i=1

while i <=30

  if i%15==0
    puts "FizzBuzz"
  elsif i%3==0
    puts "Fizz"
  elsif i%5==0
    puts "Buzz"
  else
    puts i
  end

  i+=1

end

他にもfor文やeach文でも実現可能なのでぜひやってみてください!!

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

Ruby入門メモ(プログラミング経験者用)2

TIPS

定数

英大文字で定義する(慣習的に全部大文字)。

VERSION = 1.1

オブジェクトの型変換

to_i : 数値変換
to_f : 浮動小数点変換
to_s : 文字列変換

number = 100
f_number = number.to_i * 0.01
l_number = number.to_s + "円"
d_number = f_number.to_i

puts "#{number} #{f_number} #{l_number} #{d_number}"

[実行結果]

100 1.0 100円 1

書式付きの値埋め込み

%d 整数
%f 浮動小数
%s 文字列

puts "文字列と書式埋め込み" %[対応する値]
※埋め込む書式が複数なら配列で渡す。

diagram = "円"
radius = 1
ratio = 3.14

puts "半径%dcmの%sの直径は%.2fcmです" %[radius, diagram, 2 * ratio * radius]

[実行結果]

半径1cmの円の直径は6.28cmです

ハッシュ

C#でいう辞書、連想配列。
keyとそれに対応する値(value)をもつ。
型は数値でも文字列でも可能

presents = {"日本" => "お肉券" , "米国" => "お金", "英国" => "お金" }
puts presents["日本"]

awards = { 1 => "金" , 2 => "銀" , 3 => "銅" }
puts awards[1]

[実行結果]

お肉券
金

ハッシュのメソッド

size :サイズを返す
keys :キーを返す
values :値を返す

presents = {"日本" => "お肉券" , "米国" => "お金", "英国" => "お金" }

puts presents.size
puts "-----------------"
puts presents.keys
puts "-----------------"
puts presents.values
puts "-----------------"

[実行結果]

3
---------------------
日本
米国
英国
---------------------
お肉券
お金
お金
---------------------

Case

CやC#のswitch文に対応する。

case 変数
 when パターン1
    処理
 when パターン2
    処理
 end
input = gets.chomp

case input 
  when "red"
    puts "止まれ!"
  when "yellow"
    puts "警戒!"
#条件を複数扱いたいときはコンマで区切る
  when "blue","green"
    puts "進め"    
  end

[実行結果]

red
止まれ!

ループ

for文は入門メモ1参照

while

while 条件 do
  処理
end
input = gets.chomp.to_i
i = 0

while i < input do
  puts "あ"
  i += 1
end

[実行結果]

3
あ
あ
あ

途中でループ処理から抜けたいときは「break」
一回ループをスキップするときは「next」
条件文(if文)と共に扱うことが多い。

times

ループする回数が決まっているときに使う。

回数.times do |変数|
 処理
end
input = gets.chomp.to_i

input.times do |x|
  puts "あ"
end

[実行結果]

3
あ
あ
あ

参考文献

Progate https://prog-8.com/slides?lesson=72%2C75%2C78%2C81%2C85
ドットインストール https://dotinstall.com/lessons/basic_ruby_v3

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

Helmで導入したRabbitMQをRuby(Bunny)から利用した時のメモ

はじめに

アプリケーション同士を連携させるのに、必ずしもAPIをHTTPで呼び出すことが適切でない場合もあります。
特にQueueに入れた処理を逐次処理させる場合には、アプリケーションが依頼を直接受け取ってしまうと、アプリ側で永続化の処理を考えなければいけませんし、負荷分散も難しくなります。

とりあえずPub/Subよりも単純なQueueをRabbitMQで実現する予定なので、その作業のメモを残しておきます。

前提

  • Kubernetes v1.15.11 (by Kubespray v2.11.2)
  • Rook/Cephが導入されている
  • MetalLBが導入されている

References

次の資料を参考にしました。

HelmでのRabbitMQの導入

やり直すことを考えて、処理のためにMakefileを準備しておきます。

Makefile
REPO_NAME = stable/rabbitmq
NAMESPACE = rabbitmq
REL_NAME = myrabbitmq

RMQ_OPTIONS = --set persistence.storageClass=rook-ceph-block \
    --set replicas=2 \
    --set service.type=LoadBalancer \
    --set persistence.storageClass=rook-ceph-block \
    --set rabbitmq.erlangCookie=9a63d47049016fd933371a76af08fc8f \
    --set rabbitmq.password=70550b0ac43a2e5c

.PHONY: init update fetch install upgrade 

init:
        kubectl create ns $(NAMESPACE)

update:
        helm repo update

fetch:
        helm fetch $(REPO_NAME)

install:
        helm install $(REPO_NAME) --name $(REL_NAME) --namespace $(NAMESPACE) $(RMQ_OPTIONS)

upgrade:
        helm upgrade ${REL_NAME} $(REPO_NAME) --namespace $(NAMESPACE) $(RMQ_OPTIONS)

導入の際の手順はおおむね次ようなものです。

k8s-masterノードでの作業
$ make fetch
$ ls
Makefile rabbitmq-6.18.2.tgz
## tgzファイルを展開し、values.yamlの内容を確認する
$ tar xvzf rabbitmq-6.18.2.tgz
$ less rabbitmq/values.yaml
## 他に変更する点がなければ導入する、あればMakefileのRMQ_OPTIONSに追記
$ make install

RMQ_OPTIONSに設定していた"service.type=LoadBalancer"の部分は環境に合わせて変更が必要だと思います。設定可能な項目については、rabbitmq/values.yamlを確認してください。

service.typeにLoadBalancerを指定しているので、4369,5672,15672の全ポートが公開されてしまっています。これで問題なければ良いですが、選択的にポートを絞って公開したい場合には、service.type=ClusterIPを指定したまま次のようなServiceを指定することもできます。

metadata.name,各namespace等は環境に合わせて変更してください
apiVersion: v1
kind: Service
metadata:
  name: my-release-rabbitmq-lb  
  labels:
    app: rabbitmq
  namespace: rabbitmq
spec:
  ports:
  - name: amqp
    port: 5672
    protocol: TCP
    targetPort: amqp
  - name: stats
    port: 15672
    protocol: TCP
    targetPort: stats
  selector:
    app: rabbitmq
    release: myrabbitmq
  type: LoadBalancer

RabbitMQの準備作業

現在のサービスは次のように公開されています。

svcの状態
$ kubectl -n rabbitmq get svc
NAME                  TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                                                         AGE
myrabbitmq            LoadBalancer   10.233.25.181   192.168.1.120   4369:31028/TCP,5672:32031/TCP,25672:31175/TCP,15672:32214/TCP   35m
myrabbitmq-headless   ClusterIP      None            <none>         4369/TCP,5672/TCP,25672/TCP,15672/TCP                           35m

http://192.168.1.120:15672/ から、RabbitMQのWeb UIにアクセスします。
Helmから導入したRabbitMQにログインする場合は、それぞれ、--setで、rabbitmq.username (default:user), rabbitmq.password に指定したものを利用します。今回は (username,password) = (user, 70550b0ac43a2e5c) を使用します。

Queueの作成

WebUIのQueuesタブをクリックし、新規のQueueを追加します。

  • Name: testq
  • Durable: yes

接続用Userの作成

Adminタブから新規のユーザーを追加します。

追加したら、そのユーザー名をクリックし、権限を付与します。

  • username: user01
  • password: secret

今回はテストなので、作成したユーザーにデフォルトで権限を付与しておきます。

  • vhost: /
  • Configure regexp: .*
  • Write regexp: .*
  • Read regexp: .*

Bunnyからの接続

Rubyのアプリケーションを作成するディレクトリに、Gemfileを準備し、bundleからlibディレクトリに配置します。

Gemfile
source 'https://rubygems.org'

gem "bunny"
bunnyライブラリのダウンロード
$ bundle install --path lib

Put/Getのテスト

準備はできたので、次のようなRubyスクリプトを配置します。

put.rb
#!/usr/bin/ruby
#
require 'bundler/setup'
Bundler.require

require 'bunny'

conn = Bunny.new(host: "192.168.1.120", vhost: "/", user: "user01", password: "secret")
conn.start
ch = conn.create_channel
## x-max-lengthなどをQueueに設定している場合には、次のように:argumentsに設定を加える
q = ch.queue("testq",
             durable: true,
             arguments: { 'x-max-length' => 1024 , 
                          'x-max-length-bytes' => 1048576, 
                          'x-queue-type' => 'classic' } )  ## arguments:の設定はQueue定義に応じて要変更

q.publish("Hello", persistent: true)

ch.close
conn.close

ここでは、送信するだけで結果は受け取れません。受信用には次のスクリプトを作成しています。

get.rb
#!/usr/bin/ruby
#
require 'bundler/setup'
Bundler.require

require 'bunny'

conn = Bunny.new(host: "192.168.1.120", vhost: "/", user: "yasu", password: "zaq12wsx")
conn.start
ch = conn.create_channel
q = ch.queue("testq", 
             durable: true,
             arguments: { 'x-max-length' => 1024,
                          'x-max-length-bytes' => 1048576,
                          'x-queue-type' => 'classic' } ) ## arguments:の設定はQueue定義に応じて要変更
puts "Message Count: #{q.message_count}"

delivery_info, metadata, payload = q.pop
puts "Received: #{payload}"

ch.close
conn.close

Pub/Subのテスト

実際には、アプリケーション側でメッセージが届くまでWaitしたいので、get.rbのコードを少し変更しました。

sub.rb
#!/usr/bin/ruby
#
require 'bundler/setup'
Bundler.require

require 'bunny'

conn = Bunny.new(host: "192.168.1.120", vhost: "/", user: "yasu", password: "zaq12wsx")
conn.start
ch = conn.create_channel
q = ch.queue("testq", 
             durable: true,
             arguments: { 'x-max-length' => 1024,
                          'x-max-length-bytes' => 1048576,
                          'x-queue-type' => 'classic' } ) ## arguments:の設定はQueue定義に応じて要変更

q.subscribe(manual_ack: true) do |delivery_info, metadata, payload|
  puts "-------"
  puts "Message Count: #{q.message_count}"
  puts "routing_key: #{delivery_info.routing_key}"
  puts "Received: #{payload}"
  ch.ack(delivery_info.delivery_tag)

  sleep 10
end

## waiting for never
loop { sleep 5 }

ch.close
conn.close

以上

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

Reactのチュートリアルの三目並べで少しRails使う #2

前回の続きになります。

手順を履歴に保持します。

履歴機能を作成

index.html.erb
<head>
  <!--<link rel="stylesheet" href="css/index.css" />-->
  <!--<script src="js/index.js"></script>-->
  <%= javascript_pack_tag 'tictactoes', 'data-turbolinks-track': 'reload' %>
</head>

<body id="tictactoes-body">
  <%= form_with(url: "/tictactoes/sqClick", method: "post") do |f| %>
    <div id="root">
      <div class="game">
        <div class="gmae-board">
          <div>
            <% @Squares.each.with_index do |sq, i| %>
              <% if i % 3 == 0%>
                <div class="board-row"></div>
              <% end %>
              <%= f.text_field '', :name => "item[]", :class => "square", :readonly => true , :value => sq.content%>              

            <% end %>
          </div>
        </div>
        <div class="game-info">
          <div><%= @nextStepMessage %></div>
          <div>
            <li><button type="button">Go to game start</button></li>
              <% (0...@buttonCount).each do |num| %>
                <li><button type="button">Go to move #<%= num + 1 %></button></li> 
              <% end %>
          </div>
        </div>
      </div>
    </div>
    <%= f.hidden_field :clickButtonIndex, :value => @clickButtonIndex %>
    <%= f.hidden_field :clickNo, :value => @clickNo %>    
    <%= f.hidden_field :stepNumber, :value => @stepNumber %>
  <% end %>
</body>

履歴ボタンのところはサーバーサイドでボタンの数を埋め込む形としました。
実装中に@buttonCountを定義し忘れることがあって、そのまま動かすとブラウザが固まって、CPUとメモリが上昇し続けるという状態になり、めっちゃハマりましたorz
PCの寿命かとも思いましたw

0...@変数みたいな使い方したらダメなんでしょうね多分
※SCSSは前回と同様です。

tictactoes.js
(function() {
  window.addEventListener("DOMContentLoaded", () => {
    // 全square
    document.querySelectorAll(".square").forEach((element, index) => {
      // クリックイベント
      element.addEventListener("click", squareClick.bind(element, index));
    });

    // 全button
    document
      .querySelectorAll(".game-info li > button")
      .forEach((element, index) => {
        // クリックイベント
        element.addEventListener("click", historyButtonClick.bind(null, index));
      });

    let isClick = false;
    function squareClick(index) {
      // 2度押し防止、既に値が埋まっているか
      if (isClick || this.value) {
        return;
      } else {
        isClick = true;
      }

      document.getElementById("clickNo").value = index;
      document.forms[0].submit();
    }

    function historyButtonClick(index) {
      // 2度押し防止
      if (isClick) {
        return;
      } else {
        isClick = true;
      }

      document.getElementById("clickButtonIndex").value = index;
      document.forms[0].action = "/tictactoes/hysBtClick";
      document.forms[0].submit();
    }
  });
})();

js側はなんだかんだ結構記述しました・・・。

 

routes.rb
Rails.application.routes.draw do
  get '/' => 'home#top'
  get 'posts/index' => 'posts#index'
  get 'top' => 'home#top'
  get 'about' => 'home#about'
  get 'tictactoes' => 'tictactoes#index'
  get 'tictactoes/sqClick' => 'tictactoes#index'
  get 'tictactoes/hysBtClick' => 'tictactoes#index'
  post 'tictactoes/sqClick' => 'tictactoes#sqClick'
  post 'tictactoes/hysBtClick' => 'tictactoes#hysBtClick'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

現状POSTした後のページでF5を押すと、CSRFチェック?に引っ掛かり、エラーになっていたのでルーティングを追加しました。
普通こんなやり方しないのでしょうか。
 

tictactoes_controller.rb
class TictactoesController < ApplicationController
    #初期画面表示
    def index

        # 手順1以降を削除して初期化
        Square.where.not(stepNumber: 0).delete_all

        #手順0を全件取得
        @Squares = Square.all

        #手順の開始は0から
        @stepNumber = 0
        @clickButtonIndex = 0
        @buttonCount = 0

        #次の手番の文字列
        @nextStepMessage = "次の手番: X"
    end

    #マス目クリック
    def sqClick

        #現在の手順インクリメント
        stepNumber = params[:stepNumber].to_i
        @stepNumber = stepNumber.succ

        #マス目の配列
        items = params[:item]                
        sq = Square.new

        #既に勝敗が着いた      
        if sq.calculateWinner(items)
            return
        else 
            xIsNext = stepNumber % 2 == 0

            clickButtonIndex = params[:clickButtonIndex]

            #XとOを設定
            items[params[:clickNo].to_i] = xIsNext ? "X": "O"            
            isWinner = sq.calculateWinner(items)

            #現在より後ろの手順は削除
            Square.where("stepNumber > ?", clickButtonIndex).delete_all

            #新しい手順をDBに保存          
            sq.saveSquare(@stepNumber, items)

            #次の手番の文字列更新    
            if isWinner
                @nextStepMessage = "勝者:" + (xIsNext ? "X": "O")    
            else
                @nextStepMessage = xIsNext ? "次の手番: O": "次の手番: X"
            end

            #最新の手順を取り直す
            @Squares = Square.where(stepNumber: @stepNumber)            
        end

        @buttonCount = @stepNumber

        #indexのhtmlを使いまわす
        render action: :index
    end

    #履歴ボタン押下
    def hysBtClick
        @clickButtonIndex = params[:clickButtonIndex].to_i
        @Squares = Square.where(stepNumber: @clickButtonIndex)

        #contentの配列を作る
        items = @Squares.map { |s| s.content }

        sq = Square.new        
        if sq.calculateWinner(items)
            @nextStepMessage = "勝者:" + (@clickButtonIndex % 2 == 0 ? "O": "X")    
        else
            @nextStepMessage = @clickButtonIndex % 2 == 0 ? "次の手番: X": "次の手番: O"
        end        

        @stepNumber = @clickButtonIndex
        @buttonCount = Square.count / 9 - 1

        render action: :index
    end
end

jsでやっていた時のようにxIsNext変数は削除し、stepNumberの余りで次の手順を判断するようにしました。

Square.rb
class Square < ApplicationRecord

    Lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
      ]

    def saveSquare(stepNumber, items)                
        #新しい手順のマス目保存処理
        (1..9).each do |i|
            Square.create(stepNumber: stepNumber, squareNumber: i, content: items[i - 1])
        end     
    end

    #勝敗判定関数(公式チュートリアルから拝借)
    def calculateWinner(items)

        Lines.each do |l|
            a, b, c = l

            if items[a] != "" && items[a] == items[b] && items[a] == items[c]
                return true
            end
        end

        return false
    end
end

モデルはほぼ変更はないです。
ネットで調べると、今の時代forはあまり使わないとのことなのでeach doにしました。
なぜ使わないのかをちゃんと理解しないとな・・・

結果
image.png

感想

formをsubmitしているんですんごいもっさりします。
いずれajaxで組んでみようと思います。
その場合はクライアントにはVue.jsが使えたらいいなぁと思います。

Railsのお作法や知識が浅いだけかもしれませんが、今のところあまりRailsのメリットが感じられませんでした。
独特な記法すぎて、他のプログラミング言語と類似点が見つけにくく、習得しにくいし、習得しても他のプログラミングを覚えるときに役に立ちにくいと感じました。

これからも触っていっていつか本感想を振り返ってみたいと思います。

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

【Rails】collectionとmemberの違い

はじめに

学習中の備忘録です。

概要

7つの基本アクション以外でルーティングを定義する時の、collectionとmemberの違い。
結論:collectionはルーティングに:idがつかない、memberは:idがつく。

前提

  • rails 5.2.3
  • postsテーブルを検索するsearchアクションを定義
  • 今回はcollectionとmemberの違いのみ。searchメソッドは定義済とする。

collectionで定義した場合

Rails.application.routes.draw do
  resources :posts do
    collection do
      get 'search'
    end
  end
end

collectionのルーティング

Prefix          Verb    URI                                 Pattern
search_posts    GET     /posts/search(.:format)             posts#search

memberで定義した場合

Rails.application.routes.draw do
  resources :posts do
    member do
      get 'search'
    end
  end
end

memberのルーティング

Prefix           Verb    URI                               Pattern
search_post      GET    /posts/:id/search(.:format)        posts#search

まとめ

URIの指定先が、collectionが:idなし、memberが:idありとなっていることが確認できます。

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