Skip to content

Commit 35eb9e1

Browse files
author
Steve Lamb
committed
Merge pull request #58 from azavea/topic/54_56_add_support_for_csv_kwargs
Topic/54 56 add support for csv kwargs
2 parents a91df60 + 414e5d1 commit 35eb9e1

File tree

7 files changed

+96
-17
lines changed

7 files changed

+96
-17
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ python:
55
env:
66
- DJANGO=1.5 RUNNER="coverage run --source=djqscsv" SUCCESS="coveralls"
77
- DJANGO=1.6 RUNNER="python" SUCCESS="echo DONE"
8+
- DJANGO=1.7 RUNNER="python" SUCCESS="echo DONE"
9+
matrix:
10+
exclude:
11+
- python: "2.6"
12+
env: DJANGO=1.7 RUNNER="python" SUCCESS="echo DONE"
813
install:
914
- pip install -r dev_requirements.txt --use-mirrors
1015
- python setup.py install

README.rst

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Run::
3131

3232
pip install django-queryset-csv
3333
34-
Supports Python 2.6 and 2.7, Django 1.5 and 1.6.
34+
Supports Python 2.6 and 2.7, Django 1.5, 1.6, and 1.7.
3535

3636
usage
3737
-----
@@ -69,3 +69,24 @@ views.py::
6969
def csv_view(request):
7070
people = Person.objects.values('name', 'favorite_food__name')
7171
return render_to_csv_response(people)
72+
73+
keyword arguments
74+
-----------------
75+
76+
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:
77+
78+
* ``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.
79+
* ``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.
80+
* ``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.
81+
82+
The remaining keyword arguments are *passed through* to the csv writer. For example, you can export a CSV with a different delimiter::
83+
84+
views.py::
85+
86+
from djqscsv import render_to_csv_response
87+
88+
def csv_view(request):
89+
people = Person.objects.values('name', 'favorite_food__name')
90+
return render_to_csv_response(people, delimiter='|')
91+
92+
For more details on possible arguments, see the documentation on `DictWriter <https://docs.python.org/2/library/csv.html#csv.DictWriter>`_.

