20200626のRubyに関する記事は17件です。

curlコマンドにより作成したapiでデータを取得する

環境

cloud9(api取得側)
local(api提供側)
ruby 2.6.3
Rails 6.0.2.1

結論

controller

def index
  uri = `curl -v -X GET "http://stage-smartyoyaku-env.ap-northeast- 1.elasticbeanstalk.com/api/v1/tasks" \
        -H "Authorization: Bearer ここにTokenを書く"`
  @test = JSON.parse(uri)
end

*取得できているかブラウザで確認します

viewfile

<%= @test %>

それまでのエラー内容

cloud9のターミナルでcurlコマンドを入力すると。。

curl: (7) Failed to connect to localhost port 3000: Connection refused

調べて見るとサーバーが立ち上がってない場合かポート番号が間違っているかのようです。
ブラウザでlocalhost:3000にアクセスしましたが問題なし。port番号もあっていましたが、、

もう一度整理

・cloud9側のアプリでcurlコマンドを叩いている。(取得する側)
・local環境の3000portでapiを提供しているアプリを立ち上げている。(提供する側)←ここ重要

ん?cloud9(インターネット上)からlocalには接続できないんじゃ、、

$ lsof -i:3000
$

cloud9上で3000portが使われているか確認→表示されない

つまりapiを提供する側はherokuなどでネットにurlを公開し、そこからデータを取ってくる必要があります。

注意点

ターミナルでcurlコマンドを打つ場合とcontrollerに書く場合は微妙に書き方が違う。
controllerではcurlコマンドを``で囲む。

ターミナルの場合

$ curl -v -X GET "http://stage-smartyoyaku-env.ap-northeast-1.elasticbeanstalk.com/api/v1/tasks" \
          -H "Authorization: Bearer ここにTokenを書く"
controllerの場合

def index
  uri = `curl -v -X GET "http://stage-smartyoyaku-env.ap-northeast- 1.elasticbeanstalk.com/api/v1/tasks" \
        -H "Authorization: Bearer ここにTokenを書く"`
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails gem rooを使って、エクセルファイルからデータ登録

目的

ユーザーが複数のデータをアプリに投稿したいとき、エクセルファイルを選択するとデータベースに保存できるようにしたい。

実装

アプリ立ち上げ

rails new sample_app -d postgresql
rails db:create

rails newで新規アプリを作成、データベースも作成する。

コントローラー作成

rails g controller users index

indexアクションを持つUserコントローラーを作成。
ルーターとビューも一緒に作成される。

モデル作成

rails g model user name:string email:string
rails db:migrate

usersテーブルには、string型のnameとemailのカラムを作成。

ビュー

app/views/users/index.html.erb
<h1>ユーザー一覧</h1>

<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>名前</th>
      <th>Eメール</th>
    </tr>
  </thead>
  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.id %></td>
        <td><%= user.name %></td>
        <td><%= user.email %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= form_with url: import_users_path, local: true do |form| %>
  <%= form.file_field :file %>
  <%= form.submit %>
<% end %>

投稿フォーム生成。ファイル自体を保存するわけではなく、params[:file]を送ったアクション:importにてUserを登録できるようにする。

ルーター

config/routes.rb
Rails.application.routes.draw do
  resources :users do
    collection do
      post :import
    end
  end
end

UserにPostメソッドの:importアクション用のルーティングを追加する。

コントローラー

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def import
    User.import(params[:file])
    redirect_to users_url
  end
end

indexアクションはscaffoldを使ったときと同じで全レコードを取得し、@usersに代入。
importアクションは、params[:file]を受け取って、usersテーブルのレコードを作成するimportメソッドを使う。モデルには、importメソッドの定義を書く。

モデル

app/models/user.rb
class User < ApplicationRecord
  def self.import(file)
    xlsx = Roo::Excelx.new(file.tempfile)
    xlsx.each_row_streaming(offset: 1) do |row|
      user = self.new(id: row[0].value, name: row[1].value, email: row[2].value)
      next if self.pluck(:id).include?(user.id)
      user.save
    end
  end
end

Userモデルのモデルメソッドなので、def self.import の形で定義。params[:file].tempfileに送信したファイルのパスが入る。Roo::Excelx.new(file.tempfile)でファイルを読み取り、変数xlsxに代入。each_row_streamingメソッドで読み込んだファイルの行毎に処理を実行。
offsetオプションによって、処理を飛ばす行数を指定できる(一行目の項目欄を飛ばす)。
next if self.pluck(:id).include?(user.id)で同じidがusersテーブルに存在している場合は処理をスキップしている。

gem rooを使えるように準備をする。

gem rooを使う準備

Gemfile
gem "roo" #追記

ターミナルでbundle installする。また、config/application.rbにrequire "roo"を追記

config/application.rb
# 省略
require 'rails/all'
require "roo"
# 省略

これで、OK。

確認

サーバーを立ち上げ "http://localhost:3000/users" にアクセスして画面を確認する。

test.png
ファイルを選択を押して、次のようなエクセルファイルからデータを登録できる。
エクセル.PNG
登録後、次のように画面が変わっていることを確認する。
test.png

参考記事

https://qiita.com/seitarooodayo/items/c9d6955a12ca0b1fd1d4
https://qiita.com/guri3/items/f20487516311b2a3db37
本当に参考になりました!ありがとうございます!

後書き

応用して、社員表を一気に登録する機能をアプリに作ってみようと思います。

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

Rails gem rooを使って、エクセルファイルからデータ保存

目的

ユーザーが複数のデータをアプリに投稿したいとき、エクセルファイルを選択するとデータベースに保存できるようにしたい。

実装

アプリ立ち上げ

rails new sample_app -d postgresql
rails db:create

rails newで新規アプリを作成、データベースも作成する。

コントローラー作成

rails g controller users index

indexアクションを持つUserコントローラーを作成。
ルーターとビューも一緒に作成される。

モデル作成

rails g model user name:string email:string
rails db:migrate

usersテーブルには、string型のnameとemailのカラムを作成。

ビュー

app/views/users/index.html.erb
<h1>ユーザー一覧</h1>

<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>名前</th>
      <th>Eメール</th>
    </tr>
  </thead>
  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.id %></td>
        <td><%= user.name %></td>
        <td><%= user.email %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= form_with url: import_users_path, local: true do |form| %>
  <%= form.file_field :file %>
  <%= form.submit %>
<% end %>

投稿フォーム生成。ファイル自体を保存するわけではなく、params[:file]を送ったアクション:importにてUserを登録できるようにする。

ルーター

config/routes.rb
Rails.application.routes.draw do
  resources :users do
    collection do
      post :import
    end
  end
end

UserにPostメソッドの:importアクション用のルーティングを追加する。

コントローラー

app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def import
    User.import(params[:file])
    redirect_to users_url
  end
end

indexアクションはscaffoldを使ったときと同じで全レコードを取得し、@usersに代入。
importアクションは、params[:file]を受け取って、usersテーブルのレコードを作成するimportメソッドを使う。モデルには、importメソッドの定義を書く。

モデル

app/models/user.rb
class User < ApplicationRecord
  def self.import(file)
    xlsx = Roo::Excelx.new(file.tempfile)
    xlsx.each_row_streaming(offset: 1) do |row|
      user = self.new(id: row[0].value, name: row[1].value, email: row[2].value)
      next if self.pluck(:id).include?(user.id)
      user.save
    end
  end
end

Userモデルのモデルメソッドなので、def self.import の形で定義。params[:file].tempfileに送信したファイルのパスが入る。Roo::Excelx.new(file.tempfile)でファイルを読み取り、変数xlsxに代入。each_row_streamingメソッドで読み込んだファイルの行毎に処理を実行。
offsetオプションによって、処理を飛ばす行数を指定できる(一行目の項目欄を飛ばす)。
next if self.pluck(:id).include?(user.id)で同じidがusersテーブルに存在している場合は処理をスキップしている。

gem rooを使えるように準備をする。

gem rooを使う準備

Gemfile
gem "roo" #追記

ターミナルでbundle installする。また、config/application.rbにrequire "roo"を追記

config/application.rb
# 省略
require 'rails/all'
require "roo"
# 省略

これで、OK。

確認

サーバーを立ち上げ "http://localhost:3000/users" にアクセスして画面を確認する。

test.png
ファイルを選択を押して、次のようなエクセルファイルからデータを登録できる。
エクセル.PNG
登録後、次のように画面が変わっていることを確認する。
test.png

参考記事

https://qiita.com/seitarooodayo/items/c9d6955a12ca0b1fd1d4
https://qiita.com/guri3/items/f20487516311b2a3db37
本当に参考になりました!ありがとうございます!

後書き

応用して、社員表を一気に登録する機能をアプリに作ってみようと思います。

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

Ruby on Rails5 速習実践ガイド5.2対応 Chapter2

Chaptrt2 Railsアプリケーションをのぞいてみよう

