我想使用 django-import-export 的表单来为普通用户实现导入功能,因此它需要位于管理部分之外。
到目前为止,我找到的所有实现都是关于
a) 扩展管理内部的功能或
b) 在管理员之外重新实现默认视图。
但是由于默认表单工作完美(特别是ConfirmImportForm,它显示了旧记录和尝试导入的新记录之间的差异),我想将它们子类化为我的项目的一部分(在管理之外),而不需要重新实现整个视图的逻辑,如果这样的事情是可能的话。
到目前为止,我尝试(恐怕是愚蠢的)将 import_export.admin.ImportMixin 子类化为两个独立的类视图,以导入带有资源 PeriodResource 的模型 Period。方法import_action和process_import被重新实现(基本上复制并粘贴相同的代码并使用self.site_admin消除任何代码)作为View.get()和View.post():
# staging/views.py
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse
from django.views import generic
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.utils.encoding import force_text
from import_export.forms import ConfirmImportForm
from import_export.signals import post_import
import import_export.admin
from .models import Period
from .resources import PeriodResource
class PeriodImportView(import_export.admin.ImportMixin, generic.View):
"""
Subclassing of ImportMixin as a generic View implementing ImportForm
"""
#: template for import view
import_template_name = 'period/import.html'
#: resource class
resource_class = PeriodResource
#: model to be imported
model = Period
def get_confirm_import_form(self):
'''
Get the form type used to display the results and confirm the upload.
'''
return ConfirmImportForm
def get(self, request, *args, **kwargs):
"""
Overriding the GET part of ImportMixin.import_action method to be used without site_admin
"""
resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))
context = self.get_import_context_data()
import_formats = self.get_import_formats()
form_type = self.get_import_form()
form = form_type(import_formats,
request.POST or None,
request.FILES or None)
# context.update(self.admin_site.each_context(request))
context['title'] = _("Import")
context['form'] = form
context['opts'] = self.model._meta
context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]
# request.current_app = self.admin_site.name
return TemplateResponse(request, [self.import_template_name],
context)
def post(self, request, *args, **kwargs):
"""
Overriding the POST part of ImportMixin.import_action method to be used without site_admin
"""
resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))
context = self.get_import_context_data()
import_formats = self.get_import_formats()
form_type = self.get_import_form()
form = form_type(import_formats,
request.POST or None,
request.FILES or None)
if request.POST and form.is_valid():
input_format = import_formats[
int(form.cleaned_data['input_format'])
]()
import_file = form.cleaned_data['import_file']
# first always write the uploaded file to disk as it may be a
# memory file or else based on settings upload handlers
tmp_storage = self.write_to_tmp_storage(import_file, input_format)
# then read the file, using the proper format-specific mode
# warning, big files may exceed memory
try:
data = tmp_storage.read(input_format.get_read_mode())
if not input_format.is_binary() and self.from_encoding:
data = force_text(data, self.from_encoding)
dataset = input_format.create_dataset(data)
except UnicodeDecodeError as ex1:
return HttpResponse(_(u"<h1>Imported file has a wrong encoding: %s</h1>" % ex1))
except Exception as ex2:
return HttpResponse(_(u"<h1>%s encountered while trying to read file: %s</h1>" % (type(ex2).__name__, import_file.name)))
result = resource.import_data(dataset, dry_run=True,
raise_errors=False,
file_name=import_file.name,
user=request.user)
context['result'] = result
if not result.has_errors() and not result.has_validation_errors():
context['confirm_form'] = self.get_confirm_import_form()(initial={
'import_file_name': tmp_storage.name,
'original_file_name': import_file.name,
'input_format': form.cleaned_data['input_format'],
})
# context.update(self.admin_site.each_context(request))
context['title'] = _("Import")
context['form'] = form
context['opts'] = self.model._meta
context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]
# request.current_app = self.admin_site.name
return TemplateResponse(request, [self.import_template_name],
context)
class PeriodConfirmImportView(import_export.admin.ImportMixin, generic.View):
"""
Subclassing of ImportMixin as a generic View implementing ConfirmImportForm
"""
#: template for import view
import_template_name = 'period/import.html'
#: resource class
resource_class = PeriodResource
#: model to be imported
model = Period
def post(self, request, *args, **kwargs):
"""
Perform the actual import action (after the user has confirmed the import)
"""
# if not self.has_import_permission(request):
# raise PermissionDenied
confirm_form = ConfirmImportForm(request.POST)
if confirm_form.is_valid():
import_formats = self.get_import_formats()
input_format = import_formats[
int(confirm_form.cleaned_data['input_format'])
]()
tmp_storage = self.get_tmp_storage_class()(name=confirm_form.cleaned_data['import_file_name'])
data = tmp_storage.read(input_format.get_read_mode())
if not input_format.is_binary() and self.from_encoding:
data = force_text(data, self.from_encoding)
dataset = input_format.create_dataset(data)
result = self.process_dataset(dataset, confirm_form, request, *args, **kwargs)
tmp_storage.remove()
self.generate_log_entries(result, request)
self.add_success_message(result, request)
post_import.send(sender=None, model=self.model)
url = reverse('staging:index')
return HttpResponseRedirect(url)
然后只显示模板中的表单:
# staging/templates/period/import.html
{% if confirm_form %}
<form action="{% url 'staging:confirm_import_period' %}" 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>
</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 %}
我的 urls.py 看起来像这样:
# staging/urls.py
from django.urls import path
from .views import PeriodIndexView, PeriodImportView, PeriodConfirmImportView
app_name = 'staging'
urlpatterns = [
path('period/', PeriodIndexView.as_view(), name='index'),
path('period/import/', PeriodImportView.as_view(), name='import_period'),
path('period/confirm_import/', PeriodConfirmImportView.as_view(), name='confirm_import_period'),
]
到目前为止,它按预期工作,但这种方法与 ImportMixin 的内部实现紧密耦合,恐怕它无法在 django-import-export 的任何版本升级中生存。
有没有办法在不重新实现整个 import_action 和 process_import 方法的情况下实现这一点?
经过多次尝试和错误,我放弃了避免重新实现 import_export.admin.ImportMixin 中的方法 import_action 和 process_import。相反,我创建了自己的 mixins 子类化 import_export.admin.ImportMixin 和 django.views.generic.View 并将所有对 self.site_admin 的引用从方法 import_action 和 process_import 删除到等效方法 get( ) 和 post().
# staging/views.py
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse
from django.views import generic
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.utils.encoding import force_text
from import_export.forms import ConfirmImportForm
from import_export.signals import post_import
import import_export.admin
from .models import Period
from .resources import PeriodResource
class ImportView(import_export.admin.ImportMixin, generic.View):
"""
Subclassing of ImportMixin as a generic View implementing ImportForm
"""
#: template for import view
import_template_name = 'import.html'
#: resource class
resource_class = None
#: model to be imported
model = None
def get_confirm_import_form(self):
'''
Get the form type used to display the results and confirm the upload.
'''
return ConfirmImportForm
def get(self, request, *args, **kwargs):
"""
Overriding the GET part of ImportMixin.import_action method to be used without site_admin
"""
return self.post(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
Overriding the POST part of ImportMixin.import_action method to be used without site_admin
"""
resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request,
*args,
**kwargs))
context = self.get_import_context_data()
import_formats = self.get_import_formats()
form = self.get_import_form()(import_formats, request.POST or None, request.FILES or None)
if request.POST and form.is_valid():
input_format = import_formats[
int(form.cleaned_data['input_format'])
]()
import_file = form.cleaned_data['import_file']
# first always write the uploaded file to disk as it may be a
# memory file or else based on settings upload handlers
tmp_storage = self.write_to_tmp_storage(import_file, input_format)
# then read the file, using the proper format-specific mode
# warning, big files may exceed memory
try:
data = tmp_storage.read(input_format.get_read_mode())
if not input_format.is_binary() and self.from_encoding:
data = force_text(data, self.from_encoding)
dataset = input_format.create_dataset(data)
except UnicodeDecodeError as ex1:
return HttpResponse(_(u"<h1>Imported file has a wrong encoding: %s</h1>" % ex1))
except Exception as ex2:
return HttpResponse(_(u"<h1>%s encountered while trying to read file: %s</h1>" % (type(ex2).__name__, import_file.name)))
result = resource.import_data(dataset, dry_run=True,
raise_errors=False,
file_name=import_file.name,
user=request.user)
context['result'] = result
if not result.has_errors() and not result.has_validation_errors():
context['confirm_form'] = self.get_confirm_import_form()(initial={
'import_file_name': tmp_storage.name,
'original_file_name': import_file.name,
'input_format': form.cleaned_data['input_format'],
})
# context.update(self.admin_site.each_context(request))
context['title'] = _("Import " + self.get_model_info()[1])
context['form'] = form
context['opts'] = self.model._meta
context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]
# request.current_app = self.admin_site.name
return TemplateResponse(request, [self.import_template_name],
context)
class ConfirmImportView(import_export.admin.ImportMixin, generic.View):
"""
Subclassing of ImportMixin as a generic View implementing ConfirmImportForm
"""
#: template for import view
import_template_name = 'import.html'
#: resource class
resource_class = None
#: model to be imported
model = None
def get_confirm_import_form(self):
'''
Get the form type used to display the results and confirm the upload.
'''
return ConfirmImportForm
def post(self, request, *args, **kwargs):
"""
Perform the actual import action (after the user has confirmed the import)
"""
confirm_form = self.get_confirm_import_form()(request.POST)
if confirm_form.is_valid():
import_formats = self.get_import_formats()
input_format = import_formats[
int(confirm_form.cleaned_data['input_format'])
]()
tmp_storage = self.get_tmp_storage_class()(name=confirm_form.cleaned_data['import_file_name'])
data = tmp_storage.read(input_format.get_read_mode())
if not input_format.is_binary() and self.from_encoding:
data = force_text(data, self.from_encoding)
dataset = input_format.create_dataset(data)
result = self.process_dataset(dataset, confirm_form, request, *args, **kwargs)
tmp_storage.remove()
self.generate_log_entries(result, request)
self.add_success_message(result, request)
post_import.send(sender=None, model=self.model)
url = reverse('{}:{}_index'.format(self.get_model_info()[0], self.get_model_info()[1]))
return HttpResponseRedirect(url)
所以现在从我的自定义 mixins ImportView 和 ConfirmImportView 中,您可以对特定类进行子类化以导入我的特定模型,只需设置 model 和 resource_class 属性,这正是我所寻找的。
class PeriodImportView(ImportView):
"""
ImportView specific for model Period and resource PeriodResource
"""
#: resource class
resource_class = PeriodResource
#: model to be imported
model = Period
class PeriodConfirmImportView(ConfirmImportView):
"""
ConfirmImportView specific for model Period and resource PeriodResource
"""
#: resource class
resource_class = PeriodResource
#: model to be imported
model = Period