Python Django 零基础破门而入篇(二)
本文介绍如何使用django-import-export 实现以下功能点
1.导入 导出
2.批量更新
3.如何导出model中 ForeignKey 字段的其它的属性值
项目github地址:https://github.com/wangcongxing/OpenTheDoor
如何新建django项目请参照
1.安装 django-import-export
pip install django-import-export
2.配置项目文件下 settings.py
修改 INSTALLED_APPS
默认值为False。它确定库是否会在数据导入中使用数据库事务,以确保安全
新增 IMPORT_EXPORT_USE_TRANSACTIONS = True
3.修改app-->admin.py 设置导出代码列头 代码如下
from django.contrib import admin from app import models from import_export import resources from import_export.admin import ImportExportModelAdmin from django.apps import apps # Register your models here. admin.site.site_header = "大佬管理" admin.site.site_title = "大佬管理后台管理" class bossInfoResource(resources.ModelResource): def __init__(self): super(bossInfoResource, self).__init__() field_list = models.bossInfo._meta.fields self.vname_dict = {} for i in field_list: self.vname_dict[i.name] = i.verbose_name # 默认导入导出field的column_name为字段的名称,这里修改为字段的verbose_name def get_export_fields(self): fields = self.get_fields() for field in fields: field_name = self.get_field_name(field) # 如果我们设置过verbose_name,则将column_name替换为verbose_name。否则维持原有的字段名 if field_name in self.vname_dict.keys(): field.column_name = self.vname_dict[field_name] return fields def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): print("after_import") def after_import_instance(self, instance, new, **kwargs): print("after_import_instance") class Meta: model = models.bossInfo skip_unchanged = True report_skipped = True fields = ( "id", "age", "gender", "likeFruit", "userUrl", "name", "desc", "createTime", "lastTime", "creator", "editor") @admin.register(models.bossInfo) class bossInfoAdmin(ImportExportModelAdmin): fields = ( "name", "age", "gender", "likeFruit", "userUrl", "desc") list_display = ( "name", "age", "gender", "likeFruit", "userUrl", "createTime", "lastTime", "creator", "editor") list_display_links = ("name",) exclude = ("createTime", "creator", "editor") search_fields = ("name",) list_filter = ("gender", "likeFruit") model_icon = "fa fa-tag" list_per_page = 20 ordering = ["-id"] resource_class = bossInfoResource def save_model(self, request, obj, form, change): if form.is_valid(): if change: obj.editor = request.user else: obj.creator = request.user obj.editor = request.user obj.save() super().save_model(request, obj, form, change)
4.运行效果
5.导出excel 存在以下问题
1.创建者 / 修改者 如何显示用户名
2.创建时间 / 修改时间如何进行格式化
3.继续修改admin.py 重写导出 export 完整代码如下
from django.contrib import admin from app import models from import_export import resources from import_export.admin import ImportExportModelAdmin from django.apps import apps from django.db.models import QuerySet import tablib # Register your models here. admin.site.site_header = "大佬管理" admin.site.site_title = "大佬管理后台管理" class bossInfoResource(resources.ModelResource): def __init__(self): super(bossInfoResource, self).__init__() field_list = models.bossInfo._meta.fields self.vname_dict = {} self.fkey = [] for i in field_list: self.vname_dict[i.name] = i.verbose_name self.fkey.append(i.name) # 默认导入导出field的column_name为字段的名称,这里修改为字段的verbose_name def get_export_fields(self): fields = self.get_fields() for field in fields: field_name = self.get_field_name(field) # 如果我们设置过verbose_name,则将column_name替换为verbose_name。否则维持原有的字段名 if field_name in self.vname_dict.keys(): field.column_name = self.vname_dict[field_name] return fields def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): print("after_import") def after_import_instance(self, instance, new, **kwargs): print("after_import_instance") # 重载resources.py的export方法,修改将要导出的data的某些外键相关数据。默认导出外键id,这里修改为导出外键对应的值 def export(self, queryset=None, *args, **kwargs): self.before_export(queryset, *args, **kwargs) if queryset is None: queryset = self.get_queryset() headers = self.get_export_headers() data = tablib.Dataset(headers=headers) # 获取所有外键名称在headers中的位置 fk_index = {} for fk in self.fkey: fk_index[fk] = headers.index(self.vname_dict[fk]) # --------------------- # if isinstance(queryset, QuerySet): # Iterate without the queryset cache, to avoid wasting memory when # exporting large datasets. iterable = queryset.iterator() else: iterable = queryset for obj in iterable: # --------------------- # # 获取将要导出的源数据,这里export_resource返回的是列表,便于更改。替换到外键的值 # 可以对所有字段值进行二次处理 res = self.export_resource(obj) res[fk_index['id']] = obj.id res[fk_index['age']] = obj.age res[fk_index['gender']] = obj.gender res[fk_index['likeFruit']] = obj.likeFruit res[fk_index['userUrl']] = obj.userUrl res[fk_index['name']] = obj.name res[fk_index['desc']] = obj.desc res[fk_index['createTime']] = obj.createTime res[fk_index['lastTime']] = obj.lastTime res[fk_index['creator']] = obj.creator.username res[fk_index['editor']] = obj.editor.username data.append(res) # --------------------- # self.after_export(queryset, data, *args, **kwargs) return data class Meta: model = models.bossInfo skip_unchanged = True report_skipped = True fields = ( "id", "age", "gender", "likeFruit", "userUrl", "name", "desc", "createTime", "lastTime", "creator", "editor") @admin.register(models.bossInfo) class bossInfoAdmin(ImportExportModelAdmin): fields = ( "name", "age", "gender", "likeFruit", "userUrl", "desc") list_display = ( "name", "age", "gender", "likeFruit", "userUrl", "createTime", "lastTime", "creator", "editor") list_display_links = ("name",) exclude = ("createTime", "creator", "editor") search_fields = ("name",) list_filter = ("gender", "likeFruit") model_icon = "fa fa-tag" list_per_page = 20 ordering = ["-id"] resource_class = bossInfoResource def save_model(self, request, obj, form, change): if form.is_valid(): if change: obj.editor = request.user else: obj.creator = request.user obj.editor = request.user obj.save() super().save_model(request, obj, form, change)
最终导出效果
6.导入配置
点击导入
提示都是字段名,用户肯定整不明白的
赶紧改成中文的 解决方法
通过重写导入页面
7.在应用目录(app)下-->templatetags-->apptags.py
8.修改项目settings.py
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], 'libraries': { 'apptags': 'app.templatetags.apptags' }, }, }, ]
9.重写django-import-export 导入页面
在项目templates下新建 admin-->import_export--> import.html
import.html代码如下 将{{ fields|join:", " }} 替换成 apptags.py 定义的 import_head_tag 方法即可
找到项目中import_export 文件夹中
10.在项目中的templates-->admin-->import_export 下把导入导出两个页面都复制过来(export.html import.html)
11.修改import.html 标注背景色的两处
{% extends "admin/import_export/base.html" %} {% load static simpletags apptags %} {% load i18n %} {% load admin_urls %} {% load import_export_tags %} {% load static %} {% block extrastyle %}{{ block.super }} <link rel="stylesheet" type="text/css" href="{% static "import_export/import.css" %}"/>{% endblock %} {% block breadcrumbs_last %} {% trans "Import" %} {% endblock %} {% block content %} {% if confirm_form %} <form action="{% url opts|admin_urlname:"process_import" %}" method="POST"> {% csrf_token %} {{ confirm_form.as_p }} <p> {% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %} </p> <div class="submit-row"> <input type="submit" class="default" name="confirm" value="{% trans "Confirm import" %}"> </div> </form> {% else %} <form action="" method="post" enctype="multipart/form-data"> {% csrf_token %} <p> {% trans "This importer will import the following fields: " %} {# <code>{{ fields|join:", " }}</code> #} <code>{% import_head_tag %}</code> </p> <fieldset class="module aligned"> {% for field in form %} <div class="form-row"> {{ field.errors }} {{ field.label_tag }} {{ field }} {% if field.field.help_text %} <p class="help">{{ field.field.help_text|safe }}</p> {% endif %} </div> {% endfor %} </fieldset> <div class="submit-row"> <input type="submit" class="default" value="{% trans "Submit" %}"> </div> </form> {% endif %} {% if result %} {% if result.has_errors %} <h2>{% trans "Errors" %}</h2> <ul> {% for error in result.base_errors %} <li> {{ error.error }} <div class="traceback">{{ error.traceback|linebreaks }}</div> </li> {% endfor %} {% for line, errors in result.row_errors %} {% for error in errors %} <li> {% trans "Line number" %}: {{ line }} - {{ error.error }} <div><code>{{ error.row.values|join:", " }}</code></div> <div class="traceback">{{ error.traceback|linebreaks }}</div> </li> {% endfor %} {% endfor %} </ul> {% elif result.has_validation_errors %} <h2>{% trans "Some rows failed to validate" %}</h2> <p>{% trans "Please correct these errors in your data where possible, then reupload it using the form above." %}</p> <table class="import-preview"> <thead> <tr> <th>{% trans "Row" %}</th> <th>{% trans "Errors" %}</th> {% for field in result.diff_headers %} <th>{{ field }}</th> {% endfor %} </tr> </thead> <tbody> {% for row in result.invalid_rows %} <tr> <td>{{ row.number }} </td> <td class="errors"> <span class="validation-error-count">{{ row.error_count }}</span> <div class="validation-error-container"> <ul class="validation-error-list"> {% for field_name, error_list in row.field_specific_errors.items %} <li> <span class="validation-error-field-label">{{ field_name }}</span> <ul> {% for error in error_list %} <li>{{ error }}</li> {% endfor %} </ul> </li> {% endfor %} {% if row.non_field_specific_errors %} <li> <span class="validation-error-field-label">{% trans "Non field specific" %}</span> <ul> {% for error in row.non_field_specific_errors %} <li>{{ error }}</li> {% endfor %} </ul> </li> {% endif %} </ul> </div> </td> {% for field in row.values %} <td>{{ field }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> {% else %} <h2>{% trans "Preview" %}</h2> <table class="import-preview"> <thead> <tr> <th></th> {% for field in result.diff_headers %} <th>{{ field }}</th> {% endfor %} </tr> </thead> {% for row in result.valid_rows %} <tr class="{{ row.import_type }}"> <td class="import-type"> {% if row.import_type == 'new' %} {% trans "New" %} {% elif row.import_type == 'skip' %} {% trans "Skipped" %} {% elif row.import_type == 'delete' %} {% trans "Delete" %} {% elif row.import_type == 'update' %} {% trans "Update" %} {% endif %} </td> {% for field in row.diff %} <td>{{ field }}</td> {% endfor %} </tr> {% endfor %} </table> {% endif %} {% endif %} {% endblock %}
12.运行效果
导出包含创建时间,修改时间,创建者,修改者
导入 创建者修改者自动复制 选项值需要特殊处理例如多选
13.自动将当前登录用户 赋值每条导入对象(instance)的 创建者/修改者字段
导入效果
14.最终admin.py代码
import traceback from copy import deepcopy from django.contrib import admin from django.core.exceptions import ValidationError from django.db.transaction import TransactionManagementError from django.utils.encoding import force_text from import_export.results import RowResult from app import models from import_export import resources from import_export.admin import ImportExportModelAdmin from django.apps import apps from django.db.models import QuerySet import tablib # Register your models here. admin.site.site_header = "大佬管理" admin.site.site_title = "大佬管理后台管理" class bossInfoResource(resources.ModelResource): def __init__(self): super(bossInfoResource, self).__init__() field_list = models.bossInfo._meta.fields self.vname_dict = {} self.fkey = [] customeFields = ( "id", "age", "gender", "likeFruit", "userUrl", "name", "desc",) for i in field_list: if i.name in customeFields: self.vname_dict[i.name] = i.verbose_name.lower() self.fkey.append(i.name) # 默认导入导出field的column_name为字段的名称,这里修改为字段的verbose_name def get_export_fields(self): fields = self.get_fields() for field in fields: field_name = self.get_field_name(field) # 如果我们设置过verbose_name,则将column_name替换为verbose_name。否则维持原有的字段名 if field_name in self.vname_dict.keys(): field.column_name = self.vname_dict[field_name] return fields def after_import(self, dataset, result, using_transactions, dry_run, **kwargs): print("after_import") def after_import_instance(self, instance, new, **kwargs): print("after_import_instance") # 重载resources.py的export方法,修改将要导出的data的某些外键相关数据。默认导出外键id,这里修改为导出外键对应的值 def export(self, queryset=None, *args, **kwargs): self.before_export(queryset, *args, **kwargs) if queryset is None: queryset = self.get_queryset() headers = self.get_export_headers() headers.append("创建时间") headers.append("修改时间") headers.append("创建者") headers.append("修改者") data = tablib.Dataset(headers=headers) # 获取所有外键名称在headers中的位置 fk_index = {} for fk in self.fkey: fk_index[fk] = headers.index(self.vname_dict[fk]) # --------------------- # if isinstance(queryset, QuerySet): # Iterate without the queryset cache, to avoid wasting memory when # exporting large datasets. iterable = queryset.iterator() else: iterable = queryset for obj in iterable: # --------------------- # # 获取将要导出的源数据,这里export_resource返回的是列表,便于更改。替换到外键的值 # 可以对所有字段值进行二次处理 res = self.export_resource(obj) res[fk_index['id']] = obj.id res[fk_index['age']] = obj.age res[fk_index['gender']] = obj.gender res[fk_index['likeFruit']] = obj.likeFruit res[fk_index['userUrl']] = obj.userUrl res[fk_index['name']] = obj.name res[fk_index['desc']] = obj.desc res.append(obj.createTime) res.append(obj.lastTime) res.append(obj.creator.username) res.append(obj.editor.username) data.append(res) # --------------------- # self.after_export(queryset, data, *args, **kwargs) return data # 导入前 def before_import_row(self, row, **kwargs): print(row) # 导入后(执行更新创建者修改) def after_import_row(self, row, row_result, **kwargs): print(row) # 导入数据 自动将当前登录用户 赋值给创建者和修改者 def import_row(self, row, instance_loader, using_transactions=True, dry_run=False, **kwargs): print(instance_loader) row_result = self.get_row_result_class()() try: self.before_import_row(row, **kwargs) instance, new = self.get_or_init_instance(instance_loader, row) self.after_import_instance(instance, new, **kwargs) if new: row_result.import_type = RowResult.IMPORT_TYPE_NEW else: row_result.import_type = RowResult.IMPORT_TYPE_UPDATE row_result.new_record = new original = deepcopy(instance) diff = self.get_diff_class()(self, original, new) if self.for_delete(row, instance): if new: row_result.import_type = RowResult.IMPORT_TYPE_SKIP diff.compare_with(self, None, dry_run) else: row_result.import_type = RowResult.IMPORT_TYPE_DELETE self.delete_instance(instance, using_transactions, dry_run) diff.compare_with(self, None, dry_run) else: import_validation_errors = {} try: self.import_obj(instance, row, dry_run) except ValidationError as e: # Validation errors from import_obj() are passed on to # validate_instance(), where they can be combined with model # instance validation errors if necessary import_validation_errors = e.update_error_dict(import_validation_errors) if self.skip_row(instance, original): row_result.import_type = RowResult.IMPORT_TYPE_SKIP else: self.validate_instance(instance, import_validation_errors) print(instance) print("dry_run", dry_run) if instance.creator is None: instance.creator = kwargs["user"] instance.editor = kwargs["user"] self.save_instance(instance, using_transactions, dry_run) self.save_m2m(instance, row, using_transactions, dry_run) # Add object info to RowResult for LogEntry row_result.object_id = instance.pk row_result.object_repr = force_text(instance) diff.compare_with(self, instance, dry_run) row_result.diff = diff.as_html() self.after_import_row(row, row_result, **kwargs) except ValidationError as e: row_result.import_type = RowResult.IMPORT_TYPE_INVALID row_result.validation_error = e except Exception as e: row_result.import_type = RowResult.IMPORT_TYPE_ERROR # There is no point logging a transaction error for each row # when only the original error is likely to be relevant if not isinstance(e, TransactionManagementError): print(e) tb_info = traceback.format_exc() row_result.errors.append(self.get_error_result_class()(e, tb_info, row)) return row_result class Meta: model = models.bossInfo skip_unchanged = True report_skipped = True fields = ( "id", "age", "gender", "likeFruit", "userUrl", "name", "desc",) @admin.register(models.bossInfo) class bossInfoAdmin(ImportExportModelAdmin): fields = ( "name", "age", "gender", "likeFruit", "userUrl", "desc") list_display = ( "name", "age", "gender", "likeFruit", "userUrl", "createTime", "lastTime", "creator", "editor") list_display_links = ("name",) exclude = ("createTime", "creator", "editor") search_fields = ("name",) list_filter = ("gender", "likeFruit") model_icon = "fa fa-tag" list_per_page = 20 ordering = ["-id"] resource_class = bossInfoResource def save_model(self, request, obj, form, change): if form.is_valid(): if change: obj.editor = request.user else: obj.creator = request.user obj.editor = request.user obj.save() super().save_model(request, obj, form, change)
15.给导入导出页面 (export.html import.html) 新增返回按钮
{% extends "admin/import_export/base.html" %} {% load static simpletags apptags %} {% load i18n %} {% load admin_urls %} {% load import_export_tags %} {% load static %} {% block extrastyle %}{{ block.super }} <link rel="stylesheet" type="text/css" href="{% static "import_export/import.css" %}"/>{% endblock %} {% block breadcrumbs_last %} {% trans "Import" %} {% endblock %} {% block content %} {% if confirm_form %} <form action="{% url opts|admin_urlname:"process_import" %}" method="POST"> {% csrf_token %} {{ confirm_form.as_p }} <p> {% trans "Below is a preview of data to be imported. If you are satisfied with the results, click 'Confirm import'" %} </p> <div class="submit-row"> <input type="submit" class="default" name="confirm" value="{% trans "Confirm import" %}"> </div> </form> {% else %} <form action="" method="post" enctype="multipart/form-data"> {% csrf_token %} <p> {% trans "This importer will import the following fields: " %} {# <code>{{ fields|join:", " }}</code> #} <code>{% import_head_tag %}</code> </p> <fieldset class="module aligned"> {% for field in form %} <div class="form-row"> {{ field.errors }} {{ field.label_tag }} {{ field }} {% if field.field.help_text %} <p class="help">{{ field.field.help_text|safe }}</p> {% endif %} </div> {% endfor %} </fieldset> <div class="submit-row"> <input type="button" class="el-button el-button--default is-plain" style="float: left" onclick="javascript:window.history.go(-1)" value="返回"/> <input type="submit" class="default" value="{% trans "Submit" %}"> </div> </form> {% endif %} {% if result %} {% if result.has_errors %} <h2>{% trans "Errors" %}</h2> <ul> {% for error in result.base_errors %} <li> {{ error.error }} <div class="traceback">{{ error.traceback|linebreaks }}</div> </li> {% endfor %} {% for line, errors in result.row_errors %} {% for error in errors %} <li> {% trans "Line number" %}: {{ line }} - {{ error.error }} <div><code>{{ error.row.values|join:", " }}</code></div> <div class="traceback">{{ error.traceback|linebreaks }}</div> </li> {% endfor %} {% endfor %} </ul> {% elif result.has_validation_errors %} <h2>{% trans "Some rows failed to validate" %}</h2> <p>{% trans "Please correct these errors in your data where possible, then reupload it using the form above." %}</p> <table class="import-preview"> <thead> <tr> <th>{% trans "Row" %}</th> <th>{% trans "Errors" %}</th> {% for field in result.diff_headers %} <th>{{ field }}</th> {% endfor %} </tr> </thead> <tbody> {% for row in result.invalid_rows %} <tr> <td>{{ row.number }} </td> <td class="errors"> <span class="validation-error-count">{{ row.error_count }}</span> <div class="validation-error-container"> <ul class="validation-error-list"> {% for field_name, error_list in row.field_specific_errors.items %} <li> <span class="validation-error-field-label">{{ field_name }}</span> <ul> {% for error in error_list %} <li>{{ error }}</li> {% endfor %} </ul> </li> {% endfor %} {% if row.non_field_specific_errors %} <li> <span class="validation-error-field-label">{% trans "Non field specific" %}</span> <ul> {% for error in row.non_field_specific_errors %} <li>{{ error }}</li> {% endfor %} </ul> </li> {% endif %} </ul> </div> </td> {% for field in row.values %} <td>{{ field }}</td> {% endfor %} </tr> {% endfor %} </tbody> </table> {% else %} <h2>{% trans "Preview" %}</h2> <table class="import-preview"> <thead> <tr> <th></th> {% for field in result.diff_headers %} <th>{{ field }}</th> {% endfor %} </tr> </thead> {% for row in result.valid_rows %} <tr class="{{ row.import_type }}"> <td class="import-type"> {% if row.import_type == 'new' %} {% trans "New" %} {% elif row.import_type == 'skip' %} {% trans "Skipped" %} {% elif row.import_type == 'delete' %} {% trans "Delete" %} {% elif row.import_type == 'update' %} {% trans "Update" %} {% endif %} </td> {% for field in row.diff %} <td>{{ field }}</td> {% endfor %} </tr> {% endfor %} </table> {% endif %} {% endif %} {% endblock %}
14.项目运行安装包版本
需要留意tablib 库版本