Windows10の環境構築
実習でHyperVマネージャーにより、仮想環境はすでにあり。
Ubuntu 18.04.3をインストール
rbenv 1.1.2-30-gc879cb0 Gitを使ってGitHubからをインストール。
Ruby 2.5.1 インストール
Rails インストール
Node.js のインストール

JavaScriptを用いる場合、効率よく配信するためにJavaScriptを圧縮する機能が備わっているが、そのためにJavaScriptランタイムが必要となるためにインストール。

PostgreSQL をインストールしセットアップ
PostgreSQLを起動。
 sudo service postgresql start

 
 sudo su postgres -c 'createuser -s {Ubuntuのログインユーザ}'
 を打つと、以下のエラーが出た。

 psql: FATAL: role “postgres” does not exist
 {}無しでUbuntuのログインユーザー名を記載したらすんなり通りました。

scaffoldを使ってscaffold_appという名前で「ユーザー管理アプリケーション」をRailsのコマンドで自動生成させる。
 rails new scaffold_app -d postgresql

 rails new コマンドはRailsアプリケーションのひな型を生成。
アプリケーションの名前をscaffold_appと指定。
scaffold_appディレクトリと関連ファイルが生成されて、ログ出力される。
この後 bundle installが自動的に実行されて、Railsの実行に必要なgemがインストールされる。
データベースの作成。config/databese.ymlというファイルの定義をもとに。

bin/rails s db:create

「rails」ではなく「bin/rails」で、アプリケーションのルートディレクトリ直下のbinディレクトリにあるrailsというスクリプトを呼び出している。Gemfileどおりのgemを利用できる環境上でrailsコマンドを実行できる。
railsサーバー起動
 bin/rails s

railsは標準のHTTPサーバとしてPumaを採用。

WEBブラウザでサーバーの起動を確認
 アドレスバーにhttp://localhost:3000/を入力。

ユーザー管理機能のひな型を作る
「ユーザー」に関するscaffoldを自動生成。
 bin/rails generate scaffold user name:string address:string age:integer

ユーザー管理機能に使うテーブルをデータベースに作成。
 bin/rails db:migrate

ユーザー一覧画面を表示
 http://localhost:3000/users
WEBブラウザへ上のURLを入力してアクセス。

重要だと思ったポイント

  • ユーザーを管理するために必要な機能「CRUD」が生成されている。  「作成(create)」「読み出し(read)」「更新(update)」「削除(delete)」
  • コードの通り道 Railsアプリケーションは、MVC(モデル・ビュー・コントローラ)という考え方で構成。 コントローラーが呼ばれる。コントローラーにはリクエストに対する処理が書かれている「アクション」と呼ばれるメソッドが定義(一覧画面にアクセスした際はidnexアクション)されている。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails5 速習実践ガイド5.2対応 Chapter3

Capter3タスク管理アプリケーションを作ろう

CRUDを備えたシンプルな「タスク管理アプリケーション」を、scaffoldを使わずにゼロから作成。

環境

  • Ruby 2.5.1

  • Ruby on Rails 5.2.1

  • PostgreSQL 10.5

Railsのエラーメッセージなどを日本語で出せるようにする
GithubのRails-i18nリポジトリにある翻訳されたファイル(rawファイル)をconfig/locales/ja.ymlとしてダウンロード。

wget https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml --output-file=config/locales/ja.yml

config/locales/ja.ymlを確認すると、以下のログが記載されている。

--2020-06-26 14:40:30-- https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml
192.168.24.1:8080 に接続しています... 接続しました。
Proxy による接続要求を送信しました、応答を待っています... 200 OK
長さ: 5117 (5.0K) [text/plain]
`ja.yml' に保存中

0K ....                                                  100% 10.3M=0s

2020-06-26 14:40:31 (10.3 MB/s) - `ja.yml' へ保存完了 [5117/5117]

解決策としてWEBブラウザへ
https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml
を張り付ける。
表示された以下のテキストの内容をja.yml内に張り付けて保存。


ja:
activerecord:
errors:
messages:
record_invalid: 'バリデーションに失敗しました: %{errors}'
restrict_dependent_destroy:
has_one: "%{record}が存在しているので削除できません"
has_many: "%{record}が存在しているので削除できません"
date:
abbr_day_names:
- 日
- 月
- 火
- 水
- 木
- 金
- 土
abbr_month_names:
-
- 1月
- 2月
- 3月
- 4月
- 5月
- 6月
- 7月
- 8月
- 9月
- 10月
- 11月
- 12月
day_names:
- 日曜日
- 月曜日
- 火曜日
- 水曜日
- 木曜日
- 金曜日
- 土曜日
formats:
default: "%Y/%m/%d"
long: "%Y年%m月%d日(%a)"
short: "%m/%d"
month_names:
-
- 1月
- 2月
- 3月
- 4月
- 5月
- 6月
- 7月
- 8月
- 9月
- 10月
- 11月
- 12月
order:
- :year
- :month
- :day
datetime:
distance_in_words:
about_x_hours:
one: 約1時間
other: 約%{count}時間
about_x_months:
one: 約1ヶ月
other: 約%{count}ヶ月
about_x_years:
one: 約1年
other: 約%{count}年
almost_x_years:
one: 1年弱
other: "%{count}年弱"
half_a_minute: 30秒前後
less_than_x_seconds:
one: 1秒以内
other: "%{count}秒未満"
less_than_x_minutes:
one: 1分以内
other: "%{count}分未満"
over_x_years:
one: 1年以上
other: "%{count}年以上"
x_seconds:
one: 1秒
other: "%{count}秒"
x_minutes:
one: 1分
other: "%{count}分"
x_days:
one: 1日
other: "%{count}日"
x_months:
one: 1ヶ月
other: "%{count}ヶ月"
x_years:
one: 1年
other: "%{count}年"
prompts:
second: 秒
minute: 分
hour: 時
day: 日
month: 月
year: 年
errors:
format: "%{attribute}%{message}"
messages:
accepted: を受諾してください
blank: を入力してください
confirmation: と%{attribute}の入力が一致しません
empty: を入力してください
equal_to: は%{count}にしてください
even: は偶数にしてください
exclusion: は予約されています
greater_than: は%{count}より大きい値にしてください
greater_than_or_equal_to: は%{count}以上の値にしてください
inclusion: は一覧にありません
invalid: は不正な値です
less_than: は%{count}より小さい値にしてください
less_than_or_equal_to: は%{count}以下の値にしてください
model_invalid: 'バリデーションに失敗しました: %{errors}'
not_a_number: は数値で入力してください
not_an_integer: は整数で入力してください
odd: は奇数にしてください
other_than: は%{count}以外の値にしてください
present: は入力しないでください
required: を入力してください
taken: はすでに存在します
too_long: は%{count}文字以内で入力してください
too_short: は%{count}文字以上で入力してください
wrong_length: は%{count}文字で入力してください
template:
body: 次の項目を確認してください
header:
one: "%{model}にエラーが発生しました"
other: "%{model}に%{count}個のエラーが発生しました"
helpers:
select:
prompt: 選択してください
submit:
create: 登録する
submit: 保存する
update: 更新する
number:
currency:
format:
delimiter: ","
format: "%n%u"
precision: 0
separator: "."
significant: false
strip_insignificant_zeros: false
unit: 円
format:
delimiter: ","
precision: 3
separator: "."
significant: false
strip_insignificant_zeros: false
human:
decimal_units:
format: "%n %u"
units:
billion: 十億
million: 百万
quadrillion: 千兆
thousand: 千
trillion: 兆
unit: ''
format:
delimiter: ''
precision: 3
significant: true
strip_insignificant_zeros: true
storage_units:
format: "%n%u"
units:
byte: バイト
eb: EB
gb: GB
kb: KB
mb: MB
pb: PB
tb: TB
percentage:
format:
delimiter: ''
format: "%n%"
precision:
format:
delimiter: ''
support:
array:
last_word_connector: "、"
two_words_connector: "、"
words_connector: "、"
time:
am: 午前
formats:
default: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
long: "%Y/%m/%d %H:%M"
short: "%m/%d %H:%M"
pm: 午後

これでアプリケーション作成の下準備が整った。

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

【Rails】なんでsaveに成功したらredirect_toで失敗したらrenderなの?

はじめに

Railsで開発中、フォームの実装時にrenderの扱いについて悩むことがあったため、調べてく内にredirect_toとrenderの違いを改めて再確認出来たので、備忘として残しておくものです。
ついでにrenderで一瞬悩んだことも書いておきます。(具体的にはbefore_actionの扱い)

環境

・Ruby 2.6.5
・Rails 6.0.3

状況

飲食店情報を登録する様なアプリケーションを作っており、コントローラーに以下の様なアクションを設定していました。

