Skip to content

Commit

Permalink
Merge pull request #58 from azavea/topic/54_56_add_support_for_csv_kw…
Browse files Browse the repository at this point in the history
…args

Topic/54 56 add support for csv kwargs
  • Loading branch information
Steve Lamb committed Oct 8, 2014
2 parents a91df60 + 414e5d1 commit 35eb9e1
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ python:
env:
- DJANGO=1.5 RUNNER="coverage run --source=djqscsv" SUCCESS="coveralls"
- DJANGO=1.6 RUNNER="python" SUCCESS="echo DONE"
- DJANGO=1.7 RUNNER="python" SUCCESS="echo DONE"
matrix:
exclude:
- python: "2.6"
env: DJANGO=1.7 RUNNER="python" SUCCESS="echo DONE"
install:
- pip install -r dev_requirements.txt --use-mirrors
- python setup.py install
Expand Down
23 changes: 22 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Run::

pip install django-queryset-csv
Supports Python 2.6 and 2.7, Django 1.5 and 1.6.
Supports Python 2.6 and 2.7, Django 1.5, 1.6, and 1.7.

usage
-----
Expand Down Expand Up @@ -69,3 +69,24 @@ views.py::
def csv_view(request):
people = Person.objects.values('name', 'favorite_food__name')
return render_to_csv_response(people)

keyword arguments
-----------------

This module exports two functions that write CSVs, ``render_to_csv_response`` and ``write_csv``. Both of these functions require their own positional arguments. In addition, they both take three optional keyword arguments:

* ``field_header_map`` - (default: ``None``) A dictionary mapping names of model fields to column header names. If specified, the csv writer will use these column headers. Otherwise, it will use defer to other parameters for rendering column names.
* ``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.

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

views.py::

from djqscsv import render_to_csv_response

def csv_view(request):
people = Person.objects.values('name', 'favorite_food__name')
return render_to_csv_response(people, delimiter='|')

For more details on possible arguments, see the documentation on `DictWriter <https://docs.python.org/2/library/csv.html#csv.DictWriter>`_.
6 changes: 5 additions & 1 deletion djqscsv/_csql.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
This module may later be officially supported.
"""


def _transform(dataset, arg):
if isinstance(arg, str):
return (dataset[0].index(arg), arg)
elif isinstance(arg, tuple):
return (dataset[0].index(arg[0]), arg[1])


def SELECT(dataset, *args):
# turn the args into indices based on the first row
index_headers = [_transform(dataset, arg) for arg in args]
Expand All @@ -25,10 +27,12 @@ def SELECT(dataset, *args):
for datarow in dataset[1:]]
return results


def EXCLUDE(dataset, *args):
antiargs = [value for index, value in enumerate(dataset[0])
if not index in args and not value in args]
if index not in args and value not in args]
return SELECT(dataset, *antiargs)


def AS(field, display_name):
return (field, display_name)
33 changes: 23 additions & 10 deletions djqscsv/djqscsv.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,27 @@
from django.conf import settings
if not settings.configured:
# required to import ValuesQuerySet
settings.configure() # pragma: no cover
settings.configure() # pragma: no cover

from django.db.models.query import ValuesQuerySet

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,
'use_verbose_names': True,
'field_order': None}


class CSVException(Exception):
pass


def render_to_csv_response(queryset, filename=None, append_datestamp=False,
field_header_map=None, use_verbose_names=True,
field_order=None):
**kwargs):
"""
provides the boilerplate for making a CSV http response.
takes a filename or generates one from the queryset's model.
Expand All @@ -41,18 +46,28 @@ def render_to_csv_response(queryset, filename=None, append_datestamp=False,
response['Content-Disposition'] = 'attachment; filename=%s;' % filename
response['Cache-Control'] = 'no-cache'

write_csv(queryset, response, field_header_map, use_verbose_names, field_order)
write_csv(queryset, response, **kwargs)

return response


def write_csv(queryset, file_obj, field_header_map=None,
use_verbose_names=True, field_order=None):
def write_csv(queryset, file_obj, **kwargs):
"""
The main worker function. Writes CSV data to a file object based on the
contents of the queryset.
"""

# process keyword arguments to pull out the ones used by this function
field_header_map = kwargs.get('field_header_map', {})
use_verbose_names = kwargs.get('use_verbose_names', True)
field_order = kwargs.get('field_order', None)

csv_kwargs = {}

for key, val in six.iteritems(kwargs):
if key not in DJQSCSV_KWARGS:
csv_kwargs[key] = val

# add BOM to suppor CSVs in MS Excel
file_obj.write(u'\ufeff'.encode('utf8'))

Expand Down Expand Up @@ -83,8 +98,7 @@ def write_csv(queryset, file_obj, field_header_map=None,
[field for field in field_names
if field not in field_order])


writer = csv.DictWriter(file_obj, field_names)
writer = csv.DictWriter(file_obj, field_names, **csv_kwargs)

# verbose_name defaults to the raw field name, so in either case
# this will produce a complete mapping of field names to column names
Expand All @@ -96,9 +110,8 @@ def write_csv(queryset, file_obj, field_header_map=None,
if field.name in field_names))

