おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

Ruby on Railsでcsvダウンロード機能の作り方

Ruby on Railsを使ってcsvファイルのダウンロード機能を作りたくて調べました。

大枠の手順

  1. routesを設定
  2. csvを受け付けるアクションをコントローラーに設定
  3. 対象データを抽出しcsvフォーマットのファイルへ渡す
  4. CSVデータを作成する
  5. 画面にダウンロードボタンを作成する

コントローラーにアクションを定義する

# controller/users_controller.rb
  def index
    @users = User.all

    respond_to do |format|
      format.html
      format.csv do
        filename = ERB::Util.url_encode('ユーザー一覧.csv')
        send_data render_to_string, filename: filename, type: :csv
      end
    end
  end

ERB::Util.url_encodeは引数に渡された文字列をURLエンコードしてくれる。これによって日本語や半角スペースをよしなにしてくれる

send_dataは動的に生成されたデータをダウンロードする。 render_to_stringは表示結果を文字列として取得するメソッド。

なので、これらを組み合わせる事でCSVファイルに出力する為の文字列を作ってダウンロードを可能にしている。

参考: https://docs.ruby-lang.org/ja/latest/class/ERB=3a=3aUtil.html

参考: http://railsdoc.com/references/send_data

参考: http://railsdoc.com/references/render_to_string

View

# views/users/index.haml
= link_to 'CSVダウンロード' users_path(format: :csv)

ポイントはリンクを生成するpathにフォーマット: csvを指定してあげます。これによってコントローラー側で切り分けれるようになります。

csvファイル

# views/users/index.csv.ruby
require 'csv'
require 'nkf'
csv_data = CSV.generate do |csv|
  csv << %w(id 名前 年齢)
  @users.each do |user|
    csv << [
        user.id,
        user.name,
        user.age
    ]
  end
end
NKF::nkf('--sjis -Lw', csv_data)

CSV.generateは文字列csv形式=カンマ区切りの文字列を生成してくれます。 NKF::nkfは文字コードを強制的に変換する為のプログラムです。

参考: https://docs.ruby-lang.org/ja/1.9.3/class/NKF.html

【Rails】Strong Parametersで`param is missing...`エラーになる

こんちには。opiyoです。

ActionController::ParameterMissing in ImagesController#create
param is missing or the value is empty: image

    def image_paprams
      params.require(:image).permit(:name, :picture)
    end

end

こんな感じでエラーになるのだが、paramsが空っぽだよと。 実際にparams[:image]ってやるとnilが返ってくる。

が、すげー凡ミスでpermitて定義している:nameをview側に書いてなかった。 だから、来るはずのデータが来てなくてエラーになったのではなかろうかと思ってる。

<%= form_for(@image) do |f| %>
  <div class="form-group">
    <%= f.text_field :name %> # ここを書いてなかったけど、書いたらエラーにならなくなった    
    <%= f.file_field :picture %>
    <%= f.submit "画像を投稿する" %>
  </div>
<% end %>

そんな感じ。

Docker For MacでPostgreSQL9.5系の環境を作ってみた!

こんばんは。opiyoです。

brewでインストールするとなんだかPostgreSQL9.6系がインストールされてしまうのだけど、本番環境と同じバージョンをローカルに使いたくてウズウズしていたのだが...

Docker使えば良いじゃんってアドバイス貰ったので教えてもらった

Docker For Macのインストール

https://www.docker.com/docker-mac

docker-compose.ymlを作る

docker-compose.ymlの場所を作る

$ mkdir -p ~/compose/postgresql95/
$ vi docker-compose.yml

PostgreSQL9.5を配置する場所を作る

$ mkdir ~/postgres95

docker-compose.ymlを修正

postgresql:
  image: postgres:9.5.8-alpine
  environment:
    POSTGRES_USER: taku
    POSTGRES_INITDB_ARGS: "--encoding=UTF8 --no-locale"
    LANG: ja_JP.UTF-8
  ports:
    - "5432:5432"
  volumes:
    - /Users/taku/postgres95:/var/lib/postgresql/data #「/Users/taku/postgres95」の箇所を、さっき作った場所を指定する      
  restart: always

やることは、

  • POSTGRES_USERを各自設定する
  • volumesのパスをさっき作った場所を指定する

imageの部分には、こちらのサイトに書いてあるように好きなバージョンを書けばおk https://hub.docker.com/r/library/postgres/tags/

自動起動されているpostgresqlを止める

本当は9.6系と9.5系を共存させたいのだけど、上手くいかないので既にPostgreが動いている場合は止める。 私の場合は、brewでインストールしてたので簡単だった。