shops_controller.rb
 def new
    @shop = Shop.new
 end

 def create
    @shop = @owner.shops.new(shop_params)
    if @shop.save
      redirect_to :index
    else
      render :new
    end
  end

*ストロングパラメータは今回関係ないので割愛

もはや何も考えずこう書いていましたが、保存に失敗した場合、renderを用いてもう一度投稿フォームに戻すのは何故か?という所を振り返ってみます。

結論としては、入力した内容を保持したままもう一度入力フォームに戻したいからです。

では何故その場合redirect_toではなくrenderを使えば実現できるのでしょうか?
redirect_toとrenderの挙動について振り返ってみます。

redirect_toとは

redircet_toは指定したアクション(やパス)へのアクセスをクライアントサイドに行わせる処理です。
これにより、MVCの流れを再度踏まえた上でそのページにアクセスすることになります。
具体的には「指定されたアクションに再度アクセス⇨コントローラーのアクションを実行⇨対応するビューを表示」という流れですね。

アクションのビューにurlからアクセスする際の挙動と全く同じことをやってくれるわけです。

renderとは

それに対しrenderは、アクションを再度経由することなく指定したアクションに対応するビューをただ表示してくれます。
redirect_toと違い、newアクションの中身を実行することなくビューを返してくれるわけです。

renderで入力内容が保持される訳

先程の説明を踏まえてもう一度コードを見てみましょう。

shops_controller.rb
 def new
    @shop = Shop.new
 end

 def create
    @shop = @owner.shops.new(shop_params)
    if @shop.save
      redirect_to :index
    else
      render :new
    end
  end

newアクションはお店情報の新規登録ページですので、アクション内で@shop = Shop.newを定義しています。(空のインスタンスを生成)
この後フォームに入力した値がparamsとして渡され、それがcreateアクションで今回作成した@shopの情報としてうまく渡されることで保存ができるわけです。

保存に失敗した場合(elseの場合)またフォームに戻す処理をする訳ですが、この時にredirect_toを使うと、再度newアクションが実行され@shopはまた空のインスタンスになってしまいます。

一方でrenderを使うとnewアクションを介さないため、@shopにはcreateアクションで受け取ったparamsの値が保持されたまま、再度フォームのビューを表示してくれる訳です。

new.html.haml
.shop-wrapper
  = form_with model: [@owner, @shop], html: {class: "shopform"}, local: true do |f|

上記はビューの抜粋ですが、フォームは@owner@shopを受け取って内容を表示しているため、@shopに中身がある状態であればその中身をちゃんと表示してくれる訳です。

以上がcreateアクションに失敗した時に、renderを使うべき理由になります。

今回遭遇したエラー

ちなみに今回はrenderを設定した際、最初以下のエラーに遭遇しました。
スクリーンショット 2020-06-26 21.45.44.png

一瞬なんだこれってなったんですが、newアクションに対して以下のbefore_acitonを設定してたんですよね。

shops_controller.rb
before_action :set_select_lists, only: [:new]

# 中略

private
def set_select_lists
    @stations = Station.all.map {|station| [station.name, station.id] }.unshift(["以下から選んでください", nil])
    @genres = Genre.all.map {|genre| [genre.name, genre.id] }.unshift(["以下から選んでください(必須)", nil])
    @marks =  Mark.all.map {|mark| [mark.favorite, mark.id] }.unshift(["以下から選んでください(必須)", nil])
  end

フォームの中にあるプルダウンの選択リストを作り、before_actionで設定していたんです。

先ほども書いた様に、renderはnewアクションを再度経由しないため、before_actionも行わない→設定された値がなくビュー表示でエラーが起きるという流れだったわけです。

これを受け一瞬、「@shop意外に定義しなきゃいけない変数があったらrender使えないの?」って思ったんですが、数分考えてすぐ解決しました。

createアクションを以下の様に書き換えます。

shops_controller.rb
def create
    @shop = @owner.shops.new(shop_params)
    if @shop.save
    else
      set_select_lists
      render :new
    end
  end

createアクションでrenderする前に、before_actionで行っていたset_select_listsを行わせてあげることで、その値も取得した状態でrenderすることが出来ます(renderはあくまでcreateアクションを行った上でnewのビューを表示しているため)

一瞬???となったんですがそんな複雑な話でもなかったですね。

終わりに

実はredirect_toとrenderの挙動は、Progateで初めてプログラミングに触れ1ヶ月程度で一番悩みぬいた所だったんですよね。ここの違いに気づいた時にMVCへの理解も同時に深まったというイメージがありました。

僕と同じ様な初学者の方に何かの参考になればと思います。

*初学者ゆえ、何かあればご指摘いただけると大変ありがたいです。

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

Kinx Tips - UTF8 文字列のフォーマッティング

Kinx Tips - UTF8 文字列のフォーマッティング

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。Kinx で実装したちょっとした小技を紹介。

そう、ちょっとした小技。

UTF8 文字列のフォーマッティングの問題

C 言語の printf

UTF8 はバイト数と文字幅が一致しないので、"%-20s" とかしても微妙に揃いませんよね。半角カナも試しに使ってみましょう。

#include <stdio.h>

int main()
{
    struct fruits {
        char *name;
        int price;
    } fruits[] = {
        { .name = "りんご", .price = 230 },
        { .name = "みかん(1袋)", .price = 450 },
        { .name = "グレープフルーツ", .price = 120 },
    };
    printf("01234567890123456789\n");
    for (int i = 0; i < sizeof(fruits)/sizeof(fruits[0]); ++i) {
        printf("%-20s ... %3d 円\n", fruits[i].name, fruits[i].price);
    }
    return 0;
}

結果はこうなります。

01234567890123456789
りんご            ... 230 円
みかん(1袋)      ... 450 円
グレープフルーツ ... 120 円

基本的にバイト数なので、日本語 3 バイトであれば 3 消費します。したがって幅が縮みます。

Ruby の sprintf

C と同じかと思いきや、違います。

fruits = [
    ["りんご", 230],
    ["みかん(1袋)", 450],
    ["グレープフルーツ", 120],
]

puts "01234567890123456789"
fruits.each {|name, price|
    puts sprintf("%-20s ... %3d 円", name, price)
}

結果はこうなりました。

01234567890123456789
りんご                  ... 230 円
みかん(1袋)              ... 450 円
グレープフルーツ           ... 120 円

バイト数ではなく文字数です。日本語も(何バイトであろうと) 1 文字で 1 消費します。なので、表示幅が増えます。文字数なので、半角カナだと実は一致します。

Kinx のフォーマッタ

Kinx では UTF8 の文字幅を確認してフォーマット文字列の数値部分を自動調整します。表示幅として機能します。

var fruits = [
    ["りんご", 230],
    ["みかん(1袋)", 450],
    ["グレープフルーツ", 120],
];

System.println("01234567890123456789");
fruits.each(&(e) => {
    System.println("%-20s ... %3d 円" % e[0] % e[1]);
});

結果はこうなります。

01234567890123456789
りんご               ... 230 円
みかん(1袋)          ... 450 円
グレープフルーツ           ... 120 円

Qiita のコードブロックでは等幅で表示されていない感じなので微妙ですが、等幅フォントで見れば縦ラインはきちんと揃っています。東アジア文字幅(East Asian Width)に従って調整するので概ねうまく動くはず 1

多くの人が期待する動作は コレ ではないでしょうか。

おわりに

なかなか忙しくて記事が書けないとき用に、こういった小ネタもストックしておこう。

ではまた、次回。


  1. ここ」に従うと、東アジアの従来文字コードの文脈では Ambiguous Unicode characters は fullwidth 推奨ということでそちらに倒してます。でないと乱れる。 

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

'%02d' % 2 のパーセントとは?

'%02d' % 2

ruby で 数字の 2 を '02' と記述したい時、'%02d' % 2 のような構文を書きますよね。
引数に対して 0埋めをおこなってstringで返すという方法ですが、こちらの % とか 02 とか d って一体なんなんでしょうか?

% って何?

ruby では % という記法に様々な意味があります。

  1. 剰余を表す「%」 例: 5 % 3 => 2
  2. Stringクラスの「%」演算子 例: "%02d" % 2 => "02"
  3. リテラル記法の「%」演算子 例: %w(dog cat monkey) => ["dog", "cat", "monkey"]
  4. コマンドラインへの入力を示す 例: %x[ DATE ] => "2020年 6月24日 水曜日 09時31分57秒 JST\n"

上記のような種類があります。
参考: Rubyで使われる記号の意味(正規表現の複雑な記号は除く)

先に示した、'%02d' % 2 という構文は、上記の例でいうと 2. Stringクラスの「%」演算子 にあたりますね!

02 とか d って一体なんなんでしょうか?

d は引数の型を示す「指示子」の中の一つです。
d は 10進数の整数 (Decimal number) を表しています。
指示子は他にも、文字列を表すものや浮動小数点を表すもの、指数表現を表すものなどがあります。

