Skip to content

Commit

Permalink
Change render_to_csv_response to return a StreamingHttpResponse.
Browse files Browse the repository at this point in the history
The old behaviour is available by setting the keyword argument
`streaming` to `False`.

Also cleans up the README a bit.

Fixes #86, Fixes #87
  • Loading branch information
maurizi committed Nov 1, 2016
1 parent 3ab7a2c commit a4b43b4
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 18 deletions.
16 changes: 14 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ installation
Run::

pip install django-queryset-csv

Supports Python 2.6 and 2.7, Django >= 1.5.

usage
-----
Perform all filtering and field authorization in your view using ``.filter()`` and ``.values()``.
Then, use ``render_to_csv_response`` to turn a queryset into a respone with a CSV attachment.
Then, use ``render_to_csv_response`` to turn a queryset into a response with a CSV attachment.
Pass it a ``QuerySet`` or ``ValuesQuerySet`` instance::

from djqscsv import render_to_csv_response
Expand All @@ -42,6 +42,14 @@ Pass it a ``QuerySet`` or ``ValuesQuerySet`` instance::
qs = Foo.objects.filter(bar=True).values('id', 'bar')
return render_to_csv_response(qs)

If you need to write the CSV to a file you can use ``write_csv`` instead::

from djqscsv import write_csv

qs = Foo.objects.filter(bar=True).values('id', 'bar')
with open('foo.csv', 'w') as csv_file:
write_csv(qs, csv_file)

foreign keys
------------

Expand Down Expand Up @@ -77,6 +85,10 @@ This module exports two functions that write CSVs, ``render_to_csv_response`` an
- ``use_verbose_names`` - (default: ``True``) A boolean determining whether to use the django field's ``verbose_name``, or to use it's regular field name as a column header. Note that if a given field is found in the ``field_header_map``, this value will take precendence.
- ``field_order`` - (default: ``None``) A list of fields to determine the sort order. This list need not be complete: any fields not specified will follow those in the list with the order they would have otherwise used.

In addition to the above arguments, ``render_to_csv_response`` takes the
following optional keyword argument:
- ``streaming`` - (default: ``True``) A boolean determining whether to use ``StreamingHttpResponse`` instead of the normal ``HttpResponse``.

The remaining keyword arguments are *passed through* to the csv writer. For example, you can export a CSV with a different delimiter.

views.py::
Expand Down
46 changes: 34 additions & 12 deletions djqscsv/djqscsv.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@

from django.core.exceptions import ValidationError
from django.utils.text import slugify
from django.http import HttpResponse
from django.http import HttpResponse, StreamingHttpResponse

from django.utils import six

""" A simple python package for turning django models into csvs """

# Keyword arguments that will be used by this module
# the rest will be passed along to the csv writer
DJQSCSV_KWARGS = {'field_header_map': None,
'field_serializer_map': None,
'use_verbose_names': True,
'field_order': None}
DJQSCSV_KWARGS = {
'field_header_map', 'field_serializer_map', 'use_verbose_names',
'field_order', 'streaming'}


class CSVException(Exception):
pass


class _Echo(object):
"""An object that implements just the write method of the file-like
interface.
"""
def write(self, value):
"""Write the value by returning it, instead of storing in a buffer."""
return value


def render_to_csv_response(queryset, filename=None, append_datestamp=False,
**kwargs):
"""
Expand All @@ -36,19 +44,33 @@ def render_to_csv_response(queryset, filename=None, append_datestamp=False,
filename = generate_filename(queryset,
append_datestamp=append_datestamp)

response = HttpResponse(content_type='text/csv')
if not kwargs.get('streaming', True):
response = HttpResponse(content_type='text/csv')
write_csv(queryset, response, **kwargs)
else:
response = StreamingHttpResponse(
_iter_csv(queryset, _Echo(), **kwargs),
content_type='text/csv')

response['Content-Disposition'] = 'attachment; filename=%s;' % filename
response['Cache-Control'] = 'no-cache'

write_csv(queryset, response, **kwargs)

return response


def write_csv(queryset, file_obj, **kwargs):
"""
Writes CSV data to a file object based on the contents of the queryset.
"""
# Force iteration over all rows so they all get written to the file
for _ in _iter_csv(queryset, file_obj, **kwargs):
pass


def _iter_csv(queryset, file_obj, **kwargs):
"""
The main worker function. Writes CSV data to a file object based on the
contents of the queryset.
contents of the queryset and yields each row.
"""

# process keyword arguments to pull out the ones used by this function
Expand All @@ -64,7 +86,7 @@ def write_csv(queryset, file_obj, **kwargs):
csv_kwargs[key] = val

# add BOM to support CSVs in MS Excel (for Windows only)
file_obj.write(b'\xef\xbb\xbf')
yield file_obj.write(b'\xef\xbb\xbf')

# the CSV must always be built from a values queryset
# in order to introspect the necessary fields.
Expand Down Expand Up @@ -134,11 +156,11 @@ def write_csv(queryset, file_obj, **kwargs):
merged_header_map.update(dict((k, k) for k in extra_columns))
merged_header_map.update(field_header_map)

writer.writerow(merged_header_map)
yield writer.writerow(merged_header_map)

for record in values_qs:
record = _sanitize_record(field_serializer_map, record)
writer.writerow(record)
yield writer.writerow(record)


def generate_filename(queryset, append_datestamp=False):
Expand Down
18 changes: 14 additions & 4 deletions test_app/djqscsv_tests/tests/test_csv_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ def test_render_to_csv_response_no_filename(self):
response = djqscsv.render_to_csv_response(self.qs,
use_verbose_names=False)
self.assertEqual(response['Content-Type'], 'text/csv')
self.assertMatchesCsv(response.content.splitlines(),
self.assertMatchesCsv(''.join(response.streaming_content).splitlines(),
self.FULL_PERSON_CSV_NO_VERBOSE)

self.assertRegexpMatches(response['Content-Disposition'],
Expand All @@ -309,6 +309,15 @@ def test_render_to_csv_response(self):
filename="test_csv",
use_verbose_names=False)
self.assertEqual(response['Content-Type'], 'text/csv')
self.assertMatchesCsv(''.join(response.streaming_content).splitlines(),
self.FULL_PERSON_CSV_NO_VERBOSE)

def test_render_to_csv_response_non_streaming(self):
response = djqscsv.render_to_csv_response(self.qs,
filename="test_csv",
use_verbose_names=False,
streaming=False)
self.assertEqual(response['Content-Type'], 'text/csv')
self.assertMatchesCsv(response.content.splitlines(),
self.FULL_PERSON_CSV_NO_VERBOSE)

Expand All @@ -319,7 +328,7 @@ def test_render_to_csv_response_other_delimiter(self):
delimiter='|')

self.assertEqual(response['Content-Type'], 'text/csv')
self.assertMatchesCsv(response.content.splitlines(),
self.assertMatchesCsv(''.join(response.streaming_content).splitlines(),
self.FULL_PERSON_CSV_NO_VERBOSE,
delimiter="|")

Expand All @@ -330,5 +339,6 @@ def test_render_to_csv_fails_on_delimiter_mismatch(self):
delimiter='|')

self.assertEqual(response['Content-Type'], 'text/csv')
self.assertNotMatchesCsv(response.content.splitlines(),
self.FULL_PERSON_CSV_NO_VERBOSE)
self.assertNotMatchesCsv(
''.join(response.streaming_content).splitlines(),
self.FULL_PERSON_CSV_NO_VERBOSE)

0 comments on commit a4b43b4

Please sign in to comment.