Skip to content

Commit 385fdb7

Browse files
author
Steve Lamb
committed
Merge pull request #65 from azavea/topic/support_custom_serialization
Topic/support custom serialization
2 parents 2304dcf + b53bf16 commit 385fdb7

File tree

4 files changed

+52
-12
lines changed

4 files changed

+52
-12
lines changed

README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ views.py::
7373
keyword arguments
7474
-----------------
7575

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:
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 the following optional keyword arguments:
7777

7878
- ``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+
- ``field_serializer_map`` - (default: ``{}``) A dictionary mapping names of model fields to functions that serialize them to text. For example, ``{'created': (lambda x: x.strftime('%Y/%m/%d')) }`` will serialize a datetime field called ``created``.
7980
- ``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.
8081
- ``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.
8182

djqscsv/djqscsv.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import datetime
33

44
from django.core.exceptions import ValidationError
5-
from django.templatetags.l10n import localize
65
from django.utils.text import slugify
76
from django.http import HttpResponse
87

@@ -20,6 +19,7 @@
2019
# Keyword arguments that will be used by this module
2120
# the rest will be passed along to the csv writer
2221
DJQSCSV_KWARGS = {'field_header_map': None,
22+
'field_serializer_map': None,
2323
'use_verbose_names': True,
2424
'field_order': None}
2525

@@ -59,6 +59,7 @@ def write_csv(queryset, file_obj, **kwargs):
5959

6060
# process keyword arguments to pull out the ones used by this function
6161
field_header_map = kwargs.get('field_header_map', {})
62+
field_serializer_map = kwargs.get('field_serializer_map', {})
6263
use_verbose_names = kwargs.get('use_verbose_names', True)
6364
field_order = kwargs.get('field_order', None)
6465

@@ -121,7 +122,7 @@ def write_csv(queryset, file_obj, **kwargs):
121122
writer.writerow(merged_header_map)
122123

123124
for record in values_qs:
124-
record = _sanitize_unicode_record(record)
125+
record = _sanitize_unicode_record(field_serializer_map, record)
125126
writer.writerow(record)
126127

127128

@@ -154,20 +155,31 @@ def _validate_and_clean_filename(filename):
154155
return filename
155156

156157

157-
def _sanitize_unicode_record(record):
158+
def _sanitize_unicode_record(field_serializer_map, record):
158159

159-
def _sanitize_value(value):
160+
def _serialize_value(value):
161+
# provide default serializer for the case when
162+
# non text values get sent without a serializer
163+
if isinstance(value, datetime.datetime):
164+
return value.isoformat()
165+
else:
166+
return unicode(value)
167+
168+
def _sanitize_text(value):
169+
# make sure every text value is of type 'str', coercing unicode
160170
if isinstance(value, unicode):
161171
return value.encode("utf-8")
162-
elif isinstance(value, datetime.datetime):
163-
return value.isoformat().encode("utf-8")
172+
elif isinstance(value, str):
173+
return value
164174
else:
165-
return localize(value)
175+
return str(value).encode("utf-8")
166176

167177
obj = {}
168178
for key, val in six.iteritems(record):
169179
if val is not None:
170-
obj[_sanitize_value(key)] = _sanitize_value(val)
180+
serializer = field_serializer_map.get(key, _serialize_value)
181+
newval = serializer(val)
182+
obj[_sanitize_text(key)] = _sanitize_text(newval)
171183

172184
return obj
173185

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.10',
8+
version='0.3.0',
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_utilities.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,46 @@ class SanitizeUnicodeRecordTests(TestCase):
4343
def test_sanitize(self):
4444
record = {'name': 'Tenar',
4545
'nickname': u'\ufeffThe White Lady of Gont'}
46-
sanitized = djqscsv._sanitize_unicode_record(record)
46+
sanitized = djqscsv._sanitize_unicode_record({}, record)
4747
self.assertEqual(sanitized,
4848
{'name': 'Tenar',
4949
'nickname': '\xef\xbb\xbfThe White Lady of Gont'})
5050

5151
def test_sanitize_date(self):
5252
record = {'name': 'Tenar',
5353
'created': datetime.datetime(1, 1, 1)}
54-
sanitized = djqscsv._sanitize_unicode_record(record)
54+
sanitized = djqscsv._sanitize_unicode_record({}, record)
5555
self.assertEqual(sanitized,
5656
{'name': 'Tenar',
5757
'created': '0001-01-01T00:00:00'})
5858

59+
def test_sanitize_date_with_non_string_formatter(self):
60+
"""
61+
This test is only to make sure an edge case provides a sane
62+
default and works as expected. It is not recommended to follow
63+
this practice.
64+
"""
65+
record = {'name': 'Tenar'}
66+
serializer = {'name': lambda d: len(d) }
67+
sanitized = djqscsv._sanitize_unicode_record(serializer, record)
68+
self.assertEqual(sanitized, {'name': '5'})
69+
70+
def test_sanitize_date_with_formatter(self):
71+
record = {'name': 'Tenar',
72+
'created': datetime.datetime(1973, 5, 13)}
73+
serializer = {'created': lambda d: d.strftime('%Y-%m-%d') }
74+
sanitized = djqscsv._sanitize_unicode_record(serializer, record)
75+
self.assertEqual(sanitized,
76+
{'name': 'Tenar',
77+
'created': '1973-05-13'})
78+
79+
def test_sanitize_date_with_bad_formatter(self):
80+
record = {'name': 'Tenar',
81+
'created': datetime.datetime(1973, 5, 13)}
82+
formatter = lambda d: d.day
83+
with self.assertRaises(AttributeError):
84+
djqscsv._sanitize_unicode_record(formatter, record)
85+
5986

6087
class AppendDatestampTests(TestCase):
6188

0 commit comments

Comments
 (0)