02 は 0 と 2 に分解することができ、それぞれ「フラグ」「幅」を示しています。
「フラグ」にもいくつか種類があるのですが、 0 は余白を 0で埋めるという意味になります。
「幅」はそのまま、出力時の長さを指定します。

これらをまとめると、'%02d' % 2 という記法は、

'%0(余白は0で埋めてくださいね〜)2(出力時の長さは2ですよ)d(引数を10進数の整数で表してくださいね' %(Stringクラスの%演算子ですよ〜) 2(引数は2ですよ!)

という意味が込められている事になります。

おわりに

引数は配列で渡す事もできますので、
「10進数で表している色コードを16進数の色コードに変換したいよ」という時には、
'%02x%02x%02x' % [255, 255, 255] => "ffffff" と書く事ができます!

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

railsでgoogle認証を実装するときにハマったこと

今回は、自分がrailsでgoogle認証を実装するときにハマったことをまとめたので参考にでも。
自分がGoogle認証を実装するときに参考にした記事 ↓
[Rails] Facebook/Twitter/Googleでのユーザー登録をDevise & Omniauthを使って爆速で実装する

エラー 400: invalid_request Missing required parameter: client_id

まず1つ目。グーグル認証のページでアカウントを選択肢して認証しようとしたときに出たエラーです。
client_idがないと言われました。
原因は2つあり
①スペースを入れていた
②.envを使わずそのままキーを入れていたこと

①スペース

devise.rb
config.omniauth :google_oauth2, ENV['GOOGLE_CLIENT_ID'] ,ENV['GOOGLE_CLIENT_SECRET'], skip_jwt: true

から

devise.rb
config.omniauth :google_oauth2,ENV['GOOGLE_CLIENT_ID'],ENV['GOOGLE_CLIENT_SECRET'],skip_jwt: true

に変更しました。スペースがいらなかったみたいです。参考サイトではスペースはありました。

②.envを使わずそのままキーを入れていたこと
これは他のサイトを見る限りそのままでも行けた人がいるみたいですが僕の環境ではだめでした。
どうしても解決しない人はgem 'dotenv-rails'をいれてやってみてください。

メール認証がされていません

その次に出たのがこのエラー
普通にやるとメール認証がされていませんと表示されてしまいます。これは

qiita.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController

  # callback for google
  def google_oauth2
    callback_for(:google)
  end

  # common callback method
  def callback_for(provider)
    @user = User.from_omniauth(request.env["omniauth.auth"])
    @user.skip_confirmation!   ←これと
    @user.save!                ←これ!
    if @user.persisted?
      sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
      set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
    else
      session["devise.#{provider}_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_registration_url
    end
  end

  def failure
    redirect_to root_path
  end

end

@user.skip_confirmation!
@user.save!
をコントローラーに入れることで解決しました。

ぷち宣伝

公開日記というアプリを作りました。日記を公開できるアプリです(公開しないことも可)使っている人が僕しかいません(泣)。ほぼ毎日更新しているのでみてください。お願いします。

URL: https://public-diary.herokuapp.com/

まとめ

実は僕はいままで4回くらいsns認証を実装しようとし失敗しを繰り返しています。今回始めて出来たのですができたときはかなり嬉しかったです。ここまで見てくださりありがとうございました。

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

1つの投稿に対して複数枚の画像を紐付け、同時に投稿する実装方法

はじめに

複数枚の画像を投稿する機能の実装に、苦労したてめメモとして記事を投稿します

アプリを立ち上げる

$ rails _5.2.3_ new  アプリ名 -d mysql
$ cd アプリ名
$ bin/rails db:create

hamlの導入

今回実装はhamlで行うため、hamlを導入します

gemファイルに以下を追加します
忘れずにbundle installをしてください

gem 'haml-rails'

簡単に投稿機能を作成します

routes

Rails.application.routes.draw do
  root 'products#index'
  resources :products, only: [:index, :new, :create]
end

controller

class ProductsController < ApplicationController
  def index
    @products = Product.includes(:images).order('created_at DESC')
  end

  def new
    @product = Product.new
    @product.images.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to root_path
    else
      render :new
    end
  end


  private

  def product_params
    params.require(:product).permit(:name, images_attributes: [:src])
  end

end

modelとマイグレーション

productマイグレーションファイル

class CreateProducts < ActiveRecord::Migration[5.2]
  def change
    create_table :products do |t|
      t.string :name

      t.timestamps
    end
  end
end

imageマイグレーションファイル

 class CreateImages < ActiveRecord::Migration[5.2]
  def change
    create_table :images do |t|
      t.string :src
      t.references :product, foreign_key: true

      t.timestamps
    end
  end
end

productsモデル

class Product < ApplicationRecord
  has_many :images
  accepts_nested_attributes_for :images, allow_destroy: true
end

imageモデル

class Image < ApplicationRecord
  mount_uploader :src, ImageUploader
  belongs_to :product
end

imageモデルで画像をアップロードできるようにしていきます。

Gemfileに以下を追記して$ bundle install

gem 'carrierwave'
gem 'mini_magick'

uploaderを作成します

ターミナルで以下を実行します

$ rails g uploader image

続いて、image_uploader.rbファイルが生成されたので以下を編集します

include CarrierWave::MiniMagick  // この記述を探し、コメントアウトを外す

process resize_to_fit: [100, 100]  // この記述は追記

最後にhamlとscssの編集

haml/new

.lead
  =link_to "/products" do
    =image_tag "http://furima.tokyo/assets/logo-d3d78326971d78b06e3d6f0ba666d025a8ad681286b4d9e00e7dbe8673bcfd23.svg", class: "lead__img"

= form_with model: @product, local: true do |f|
  .input-field
    .input-field__contents
      .input-field__contents-image
        .input-field__contents-image__headline
          .headlabel
            出品画像
            %span.necessary
              必須
        %p.upload
          最大5枚までアップロードできます

        #image-box-1 
          .item-num-0#image-box__container
            = f.fields_for :images do |i|
              .input-field__contents-image__drop__js-file
                .input-area
                  = i.file_field :src

      .input-field__contents-name
        .input-field__contents-image__headline
          .headlabel
            %label
              商品名
              %span.necessary
                必須
          .name-input
            = f.text_field :name, {class: "drop-input", placeholder: "40文字まで"}

    .input-field
      .input-field__contents
        .input-field__contents-price
          .sell
            = f.submit "出品する", class: "sellbtn", tabindex: "0"

new.scss

.lead {
  background-color: rgb(245, 245, 245);
  text-align: center;
  height: 128px;
  line-height: 10;
}
.input-field {
  background-color: rgb(245, 245, 245);
  width: 100%;
  .input-field__contents {
    left: 0;
    background-color: white;
    max-width: 800px;
    margin: 0 auto;
    padding: 40px;
    border-bottom: 1px solid hsl(0, 0%, 77%);
    height: 100%;
    .input-field__contents-image {
      width: 800px;
      border-bottom: rgb(204, 204, 204);
      .input-field__contents-image__headline{
        margin-top: 20px;
        margin-left: 5px;
      }
      .upload {
        margin-top: 16px;
        margin-left: 5px;
      }
      #image-box-1 {
        display: flex;
        height: 130px;
        width: 100%;
        margin-right: 0px;
        text-align: center;
        i{
          padding-top: 50px;     
        }
        .item-num-0#image-box__container  {
        background-color: rgb(245, 245, 245);
        height: 100%;
        width: 100%;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;
        text-align: center;
        }
      }
    }
  }
  .drop-input {
    width: 60%;
    height: 50px;
    border-color: #cccccc;
    border-radius: 4px;
    border-style: solid;
    border-width: 1px;
    margin: 10px 10px 0 0;
  }  
  .name-input{
    .drop-input{
      width: 100%;
      height: 50px;
      border-color: #cccccc;
      border-style: solid;
    }
  } 
  .sell {
    text-align: center;
    display: grid;
    width: 50%;
    margin-left: 200px;
    .sellbtn {
      background-color: #3ccace;
      color: white;
      border-color: transparent;
      font-weight: 600;
      line-height: 3;
      cursor: pointer;
    }
  }
}

以下の様になれば、完成です。
https://gyazo.com/a1d705516656f50c689abc7c18de5ec9

画像を複数枚投稿する

jQueryの導入

gem 'jquery-rails'

続いて、bundle installをしてください

application.jsの編集

//= require rails-ujs
//= require activestorage
//= require jquery
//= require_tree .

new.hamlの編集

-# 編集前
#image-box-1 
 .item-num-0#image-box__container
  = f.fields_for :images do |i|
   .input-field__contents-image__drop__js-file
    .input-area
     = i.file_field :src
-# 編集後

#image-box-1 
 .item-num-0#image-box__container
  = f.fields_for :images do |i|
   .input-field__contents-image__drop__js-file
    .input-area
    = i.file_field :src, type: 'file', name: "product[images_attributes][][name]", value:"", style: "display:none", id:"img-file"
    %label{for: "img-file"}
     %i.fas.fa-camera