$ brew services list
Name       Status  User
postgresql started taku

$ brew services stop postgresql

$ brew services list
Name       Status  User
postgresql stopped

パスを通す

$ sudo vi ~/.bash_profile
export PGHOST=localhost

$ env | grep HOST
PGHOST=localhost

これは教えてもらうがままにやったけど、何してるのかいまいち分かってないぞ。

構築と起動

$ docker-compose up -d

psql -l -p5432とかやればアクセスできるはず! めちゃくちゃ簡単でした。

その他Dockerコマンド

ステータス

$ docker ps

停止

$ docker stop CONTAINER_NAMES

コンテナに入る

$ docker exec -it CONTAINER_NAMES /bin/bash

こんな感じ。

【Rails】ファイルのフルパス、ファイル名を取得する

過去データとかでファイルを一括で読み込みたい場合で使える技です。

> files = Dir.glob("/Users/taku/rails/gist/test/*.xls")
=> ["/Users/taku/rails/gist/test/1.xls", "/Users/taku/rails/gist/test/2.xls", "/Users/taku/rails/gist/test/3.xls", "/Users/taku/rails/gist/test/4.xls", "/Users/taku/rails/gist/test/5.xls"]

これでfilesをグルグル回して一つ一つのファイルを読み込んでやれば色々処理できますね。 取り込み処理も出来るし、csvを作ったりも出来るし。

globを使うと拡張子で絞り込めるので、結構便利ですね。

> files = Dir.entries("/Users/taku/rails/gist/test/")
=> [".", "..", ".DS_Store", "1.csv", "1.txt", "1.xls", "1.xlsx", "2.csv", "2.txt", "2.xls", "2.xlsx", "3.csv", "3.txt", "3.xls", "3.xlsx", "4.xls", "5.xls"]

単純にディレクトリの中にあるファイル名だけ取得したい場合はentriesを使うそうです。

【Ruby】配列の中身が重複しているかをチェックする方法(select、find)

csvのデータを取り込み別のcsvへ吐き出す処理をしていたのですが、値が重複していることに気がつきました。

こんな感じ。

data = []
inport = CSV.read("./inport.csv")
inport.each do |c|
  data << c
end

CSV.open("./export.csv", "w") do |export|
  data.each do |d|
    export << d
  end
end

この中で取り込んだデータが重複しているかどうかチェックする方法が分からず、グルグル回さないとダメなのかと思っていたのですが良い方法がありました。

> ff6 = [["Tina", 1],["Rokku", 2],["Edoga", 3],["Masshu", 4]]
=> [["Tina", 1], ["Rokku", 2], ["Edoga", 3], ["Masshu", 4]]

> ff6.select{|f| f[0] == "Masshu"}
=> [["Masshu", 4]]
irb(main):003:0>

selectを使うと合致したものだけを取ってきてくれます。 なので重複しているかチェックしたい場合は、sizeを使って数を調べてやれば良いです。

> ff6.select{|f| f[0] == "Masshu"}.size
=> 1

> if ff6.select{|f| f[0] == "Masshu"}.size > 0
>   # 処理
> end

めでたし、めでたし

追記

findを使った方が早いとのこと。

> ff6
=> [["Tina", 1], ["Rokku", 2], ["Edoga", 3], ["Masshu", 4]]

> ff6.find {|ff| ff[0] == "Masshu" }
=> ["Masshu", 4]

> ff6.find {|ff| ff[0] == "Masshu" }.size
=> 2

selectとの違いは、見つけた最初の配列を一つだけ返す。ってことですかね。 例えば、

# 「カイエン 4」を追加します
> ff6 = [["Tina", 1],["Rokku", 2],["Edoga", 3],["Masshu", 4], ["Kaien", 4]]
=> [["Tina", 1], ["Rokku", 2], ["Edoga", 3], ["Masshu", 4], ["Kaien", 4]]


# selectの場合
> ff6.select {|ff| ff[1] == 4 }
=> [["Masshu", 4], ["Kaien", 4]]

# findの場合
> ff6.find {|ff| ff[1] == 4 }
=> ["Masshu", 4]

最初に見つけた一つを返すんだから、そりゃー早いは。 今回やりたいことは、重複しているかのチェックだから1件でもあればNG。 ってことでfindを使うことにした。

【bootstrap3】ナビゲーションバー(Navbar)で起きたレイアウト崩れと右寄せへの対応方法

こんばんは。opiyoです。

結論としては勝手な解釈をして余計なことしてた!に尽きるのだけど、同じようなハマり方すると時間もったい無いので共有です。

