こんにちは、クックパッド編集室の加々美です。
現在、食や暮らしのトレンドを発信するメディアであるクックパッドニュースの開発に携わっています。
クックパッドニュースは、1週間に100本以上の記事を配信しています。
このように比較的多くの記事コンテンツを作成する際、記事の基本的なパラメータ(例えば配信時間や記事の執筆者)をWebアプリケーション上で一つ一つ設定して記事を作成するのは時に煩雑な作業になりがちで、特に編集スタッフにとっては、スプレッドシート上で記事のパラメータを設定できた方が分かりやすく、作業がより確実になる場合があるかと思います。
(また、スプレッドシートであれば楽に複数人で編集できるというメリットもあります)
今回は、スプレッドシートからエクスポートしたcsvを用いて、モデルオブジェクトを生成する際に気をつけたことを紹介します。
※ 本稿ではGoogle DriveのスプレッドシートからエクスポートしたCSVを用いることを前提としています。
例として、タイトル(string型)、執筆者(integer型)、公開日時(datetime型)といった属性を持つArticleオブジェクトを生成することを考えてみます。
以下のようなスプレッドシートから、適宜バリデーションなどを行いつつArticle(記事)モデルを生成することを目指します。 執筆者に関して、ArticleモデルはEditor(執筆者)モデルとの関連を持っているものとします。
最低限のバリデーションは、スプレッドシート側で行う
例えば日時など、一定の書式で縛りたい値がある場合、Googleスプレッドシートはエクセル同様、入力された値の書式の検証をすることができます。 メニューから「データ → 検証」を選択し、検証したい書式を設定すれば、特定の行列で異常な値を入力できないようにしたり、警告を表示させることができます。
これによって「2016//01/01」、「10;00」等といった、パースエラーを引き起こしうる不適切な値を入力することを事前に防ぐことができます。
ActiveModelをincludeして、Railに従う
データベースと紐付かないモデルであっても、モデル内でActiveModelをincludeすることで、通常のモデルと同様に、バリデーション等の機能を使うことが出来ます。
以下のCsvArticleモデルは、インポートしたCSVの1行1行が、CsvArticleモデルオブジェクトに対応するように実装しています。 このように実装することで、CsvArticleも通常のモデルと同じようにバリデーションを行いモデルにエラーを追加することで、View上で特定のCsvArticleに発生したエラーの確認をすることができるようになります。
※ 分かりやすくするために実装をある程度簡略化しています
csv_article.rb
require"csv"classCsvArticleincludeActiveModel::Modelattr_accessor:title, :editor_name, :published_at# 通常のモデルと同じようにバリデーションを設定できる validates :title, presence: true validates :editor_name, presence: true validates :published_at, presence: true# editorsテーブルに、入力された執筆者に該当するものがあることをバリデーションする validate :validate_editor_exsistenseclass<< self# CSVを元に生成された、CsvArticleの配列を作成defcreate_list_from_csv(file) csv_articles = [] CSV.foreach(file.path, encoding: "UTF-8", headers: true, converters: :integer) do |row| values = row.to_h published_at = parse_date_time(values["公開日"], values["公開時間"]) csv_articles << CsvArticle.new( title: values["タイトル"], editor_name: values["執筆者"], published_at: published_at, ) end csv_articles endprivatedefparse_date_time(date, time) Time.parse("#{date}#{time}") endenddefconvert_articleArticle.create!( title: title, editor: editor, published_at: published_at, ) endprivatedefeditorreturnnilif editor_name.nil? || @editor_not_found@editor ||= begin# Editorモデルのname attributeが、editor_nameに合致するレコードを取得する editor = Editor.find_by(name: editor_name) @editor_not_found = trueunless editor editor endenddefvalidate_editor_exsistense errors.add(:editor_name, "「#{editor_name}」が存在しません") if editor_name && editor.nil? endend
csv_articles_controller.rb
classCsvArticlesController< ApplicationControllerdefshowenddefcreate@csv_articles = CsvArticle.create_list_from_csv(params[:file]) if@csv_articles.all?(&:valid?) @csv_articles.each do |csv_article| csv_article.convert_article end flash[:ok] = "#{@csv_articles.size}件、作成しました" redirect_to articles_path else render :showendend
show.html.haml
... - if@csv_articles&& @csv_articles.any?(&:invalid?) -# 2 is csv's row start num - @csv_articles.each.with_index(2) do |csv_article, row_num| - if csv_article.invalid? - csv_article.errors.full_messages.each do |message| -# エラーの発生した行と、エラーメッセージの内容を表示 .flash_message.error_message #{row_num}行目: #{message} %table %thead %tr %th タイトル %th 執筆者 %th 公開日 %tbody - @csv_articles.each do |csv_article| %tr -# view上でも以下のようにしてエラーのあったカラムを特定できる %td{ class: ("error"if csv_article.errors.key?(:title)) } = csv_article.title %td{ class: ("error"if csv_article.errors.key?(:editor_name)) } = csv_article.editor_name %td{ class: ("error"if csv_article.errors.key?(:published_at)) } = csv_article.published_at ...
このように、ActiveModelを活用すると、データベースと紐付かないオブジェクトも楽に扱うことができます。
以上、サンプルコードと共に、CSVからモデルオブジェクトを生成する例を紹介しました。