djqscsv/_csql.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
This module may later be officially supported.
77
"""
88

9+
910
def _transform(dataset, arg):
1011
if isinstance(arg, str):
1112
return (dataset[0].index(arg), arg)
1213
elif isinstance(arg, tuple):
1314
return (dataset[0].index(arg[0]), arg[1])
1415

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

30+
2831
def EXCLUDE(dataset, *args):
2932
antiargs = [value for index, value in enumerate(dataset[0])
30-
if not index in args and not value in args]
33+
if index not in args and value not in args]
3134
return SELECT(dataset, *antiargs)
3235

36+
3337
def AS(field, display_name):
3438
return (field, display_name)

djqscsv/djqscsv.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,27 @@
99
from django.conf import settings
1010
if not settings.configured:
1111
# required to import ValuesQuerySet
12-
settings.configure() # pragma: no cover
12+
settings.configure() # pragma: no cover
1313

1414
from django.db.models.query import ValuesQuerySet
1515

1616
from django.utils import six
1717

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

20+
# Keyword arguments that will be used by this module
21+
# the rest will be passed along to the csv writer
22+
DJQSCSV_KWARGS = {'field_header_map': None,
23+
'use_verbose_names': True,
24+
'field_order': None}
25+
2026

2127
class CSVException(Exception):
2228
pass
2329

2430

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

44-
write_csv(queryset, response, field_header_map, use_verbose_names, field_order)
49+
write_csv(queryset, response, **kwargs)
4550

4651
return response
4752

4853

49-
def write_csv(queryset, file_obj, field_header_map=None,
50-
use_verbose_names=True, field_order=None):
54+
def write_csv(queryset, file_obj, **kwargs):
5155
"""
5256
The main worker function. Writes CSV data to a file object based on the
5357
contents of the queryset.
5458
"""
5559

60+
# process keyword arguments to pull out the ones used by this function
61+
field_header_map = kwargs.get('field_header_map', {})
62+
use_verbose_names = kwargs.get('use_verbose_names', True)
63+
field_order = kwargs.get('field_order', None)
64+
65+
csv_kwargs = {}
66+
67+
for key, val in six.iteritems(kwargs):
68+
if key not in DJQSCSV_KWARGS:
69+
csv_kwargs[key] = val
70+
5671
# add BOM to suppor CSVs in MS Excel
5772
file_obj.write(u'\ufeff'.encode('utf8'))
5873

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

86-
87-
writer = csv.DictWriter(file_obj, field_names)
101+
writer = csv.DictWriter(file_obj, field_names, **csv_kwargs)
88102

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

98112
# merge the custom field headers into the verbose/raw defaults, if provided
99-
_field_header_map = field_header_map or {}
100113
merged_header_map = name_map.copy()
101-
merged_header_map.update(_field_header_map)
114+
merged_header_map.update(field_header_map)
102115
if extra_columns:
103116
merged_header_map.update(dict((k, k) for k in extra_columns))
104117
writer.writerow(merged_header_map)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name='django-queryset-csv',
8-
version='0.2.9',
8+
version='0.2.10',
99
description='A simple python module for writing querysets to csv',
1010
long_description=open('README.rst').read(),
1111
author=author,

test_app/djqscsv_tests/tests/test_csv_creation.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ class CSVTestCase(TestCase):
2727
def setUp(self):
2828
self.qs = create_people_and_get_queryset()
2929

30-
def assertMatchesCsv(self, csv_file, expected_data):
31-
csv_data = csv.reader(csv_file)
30+
def csv_match(self, csv_file, expected_data, **csv_kwargs):
31+
assertion_results = []
32+
csv_data = csv.reader(csv_file, **csv_kwargs)
3233
iteration_happened = False
3334
is_first = True
3435
test_pairs = itertools.izip_longest(csv_data, expected_data,
@@ -39,9 +40,20 @@ def assertMatchesCsv(self, csv_file, expected_data):
3940
expected_row = ['\xef\xbb\xbf' + expected_row[0]] + expected_row[1:]
4041
is_first = False
4142
iteration_happened = True
42-
self.assertEqual(csv_row, expected_row)
43+
assertion_results.append(csv_row == expected_row)
44+
45+
assertion_results.append(iteration_happened is True)
46+
47+
return assertion_results
48+
49+
def assertMatchesCsv(self, *args, **kwargs):
50+
assertion_results = self.csv_match(*args, **kwargs)
51+
self.assertTrue(all(assertion_results))
52+
53+
def assertNotMatchesCsv(self, *args, **kwargs):
54+
assertion_results = self.csv_match(*args, **kwargs)
55+
self.assertFalse(all(assertion_results))
4356

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

4658
def assertQuerySetBecomesCsv(self, qs, expected_data, **kwargs):
4759
obj = StringIO()
@@ -263,3 +275,25 @@ def test_render_to_csv_response(self):
263275
self.assertMatchesCsv(response.content.split('\n'),
264276
self.FULL_PERSON_CSV_NO_VERBOSE)
265277

278+
279+
def test_render_to_csv_response_other_delimiter(self):
280+
response = djqscsv.render_to_csv_response(self.qs,
281+
filename="test_csv",
282+
use_verbose_names=False,
283+
delimiter='|')
284+
285+
self.assertEqual(response['Content-Type'], 'text/csv')
286+
self.assertMatchesCsv(response.content.split('\n'),
287+
self.FULL_PERSON_CSV_NO_VERBOSE,
288+
delimiter="|")
289+
290+
291+
def test_render_to_csv_fails_on_delimiter_mismatch(self):
292+
response = djqscsv.render_to_csv_response(self.qs,
293+
filename="test_csv",
294+
use_verbose_names=False,
295+
delimiter='|')
296+
297+
self.assertEqual(response['Content-Type'], 'text/csv')
298+
self.assertNotMatchesCsv(response.content.split('\n'),
299+
self.FULL_PERSON_CSV_NO_VERBOSE)

test_app/test_app/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
}
77
}
88

9+
MIDDLEWARE_CLASSES = ()
10+
911
SECRET_KEY = 'NO_SECRET_KEY'
1012

1113
INSTALLED_APPS = ('djqscsv_tests',)

0 commit comments

Comments
 (0)