# coding: utf-8
"""
Export Data
===========
.. autoclass:: ExportData
"""
from __future__ import unicode_literals
import warnings
import django
from django.utils import six
from django.http import HttpResponse
from django.contrib import admin
from django.utils.encoding import force_text, smart_bytes, force_str
from django.db import models
try:
from django.contrib.admin.utils import lookup_field, display_for_field
except ImportError: # dja <= 1.6
from django.contrib.admin.util import lookup_field, display_for_field
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as ugt
[documenti]class ExportData(object):
"""
Utility to export data from a model.
It can export data from model fields, model methods, model properties, model admin methods.
For a more precise export of data inside models, it's possible to extend this class. The default get_data() will
parse also methods inside of the export class, enabling the creation of export-specif fields.
It can export the data in different file output: csv, xls, xlsx.
To do so, it uses different packages:
#. unicodecsv for .CSV
#. xlwt for .XLS
#. openpyxl for .XLSX.
JAdmin is NOT dependent from any of these packages.
To enable the export in a specific format, it's necessary to install one or more of these packages.
In case unicodecsv is not installed, the function will try to export data using the default csv python package,
which cannot, however, grant data integrity in case of six.text_type strings.
In the other two cases, the export won't work, returning an ImportError.
Whenever any of the packages is installed, the corresponding action will get enabled into the admin
extra_action_form.
"""
def __init__(self, modeladmin=None, queryset=None, request=None, model=None, fields_list=None):
"""
:arg modeladmin: the modeladmin.
:arg request: the request, not used
:arg queryset: the queryset or a list of
:arg model: the model (used to export directly from model).
:arg fields_list: the list of fields to be exported
"""
self.modeladmin = modeladmin
self.queryset = queryset
self.request = request
self.model = model or modeladmin.model
self.fields_list = fields_list
def get_field_list(self):
"""return the applicable field list
A field can be a callable, an attribute of modeladmin,
an attribute of the model or a property of the model
"""
if self.fields_list:
return self.fields_list
if self.modeladmin:
if hasattr(self.modeladmin, 'list_display_csv'):
field_list = self.modeladmin.list_display_csv
else:
field_list = self.modeladmin.get_list_display(self.request)
elif self.model and hasattr(self.model, 'list_display_csv'):
field_list = self.model.list_display_csv
else:
field_list = self.get_exportable_fields_from_model()
if 'action_checkbox' in field_list:
field_list.remove('action_checkbox')
return field_list
def get_exportable_fields_from_model(self):
"""return the list of fields and properties from model
"""
fields = [field.attname for field in self.model._meta.fields]
## nel caso si voglia attivare l'import automatico delle properties del modello
# fields += [name for name in dir(self.model) if (isinstance(getattr(self.model, name), property))]
return fields
def get_headers(self, field_list):
"""
return a translated list. If field is a callable, test if short_description exists
"""
def descr(item):
if self.modeladmin and hasattr(self.modeladmin, item):
try:
return str(getattr(self.modeladmin, item).short_description)
except AttributeError:
return item
if self.model and hasattr(self.model, item):
try:
return str(getattr(self.model, item).short_description)
except AttributeError:
return item
return item
return [descr(name) for name in field_list]
@property
def plural_name(self):
"""
return the plural name of the model
"""
return ugt(self.model._meta.verbose_name_plural)
def get_data(self):
"""
return the rows of the document to be exported
"""
# Attempt to split into a simple part that uses modeladmin that is difficult to
# serialize and another that can be serialized so as to use it when passing over
# to celery
field_list = self.get_field_list()
yield self.get_headers(field_list)
for obj in self.queryset:
row = []
for field_name in field_list:
if field_name == 'action_checkbox':
continue
field, attr, value = lookup_field(field_name, obj, self.modeladmin)
if not field:
# If modeladmin is defined and it has field_name attr we get value from there
if self.modeladmin and hasattr(self.modeladmin, field_name):
result_repr = getattr(self.modeladmin, field_name)(obj)
# If modeladmin is defined but it hasn't field_name attr we get value from obj
elif self.modeladmin and hasattr(obj, field_name):
result_repr = getattr(obj, field_name) if isinstance(
getattr(self.model, field_name), property
) else getattr(obj, field_name)()
# If modeladmin is not defined we get the value from obj
elif not self.modeladmin and hasattr(obj, field_name):
result_repr = getattr(obj, field_name) if isinstance(
getattr(self.model, field_name), property
) else getattr(obj, field_name)()
# Finally, tries to interpret it as a function of the ExportData class (or
# an inherited class)
elif hasattr(self, field_name):
result_repr = getattr(obj, field_name)
# If there is no match at all, we need to raise a ConfigurationError
else:
ConfigurationError(
ugt(
"field name %(field_name)s does not appear to be valid"
) % {'field_name': field_name}
)
elif value is None:
if isinstance(field, models.NullBooleanField):
result_repr = ugt("None")
else:
result_repr = ''
else:
if isinstance(field, (models.NullBooleanField, models.BooleanField)):
result_repr = value and _('Yes') or _('No')
else:
if django.VERSION[:2] >= (1, 9):
result_repr = display_for_field(value, field, '')
else:
result_repr = display_for_field(value, field)
# Tolgo eventuali 'a capo'
row.append(result_repr)
yield row
def export(self, filename=None, output_type='csv'):
"""Export data according to specific output_type
:arg filename: the output filename, missing the filename a buffer
is returned created with six.StringIO
:arg output_type: csv (default), xls or xlsx. Note that each output type
depends on external pachakes that you must add to your env
"""
if output_type == 'csv':
return export_csv(self.get_data(), filename)
elif output_type == 'xls':
return export_xls(self.get_data(), filename, sheet_name=self.model.__name__.lower())
elif output_type == 'xlsx':
return export_xlsx(self.get_data(), filename)
def export_csv(data, filename, delimiter=";", quotechar='"', quoting=None):
"""Write data to a csv file
:arg data: a list of lists to be saved
:arg filename: the name of the file to be created
:arg delimiter: delimiter (default ';')
:arg quotechar: quotechar (default '"')
:arg quoting: default csv.QUTE_ALL
"""
try:
import unicodecsv as csv
except ImportError:
warnings.warn(
"To properly manage six.text_type string, "
"it's recommended to install unicodecsv package",
UnicodeWarning
)
import csv
if filename:
f_obj = open(filename, 'wb')
else:
f_obj = six.BytesIO()
if six.PY2:
delimiter = smart_bytes(delimiter)
quotechar = smart_bytes(quotechar)
else:
delimiter = force_text(delimiter)
quotechar = force_text(quotechar)
writer = csv.writer(
f_obj, delimiter=delimiter,
quotechar=quotechar, quoting=quoting or csv.QUOTE_ALL
)
writer.writerows(data)
if not filename:
return f_obj
def export_xls(data, filename=None, sheet_name='sheet'):
"""Write data to an xls file or return a buffer
:arg data: a list of lists to be saved
:arg filename: the name of the file to be created, if missing a buffer is returned
:arg save: It False a buffer is created and return instead of a file
:arg quotechar: quotechar (default '"')
:arg quoting: default csv.QUTE_ALL
"""
try:
from xlwt import Workbook
except ImportError:
raise ImportError(
"To export data to a .xls file it's necessary to install xlwt package"
)
workbook = Workbook(encoding='utf-8')
page_index = 0
for row_index, row_content in enumerate(data):
if row_index == page_index * 65530:
sheet = workbook.add_sheet(
"{0}-{1}".format(sheet_name, page_index)
)
page_index += 1
page_row_index = 0
for column_index, cell_value in enumerate(row_content):
try:
sheet.write(page_row_index, column_index, cell_value)
except Exception:
# Purtroppo xlwt solleva un'eccezione generica
cell_value = force_text(cell_value, strings_only=True)
sheet.write(page_row_index, column_index, cell_value)
page_row_index += 1
if filename:
workbook.save(filename)
else:
buffer = six.StringIO()
workbook.save(buffer)
return buffer
def export_xlsx(data, filename):
"""Write data to an xlsx file or return a buffer
:arg data: a list of lists to be saved
:arg filename: the name of the file to be created, if missing a buffer is returned
"""
try:
from openpyxl.workbook import Workbook
except ImportError:
raise ImportError(
"To export data to a .xlsx file it's necessary to install openpyxl package"
)
wb = Workbook(write_only=True)
ws = wb.create_sheet()
for row_content in data:
ws.append(row_content)
if filename:
wb.save(filename)
else:
buffer = six.StringIO()
wb.save(buffer)
return buffer
def attach_response(data, filename, output_type='csv'):
"""Return an HttpResponse containing data and content_type according
to output_type
:arg data: data (list of lists) or a file-like object
:param filename: Name of the retured file. Default: file.xls
:return: HttpResponse object
"""
assert output_type in ('csv', 'xls', 'xlsx'), "Output type {} not supported".format(
output_type)
content_types = {
'csv': 'text/csv',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
if not filename:
filename = 'file.{}'.format(output_type)
response = HttpResponse(content_type=content_types[output_type])
response['Content-Disposition'] = 'attachment; filename= %s' % filename
response.write(data.getvalue())
return response