# merge the custom field headers into the verbose/raw defaults, if provided
_field_header_map = field_header_map or {}
merged_header_map = name_map.copy()
merged_header_map.update(_field_header_map)
merged_header_map.update(field_header_map)
if extra_columns:
merged_header_map.update(dict((k, k) for k in extra_columns))
writer.writerow(merged_header_map)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name='django-queryset-csv',
version='0.2.9',
version='0.2.10',
description='A simple python module for writing querysets to csv',
long_description=open('README.rst').read(),
author=author,
Expand Down
42 changes: 38 additions & 4 deletions test_app/djqscsv_tests/tests/test_csv_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ class CSVTestCase(TestCase):
def setUp(self):
self.qs = create_people_and_get_queryset()

def assertMatchesCsv(self, csv_file, expected_data):
csv_data = csv.reader(csv_file)
def csv_match(self, csv_file, expected_data, **csv_kwargs):
assertion_results = []
csv_data = csv.reader(csv_file, **csv_kwargs)
iteration_happened = False
is_first = True
test_pairs = itertools.izip_longest(csv_data, expected_data,
Expand All @@ -39,9 +40,20 @@ def assertMatchesCsv(self, csv_file, expected_data):
expected_row = ['\xef\xbb\xbf' + expected_row[0]] + expected_row[1:]
is_first = False
iteration_happened = True
self.assertEqual(csv_row, expected_row)
assertion_results.append(csv_row == expected_row)

assertion_results.append(iteration_happened is True)

return assertion_results

def assertMatchesCsv(self, *args, **kwargs):
assertion_results = self.csv_match(*args, **kwargs)
self.assertTrue(all(assertion_results))

def assertNotMatchesCsv(self, *args, **kwargs):
assertion_results = self.csv_match(*args, **kwargs)
self.assertFalse(all(assertion_results))

self.assertTrue(iteration_happened, "The CSV does not contain data.")

def assertQuerySetBecomesCsv(self, qs, expected_data, **kwargs):
obj = StringIO()
Expand Down Expand Up @@ -263,3 +275,25 @@ def test_render_to_csv_response(self):
self.assertMatchesCsv(response.content.split('\n'),
self.FULL_PERSON_CSV_NO_VERBOSE)


def test_render_to_csv_response_other_delimiter(self):
response = djqscsv.render_to_csv_response(self.qs,
filename="test_csv",
use_verbose_names=False,
delimiter='|')

self.assertEqual(response['Content-Type'], 'text/csv')
self.assertMatchesCsv(response.content.split('\n'),
self.FULL_PERSON_CSV_NO_VERBOSE,
delimiter="|")


def test_render_to_csv_fails_on_delimiter_mismatch(self):
response = djqscsv.render_to_csv_response(self.qs,
filename="test_csv",
use_verbose_names=False,
delimiter='|')

self.assertEqual(response['Content-Type'], 'text/csv')
self.assertNotMatchesCsv(response.content.split('\n'),
self.FULL_PERSON_CSV_NO_VERBOSE)
2 changes: 2 additions & 0 deletions test_app/test_app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
}
}

MIDDLEWARE_CLASSES = ()

SECRET_KEY = 'NO_SECRET_KEY'

INSTALLED_APPS = ('djqscsv_tests',)
Expand Down

0 comments on commit 35eb9e1

Please sign in to comment.