<header>
  <nav class="navbar navbar-inverse">
      <div class="container-fluid">
          <div class="navbar-header">
              <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbarEexample1">
                  <span class="sr-only">Toggle navigation</span>
          <% if logged_in? %>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
          <% else %>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          <% end %>
              </button>
              <a class="navbar-brand" href="#">
          <%= link_to "RChannel", root_path, id: 'logo' %>
              </a>
          </div>
          <div class="collapse navbar-collapse" id="navbarEexample1">
              <ul class="nav navbar-nav navbar-right">
          <% if logged_in? %>
            <li class="inline-block"><%= link_to "キーワード登録", new_user_keyword_path(user_id: @current_user.id) %></li>
            <p class="navbar-text inline-block"><%= @current_user.name %> さん</p>
            <li class="inline-block"><%= link_to "マイリスト", my_list_topic_path(id: @current_user.id) %></li>
            <li class="inline-block"><%= link_to "ログアウト", logout_path, method: :delete %></li>
          <% else %>
            <li class="inline-block"><%= link_to "アカウント登録", new_user_path %></li>
            <li class="inline-block"><%= link_to "ログイン", login_path %></li>
          <% end %>
              </ul>
          </div>
      </div>
  </nav>
</header>

ポイントはサンプル通りそのまま記載してください!

で僕が何をしていたのかご覧ください。

画面小さくした時に文字が隠れない

header側にもリンクが必要だと思ったんだよ。

          <div class="navbar-header">
              <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbarEexample4">
                  <span class="sr-only">Toggle navigation</span>
          <% if logged_in? %>
                    <span class="icon-bar"><%= link_to "キーワード登録", new_user_keyword_path(user_id: @current_user.id) %></span>
                    <span class="icon-bar"><%= link_to "マイリスト", my_list_topic_path(id: @current_user.id) %></span>
                    <span class="icon-bar"><%= link_to "ログアウト", logout_path, method: :delete %></span>
          <% else %>
            <span class="icon-bar"><%= link_to "アカウント登録", new_user_path %></span>
            <span class="icon-bar"><%= link_to "ログイン", login_path %></span>
          <% end %>
              </button>
              <a class="navbar-brand" href="#">
          <%= link_to "RChannel", root_path, id: 'logo' %>
              </a>
          </div>

最初こんな感じで.navbar-headerspanタグにもリンクが必要だと思って記載していたのですが、これやるとレイアウトが崩れます!

f:id:opiyotan:20180202201534p:plain

なので、spanタグには何も書いてはいけません!

左寄せが出来ない

          <div class="collapse navbar-collapse" id="navbarEexample1">
              <ul class="nav navbar-nav">
          <% if logged_in? %>
            <li class="inline-block navbar-right"><%= link_to "キーワード登録", new_user_keyword_path(user_id: @current_user.id) %></li>
            <p class="navbar-text inline-block navbar-right"><%= @current_user.name %> さん</p>
            <li class="inline-block"><%= link_to "マイリスト", my_list_topic_path(id: @current_user.id) %></li>
            <li class="inline-block"><%= link_to "ログアウト", logout_path, method: :delete %></li>
          <% else %>
            <li class="inline-block navbar-right"><%= link_to "アカウント登録", new_user_path %></li>
            <li class="inline-block navbar-right"><%= link_to "ログイン", login_path %></li>
          <% end %>
              </ul>
          </div>

サンプル見ると、pタグにnavbar-right設定しているから真似してたのだけど全然右寄せにならない。

で、Chromeで確認してみると

f:id:opiyotan:20180202201709p:plain

分かりづらいけど、既に領域が固定されているからfloatかけたって無駄。 なので、上の要素divタグに対して.navbar-rightを設定してやることでうまくいった!

まとめ

勝手な解釈はせずに、基本に忠実にやりましょう!

【Rails】XX件以上登録できないようにする独自バリデーションメソッドを設定する方法

こんばんは。opiyoです。

Railsアプリで「XX以上は登録できない!」というバリデーションを設定したかったので、やり方を調べてみました。

やりたいこと

Keywordというテーブルにデータを登録する処理で、5つまでしか登録できないようにバリデーションを設定します。

もし5つ以上の登録を行おうとした場合はsaveでエラーになるようにします。

Model

class Keyword < ApplicationRecord
  validates :title, presence: true
  validates :url, presence: true
  validate :check_count

  def check_count
    errors.add(:keyword, "は5つまで登録可能です。") if Keyword.count >= 5
    # errors.add(:keyword, "は5つまで登録可能です。") if Keyword.where(user_id: self.user_id).count >= 5
  end
end

