From cc606f02ff24db3909fd1f6878ceae022c72e973 Mon Sep 17 00:00:00 2001 From: Steve Lamb Date: Mon, 6 Oct 2014 11:48:43 -0400 Subject: [PATCH 1/3] Misc cleanup Fix lint Pass kwargs straight through to fn that uses them --- djqscsv/_csql.py | 6 +++++- djqscsv/djqscsv.py | 8 +++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/djqscsv/_csql.py b/djqscsv/_csql.py index a17bc88..6d8cb72 100644 --- a/djqscsv/_csql.py +++ b/djqscsv/_csql.py @@ -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] @@ -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) diff --git a/djqscsv/djqscsv.py b/djqscsv/djqscsv.py index 660b701..77e4f00 100644 --- a/djqscsv/djqscsv.py +++ b/djqscsv/djqscsv.py @@ -9,7 +9,7 @@ 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 @@ -23,8 +23,7 @@ class CSVException(Exception): 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. @@ -41,7 +40,7 @@ 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 @@ -83,7 +82,6 @@ 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) # verbose_name defaults to the raw field name, so in either case From 7483dbeae5c8aa4f35078aa87dec0ae76f7d25b5 Mon Sep 17 00:00:00 2001 From: Steve Lamb Date: Mon, 6 Oct 2014 11:58:06 -0400 Subject: [PATCH 2/3] Add django 1.7 to ci build Silence warnings from 1.7 test runner --- .travis.yml | 5 +++++ test_app/test_app/settings.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 876e234..13bc8c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/test_app/test_app/settings.py b/test_app/test_app/settings.py index b5ab87f..6c387d9 100644 --- a/test_app/test_app/settings.py +++ b/test_app/test_app/settings.py @@ -6,6 +6,8 @@ } } +MIDDLEWARE_CLASSES = () + SECRET_KEY = 'NO_SECRET_KEY' INSTALLED_APPS = ('djqscsv_tests',) From 414e5d1f3efe94351765e5866b8e23311194d205 Mon Sep 17 00:00:00 2001 From: Steve Lamb Date: Mon, 6 Oct 2014 11:50:03 -0400 Subject: [PATCH 3/3] Add support for passing kwargs to csv writer fixes #54 on github fixes #56 on github Bump version number for release Add unit tests Update documentation --- README.rst | 23 +++++++++- djqscsv/djqscsv.py | 25 ++++++++--- setup.py | 2 +- .../djqscsv_tests/tests/test_csv_creation.py | 42 +++++++++++++++++-- 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index cf15f1e..07df7cc 100644 --- a/README.rst +++ b/README.rst @@ -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 ----- @@ -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 `_. diff --git a/djqscsv/djqscsv.py b/djqscsv/djqscsv.py index 77e4f00..42c52fb 100644 --- a/djqscsv/djqscsv.py +++ b/djqscsv/djqscsv.py @@ -17,6 +17,12 @@ """ 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 @@ -45,13 +51,23 @@ def render_to_csv_response(queryset, filename=None, append_datestamp=False, 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')) @@ -82,7 +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 @@ -94,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) diff --git a/setup.py b/setup.py index 74e6ce5..3253191 100644 --- a/setup.py +++ b/setup.py @@ -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, diff --git a/test_app/djqscsv_tests/tests/test_csv_creation.py b/test_app/djqscsv_tests/tests/test_csv_creation.py index cbd3171..1310f1a 100644 --- a/test_app/djqscsv_tests/tests/test_csv_creation.py +++ b/test_app/djqscsv_tests/tests/test_csv_creation.py @@ -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, @@ -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() @@ -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)