导出csv文件,导出axlsx文件。gem 'Axlsx-Rails' (470🌟);导入csv文件。
汇出 CSV 档案
需求:后台可以汇出报名资料
有时候后台功能做再多,也不如 Microsoft Excel 或 Apple Numbers 试算表软件提供的分析功能,这时候如果有汇出功能,就可以很方便地把资料汇出来,用软件来打开浏览。
CSV 逗号分隔值(Comma-separated-values)是一种简单的资料格式,其文件以纯文本形式存储表格数据(数字和文本)。一行一笔资料,不同字段用逗号隔开。
例子:
app/views/admin/event_registrations/index.html.erb
<p>
<%= link_to "汇出 CSV", admin_event_registrations_path(:format => :csv) %>
</p>
app/controllers/admin/event_registrations_controller.rb
+ require 'csv'
class Admin::EventRegistrationsController < AdminController
def index
# (略)
+ respond_to do |format|
+ format.html
+ format.csv {
+ @registrations = @registrations.reorder("id ASC")
+ csv_string = CSV.generate do |csv|
+ csv << ["报名ID", "票种", "姓名", "状态", "Email", "报名时间"]
+ @registrations.each do |r|
+ csv << [r.id, r.ticket.name, r.name, t(r.status, :scope => "registration.status"), r.email, r.created_at]
+ end
+ end
+ send_data csv_string, :filename => "#{@event.friendly_id}-registrations-#{Time.now.to_s(:number)}.csv"
+ }
+ end
end
CSV 是 Ruby 内建的库,这里第一行需要先 require
它。使用 CSV.generate
可以产生出 csv_string
字符串,也就是要输出的 CSV 资料,接着透过 send_data
传给浏览器进行档案下载。
Time.now.to_s(:number)生成"20180811174126", to_s是简写:to_formatted_s(format=:default)
time = Time.now # => 2007-01-18 06:10:17 -06:00
time.to_formatted_s(:time) # => "06:10"
time.to_s(:time) # => "06:10"
time.to_formatted_s(:db) # => "2007-01-18 06:10:17"
time.to_formatted_s(:number) # => "20070118061017"
time.to_formatted_s(:short) # => "18 Jan 06:10"
send_data(data, options={})
Axlsx-Rails — Spreadsheet templates for Rails(470🌟)
CSV 有个缺点,就是用 Microsoft Excel 打开时默认会变成乱码。这是因为汇出的 CSV 的字串编码是 UTF-8,但是 Excel 默认会用本地编码,例如中国大陆地区用 GB 2312
解决办法有二:
方法一: 以记事本开启后储存,再以 Excel 开启即可正常显示。
方法二: 开启 Excel 软件,新增空白活页簿(Workbook),然后在上方功能选项中点选「资料(Data)」->「取得外部资料 Get External Data」->「从文字档 From Text File...」→「选择汇出的 CSV 档案」→ 选择符号分隔(Delimited)、选择 File origin 编码是 Unicode (UTF-8) → 选择分隔符号是 Comma 逗点,即可正常显示。
如果上述 Excel 打开 CSV 档案的解法没办法接受的话,那只好想办法汇出 Excel 专用的 xlsx 格式了。这需要额外装 gem。
使用 axlsx_rails gem(470✨)。
⚠️ Rails 4.2, 5.0 or 5.1 (tested), 还不支持5.2
沿用上例子:安装3个gem后:
app/views/admin/event_registrations/index.html.erb
+ <%= link_to "汇出 Excel", admin_event_registrations_path(:format => :xlsx)
app/controllers/admin/event_registrations_controller.rb
在index中加上 format.xlsx
创建template:
app/views/admin/event_registrations/index.xlsx.axlsx
⚠️:需要使用action_name.xlsx.axlsx格式作为名字。
wb = xlsx_package.workbook
wb.add_worksheet(name: "Buttons") do |sheet|
sheet.add_row ["报名ID", "票种", "姓名", "状态", "Email", "报名时间"]
@registrations.each do |r|
sheet.add_row [r.id, r.ticket.name, r.name, t(r.status, :scope => "registration.status"), r.email, r.created_at]
end
end
导入 CSV 档案 (Rake直接导入)
假设我们拿到这样的 CSV 档案,接下来要如果汇入数据库呢?总不能一笔一笔输入太慢了,当然要用写程序的方式来做汇入。
如果汇入是程序员的一次性的任务,我们可以不需要实作 Web UI,只需要一个 rake 任务可以执行就好了。
编辑 lib/tasks/dev.rake
,让我们新增一个 import_registration_csv_file
任务
lib/tasks/dev.rake
+ require 'csv'
namespace :dev do
+ task :import_registration_csv_file => :environment do
+ event = Event.find_by_friendly_id("fullstack-meetup")
+ tickets = event.tickets
+
+ success = 0
+ failed_records = []
+
+ CSV.foreach("#{Rails.root}/tmp/registrations.csv") do |row|
+ registration = event.registrations.new( :status => "confirmed",
+ :ticket => tickets.find{ |t| t.name == row[0] },
+ :name => row[1],
+ :email => row[2],
+ :cellphone => row[3],
+ :website => row[4],
+ :bio => row[5],
+ :created_at => Time.parse(row[6]) )
+
+ if registration.save
+ success += 1
+ else
+ failed_records << [row, registration]
+ end
+ end
+
+ puts "总共汇入 #{success} 笔,失败 #{failed_records.size} 笔"
+
+ failed_records.each do |record|
+ puts "#{record[0]} ---> #{record[1].errors.full_messages}"
+ end
+
+ end
执行 rake dev:import_registration_csv_file
就会执行了汇入的操作到 fullstack-meetup
这个活动
解说:
- 和汇出 CSV 一样,Ruby 内建了 CSV 库可以解析 CSV,所以第一行先
require 'csv'
CSV.foreach
会打开这个 CSV 档案跑循环,每笔资料就是一行row
,那一行的第一列是row[0]
、第二列是row[1]
。只要依序塞给event.registrations.new
即可。- CSV 中的票种是string,但是转进我们的数据库中需要转换成 Ticket model,因此这里写成
tickets.find{ |t| t.name == row[0] }
用票种名称去找是哪一个对象。 - 时间也是一样,透过
Time.parse
把string转成时间对象 - 因为汇入会一次汇入非常多笔,我们希望不管每笔资料 save 成功或失败,都能跑完全部资料,最后印出一个总结:告诉我们总共几笔成功,总共几笔失败,是哪些笔失败又是什么原因。
把汇入档案存下来纪录过程
如果这是一个面向终端用户的功能,会需要实作的更完整,例如:
- 把上传的档案先存下来,先给用户预览字段顺序是否正确、有多少数据要汇入、哪些数据有问题
- 确认后,才开始汇入数据库
- 可以浏览过往的汇入历史纪录
执行 rails g model registration_import
,这个 Model 会存下上传的 CSV 档案,并记录汇入的结果。
编辑 db/migrate/2017XXXXXX4512_create_registration_imports.rb
db/migrate/2017XXXXXX4512_create_registration_imports.rb
class CreateRegistrationImports < ActiveRecord::Migration[5.0]
def change
create_table :registration_imports do |t|
+ t.string :status
+ t.string :csv_file
+ t.integer :event_id, :index => true
+ t.integer :user_id
+ t.integer :total_count
+ t.integer :success_count
+ t.text :error_messages
t.timestamps
end
end
end
执行 rake db:migrate
执行 rails g uploader registration_import_csv
编辑 app/models/registration_import.rb
,其中的 process!
方法就是要执行的汇入操作。
app/models/registration_import.rb
+ require 'csv'
class RegistrationImport < ApplicationRecord
+ mount_uploader :csv_file, RegistrationImportCsvUploader
+
+ validates_presence_of :csv_file
+
+ belongs_to :event
+ belongs_to :user
+
+ serialize :error_messages, JSON
+
+ def process!
+ csv_string = self.csv_file.read.force_encoding('utf-8')
+ tickets = self.event.tickets
+
+ success = 0
+ failed_records = []
+
+ CSV.parse(csv_string) do |row|
+ registration = self.event.registrations.new( :status => "confirmed",
+ :ticket => tickets.find{ |t| t.name == row[0] },
+ :name => row[1],
+ :email => row[2],
+ :cellphone => row[3],
+ :website => row[4],
+ :bio => row[5],
+ :created_at => Time.parse(row[6]) )
+
+ if registration.save
+ success += 1
+ else
+ failed_records << [row, registration.errors.full_messages]
+ end
+ end
+
+ self.status = "imported"
+ self.success_count = success
+ self.total_count = success + failed_records.size
+ self.error_messages = failed_records
+
+ self.save!
+ end
end
编辑 app/models/event.rb
app/models/event.rb
has_many :registrations, :dependent => :destroy
+ has_many :registration_imports, :dependent => :destroy
编辑 config/routes.rb
config/routes.rb
namespace :admin do
# (略)
resources :events do
+ resources :registration_imports
编辑 app/views/admin/event_registrations/index.html.erb
加上一个按钮
app/views/admin/event_registrations/index.html.erb
<p class="text-right">
<%= link_to "New Registration", new_admin_event_registration_path(@event), :class => "btn btn-primary" %>
+ <%= link_to "Import Registration", admin_event_registration_imports_path(@event), :class => "btn btn-primary" %>
</p>
执行 rails g controller admin::registration_imports
编辑 app/controllers/admin/registration_imports_controller.rb
- class Admin::RegistrationImportsController < ApplicationController
+ class Admin::RegistrationImportsController < AdminController
+ before_action :require_editor!
+ before_action :find_event
+
+ def index
+ @imports = @event.registration_imports.order("id DESC")
+ end
+
+ def create
+ @import = @event.registration_imports.new(registration_import_params)
+ @import.status = "pending"
+ @import.user = current_user
+
+ if @import.save
+ @import.process!
+ flash[:notice] = "汇入完成"
+ end
+
+ redirect_to admin_event_registration_imports_path(@event)
+ end
+
+ protected
+
+ def find_event
+ @event = Event.find_by_friendly_id!(params[:event_id])
+ end
+
+ def registration_import_params
+ params.require(:registration_import).permit(:csv_file)
+ end
end
在 @import.save
之后,随即呼叫 process!
开始汇入。
另外,⚠️:这里的用户需求第1步和第2步这里并没有实现。
如果要实现的话,标红的4行代码,去掉@import.process!
把process方法单独放入一个controller#action中,并在index.html.erb中添加一个确认的button
在点击button后才会汇入数据到数据库中。
(或者,新增一个Model,数据临时存入这个model层,然后预览一下,该删除的删除,该保留的保留
然后再,存入真正储存数据的Model。)
新增 app/views/admin/registration_imports/index.html.erb
显示档案上传的输入框,以及历史汇入纪录。
<h1><%= @event.name %> / Registrations Import</h1>
<%= form_for [:admin, @event, RegistrationImport.new] do |f| %>
<div class="form-group">
<%= f.label :csv_file %>
<%= f.file_field :csv_file, :required => true, :class => "form-control" %>
</div>
<div class="form-group">
<%= f.submit "送出", :class => "btn btn-primary" %>
</div>
<% end %>
<table class="table">
<tr>
<th>ID</th>
<th>状态</th>
<th>CSV档案</th>
<th>总笔数</th>
<th>汇入成功笔数</th>
<th>错误讯息</th>
</tr>
<% @imports.each do |import| %>
<tr>
<td><%= import.id %></td>
<td><%= import.status %></td>
<td><%= link_to import.csv_file.url, import.csv_file.url %></td>
<td><%= import.total_count %></td>
<td><%= import.success_count %></td>
<td>
<ul>
<% Array(import.error_messages).each do |e| %>
<li><%= e[0] %> ----> <strong><%= e[1] %></strong></li>
<% end %>
</ul>
</td>
</tr>
<% end %>
</table>
这样就完工啦。
![](https://s3-ap-northeast-1.amazonaws.com/ontrackapp-production/wy7rA3aRrSGT7BtGhFlF_25-5.png)