モデルにバリデーションメソッドを定義します。 validateにメソッド名を記述し、そのメソッド内でKeywordの件数を取得しif文を記述します。 もし5件以上あれば、errorsの中に名前とエラーメッセージを追加します。

またコメントアウトしている部分ですが、selfを使うことで自分が登録したデータのみを対象とすることが出来ます。こうやって使うことが基本になりそうですね。

また、errorsの正体についてはActiveModel::Errorsで詳細はこちらを。

Controller

class KeywordsController < ApplicationController
  def create
    @keyword = Keyword.new(keyword_params)
    @keyword.check_count

    if @keyword.save
      redirect_to root_url
    else
      render 'new'
    end
  end

  private
    def keyword_params
      params.require(:keyword).permit(:title, :url)
    end
end

コントローラーではKeywordをインスタンス化して、メソッドを呼び出すだけです。 5件以上ある場合はerrorsにエラーが追加されているので、saveする時に失敗します。

という流れです。

【Bootstrap】navbarの背景色、文字色の悩みはこれ一発で解決だわー

何か作る時bootstrapにお世話になることあると思うのですが、cssいじっただけだと何故か上手くいかない。

例えば今回のnavbarですね。

ナビゲーションメニューがかっこよく決まらないとモチベーションが全く上がらないので、いろいろ探していたらあったわー。神様が。 https://work.smarchal.com/twbscolor/

自分の好きなカラーコードを入力したら勝手にcssのコード吐き出してくれる。

  • scss
  • sass
  • less
  • css

全部あるよ。

OhkEventRails.png

haml

    .navbar.navbar-default.navbar-static-top
      .container
        %button.navbar-toggle(type="button" data-toggle="collapse" data-target=".navbar-responsive-collapse")
          %span.icon-bar
          %span.icon-bar
          %span.icon-bar
        %a.navbar-brand(href="#") ホーム
        .navbar-collapse.collapse.navbar-responsive-collapse
          %ul.nav.navbar-nav
            %li= link_to "Link 1", "/path1"
            %li= link_to "Link 2", "/path2"
            %li= link_to "Link 3", "/path3"

scss

$bgDefault      : #dd4444;
$bgHighlight    : #e8350e;
$colDefault     : #ecf0f1;
$colHighlight   : #ecdbff;
$dropDown       : false;

.navbar-default {
  background-color: $bgDefault;
  border-color: $bgHighlight;
  .navbar-brand {
    color: $colDefault;
    &:hover, &:focus {
      color: $colHighlight; }}
  .navbar-text {
    color: $colDefault; }
  .navbar-nav {
    > li {
      > a {
        color: $colDefault;
        &:hover,  &:focus {
          color: $colHighlight; }}
      @if $dropDown {
        > .dropdown-menu {
          background-color: $bgDefault;
          > li {
            > a {
              color: $colDefault;
              &:hover,  &:focus {
                color: $colHighlight;
                background-color: $bgHighlight; }}
            > .divider {
              background-color: $bgHighlight;}}}}}
    @if $dropDown {
      .open .dropdown-menu > .active {
        > a, > a:hover, > a:focus {
          color: $colHighlight;
          background-color: $bgHighlight; }}}
    > .active {
      > a, > a:hover, > a:focus {
        color: $colHighlight;
        background-color: $bgHighlight; }}
    > .open {
      > a, > a:hover, > a:focus {
        color: $colHighlight;
        background-color: $bgHighlight; }}}
  .navbar-toggle {
    border-color: $bgHighlight;
    &:hover, &:focus {
      background-color: $bgHighlight; }
    .icon-bar {
      background-color: $colDefault; }}
  .navbar-collapse,
  .navbar-form {
    border-color: $colDefault; }
  .navbar-link {
    color: $colDefault;
    &:hover {
      color: $colHighlight; }}}
@media (max-width: 767px) {
  .navbar-default .navbar-nav .open .dropdown-menu {
    > li > a {
      color: $colDefault;
      &:hover, &:focus {
        color: $colHighlight; }}
    > .active {
      > a, > a:hover, > a:focus {
        color: $colHighlight;
        background-color: $bgHighlight; }}}
}

【Rails】ファイルをデータベースに保存する方法

ファイルを保存する方法はRailsチュートリアルとかでは、CarrierWaveというgemを使う。 だがちょっとしたアプリとか作りたい時には、DBに保存した方が都合がいい時もあるはず。

gemを使う方法は別の機会でまとめるとして、今日はDBに保存する方法を。

モデルを作る

$ rails g model Image name:string picture:binary
  invoke  active_record
  create    db/migrate/20171215092903_create_images.rb
  create    app/models/image.rb