画像を複数枚投稿する様にする new.js作成し、編集する

new.js

$(function(){
  //DataTransferオブジェクトで、データを格納する箱を作る
  var dataBox = new DataTransfer(); //ステップ②
  //querySelectorでfile_fieldを取得
  var file_field = document.querySelector('input[type=file]')
  //fileが選択された時に発火するイベント
  $('#img-file').change(function(){
    //選択したfileのオブジェクトをpropで取得
    var files = $('input[type=file]').prop('files')[0];
    $.each(this.files, function(i,file){
    //FileReaderのreadAsDataURLで指定したFileオブジェクトを読み込む
    var fileReader = new FileReader();

    //DataTransferオブジェクトに対して、fileを追加
    dataBox.items.add(file) //ステップ②
    //dataTransferオブジェクトに入ったfile一覧をfile_fieldの中に代入
    file_field.files =  dataBox.files //ステップ②

    var num = $('.item-image').length + 1 + i //ステップ②
    fileReader.readAsDataURL(file); //ステップ②
     //画像が10枚になったら超えたらドロップボックスを削除する
     if (num == 5){ //ステップ②
      $('#image-box__container').css('display', 'none')
     }
    //読み込みが完了すると、srcにfileのURLを格納
    fileReader.onloadend = function() {
      var src = fileReader.result
      var html = `<div class='item-image' data-image="${file.name}">
                    <div class=' item-image__content'>
                      <div class='item-image__content--icon'>
                        <img src=${src} width="150" height="90" >
                      </div>
                    </div>
                    <div class='item-image__operetion'>
                      <div class='item-image__operetion--delete'>削除</div>
                    </div>
                  </div>`
     //image_box__container要素の前にhtmlを差し込む
      $('#image-box__container').before(html);
      };
    //   fileReader.readAsDataURL(file);
    //  });
     //image-box__containerのクラスを変更し、CSSでドロップボックスの大きさを変えてやる。
     $('#image-box__container').attr('class', `item-num-${num}`)
    });
  });
    $(document).on("click", '.item-image__operetion--delete', function(){
      //プレビュー要素を取得
      var target_image = $(this).parent().parent()
      //プレビューを削除
      target_image.remove();
      //inputタグに入ったファイルを削除
      file_field.val("")
    })
});

scssの編集

.lead {
  background-color: rgb(245, 245, 245);
  text-align: center;
  height: 128px;
  line-height: 10;
}

.input-field {
  background-color: rgb(245, 245, 245);
  width: 100%;
  &__contents {
    left: 0;
    background-color: white;
    max-width: 800px;
    margin: 0 auto;
    padding: 40px;
    border-bottom: 1px solid hsl(0, 0%, 77%);
    height: 100%;
  }

  .input-field__contents-image {
    width: 800px;
    border-bottom: rgb(204, 204, 204);
    .input-field__contents-image__headline{
      font-weight: 600;
      margin-top: 20px;
      margin-left: 5px;
      .name-input {
        height: 54px;
        .option-input {
          display: block;
          width: 93%;
          border-color: #cccccc;
          height: 100%;
          border-radius: 4px;
          font-weight: bolder;
          padding: 0px 2px 1px;
          border-width: 1px;
        }
      }
    }
    .upload {
      margin-top: 16px;
      margin-left: 5px;
    }
    #image-box-1 {
      display: flex;
      height: 130px;
      width: 100%;
      margin-right: 0px;
      text-align: center;
      i{
        padding-top: 50px;
        cursor: pointer;
      }
      .item-num-0#image-box__container  {
      background-color: rgb(245, 245, 245);
      height: 100%;
      width: 100%;
      border-width: 1px;
      border-style: dashed;
      border-color: rgb(204, 204, 204);
      border-image: initial;
      text-align: center;
      }
      .item-num-1{
        background-color: rgb(245, 245, 245);
      height: 100%;
      width: 100%;
      border-width: 1px;
      border-style: dashed;
      border-color: rgb(204, 204, 204);
      border-image: initial;
      text-align: center;
      }
      .item-num-2{
        background-color: rgb(245, 245, 245);
        height: 100%;
        width: 100%;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;
        text-align: center;
      }
      .item-num-3{
        background-color: rgb(245, 245, 245);
        height: 100%;
        width: 100%;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;
        text-align: center;
      }
      .item-num-4{
        background-color: rgb(245, 245, 245);
      height: 100%;
      width: 100%;
      border-width: 1px;
      border-style: dashed;
      border-color: rgb(204, 204, 204);
      border-image: initial;
      text-align: center;

      }
      .item-num-5{
        background-color: rgb(245, 245, 245);
        height: 100%;
        width: 100%;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;
        text-align: center;
      }  
    }
    //レビュー表示のCSS
    .item-image{
      height: 130px;
      width: 160px;
      border: 1px solid #eee;
      margin-right: 10px;
      .item-image__content{
        padding-top: 10px;
        .item-image__content--icon{
        }
      }
      .item-image__operetion{
        .item-image__operetion--delete{
          color: #00b0ff;
          cursor: pointer;
          padding-top: 5px;
         text-align: center;

        }
      }
    }
  }

  .text-area {
    border-radius: 4px;
    font-size: 16px;
    padding: 13px 16px;
    border-color: #cccccc;
    margin-top: 30px;

  }
  .drop-input {
    width: 100%;
    height: 50px;
    border-color: #cccccc;
    border-radius: 4px;
    border-style: solid;
    border-width: 1px;
    margin: 10px 10px 0 0;
    ::placeholder {
      padding: 20px;
      font-weight: inherit;
    } 
  }

  .headlabel {
    margin-top: 30px;
    .necessary {
      background-color: #3ccace;
      color: white;
      padding: 2px 4px;
      font-size: 14px;
      margin-left: 3px;
      cursor: pointer;
      border-radius: 2px;
     }
  }
  .sell {
    text-align: center;
    display: grid;
    width: 50%;
    margin-left: 200px;
    .sellbtn {
      background-color: #3ccace;
      color: white;
      font-size: 20px;
      min-height: 48px;
      padding: 0 24px;
      border-color: transparent;
      border-radius: 2px;
      font-weight: 600;
      line-height: 3;
    }
  }
}

これで一通りは完成
動作を確認
https://gyazo.com/372657130a2696e1865c02fdd6e9e303

編集や削除の機能は別記事で続きを書きます

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

rubyでredmineのAPIkeyを取得してみる

経緯

 仕事でrubyを使ってredmineのAPIkeyを取得できるようにしろと仰せつかった。
 ひとまずの完成を見たので、自分の備忘録代わりにまとめておく。
 
 記事としては独学で勉強しただけでプログラマに就職した社会人一年目初心者プログラマが検索とコピペを繰り返して作ったものでしかない。もし自分と同じような人間がいたら今度はこの記事だけで一発成功することを祈って記述する。

方法

 まずは検索して見たが、該当する記述は見つからなかった。APIkeyはredmineの個人設定画面から受け取ってコードに入力するのが前提で、APIkeyをわざわざプログラムで取得するなどという方法は全然見つからなかった。が、rubyという条件を除外して検索した結果、以下の記事を発見した。

RedmineのJavaScriptから各種データを取得する方法
https://qiita.com/forenoonM/items/7f42701b2ea40353a820

 この記事内にはJavaScriptを使ってAPIkeyを取得する方法が記述されている。ざっくりいえばスクレイピング を使った方法だ。

スクレイピング 実践

 なるほど、スクレイピング か。さて、恥ずかしながら私はスクレイピング がなんたるかすら知らなかった。なにせrubyを独学で勉強していただけの初心者なので。
 というわけで、rubyでスクレイピング する方法を検索し、以下のサイトにたどり着いた。

【5分で理解】Rubyでスクレイピングの基礎を解説!
https://www.sejuku.net/blog/13230

 こうして作ったコードがこれ

equire 'open-uri'
require 'nokogiri' #APIキー取得に使う
url = "http://192.168.33.11/my/api_key"

charset = nil

html = open(url) do |page|
 charaset = page.charset
 page.read
end

contents = Nokogiri::HTML.parse(html,nil,charset)
apikey = contents.search("div[@class='box']").search('pre')
puts apikey

 さて、実際に動かすと動かない。
 色々調べた結果、ログイン画面で止まっていることに気づいた。

 という事はログインの処理を作らねばならない

ログイン処理作成

 で、例によって検索でたどり着いたのがこのページ。

Mechanizeでログインしてスクレイピングする
https://qiita.com/katsuyuki/items/1a78360988d96eec1d54

 なるほど完全に理解した。

