目次
DjangoAdminでcsvをインポートしたかった
DjangoAdminでcsvをインポートする機能を付けたかったのですが、これを自分で実装したら大分面倒だなと思い、パッケージを探していたら良さそうなものがありました。それがこちら。
django-import-export (this link opens in a new window) by django-import-export (this link opens in a new window)
Django application and library for importing and exporting data with admin integration.
とても便利なのですが、ドキュメントがそこまで充実しておらず、癖がちょっとあったので、使い方を紹介したいと思います。
ModelResourceのサブクラスを作成し、ModelAdminのサブクラスで設定する
基本的な使い方は以下のような感じです。
django-import-exportでは、csvの一行目をカラム名として扱います。なので、このカラム名がDjangoのモデルのフィールド名と合っていれば、ModelResourceのサブクラスでモデル名とカラム名を書き、ModelAdminのサブクラスでresource_classとformatsを指定するだけです。
今回は、csvファイルのインポートだけだったので、ImportMixinを指定していますが、これを指定しなければエクスポートも出来ます。また、フォーマットもcsv以外にもyamlやjson、tsvなどの指定も出来ます。
Model
from django.db import models class Book(models.Model): author = models.ForeignKey(Author, null=True, on_delete=models.CASCADE) name = models.CharField(max_length=300) status = models.BooleanField(default=True) class Meta: verbose_name = 'Book' class Author(models.Model): name = models.CharField(max_length=50, unique=True) class Meta: verbose_name = 'Author'
Admin
from django.contrib import admin from import_export.resources import ModelResource from import_export.admin import ImportMixin from import_export.formats import base_formats from books.models import Book class BookResource(ModelResource): class Meta: model = Book import_order = ('id', 'name', 'status') import_id_fields = ['id'] class BookAdmin(ImportMixin, admin.ModelAdmin): list_display = ('id', 'name', 'status') resource_class = BookResource formats = [base_formats.CSV] admin.site.register(Book, BookAdmin)
csvのカラム名とモデルのフィールド名が違う場合
こういうケースが殆どだと思いますが、自分も例外なくcsvのカラム名とモデルのフィールド名が違いまして、ここで一度詰まりました。
そういった場合には、fieldをoverwriteすることで可能でした。やり方はこのような感じで、ModelResource内でfieldを追加してあげます。
from import_export.resources import ModelResource from import_export.fields import Field class BookResource(ModelResource): id = Field(attribute='id', column_name='UUID') name = Field(attribute='name', column_name='Book Name') status = Field(attribute='status', column_name='Status') class Meta: model = Book import_order = ('id', 'name', 'status') import_id_fields = ['id']
このようにFieldを追加してあげて、column_nameにcsv側のカラム名をしてあげることで、簡単にマッチングさせることが出来ました。
ForeignKeyの先にある値とマッチングさせたい
モデル側では、ForeignKeyを指定していて、その先にある値を使ってPrimaryKeyを取ってきてセットしたいということがありました。このパッケージでは、そういったこともとても簡単に出来ます。
from import_export.resources import ModelResource from import_export.fields import Field class BookResource(ModelResource): id = Field(attribute='id', column_name='UUID') author = Field(attribute='author', column_name='Author', widget=ForeignKeyWidget(Author, 'name')) name = Field(attribute='name', column_name='Book Name') status = Field(attribute='status', column_name='Status') class Meta: model = Book import_order = ('id', 'author', 'name', 'status') import_id_fields = ['id']
この例では、csvファイル側ではauthorのidは分からず、authorのnameしか取れないというような状態です。
そこで、既にAuthorモデル側に入っているデータとnameでマッチしたidをForeignKeyWidgetを使用することで引いてくることが出来ました。とても便利ですね。
validationが掛かる前にデータを書き換えたい
django-import-exportでは、インポート前にvalidationが掛かります。もし必要があれば、その前にデータを書き換える必要があります。
例えば、statusをBooleanで設定しているにも関わらず、csv側はOK/NGで書かれているなどです。
そういった場合には、データを読み込み、validationが掛かる前にbefore_import_rowでデータを書き換えましょう。
class BookResource(ModelResource): class Meta: model = Book import_order = ('id', 'name', 'status') import_id_fields = ['id'] def before_import_row(self, row, row_number=None, **kwargs): row['status'] = True if row['status'] == 'OK' else False
before_import_rowは、行毎にデータを判定出来るので、rowにキーを指定してあげれば値を取得出来ます。また、その値をチェックし、上記のように入れ直してあげることで、データの書き換えが可能です。
インポート時にエラーのあった行はスキップする
ForeignKeyが存在しない場合や、フィールドの型が違う場合などでvalidationに引っかかった場合も、それらの行はスキップしてインポートに進みたい場合もあるかと思います。
そのような場合は、確認画面でエラー内容は表示しつつ、インポートまで進めるように以下の行を追加することで可能になります。
class SubModelResource(ModelResource): def get_field_names(self): names = [] for field in self.get_fields(): names.append(self.get_field_name(field)) return names def import_row(self, row, instance_loader, **kwargs): # overriding import_row to ignore errors and skip rows that fail to import # without failing the entire import import_result = super(ModelResource, self).import_row( row, instance_loader, **kwargs ) if import_result.import_type == RowResult.IMPORT_TYPE_ERROR: import_result.diff = [ row.get(name, '') for name in self.get_field_names() ] # Add a column with the error message import_result.diff.append( "Errors: {}".format( [err.error for err in import_result.errors] ) ) # clear errors and mark the record to skip import_result.errors = [] import_result.import_type = RowResult.IMPORT_TYPE_SKIP return import_result
ちょっと癖のあるパッケージですが、使いこなすととても便利です。
他にもManyToManyFieldの場合など使っていない要素もあるので、また使う機会があれば、追記していきたいと思います。