DjangoAdminでcsvをインポートしたかった
DjangoAdminでcsvをインポートする機能を付けたかったのですが、これを自分で実装したら大分面倒だなと思い、パッケージを探していたら良さそうなものがありました。それがこちら。
とても便利なのですが、ドキュメントがそこまで充実しておらず、癖がちょっとあったので、使い方を紹介したいと思います。
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 Author(models.Model):
name = models.CharField(max_length=50, unique=True)
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'
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):
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)
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)
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')
import_order = ('id', 'name', 'status')
import_id_fields = ['id']
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']
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')
import_order = ('id', 'author', 'name', 'status')
import_id_fields = ['id']
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']
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):
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
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
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):
for field in self.get_fields():
names.append(self.get_field_name(field))
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:
row.get(name, '') for name in self.get_field_names()
# Add a column with the error message
import_result.diff.append(
[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
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
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の場合など使っていない要素もあるので、また使う機会があれば、追記していきたいと思います。