agent = Mechanize.new
agent.user_agent = 'Mac Safari'
agent.get("http://#{adress}/my/api_key") do |page|
  response = page.form_with(:action => '/login') do |form|
    formdata = {
       :mail => mail,
       :password => password,
    }
    form.field_with(:name => 'username').value = formdata[:mail]
    form.field_with(:name => 'password').value = formdata[:password]
  end.submit
 end
 url = "http://#{adress}/my/api_key"
 html = agent.get("#{url}").content.toutf8
 contents = Nokogiri::HTML(html, nil, 'utf-8')
 apikey = contents.search("div[@class='box']").search('pre')
 puts apikey

 追加部分だけを抜き出した。

 ちなみに#{adress}については「今はローカルでテストしているが、この後会社のredmineとクライアントのredmineでも動かすんだから」と

require 'yaml' #コンフィグを読むのに使う

set = YAML.load_file("config.yml")
puts set
adress = set["train"]["adress"]
puts adress

 こんな感じでconfig.ymlにアドレスを入力することで指定できるようにしたものだ。

結果

> <pre>[APIkey]</pre>

 これではダメだ。

stringkey = "#{apikey}"
stringkey.slice!("</pre>")
stringkey.slice!("<pre>")
puts "APIKey = #{stringkey}"

 これを直後に加えて、ひとまずAPIkeyを取得する処理は完成。

もしログイン済みだったら?

 実はこっちを先に取り掛かっていたのだが、もしログイン画面ではなかったらどうだろう? そんなことがあるのかわからないが、もしあったら逆にログインする処理でエラーを起こしてしまう。

page = RedmineController.new
pagetitle = page.Myaccount_access(adress)
puts "pagetitle = #{pagetitle}"

if pagetitle == "個人設定 - redmine" then # ログイン済みかどうかチェックする
     apikey = page.GetAPIkey(adress)
     puts "APIKey = #{apikey}"
else
     apikey = page.Login(adress,mail,password) 
end

 ログイン後の個人設定画面はタイトルが「個人設定 - redmine」になっていたので、これに対応した。

 ここでRedmineContorollerなるクラスが作られているが、このクラスについては後述する。

 この後、会社のredmineで試して気づいた。うちの会社のredmineは「個人設定 - [会社名]redmine 」になっている。
 調べたところ、トップページなども全て「[会社名]redmine」だ。つまり、「個人設定 - [ページ名]」というスタイルのようだ。対応しよう。

   page = RedmineController.new
   toptitle = page.toptitle(adress)
   pagetitle = page.Myaccount_access(adress)
   puts "pagetitle = #{pagetitle}"

   if pagetitle == "個人設定 - #{toptitle}" then # ログイン済みかどうかチェックする
     apikey = page.GetAPIkey(adress)
     puts "APIKey = #{apikey}"
   else
     apikey = page.Login(adress,mail,password) 
   end

 こうだ。つまり、トップのタイトルを先に把握しておく作戦だ。
 さて、ではクラス「RedmineController」を公開する。そう、toptitleメソッドのことを紹介しておかないと二度手間なので紹介した次第である。

class RedmineController

 def toptitle(adress)
    url = "http://#{adress}/"

    charset = nil

    html = open(url) do |page|
     charaset = page.charset
     page.read
    end

    contents = Nokogiri::HTML.parse(html,nil,charset)
 return contents.title
 end

 def Myaccount_access(adress)
    url = "http://#{adress}/my/account"

    charset = nil

    html = open(url) do |page|
     charaset = page.charset
     page.read
    end

    contents = Nokogiri::HTML.parse(html,nil,charset)
 return contents.title
 end

 def Login(adress,mail,password)
    agent = Mechanize.new
    agent.user_agent = 'Mac Safari'
    agent.get("http://#{adress}/my/api_key") do |page|
      response = page.form_with(:action => '/login') do |form|
        formdata = {
           :mail => mail,
           :password => password,
        }
        form.field_with(:name => 'username').value = formdata[:mail]
        form.field_with(:name => 'password').value = formdata[:password]
      end.submit
     end
     url = "http://#{adress}/my/api_key"
     html = agent.get("#{url}").content.toutf8
         contents = Nokogiri::HTML(html, nil, 'utf-8')
         apikey = contents.search("div[@class='box']").search('pre')
         stringkey = "#{apikey}"
     stringkey.slice!("</pre>")
     stringkey.slice!("<pre>")
     puts "APIKey = #{stringkey}"
     return stringkey
 end

 def GetAPIkey(adress)
        url = "http://#{adress}/my/api_key"

        charset = nil

        html = open(url) do |page|
         charaset = page.charset
         page.read
        end

        contents = Nokogiri::HTML.parse(html,nil,charset)
    apikey = contents.search("div[@class='box']").search('pre')
        stringkey = "#{apikey}"
    stringkey.slice!("</pre>")
    stringkey.slice!("<pre>")
    puts "APIKey = #{stringkey}"
    return stringkey
 end

end

そして完成へ

 で、この後APIを使ってなんやかんやする処理を作る必要があるので、APIkeyを得るための処理はメソッドに押し込もう。

class Getter
 def APIKey_Getter(adress,mail,password)
   page = RedmineController.new
   toptitle = page.toptitle(adress)
   pagetitle = page.Myaccount_access(adress)
   puts "pagetitle = #{pagetitle}"

   if pagetitle == "個人設定 - #{toptitle}" then # ログイン済みかどうかチェックする
     apikey = page.GetAPIkey(adress)
     puts "APIKey = #{apikey}"
   else
     apikey = page.Login(adress,mail,password) 
   end
   return apikey
 end

 ちなみにこの後、今はこのゲッタークラスに各種APIを呼び出すメソッドを追加している。

全容

 以上で持って完成。全容はこんな感じである。

require 'open-uri'
require 'nokogiri' #APIキー取得に使う
require 'mechanize' #ログインに使う
require 'yaml' #コンフィグを読むのに使う
require 'io/console' #パソワードの伏字に使う

class RedmineController

 def toptitle(adress)
    url = "http://#{adress}/"

    charset = nil

    html = open(url) do |page|
     charaset = page.charset
     page.read
    end

    contents = Nokogiri::HTML.parse(html,nil,charset)
 return contents.title
 end

 def Myaccount_access(adress)
    url = "http://#{adress}/my/account"

    charset = nil

    html = open(url) do |page|
     charaset = page.charset
     page.read
    end

    contents = Nokogiri::HTML.parse(html,nil,charset)
 return contents.title
 end

 def Login(adress,mail,password)
    agent = Mechanize.new
    agent.user_agent = 'Mac Safari'
    agent.get("http://#{adress}/my/api_key") do |page|
      response = page.form_with(:action => '/login') do |form|
        formdata = {
           :mail => mail,
           :password => password,
        }
        form.field_with(:name => 'username').value = formdata[:mail]
        form.field_with(:name => 'password').value = formdata[:password]
      end.submit
     end
     url = "http://#{adress}/my/api_key"
     html = agent.get("#{url}").content.toutf8
         contents = Nokogiri::HTML(html, nil, 'utf-8')
         apikey = contents.search("div[@class='box']").search('pre')
         stringkey = "#{apikey}"
     stringkey.slice!("</pre>")
     stringkey.slice!("<pre>")
     puts "APIKey = #{stringkey}"
     return stringkey
 end

 def GetAPIkey(adress)
        url = "http://#{adress}/my/api_key"

        charset = nil

        html = open(url) do |page|
         charaset = page.charset
         page.read
        end

        contents = Nokogiri::HTML.parse(html,nil,charset)
    apikey = contents.search("div[@class='box']").search('pre')
        stringkey = "#{apikey}"
    stringkey.slice!("</pre>")
    stringkey.slice!("<pre>")
    puts "APIKey = #{stringkey}"
    return stringkey
 end
end

class Getter
 def APIKey_Getter(adress,mail,password)
   page = RedmineController.new
   toptitle = page.toptitle(adress)
   pagetitle = page.Myaccount_access(adress)
   puts "pagetitle = #{pagetitle}"

   if pagetitle == "個人設定 - #{toptitle}" then # ログイン済みかどうかチェックする
     apikey = page.GetAPIkey(adress)
     puts "APIKey = #{apikey}"
   else
     apikey = page.Login(adress,mail,password) 
   end
   return apikey
 end
end


#ここから処理開始。実際にはsinatraでgetかpostで情報を受け取る
puts "アカウント名を入力してください"
mail = gets
mail.chomp!
puts "パスワードを入力してください(表示されませんが入力できています)"
password = STDIN.noecho(&:gets)
password.chomp!
#実際にはgetsを使って取得する訳ではないが、一応設定。最終的にはこの範囲は全て消える

set = YAML.load_file("config.yml")
puts set
adress = set["train"]["adress"]
puts adress
f = Getter.new
apikey = f.APIKey_Getter(adress,mail,password) 
puts "APIKey = #{apikey}"

以上、お目汚し失礼しました。

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

Rubyで無限に繰り返し処理をする方法

