importexport.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. #!/usr/bin/env python
  2. # encoding=utf-8
  3. """
  4. Author:zcyuefan
  5. Topic:django-import-export plugin for xadmin to help importing and exporting data using .csv/.xls/.../.json files
  6. Use:
  7. +++ settings.py +++
  8. INSTALLED_APPS = (
  9. ...
  10. 'import_export',
  11. )
  12. +++ model.py +++
  13. from django.db import models
  14. class Foo(models.Model):
  15. name = models.CharField(max_length=64)
  16. description = models.TextField()
  17. +++ adminx.py +++
  18. import xadmin
  19. from import_export import resources
  20. from .models import Foo
  21. class FooResource(resources.ModelResource):
  22. class Meta:
  23. model = Foo
  24. # fields = ('name', 'description',)
  25. # exclude = ()
  26. @xadmin.sites.register(Foo)
  27. class FooAdmin(object):
  28. import_export_args = {'import_resource_class': FooResource, 'export_resource_class': FooResource}
  29. ++++++++++++++++
  30. More info about django-import-export please refer https://github.com/django-import-export/django-import-export
  31. """
  32. from datetime import datetime
  33. from django.template import loader
  34. from xadmin.plugins.utils import get_context_dict
  35. from xadmin.sites import site
  36. from xadmin.views import BaseAdminPlugin, ListAdminView, ModelAdminView
  37. from xadmin.views.base import csrf_protect_m, filter_hook
  38. from django.db import transaction
  39. from import_export.admin import DEFAULT_FORMATS, SKIP_ADMIN_LOG, TMP_STORAGE_CLASS
  40. from import_export.resources import modelresource_factory
  41. from import_export.forms import (
  42. ImportForm,
  43. ConfirmImportForm,
  44. ExportForm,
  45. )
  46. from import_export.results import RowResult
  47. from import_export.signals import post_export, post_import
  48. try:
  49. from django.utils.encoding import force_text
  50. except ImportError:
  51. from django.utils.encoding import force_unicode as force_text
  52. from django.utils.translation import ugettext_lazy as _
  53. from django.template.response import TemplateResponse
  54. from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
  55. from django.contrib.contenttypes.models import ContentType
  56. from django.contrib import messages
  57. from django.urls.base import reverse
  58. from django.core.exceptions import PermissionDenied
  59. from django.http import HttpResponseRedirect, HttpResponse
  60. class ImportMenuPlugin(BaseAdminPlugin):
  61. import_export_args = {}
  62. def init_request(self, *args, **kwargs):
  63. return bool(self.import_export_args.get('import_resource_class'))
  64. def block_top_toolbar(self, context, nodes):
  65. has_change_perm = self.has_model_perm(self.model, 'change')
  66. has_add_perm = self.has_model_perm(self.model, 'add')
  67. if has_change_perm and has_add_perm:
  68. model_info = (self.opts.app_label, self.opts.model_name)
  69. import_url = reverse('xadmin:%s_%s_import' % model_info, current_app=self.admin_site.name)
  70. context = get_context_dict(context or {}) # no error!
  71. context.update({
  72. 'import_url': import_url,
  73. })
  74. nodes.append(loader.render_to_string('xadmin/blocks/model_list.top_toolbar.importexport.import.html',
  75. context=context))
  76. class ImportBaseView(ModelAdminView):
  77. """
  78. """
  79. resource_class = None
  80. import_export_args = {}
  81. #: template for import view
  82. import_template_name = 'xadmin/import_export/import.html'
  83. #: resource class
  84. #: available import formats
  85. formats = DEFAULT_FORMATS
  86. #: import data encoding
  87. from_encoding = "utf-8"
  88. skip_admin_log = None
  89. # storage class for saving temporary files
  90. tmp_storage_class = None
  91. def get_skip_admin_log(self):
  92. if self.skip_admin_log is None:
  93. return SKIP_ADMIN_LOG
  94. else:
  95. return self.skip_admin_log
  96. def get_tmp_storage_class(self):
  97. if self.tmp_storage_class is None:
  98. return TMP_STORAGE_CLASS
  99. else:
  100. return self.tmp_storage_class
  101. def get_resource_kwargs(self, request, *args, **kwargs):
  102. return {}
  103. def get_import_resource_kwargs(self, request, *args, **kwargs):
  104. return self.get_resource_kwargs(request, *args, **kwargs)
  105. def get_resource_class(self, usage):
  106. if usage == 'import':
  107. return self.import_export_args.get('import_resource_class') if self.import_export_args.get(
  108. 'import_resource_class') else modelresource_factory(self.model)
  109. elif usage == 'export':
  110. return self.import_export_args.get('export_resource_class') if self.import_export_args.get(
  111. 'export_resource_class') else modelresource_factory(self.model)
  112. else:
  113. return modelresource_factory(self.model)
  114. def get_import_resource_class(self):
  115. """
  116. Returns ResourceClass to use for import.
  117. """
  118. return self.process_import_resource(self.get_resource_class(usage='import'))
  119. def process_import_resource(self, resource):
  120. """
  121. Returns processed ResourceClass to use for import.
  122. Override to custom your own process
  123. """
  124. return resource
  125. def get_import_formats(self):
  126. """
  127. Returns available import formats.
  128. """
  129. return [f for f in self.formats if f().can_import()]
  130. class ImportView(ImportBaseView):
  131. def get_media(self):
  132. media = super(ImportView, self).get_media()
  133. media = media + self.vendor('xadmin.plugin.importexport.css')
  134. return media
  135. @filter_hook
  136. def get(self, request, *args, **kwargs):
  137. if not (self.has_change_permission() and self.has_add_permission()):
  138. raise PermissionDenied
  139. resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))
  140. context = super(ImportView, self).get_context()
  141. import_formats = self.get_import_formats()
  142. form = ImportForm(import_formats,
  143. request.POST or None,
  144. request.FILES or None)
  145. context['title'] = _("Import") + ' ' + self.opts.verbose_name
  146. context['form'] = form
  147. context['opts'] = self.model._meta
  148. context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]
  149. request.current_app = self.admin_site.name
  150. return TemplateResponse(request, [self.import_template_name],
  151. context)
  152. @filter_hook
  153. @csrf_protect_m
  154. @transaction.atomic
  155. def post(self, request, *args, **kwargs):
  156. """
  157. Perform a dry_run of the import to make sure the import will not
  158. result in errors. If there where no error, save the user
  159. uploaded file to a local temp file that will be used by
  160. 'process_import' for the actual import.
  161. """
  162. if not (self.has_change_permission() and self.has_add_permission()):
  163. raise PermissionDenied
  164. resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))
  165. context = super(ImportView, self).get_context()
  166. import_formats = self.get_import_formats()
  167. form = ImportForm(import_formats,
  168. request.POST or None,
  169. request.FILES or None)
  170. if request.POST and form.is_valid():
  171. input_format = import_formats[
  172. int(form.cleaned_data['input_format'])
  173. ]()
  174. import_file = form.cleaned_data['import_file']
  175. # first always write the uploaded file to disk as it may be a
  176. # memory file or else based on settings upload handlers
  177. tmp_storage = self.get_tmp_storage_class()()
  178. data = bytes()
  179. for chunk in import_file.chunks():
  180. data += chunk
  181. tmp_storage.save(data, input_format.get_read_mode())
  182. # then read the file, using the proper format-specific mode
  183. # warning, big files may exceed memory
  184. try:
  185. data = tmp_storage.read(input_format.get_read_mode())
  186. if not input_format.is_binary() and self.from_encoding:
  187. data = force_text(data, self.from_encoding)
  188. dataset = input_format.create_dataset(data)
  189. except UnicodeDecodeError as e:
  190. return HttpResponse(_(u"<h1>Imported file has a wrong encoding: %s</h1>" % e))
  191. except Exception as e:
  192. return HttpResponse(_(u"<h1>%s encountered while trying to read file: %s</h1>" % (type(e).__name__,
  193. import_file.name)))
  194. result = resource.import_data(dataset, dry_run=True,
  195. raise_errors=False,
  196. file_name=import_file.name,
  197. user=request.user)
  198. context['result'] = result
  199. if not result.has_errors():
  200. context['confirm_form'] = ConfirmImportForm(initial={
  201. 'import_file_name': tmp_storage.name,
  202. 'original_file_name': import_file.name,
  203. 'input_format': form.cleaned_data['input_format'],
  204. })
  205. context['title'] = _("Import") + ' ' + self.opts.verbose_name
  206. context['form'] = form
  207. context['opts'] = self.model._meta
  208. context['fields'] = [f.column_name for f in resource.get_user_visible_fields()]
  209. request.current_app = self.admin_site.name
  210. return TemplateResponse(request, [self.import_template_name],
  211. context)
  212. class ImportProcessView(ImportBaseView):
  213. @filter_hook
  214. @csrf_protect_m
  215. @transaction.atomic
  216. def post(self, request, *args, **kwargs):
  217. """
  218. Perform the actual import action (after the user has confirmed he
  219. wishes to import)
  220. """
  221. resource = self.get_import_resource_class()(**self.get_import_resource_kwargs(request, *args, **kwargs))
  222. confirm_form = ConfirmImportForm(request.POST)
  223. if confirm_form.is_valid():
  224. import_formats = self.get_import_formats()
  225. input_format = import_formats[
  226. int(confirm_form.cleaned_data['input_format'])
  227. ]()
  228. tmp_storage = self.get_tmp_storage_class()(name=confirm_form.cleaned_data['import_file_name'])
  229. data = tmp_storage.read(input_format.get_read_mode())
  230. if not input_format.is_binary() and self.from_encoding:
  231. data = force_text(data, self.from_encoding)
  232. dataset = input_format.create_dataset(data)
  233. result = resource.import_data(dataset, dry_run=False,
  234. raise_errors=True,
  235. file_name=confirm_form.cleaned_data['original_file_name'],
  236. user=request.user)
  237. if not self.get_skip_admin_log():
  238. # Add imported objects to LogEntry
  239. logentry_map = {
  240. RowResult.IMPORT_TYPE_NEW: ADDITION,
  241. RowResult.IMPORT_TYPE_UPDATE: CHANGE,
  242. RowResult.IMPORT_TYPE_DELETE: DELETION,
  243. }
  244. content_type_id = ContentType.objects.get_for_model(self.model).pk
  245. for row in result:
  246. if row.import_type != row.IMPORT_TYPE_ERROR and row.import_type != row.IMPORT_TYPE_SKIP:
  247. LogEntry.objects.log_action(
  248. user_id=request.user.pk,
  249. content_type_id=content_type_id,
  250. object_id=row.object_id,
  251. object_repr=row.object_repr,
  252. action_flag=logentry_map[row.import_type],
  253. change_message="%s through import_export" % row.import_type,
  254. )
  255. success_message = str(_(u'Import finished')) + ' , ' + str(_(u'Add')) + ' : %d' % result.totals[
  256. RowResult.IMPORT_TYPE_NEW] + ' , ' + str(_(u'Update')) + ' : %d' % result.totals[
  257. RowResult.IMPORT_TYPE_UPDATE]
  258. messages.success(request, success_message)
  259. tmp_storage.remove()
  260. post_import.send(sender=None, model=self.model)
  261. model_info = (self.opts.app_label, self.opts.model_name)
  262. url = reverse('xadmin:%s_%s_changelist' % model_info,
  263. current_app=self.admin_site.name)
  264. return HttpResponseRedirect(url)
  265. class ExportMixin(object):
  266. #: resource class
  267. resource_class = None
  268. #: template for change_list view
  269. change_list_template = None
  270. import_export_args = {}
  271. #: template for export view
  272. # export_template_name = 'xadmin/import_export/export.html'
  273. #: available export formats
  274. formats = DEFAULT_FORMATS
  275. #: export data encoding
  276. to_encoding = "utf-8"
  277. list_select_related = None
  278. def get_resource_kwargs(self, request, *args, **kwargs):
  279. return {}
  280. def get_export_resource_kwargs(self, request, *args, **kwargs):
  281. return self.get_resource_kwargs(request, *args, **kwargs)
  282. def get_resource_class(self, usage):
  283. if usage == 'import':
  284. return self.import_export_args.get('import_resource_class') if self.import_export_args.get(
  285. 'import_resource_class') else modelresource_factory(self.model)
  286. elif usage == 'export':
  287. return self.import_export_args.get('export_resource_class') if self.import_export_args.get(
  288. 'export_resource_class') else modelresource_factory(self.model)
  289. else:
  290. return modelresource_factory(self.model)
  291. def get_export_resource_class(self):
  292. """
  293. Returns ResourceClass to use for export.
  294. """
  295. return self.get_resource_class(usage='export')
  296. def get_export_formats(self):
  297. """
  298. Returns available export formats.
  299. """
  300. return [f for f in self.formats if f().can_export()]
  301. def get_export_filename(self, file_format):
  302. date_str = datetime.now().strftime('%Y-%m-%d-%H%M%S')
  303. filename = "%s-%s.%s" % (self.opts.verbose_name.encode('utf-8'),
  304. date_str,
  305. file_format.get_extension())
  306. return filename
  307. def get_export_queryset(self, request, context):
  308. """
  309. Returns export queryset.
  310. Default implementation respects applied search and filters.
  311. """
  312. # scope = self.request.POST.get('_select_across', False) == '1'
  313. scope = request.GET.get('scope')
  314. select_across = request.GET.get('_select_across', False) == '1'
  315. selected = request.GET.get('_selected_actions', '')
  316. if scope == 'all':
  317. queryset = self.admin_view.queryset()
  318. elif scope == 'header_only':
  319. queryset = []
  320. elif scope == 'selected':
  321. if not select_across:
  322. selected_pk = selected.split(',')
  323. queryset = self.admin_view.queryset().filter(pk__in=selected_pk)
  324. else:
  325. queryset = self.admin_view.queryset()
  326. else:
  327. queryset = [r['object'] for r in context['results']]
  328. return queryset
  329. def get_export_data(self, file_format, queryset, *args, **kwargs):
  330. """
  331. Returns file_format representation for given queryset.
  332. """
  333. request = kwargs.pop("request")
  334. resource_class = self.get_export_resource_class()
  335. data = resource_class(**self.get_export_resource_kwargs(request)).export(queryset, *args, **kwargs)
  336. export_data = file_format.export_data(data)
  337. return export_data
  338. class ExportMenuPlugin(ExportMixin, BaseAdminPlugin):
  339. import_export_args = {}
  340. # Media
  341. def get_media(self, media):
  342. return media + self.vendor('xadmin.plugin.importexport.css', 'xadmin.plugin.importexport.js')
  343. def init_request(self, *args, **kwargs):
  344. return bool(self.import_export_args.get('export_resource_class'))
  345. def block_top_toolbar(self, context, nodes):
  346. formats = self.get_export_formats()
  347. form = ExportForm(formats)
  348. context = get_context_dict(context or {}) # no error!
  349. context.update({
  350. 'form': form,
  351. 'opts': self.opts,
  352. 'form_params': self.admin_view.get_form_params({'_action_': 'export'}),
  353. })
  354. nodes.append(loader.render_to_string('xadmin/blocks/model_list.top_toolbar.importexport.export.html',
  355. context=context))
  356. class ExportPlugin(ExportMixin, BaseAdminPlugin):
  357. def init_request(self, *args, **kwargs):
  358. return self.request.GET.get('_action_') == 'export'
  359. def get_response(self, response, context, *args, **kwargs):
  360. has_view_perm = self.has_model_perm(self.model, 'view')
  361. if not has_view_perm:
  362. raise PermissionDenied
  363. export_format = self.request.GET.get('file_format')
  364. if not export_format:
  365. messages.warning(self.request, _('You must select an export format.'))
  366. else:
  367. formats = self.get_export_formats()
  368. file_format = formats[int(export_format)]()
  369. queryset = self.get_export_queryset(self.request, context)
  370. export_data = self.get_export_data(file_format, queryset, request=self.request)
  371. content_type = file_format.get_content_type()
  372. # Django 1.7 uses the content_type kwarg instead of mimetype
  373. try:
  374. response = HttpResponse(export_data, content_type=content_type)
  375. except TypeError:
  376. response = HttpResponse(export_data, mimetype=content_type)
  377. response['Content-Disposition'] = 'attachment; filename=%s' % (
  378. self.get_export_filename(file_format),
  379. )
  380. post_export.send(sender=None, model=self.model)
  381. return response
  382. site.register_modelview(r'^import/$', ImportView, name='%s_%s_import')
  383. site.register_modelview(r'^process_import/$', ImportProcessView, name='%s_%s_process_import')
  384. site.register_plugin(ImportMenuPlugin, ListAdminView)
  385. site.register_plugin(ExportMenuPlugin, ListAdminView)
  386. site.register_plugin(ExportPlugin, ListAdminView)