# migrate
class CreateImages < ActiveRecord::Migration[5.0]
  def change
    create_table :images do |t|
      t.string :name
      t.binary :picture

      t.timestamps
    end
  end
end
$ rails db:migrate
== 20171215092903 CreateImages: migrating =====================================
-- create_table(:images)
   -> 0.3369s
== 20171215092903 CreateImages: migrated (0.3370s) ============================

コントラーラーを作る

$ rails g controller Images
Running via Spring preloader in process 7555
      create  app/controllers/images_controller.rb
      invoke  erb
      create    app/views/images
      invoke  helper
      create    app/helpers/images_helper.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/images.coffee
      invoke    scss
      create      app/assets/stylesheets/images.scss
# images_controller.rb
class ImagesController < ApplicationController
  def new
    @image = Image.new
    @image.name = "hogehoge"
  end

  def create
    @image = Image.new(image_paprams)
    @image.name = params[:image][:picture].original_filename
    @image.picture = params[:image][:picture].read

    if @image.save
      redirect_to root_path
    else
      render 'new'
    end
  end

  private

    def image_paprams
      params.require(:image).permit(:name, :picture)
    end

end

ビューを作る

# new.html.erb
<%= form_for(@image) do |f| %>
  <div class="form-group">
    <%= @image.name %>
    <%= f.file_field :picture %>
    <%= f.submit "画像を投稿する" %>
  </div>
<% end %>

↓htmlに変換されると...

<form class="new_image" id="new_image" enctype="multipart/form-data" action="/images" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓"><input type="hidden" name="authenticity_token" value="ZgO8WthU8KtI/rspO/oVgHavcwUUVQl1jDwm0DQAsZXwCHYLO02KUqcX+Kt54gKlUlQP4sqDe3NUltVWjtZQDw==">
  <div class="form-group">
    hogehoge
    <input type="file" name="image[picture]" id="image_picture">
    <input type="submit" name="commit" value="画像を投稿する" data-disable-with="画像を投稿する" disabled="">
  </div>
</form>

まとめ

こんな感じでしょうか。 とりあえず言いたいことは、

  • binary型のカラムをモデルに用意します。
  • form_forf.file_fieldを使います。
  • params[:image][:picture].readで画像データが保存できる

form_forがよしなにやってくれるのは分かるのだけど、いまいち理解が浅いので色々動かしてみて勉強しよう。

で、実際に登録された画像データはこんな感じです。

$ rails c
> Image.all
  Image Load (1.6ms)  SELECT "images".* FROM "images"
=> #<ActiveRecord::Relation [#<Image id: 1, name: "17704-koala-bear-pv.jpg", picture: "\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00\xFF\xDB\x00C\x00\n\a\a\b\a\x06\n\b\b\b\v\n\n\v\x0E\x18\x10\x0E\r\r\x0E\x1D\x15\x16\x11...", created_at: "2017-12-15 09:52:29", updated_at: "2017-12-15 09:52:29">]>

【Ruby on Rails】月の最初の日、最終日はどうやって取得する?

画面から入力された日付の月が何日までなのか取得する方法が分からなかったので調べてみました。

月の最終日

irb> Date
NameError: uninitialized constant Date
Did you mean?  Data
    from (irb):9
    from /Users/taku/.rbenv/versions/2.3.3/bin/irb:11:in `<main>'

irb> require "date"
=> true

irb> date = Date.today
=> #<Date: 2017-12-13 ((2458101j,0s,0n),+0s,2299161j)>

irb> date.end_of_month
NoMethodError: undefined method `end_of_month' for #<Date: 2017-12-13 ((2458101j,0s,0n),+0s,2299161j)>
  from (irb):13
  from /Users/taku/.rbenv/versions/2.3.3/bin/irb:11:in `<main>'

むむむ何でだ。 end_of_monthというメソッドを使うと月の最終日が取れる。取れたんだけど...

調べてみると、end_of_monthはRailsで定義されたメソッドみたい。 なのでrails cで検証スタート。

> rails c

> date = Date.today
=> Wed, 13 Dec 2017

> date.end_of_month
=> Sun, 31 Dec 2017

> date.end_of_month.to_s
=> "2017-12-31"

月の最初の日

月の最初の日を求める時はbeginning_of_monthを使う

> date = Date.today
=> Wed, 13 Dec 2017

> date.beginning_of_month
=> Fri, 01 Dec 2017

> date.beginning_of_month.to_s
=> "2017-12-01"

その他のちょっとしたあれこれ

> Date.today.year
=> 2017

> Date.today.month
=> 12

> Date.today.day
=> 13

> Date.today.wday
=> 3 # 「0」が日曜日なので「3」は水曜日