eachやtimesを使用すると繰り返し処理を始める前に、何回目で終わるか決めなければならない。loopを使えば、無限ループができる

loop do
  繰り返し処理
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】if文の問題

背景

Rubyの問題を解いた際、自分の回答に対して、模範回答のスマートさを見て悔しかった:confounded:ので備忘録として残します。

問題

正の整数を入力します。その整数が、
10の倍数(10,20,30...)からの差が
2以内であるときはTrue
それ以外はFalseと出力するメソッドを作りましょう。

出力例:
near_ten(12)→True
near_ten(17)→False
near_ten(19)→True

私の回答

Ruby
def near_ten(num)
  if num % 10 <= 2
    puts "True"
  elsif num % 10 >= 8
    puts "True"
  else
    puts "False"
  end
end

模範回答

Ruby
def near_ten(num)
  quotient = num % 10
  if quotient  <= 2 || quotient >= 8
    puts "True"
  else
    puts "False"
  end
end

感想

模範回答見たときに、スマートすぎやろぉ〜と心の中で叫びました!(出先のもので声出せません:unamused:

確かに、またはに該当する" || "の存在を忘れがち。。。。

コードは、正直動けば正解:sparkles:ではありますが、いかにスマート:star2:に書くかでカッコ良さがにじみ出るのだと改めて感じました。。。

リファクタリング頑張りやす。。。

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

VScodeを初めて使うRailsエンジニアへ

はじめに

エディタをVScodeに変更したRailsエンジニアのために、作業が捗るVScodeの設定をまとめてみました。
VScodeには非常に便利な拡張機能がありますが、意図しない設定まで入るブラックボックスなので、設定ファイルをカスタマイズしました。

実行環境

この記事は以下の動作環境で動作確認しています。

ruby (2.7.1)
rails (6.0.3)
Vscode
MacBook Pro

1. どうやってカスタマイズするの?

VScodeをインストール時に、メジャーな言語の設定ファイルを既に入っており、言語ごとの設定は専用のファイルで設定します。(Rubyの場合はruby.json)
言語に依存しない設定はsettings.jsonで設定します。

1-1. 設定ファイル(setting.json)の開き方

VScode上でCommand + ,と打つと設定画面が出てきます。
右上にいくつかアイコンがあり、設定を開く(JSON)とあるのでクリックするとsettings.jsonが開きます。

1-2. 設定ファイル(ruby.json)の開き方

VScode上でCommand + Shift + Pと打つとコマンドパレットが出てきます。
User snippetsで検索して出てきたConfigure User Snippetsを選択して、
次に出てきたウィンドウでrubyと検索して出てきたruby.jsonを選択すると設定が開きます。

2. 設定内容

2.1 拡張機能のRubyをインストール

カスタマイズと言いましたが、Rubyの拡張機能だけは入れてください?
VScodeの左側にある拡張機能のアイコンをクリックし、Rubyで検索して一番上にある拡張機能をインストールを押すだけでOKです。

2.2 pryと打つとbinding.pryと変換する

Railsで開発していると誰もが使用するbinding.pry(デバッグ)。ただ毎回これを全て打つのは面倒&タイポするので予測変換の設定をします。
設定方法は簡単で、ruby.jsonに下記を記載するだけです。

"prefix"の値が変換前の文字、"body"が予測変換したい文字です。
もし"prefix"の値を"pry"から"bin"に変更すると"bin"と打つとbinding.pry に変換されます。

settings.json
"Debug Console": {
  "prefix": "pry",
  "body": "binding.pry\n",
  "description": "A runtime developer console"
}

画像に alt 属性が指定されていません。ファイル名: binding_pry.gif

2.3 HTMLのタグを自動補完する

RailsでViewのコードを書くにはhtml.erbファイルを使用します。
などのタグを全て打ちたくないので、div + tabキー でタグを補完してくれるようにします。下記の設定を
settings.jsonに追加します。

settings.json
{
 "emmet.includeLanguages": {
    "erb": "html",
 },
}

2.4 関数の定義元へジャンプ

Commandを押しながら関数をクリック、定義元にジャンプできる非常に便利な設定です。
Command + Shift + P でコマンドパレットを開き、keyboardで出てくるkeybinding.jsonに下記を記載します。

keybinding.json
[
  {
    "key": "cmd+d",
    "command": "editor.action.goToDeclaration",
    "when": "editorTextFocus"
  }
]

2.5 ターミナルからVScodeを開く

Command + Shift + P でコマンドパレットを開き、shellで出てくる shell command:install 'code' command in PATH をクリック。
ターミナルでcode . と打つとカレントディレクトリのファイルを開くことができます。

2.6 末尾の改行と空白を自動で削除

末尾に改行がないものには改行を追加。末尾の不要な改行・空白は自動で削除する設定です。
Command ,で設定を開き、trimで検索して全ての項目にチェックをする。

画像に alt 属性が指定されていません。ファイル名: 末尾改行・空白-1024x430.png
おわりに
他にもいろいろな設定があるので、好みに合わせて、拡張機能を追加したり、settings.jsonに設定を追加したりしてみてください。

参考までに私のsettings.jsonを添付しておきます。
右端で折り返す、スクロールを止める、オートセーブ当たりも入れておいた方が快適になると思います。

setting.json
{
  "files.autoSave": "onFocusChange", //ファイルのオートセーブ
  "editor.formatOnSave": true,
  "editor.tabSize": 2, //1つのタブに相当するスペースの数
  "editor.detectIndentation": false,
  "emmet.variables": {},
  "emmet.triggerExpansionOnTab": true,
  "ruby.rubocop.onSave": false,
  "html.format.endWithNewline": true,
  "html.format.indentInnerHtml": true,
  "emmet.includeLanguages": {
    "erb": "html",
  },
  "window.zoomLevel": 0,
  "ruby.intellisense": "rubyLocate",
  "diffEditor.renderSideBySide": false, // Git の差分を行内に表示
  "editor.wordWrap": "on", // 右端で折り返す
  "editor.minimap.renderCharacters": false, // ミニマップを簡略化
  "editor.links": true, // リンクをクリック可能に
  "indentRainbow.colors": [
    "rgba(255,255,64,0.07)",
    "rgba(127,255,127,0.07)",
    "rgba(255,127,255,0.07)",
    "rgba(79,236,236,0.07)"
  ],
  "editor.scrollBeyondLastLine": false, //下限でスクロールをストップさせる
  "editor.formatOnPaste": true, // ペースト時に自動でフォーマット
  "breadcrumbs.enabled": true,
  "files.trimTrailingWhitespace": true,
  "files.insertFinalNewline": true,
  "files.trimFinalNewlines": true, // ファイルの保存時に末尾を改行
  // Reactの設定
  "emmet.includeLanguages": {
    "javascript": "javascriptreact",
    "vue-html": "html"
    // },
    "emmet.syntaxProfiles": {
      "html": {
        "attr_quotes": "single"
      },
      "jsx": {
        "self_closing_tag": true
      }
    }
  }
}



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

懐かしのコードゲーム、Ruby Warrior はいいぞ

image.png

サイトはここ

Ruby Warrior

  • 勇者を ruby コードで操って、レベルをクリアしていくゲーム
  • オリジナルRogue みたいな見た目のテキストゲームで ryanb さん作。10年くらい前に公開
  • オリジナルを BlocWeb ゲームにしたようだ。 Good Job!

ルール

  • コードの中の def play_turn(warrior) ... end の部分が1ターンで、ここにコードを書く
  • ターンを繰り返して、敵を倒しながら、脱出すればよい
  • 1ターンで1回しか warrior.action! できない(ターン内でループの処理もできない)1
  • レベルが進むごとにスキル warrior.action! が増えていく
  • 初心者モードと中級者モードがある(今回は初心者モードでやってみた)

Level 1

You see before yourself a long hallway with stairs at the end. There is nothing in the way.

class Player
  def play_turn(warrior)
    # cool code goes here
    warrior.walk!
  end
end

このレベルの見所: 楽しく歩く :walking:

Level 2

It is too dark to see anything, but you smell sludge nearby.

class Player
  def play_turn(warrior)
    if warrior.feel.empty?
      warrior.walk!
    else
      warrior.attack!
    end
  end
end

このレベルの見所: 初めての敵 :dragon_face:

  • warrior.attack! を獲得

Level 3

The air feels thicker than before. There must be a horde of sludge.

class Player
  def play_turn(warrior)
    if warrior.feel.empty?
      if warrior.health < 15
        warrior.rest!
      else
        warrior.walk!
      end
    else
      warrior.attack!
    end
  end
end

このレベルの見所: 休む時は休む :sleeping_accommodation:

  • warrior.rest を獲得

Level 4

You can hear bow strings being stretched.

class Player
  def play_turn(warrior)
    @health = warrior.health unless @health
    if warrior.feel.empty?
      if @health <= warrior.health && warrior.health < 12
        warrior.rest!
      else
        warrior.walk!
      end
    else
      warrior.attack!
    end
    @health = warrior.health
  end
end

このレベルの見所: 体調管理 :bed:

  • warrior.health を獲得

Level 5

You hear cries for help. Captives must need rescuing.

class Player
  def play_turn(warrior)
    @health = warrior.health unless @health
    if warrior.feel.captive?
      warrior.rescue!
    elsif warrior.feel.empty?
      if @health <= warrior.health && warrior.health < 13
        warrior.rest!
      else
        warrior.walk!
      end
    else
      warrior.attack!
    end
    @health = warrior.health
  end
end

このレベルの見所: 敵か味方か? :lock:

Level 6

The wall behind you feels a bit further away in this room. And you hear more cries for help.

class Player

  def initialize()
    @captive = true
  end

  def play_turn(warrior)
    @health = warrior.health unless @health
    if @captive
      if warrior.feel(:backward).captive?
        warrior.rescue!(:backward)
        @captive = false
      else
        warrior.walk!(:backward)
      end
    else
      if warrior.feel.empty?
        if @health > warrior.health && warrior.health < 10
          warrior.walk!(:backward)
        elsif @health <= warrior.health && warrior.health < 20
          warrior.rest!
        else
          warrior.walk!
        end
      else
        warrior.attack!
      end
      @health = warrior.health
    end
  end

end

このレベルの見所: ヒット & アウェイ :back:

captive がいるかどうかをフラグでもっておいてそれで処理を変えようとしていろいろうまくいかなかったが、最終的に initialize() に書いてうまくいった

Level 7

You feel a wall right in front of you and an opening behind you.

class Player
  def play_turn(warrior)  
    @health = warrior.health unless @health
    if warrior.feel.wall?
      warrior.pivot!
    else
      if warrior.feel.empty?
        if @health > warrior.health && warrior.health < 10
          warrior.walk!(:backward)
        elsif @health <= warrior.health && warrior.health < 20
          warrior.rest!
        else
          warrior.walk!
        end
      else
        warrior.attack!
      end
    end
    @health = warrior.health
  end  
end

このレベルの見所: 振り向けば、いつかみた風景

  • warrior.pivot! を獲得

Level 8

You hear the mumbling of wizards. Beware of their deadly wands! Good thing you found a bow.

class Player

  def initialize()
    @captive = true
  end

  def play_turn(warrior)
    @health = warrior.health unless @health
    if @captive
      if warrior.feel.captive?
        warrior.rescue!
        @captive = false
      else
        warrior.walk!
      end
    else
      if warrior.look.any?{|s| s.enemy?}
        warrior.shoot!
      else
        warrior.walk!
      end
    end
  end  

end

このレベルの見所: 遠いもんがちー :bow_and_arrow:

  • warrior.lookwarrior.shoot! を獲得
  • ウィザードっぽいのが遠隔から一撃で 11 くらいライフ削ってくるので、二発くらうと死ぬ... でも意外とレンジは短い
  • any? のおかげでスッキリかけたのではないか

Level 9

Time to hone your skills and apply all of the abilities that you have learned.

class Player

  def initialize()
    @left_captive = true
  end

  def play_turn(warrior)
    if @left_captive
      if warrior.look(:backward).any?{|s| s.enemy?}
        warrior.shoot!(:backward)
      elsif warrior.feel(:backward).captive?
        warrior.rescue!(:backward)
        @left_captive = false
      else
        warrior.walk!(:backward)
      end
    else
      if warrior.look.any?{|s| s.enemy?}
        warrior.shoot!
      elsif warrior.feel.captive?
        warrior.rescue!
      elsif warrior.feel.wall?
        warrior.pivot!
      else
        warrior.walk!
      end
    end
  end

end


このレベルの見所: ムーンウォーク :walking_tone2: :bow_and_arrow:

  • めっちゃ弓だけに頼った勝ち方!後ろ向きで矢を放ち、一回も warrior.rest! しない無鉄砲野郎。学んだことを使っていない。こんなんでいいのか?

Ruby Warrior になろう

image.png

もっといいコードがあったら教えてください!


  1. Only one action can be performed per turn. のエラーがでる 

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

[エラー]自動デプロイ実行時のrbenv: ruby 2.5.1 is not installedot〜に関して

はじめに

今回の投稿は私が自動デプロイを実行した時にエラーにハマったので、その内容を共有させていただきます。

エラー内容

[Deprecation Notice] Future versions of Capistrano will not load the Git SCM
plugin by default. To silence this deprecation warning, add the following to
your Capfile after `require "capistrano/deploy"`:

    require "capistrano/scm/git"
    install_plugin Capistrano::SCM::Git

00:00 rbenv:validate
      WARN  rbenv: ruby 2.5.1 is not installed or not found in $HOME/.rbenv/versions/ruby 2.5.1 on 54.95.87.53

仮説を立てる

1. require "capistrano/scm/git"
require〜という記述からCapfileに異常があると仮説。

2. WARN rbenv: ruby 2.5.1 is not installed〜
ruby 2.5.1 is not installedという記述からruby2.5.1のインストールがされていない、またローカル環境とのverに差異があると仮説。怪しいのはdeploy.rbか...?

仮設検証 Capfile

require "capistrano/setup"
require "capistrano/deploy"
require 'capistrano/rbenv'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano3/unicorn'
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git

Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

特に異常なし。

仮設検証 ver確認

ローカル開発環境と本番環境(EC2)内でruby -vを実行したが問題なし。

仮設検証 deploy.rb

deploy.rb
# config valid only for current version of Capistrano
# capistranoのバージョンを記載。固定のバージョンを利用し続け、バージョン変更によるトラブルを防止する
lock '3.14.1'

# Capistranoのログの表示に利用する
set :application, 'chat-space'

# どのリポジトリからアプリをpullするかを指定する
set :repo_url,  'git@github.com:ken-sasaki-222/chat-space.git'

# バージョンが変わっても共通で参照するディレクトリを指定
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')

set :rbenv_type, :user
set :rbenv_ruby, 'ruby2.5.1' #ここに注目

# どの公開鍵を利用してデプロイするか
set :ssh_options, auth_methods: ['publickey'],
                  keys: ['~/.ssh/test_key.pem'] 

# プロセス番号を記載したファイルの場所
set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }

