Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
普通のCSVアップロードフォームを作りたい(改)
Search
TAKAHASHI Kazunari
August 20, 2014
1.1k
9
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
普通のCSVアップロードフォームを作りたい(改)
TAKAHASHI Kazunari
August 20, 2014
More Decks by TAKAHASHI Kazunari
See All by TAKAHASHI Kazunari
fat-settings-yml
1syo
0
910
雑につくるKPIツールのススメ
1syo
0
530
みなとRuby会議02やりたい
1syo
1
370
Testing Wercker plugin with bats
1syo
0
600
私を変えた1冊の本
1syo
0
880
普通のCSVアップロードフォームを作りたい
1syo
17
3k
kaja-2013
1syo
0
550
untestable production code
1syo
0
580
authorization-for-buktorg
1syo
0
380
Featured
See All Featured
Design in an AI World
tapps
1
350
Why You Should Never Use an ORM
jnunemaker
PRO
61
10k
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Agile Leadership in an Agile Organization
kimpetersen
PRO
0
270
Effective software design: The role of men in debugging patriarchy in IT @ Voxxed Days AMS
baasie
1
550
A better future with KSS
kneath
240
18k
Rails Girls Zürich Keynote
gr2m
96
14k
B2B Lead Gen: Tactics, Traps & Triumph
marketingsoph
0
260
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
2
2.9k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
2.2k
YesSQL, Process and Tooling at Scale
rocio
174
15k
Become a Pro
speakerdeck
PRO
31
6.3k
Transcript
ී௨ͷ CSVΞοϓϩʔυ ϑΥʔϜΛ࡞Γ͍ͨ վ
✓ Kazunari Takahashi ✓ Yokohama.rb ✓ 2012 Ruby kaja award
winner ✓ MinatoRubyKaigi01 organizer ✓ TDDBC Yokohama TA @1syo
νΣοΫ ✓ Anvilىಈ͍ͯ͠Δ͔ ✓ http://bad.dev/admin/usersͷλϒ͋Δ͔ ✓ http://good.dev/admin/usersͷλϒ͋Δ͔ ✓ gist(bad/good)tweet͔ͨ͠ ✓
֤ڥͰdb:setup࣮ߦ͔ͨ͠ʁ
formͷઃܭΛͲ͏͢Ε͍͍͔ʁ ✓ ೖॻʹࡌͬͯͳ͍ ✓ ೖॻͷ͕࣍গͳ͍ ✓ Rails Cast͘Β͍? ✓ ྑ͍։ൃݱͰڞ༗͞Ε͍ͯΔ
ྑ͍։ൃݱͰ ڞ༗͞Ε͍ͯΔϊϋΛ ίϛϡχςΟʔͰڞ༗͍ͨ͠
γφϦΦ ✓ ཧऀ͕ϢʔβʔใҰׅొ͢Δ ✓ ෳͷϢʔβʔใ͕CSVϑΝΠϧʹ͋Δ ✓ CSVϑΝΠϧͷதࢯ໊ͱemail
None
class CreateUsers < ActiveRecord::Migration! def change! create_table :users do |t|!
t.string :first_name, null:false! t.string :last_name, null:false! t.string :email, null:false! t.timestamps! end! ! add_index :users, :email, unique: true! end! end Users
͋ΔγεςϜͷ࣮
class Admin::UsersController < ApplicationController! def index! end! ! def upload!
f = params[:data_file]! flash[:notice] = 'Upload successful'! CSV.open(f.path, 'rb', encoding: 'UTF-8').each do |row|! User.create(! ! ! ! email: row[0], ! ! ! ! first_name: row[1], ! ! ! ! last_name: row[2]! ! ! )! end! redirect_to action: :index! end! end Admin:UsersController
<div class="container">! <h1>Users</h1>! <% if flash[:notice].present? %>! <div class="bg-success"><%= flash[:notice]
%></div>! <% end %>! <%= form_tag(upload_admin_users_path, ! method: :post, multipart: true, role: "form", ! ! ! ! class: "form-inline") do %>! <div class="form-group">! <%= file_field_tag :data_file, class: "form-control" %>! </div>! <div class="form-group">! <%= button_tag "Upload", class: ["btn","btn-default"] %>! </div>! </div>! <% end %>! </div> index.html.erb
ϦϦʔεͯ͠ΈͨΒ ✓ ϑΝΠϧࢦఆ͠ͳ͍ͱΤϥʔʹͳΓ·͢ ✓ ը૾͕Ξοϓϩʔυ͢ΔͱΤϥʔʹͳΓ·͢ ✓ ◦◦͞ΜͷemailΞυϨε͕͓͔͍͠Ͱ͢ ✓ ΤΫηϧ͔Β࡞ͬͨCSV͕औΓࠐΊ·ͤΜ ✓
ࡢొͨ͠ϑΝΠϧΛΞοϓϩʔυͪ͠Ό͍·ͨ͠
demo
ཁ͕݅Γͳ͍ ✓ ̎࣠ͷόϦσʔγϣϯ ✓ ϑΝΠϧϑΥʔϚοτ ✓ Ϩίʔυݸผ ✓ Ξϥʔτը໘ ✓
ςετ..
def upload! f = params[:data_file]! if f.blank?! flash[:notice] = 'File
not found'! redirect_to action: :index! return! end! ! flash[:notice] = 'Upload successful'! CSV.open(f.path, 'rb', encoding: 'UTF-8').each do |row|! unless row[0] =~ /\A[a-z.-_]+@[a-z.-_]+\z/! next! end! ! User.create(! email: row[0], first_name: row[1], last_name: row[2])! end! redirect_to action: :index! end Admin:UsersController Added!
f = params[:data_file]! if f.blank?! ! flash[:notice] = 'File not
found'! ! redirect_to action: :index! return! end Admin:UsersController
CSV.open(f.path, 'rb', encoding: 'UTF-8').each do |row|! unless row[0] =~ /\A[a-z.-_]+@[a-z.-_]+\z/!
next! end! end Admin:UsersController
✓ Controllerʹ࣮͕Ճ͞ΕΔ ✓ Validationʹແཧ͕͋Δ
Ͳ͏͢Εྑ͍ͷ͔ʁ
Ϟσϧʹ͢Δ https://gist.github.com/1syo/319f237637cf522c8c9f
[email protected]
ۭ ঝଠ
[email protected]
ՖژӃ య໌ CSVϑΝΠϧશମUserImportϞσϧ Ϟσϧͭ ֤ߦUserϞσϧ
class Admin::UsersController < ApplicationController! def index! @user_import = UserImport.new! end!
! def import! @user_import = UserImport.new(params[:user_import])! if @user_import.save! redirect_to url_for(action: :index), ! ! ! ! ! notice: 'Upload successful'! else! render :index! end! end! end admin/users_controller.rb
ScaffoldͬΆ͍Controller
<%= form_for(@user_import, url: import_admin_users_path,! html: {method: :post, multipart: true}) do
|f| %>! <% if @user_import.errors.any? %>! <div class="alert alert-danger">! <ul>! <% @user_import.errors.full_messages.each do |msg| %>! <li><%= msg %></li>! <% end %>! </ul>! </div>! <% end %>! <% end %>! … index.html.erb
form_forΛ͏
ΤοϥʔϝοηʔδΛ දࣔ͢Δ
class UserImport! CSV_OPTIONS = {encoding: 'UTF-8'}.freeze! include ActiveModel::Model! ! attr_accessor
:file! ! validates :file, presence: true! validate :csv_format, ! ! ! ! if: Proc.new { |m| m.file.present? }! ! def save; end! ! private! def csv_format;end! def path; end! end user_import.rb
class User < ActiveRecord::Base! validates :first_name, ! ! ! length:
{ maximum: 100 }, presence: true! validates :last_name, ! ! ! length: { maximum: 100 }, presence: true! validates :email, ! ! ! length: { maximum: 100 }, presence: true, uniqueness: true! end user.rb
ActiveModel::ModelΛ͏
Custom ValidationΛ࡞Δ
def save! return false unless valid?! ! User.transaction do! CSV.read(path,
"r", CSV_OPTIONS).each_with_index do |row, index|! user = User.new(! ! ! ! email: row[0], ! ! ! ! first_name: row[1], ! ! ! ! last_name: row[2]! ! ! )! unless user.save! errors.add(! ! ! ! ! "line #{index+1} : ", ! ! ! ! ! user.errors.full_messages.join(" , “)! ! ! ! )! end! end! ! if errors.any?! raise ActiveRecord::Rollback! end! end! ! !errors.any?! end user_import.rb
UserϞσϧͷerrorsΛ UserImportϞσϧͷerrorsʹ add͢Δ
private! def csv_format! CSV.open(path, "r", CSV_OPTIONS) { |csv| }! rescue!
errors.add(:file, "is invalid csv format")! end! ! def path! if file.respond_to? :path! file.path! else! file! end! end user_import.rb
demo
ςετΛͲ͏ॻ͘ʁ
describe "#save" do! subject { UserImport.new(file: file) }! context "༗ޮͳϑΥʔϚοτͷ࣌"
do! let(:file) do! ! ! Rails.root.join(“spec/fixtures/valid.csv")! ! ! end! let(:error_count) { CSV.read(file).count }! it { expect(subject.save).to be_truthy }! it do! ! ! expect { subject.save }.to! ! ! ! ! change(User, :count).by(error_count)! ! ! end! end! end user_import_spec.rb
RSpec.describe UserImport do! describe "#save" do! subject { UserImport.new(file: file)
}! context "UserϞσϧͰΤϥʔͰ͕͋Δ࣌" do! let(:file) do! ! ! Rails.root.join(“spec/fixtures/valid.csv")! ! ! end! let(:errors) { double(add: nil, clear: nil, empty?: nil) }! before do! allow_any_instance_of(User).to receive(:save) { false }! expect(subject).to receive(:errors) { errors }.twice! end! it { expect(subject.save).to be_falsey }! it do! ! ! expect { subject.save }.not_to ! ! ! ! change(User, :count)! ! ! end! end! end! end user_import_spec.rb
RSpec.describe Admin::UsersController do! describe "POST import" do! let(:attributes) { {
file: Rack::Test::UploadedFile.new(file, "text/plan") } }! ! describe "with valid params" do! let(:file) { Rails.root.join("spec/fixtures/valid.csv") }! let(:record_count) { CSV.read(file).count }! ! it "creates new Users" do! expect {! post :import, {user_import: attributes}! }.to change(User, :count).by(record_count)! end! ! it "redirects to the created user" do! post :import, {user_import: attributes}! expect(response).to redirect_to(action: :index)! end! end! end! end admin/users_controller_spec.rb
·ͱΊ ✓ form_forͰݕ౼ͯ͠ΈΔ ✓ ScaffoldͰͰ͖ͨControllerͷઃܭΛ·ͶΔ ✓ ActiveModel::ModelΛ͏ ✓ Custom ValidationΛ͏
͋ͳͨͳΒͲ͏࣮͢Δ͔ ڭ͍͑ͯͩ͘͞