# Unicornの設定ファイルの場所
set :unicorn_config_path, -> { "#{current_path}/config/unicorn.rb" }
set :keep_releases, 5

# デプロイ処理が終わった後、Unicornを再起動するための記述
after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
  task :restart do
    invoke 'unicorn:restart'
  end
end

rubyのverに問題はなさそう。と思いきや...
ver指定の箇所が'ruby 2.5.1'になっている!

正しくは'2.5.1'でOK!
修正すると。

deploy.rb
#省略

set :rbenv_type, :user
set :rbenv_ruby, '2.5.1' #ここに注目

#省略

これでローカル環境のターミナルにてbndle exec cap production deployを実行すると
無事自動デプロイが走りました。

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

Rails6で lib ディレクトリの自作クラスを使用する

自作クラスを作成し利用する方法、
今回はroot/lib配下に作る方法をご紹介します。

環境

rails 6.0.3

application.rbの設定

application.rb
module Myapp
  class Application < Rails::Application
    config.load_defaults 6.0
    config.paths.add 'lib', eager_load: true #これを追加
  end
end

lib配下をautoload_pathsに追加します。
autoload_pathsについてはこちら

lib配下の構成

lib
 ├── assets
 ├── asks
 └── utils
     └── hoge.rb

utils/hoge.rbを作成しまいした。
分かりやすい命名にしましょう。

hoge.rbの中身

hoge.rb
class Utils::Hoge
end

もしくは

hoge.rb
module Utils
  class Hoge
     def greeting
         'hello'
     end
  end
end

親ディレクトリの名称をネームスペースとして、クラスを定義しましょう。
こうすることでautoload_pathsの規則に乗っ取って以下のように呼び出せます。

application_controller.rb
class ApplicationController < ActionController::Base
   hoge = Utils::Hoge.new
   hoge.greeting
   #=> hello  
end

最後に

この記事を書いてる際以下のような投稿を発見しました。
(リサーチ不足が露呈した、、、)

autoload_pathsを修正して謎エラーをなくした話

Rails Guaidによると
「Rails5より前ではlib/以下をautoload_pathsに含めることがよく行われていたが、現在はappフォルダ以外の場所にコードを置いてautoload対象にするのは勧めない」となっています。
本番環境特有のエラーが発生するかもしれないとのこと。

app配下はautoload_pathsにデフォルトで入っているので、
そちらを使うのが良さそう。

じゃあなんでlibディレクトリは存在してるんだ?
開発上手な人はうまく使い分けているのかな。

新たな疑問が生まれたところで、今回は以上です。
日々精進

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