├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── README_CN.md ├── django_export_csv ├── __init__.py ├── function.py ├── mixin.py └── utils.py ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── core │ ├── __init__.py │ ├── data_init.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── tests │ │ ├── __init__.py │ │ ├── tests_request.py │ │ └── tests_utils.py │ ├── urls.py │ └── views.py ├── db.sqlite3 └── manage.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | env: 10 | - DJANGO="Django>=1.8,<1.9" 11 | - DJANGO="Django>=1.9,<1.10" 12 | - DJANGO="Django>=1.10,<1.11" 13 | - DJANGO="Django>=1.11,<1.12" 14 | install: 15 | - pip install -q $DJANGO 16 | - pip install -r requirements.txt 17 | - pip install coveralls 18 | script: 19 | - python -Wall `which coverage` run --omit='setup.py' --source=. tests/manage.py test core --settings= 20 | after_success: 21 | - coveralls 22 | matrix: 23 | exclude: 24 | - python: "3.3" 25 | env: DJANGO="Django>=1.9,<1.10" 26 | - python: "3.3" 27 | env: DJANGO="Django>=1.10,<1.11" 28 | - python: "3.3" 29 | env: DJANGO="Django>=1.11,<1.12" 30 | allow_failures: 31 | - env: DJANGO="https://github.com/django/django/archive/master.tar.gz" 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Azavea 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-export-csv 2 | [![Build Status](https://travis-ci.org/oddcc/django-export-csv.svg?branch=master)](https://travis-ci.org/oddcc/django-export-csv) 3 | [![Coverage Status](https://coveralls.io/repos/github/oddcc/django-export-csv/badge.svg?branch=develop)](https://coveralls.io/github/oddcc/django-export-csv?branch=develop) 4 | 5 | [中文文档](https://github.com/oddcc/django-export-csv/blob/master/README_CN.md) 6 | ## Introduction 7 | a CSV exporter for Django 8 | this tool create a shortcut to render a queryset to a CSV steaming HTTP response. 9 | 10 | ## install 11 | Run: 12 | ``` 13 | pip install django-export-csv 14 | ``` 15 | Support Python 2.7 and 3.5, Django >= 1.8. 16 | 17 | ## usage 18 | let your Class-based views which inherit `ListView` or `MultipleObjectMixin` also inherit `QueryCsvMixin`, then you can use `render_csv_response` to turn a queryset into a response with a CSV attachment. `render_csv_response` takes a `QuerySet` or a `ValuesQuerySet` instance: 19 | 20 | ### CBV 21 | ```python 22 | from django_export_csv import QueryCsvMixin 23 | from django.views.generic.list import ListView 24 | 25 | from .models import Student 26 | 27 | 28 | class StudentListView(QueryCsvMixin, ListView): 29 | queryset = Student.objects.all() 30 | 31 | def get(self, *args, **kwargs): 32 | return self.render_csv_response(queryset) 33 | ``` 34 | 35 | ### FBV 36 | ```python 37 | from django_export_csv import render_csv_response 38 | 39 | 40 | def student_list_view(request): 41 | if request.method == 'GET': 42 | queryset = Student.objects.all() 43 | return render_csv_response(queryset) 44 | ``` 45 | 46 | ## custom CSV 47 | once you inherit `QueryCsvMixin`, then you can use following arguments to custom CSV export: 48 | 49 | - Use `fields` - (default: `[]`) to set fields you want to export, or use `exclude` - (default: `[]`) exclude which you don't want to. If use neither, it will export all non relational fields by default. 50 | - If you want to export relation fields, just implement your `serializer`, it should take model object as argument and return something you want. 51 | 1. Use `extra_field` - (default: `[]`) to set relation and custom field names. 52 | 2. Use `field_serializer_map` - (default: `{}`) to map field names to your serializers, key is field name, value is corresponding serializer. 53 | 3. Note that fields in `extra_field` must be a corresponding serializer in `field_serializer_map` to work. 54 | - If you want to change the way information export (like `True`/`False` ==> Yes/No), just implement `serializer` and add it to `field_serializer_map`. 55 | - Other arguments: 56 | 1. `filename` - (default: `None`), this argument should be a `str`; if not given(means use default), the CSV filename will be generated by model name. 57 | 2. `add_datestamp` - (default: `False`), this argument should be boolean, if it is `True`, filename will be add a datestamp. 58 | 3. `use_verbose_names` - (default: `True`), this argument should be boolean, if it is `True`, CSV header will use model field's verbose_name. 59 | 4. `field_order` - (default: `[]`), this should be a list to determine field order, any fields not specified will follow those in the list. 60 | 5. `field_header_map` - (default: `{}`), A dictionary mapping model field's name to CSV column header name. Has a higher priority than the `use_verbose_names`. 61 | 62 | e.g: 63 | 64 | ```python 65 | # data_init.py 66 | import datetime 67 | from .models import Student, College 68 | 69 | 70 | def create_student_and_get_queryset(): 71 | college1, _ = College.objects.get_or_create(name="College 1st") 72 | college2, _ = College.objects.get_or_create(name="College 2nd") 73 | 74 | Student.objects.get_or_create( 75 | name='Jim', age=18, is_graduated=False, birthday=datetime.date(1998,6,6), college=college1 76 | ) 77 | Student.objects.get_or_create( 78 | name='Bing', age=22, is_graduated=True, birthday=datetime.date(1994, 2, 6), college=college1 79 | ) 80 | Student.objects.get_or_create( 81 | name='Monica', age=25, is_graduated=True, birthday=datetime.date(1991, 2, 6), college=college2 82 | ) 83 | 84 | return Student.objects.all() 85 | ``` 86 | 87 | ```python 88 | # views.py 89 | from django_export_csv import QueryCsvMixin 90 | from django_export_csv import render_csv_response 91 | from django.views.generic.list import ListView 92 | 93 | from .models import Student 94 | from .data_init import create_student_and_get_queryset 95 | 96 | 97 | def boolean_serializer(value): 98 | if value == True: 99 | return 'Y' 100 | else: 101 | return 'N' 102 | 103 | 104 | def college_serializer(obj): 105 | return obj.college.name 106 | 107 | 108 | # CBV 109 | class StudentListView(QueryCsvMixin, ListView): 110 | filename = 'export_student_list' 111 | add_datestamp = True 112 | use_verbose_names = True 113 | exclude = ['id'] 114 | field_order = ['name', 'is_graduated'] 115 | field_header_map = {'is_graduated': 'Graduated'} 116 | field_serializer_map = {'is_graduated': boolean_serializer, 'college': college_serializer} 117 | extra_field = ['college'] 118 | queryset = create_student_and_get_queryset() 119 | 120 | def get(self, *args, **kwargs): 121 | return self.render_csv_response(self.get_queryset()) 122 | 123 | 124 | # FBV 125 | def student_list_view(request): 126 | filename = 'export_student_list' 127 | add_datestamp = True 128 | use_verbose_names = True 129 | exclude = ['id'] 130 | field_order = ['name', 'is_graduated'] 131 | field_header_map = {'is_graduated': 'Graduated'} 132 | field_serializer_map = {'is_graduated': boolean_serializer, 'college': college_serializer} 133 | extra_field = ['college'] 134 | 135 | if request.method == 'GET': 136 | queryset = create_student_and_get_queryset() 137 | return render_csv_response( 138 | queryset, filename=filename, add_datestamp=add_datestamp, use_verbose_names=use_verbose_names, 139 | exclude_field=exclude, field_order=field_order, field_header_map=field_header_map, 140 | field_serializer_map=field_serializer_map, extra_field=extra_field 141 | ) 142 | ``` 143 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # django-export-csv 2 | [![Build Status](https://travis-ci.org/oddcc/django-export-csv.svg?branch=master)](https://travis-ci.org/oddcc/django-export-csv) 3 | [![Coverage Status](https://coveralls.io/repos/github/oddcc/django-export-csv/badge.svg?branch=develop)](https://coveralls.io/github/oddcc/django-export-csv?branch=develop) 4 | 5 | ## Introduction 6 | Django的一个CSV导出工具 7 | 可以很方便的把queryset导出为CSV文件, 为了避免导出文件过大, 占用服务器内存, 生成的是StreamingHttpResponset. 8 | 9 | ## install 10 | Run: 11 | ``` 12 | pip install django-export-csv 13 | ``` 14 | Support Python 2.7 and 3.5, Django >= 1.8. 15 | 16 | ## usage 17 | 使用CBV的话, 视图类需要继承`ListView`(或`MultipleObjectMixin`的子类)和`QueryCsvMixin`, 继承之后就可以调用`render_csv_response`方法把queryset导出为CSV文件, `render_csv_response`方法需要一个`QuerySet`或`ValuesQuerySet`的实例作为参数: 18 | 19 | ### 使用CBV 20 | ```python 21 | from django_export_csv import QueryCsvMixin 22 | from django.views.generic.list import ListView 23 | 24 | from .models import Student 25 | 26 | 27 | class StudentListView(QueryCsvMixin, ListView): 28 | queryset = Student.objects.all() 29 | 30 | def get(self, *args, **kwargs): 31 | return self.render_csv_response(queryset) 32 | ``` 33 | 34 | ### 使用FBV 35 | ```python 36 | from django_export_csv import render_csv_response 37 | 38 | 39 | def student_list_view(request): 40 | if request.method == 'GET': 41 | queryset = Student.objects.all() 42 | return render_csv_response(queryset) 43 | ``` 44 | 45 | ## 定制 CSV 46 | 视图类继承了 `QueryCsvMixin`之后, 就可以使用以下参数自定义CSV文件: 47 | 48 | - 使用`fields` - (default: `[]`)指定想导出的字段名, 或用`exclude` - (default: `[]`)排除不想导出的字段名. 如果都不指定, 默认导出所有非关系字段 49 | - 支持导出关系字段和自定义字段, 需要自己定义`serializer`, 接受模型对象为参数, 返回想导出的内容 50 | 1. 用`extra_field` - (default: `[]`)来添加关系字段名和自定义字段名. 51 | 2. 用`field_serializer_map` - (default: `{}`)来指定字段名和`serializer`的映射关系, key是字段名, value是对应的函数, 这个函数应该接收一个值并返回相应的内容. 52 | 3. 注意如果指定了`extra_field`参数, `field_serializer_map`中必须有相应的serializer配合才能工作. 53 | - 如果想改变默认的导出内容, 也可以自定义`serializer`后加到`field_serializer_map`里面; 比如`True`/`False`导出的时候显示为"是/否" 54 | - 其他参数说明: 55 | 1. `filename` - (default: `None`), 是个字符串, 如果不定义, CSV会根据model来生成文件名. 56 | 2. `add_datestamp` - (default: `False`), 是个布尔值, 如果为True的话, 导出的文件名末尾会添加当前时间的时间戳. 57 | 3. `use_verbose_names` - (default: `True`), 是个布尔值, 如果设为True, CSV表头的名称会使用model中定义的verbose_name. 58 | 4. `field_order` - (default: `[]`), 是个列表, 可以把想定义排序的字段名写在里面, 导出的CSV会优先按顺序排列这个参数指定的字段, 再排剩下的字段. 59 | 5. `field_header_map` - (default: `{}`), 是个字典, 用于自定义表头, key应该是字段名, value是表头中显示的内容, 这个参数的优先级比verbose_name高. 60 | 61 | 62 | e.g: 63 | 64 | ```python 65 | # data_init.py 66 | import datetime 67 | from .models import Student, College 68 | 69 | 70 | def create_student_and_get_queryset(): 71 | college1, _ = College.objects.get_or_create(name="College 1st") 72 | college2, _ = College.objects.get_or_create(name="College 2nd") 73 | 74 | Student.objects.get_or_create( 75 | name='Jim', age=18, is_graduated=False, birthday=datetime.date(1998,6,6), college=college1 76 | ) 77 | Student.objects.get_or_create( 78 | name='Bing', age=22, is_graduated=True, birthday=datetime.date(1994, 2, 6), college=college1 79 | ) 80 | Student.objects.get_or_create( 81 | name='Monica', age=25, is_graduated=True, birthday=datetime.date(1991, 2, 6), college=college2 82 | ) 83 | 84 | return Student.objects.all() 85 | ``` 86 | 87 | ```python 88 | # views.py 89 | from django_export_csv import QueryCsvMixin 90 | from django_export_csv import render_csv_response 91 | from django.views.generic.list import ListView 92 | 93 | from .models import Student 94 | from .data_init import create_student_and_get_queryset 95 | 96 | 97 | def boolean_serializer(value): 98 | if value == True: 99 | return 'Y' 100 | else: 101 | return 'N' 102 | 103 | 104 | def college_serializer(obj): 105 | return obj.college.name 106 | 107 | 108 | # CBV 109 | class StudentListView(QueryCsvMixin, ListView): 110 | filename = 'export_student_list' 111 | add_datestamp = True 112 | use_verbose_names = True 113 | exclude = ['id'] 114 | field_order = ['name', 'is_graduated'] 115 | field_header_map = {'is_graduated': 'Graduated'} 116 | field_serializer_map = {'is_graduated': boolean_serializer, 'college': college_serializer} 117 | extra_field = ['college'] 118 | queryset = create_student_and_get_queryset() 119 | 120 | def get(self, *args, **kwargs): 121 | return self.render_csv_response(self.get_queryset()) 122 | 123 | 124 | # FBV 125 | def student_list_view(request): 126 | filename = 'export_student_list' 127 | add_datestamp = True 128 | use_verbose_names = True 129 | exclude = ['id'] 130 | field_order = ['name', 'is_graduated'] 131 | field_header_map = {'is_graduated': 'Graduated'} 132 | field_serializer_map = {'is_graduated': boolean_serializer, 'college': college_serializer} 133 | extra_field = ['college'] 134 | 135 | if request.method == 'GET': 136 | queryset = create_student_and_get_queryset() 137 | return render_csv_response( 138 | queryset, filename=filename, add_datestamp=add_datestamp, use_verbose_names=use_verbose_names, 139 | exclude_field=exclude, field_order=field_order, field_header_map=field_header_map, 140 | field_serializer_map=field_serializer_map, extra_field=extra_field 141 | ) 142 | ``` 143 | -------------------------------------------------------------------------------- /django_export_csv/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .utils import * 3 | from .function import * 4 | from .mixin import * 5 | -------------------------------------------------------------------------------- /django_export_csv/function.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | import datetime 4 | import unicodecsv as csv 5 | import codecs 6 | 7 | from django.http import StreamingHttpResponse 8 | 9 | from django_export_csv import clean_filename, attach_datestamp, generate_filename, Echo, get_uncontain_field_names 10 | 11 | 12 | def render_csv_response(queryset, filename=None, add_datestamp=False, **kwargs): 13 | """ 14 | entry function, making a CSV streaming http response, take a queryset 15 | """ 16 | if filename: 17 | filename = clean_filename(filename) 18 | if add_datestamp: 19 | filename = attach_datestamp(filename) 20 | else: 21 | filename = generate_filename(queryset, add_datestamp) 22 | 23 | response_args = {'content_type': 'text/csv'} 24 | 25 | response = StreamingHttpResponse( 26 | _iter_csv(queryset, Echo(), **kwargs), **response_args) 27 | 28 | # support chinese filename 29 | response['Content-Disposition'] = b'attachment; filename=' + filename.encode(encoding='utf-8') + b';' 30 | response['Cache-Control'] = 'no-cache' 31 | 32 | return response 33 | 34 | 35 | def _iter_csv(queryset, file_obj, **kwargs): 36 | """ 37 | Writes CSV data to a file object based on the 38 | contents of the queryset and yields each row. 39 | """ 40 | csv_kwargs = {'encoding': 'utf-8'} 41 | 42 | # add BOM to support MS Excel (for Windows only) 43 | yield file_obj.write(codecs.BOM_UTF8) 44 | 45 | if type(queryset).__name__ == 'ValuesQuerySet': 46 | queryset_values = queryset 47 | else: 48 | queryset_values = queryset.values() 49 | 50 | fields = kwargs.get('fields', []) 51 | exclude = kwargs.get('exclude_field', []) 52 | extra_field = kwargs.get('extra_field', []) 53 | 54 | if fields: 55 | uncontain_field_names = get_uncontain_field_names( 56 | fields, queryset_values.field_names 57 | ) 58 | if uncontain_field_names: 59 | raise Exception(','.join(uncontain_field_names) + " aren't in default field names") 60 | 61 | field_names = fields 62 | elif exclude: 63 | field_names = [ 64 | f.name 65 | for f in queryset.model._meta.get_fields() 66 | if not (f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)) 67 | and f.name not in exclude 68 | ] 69 | else: 70 | field_names = [f.name for f in queryset.model._meta.get_fields() 71 | if not (f.is_relation 72 | or f.one_to_one 73 | or (f.many_to_one and f.related_model))] 74 | 75 | field_names += extra_field 76 | 77 | field_order = kwargs.get('field_order', []) 78 | 79 | if field_order: 80 | field_names = [field_name for field_name in field_names if field_name in field_order] + \ 81 | [field_name for field_name in field_names if field_name not in field_order] 82 | 83 | # support extra 84 | # Sometimes, the Django query syntax by itself can't easily express a complex WHERE clause. 85 | # For these edge cases, Django provides the extra() QuerySet modifier 86 | # a hook for injecting specific clauses into the SQL generated by a QuerySet. 87 | extra_columns = list(queryset_values.query.extra_select) 88 | if extra_columns: 89 | field_names += extra_columns 90 | 91 | # support annotate 92 | # Annotates each object in the QuerySet with the provided list of query expressions. 93 | # An expression may be a simple value, a reference to a field on the model (or any related models), 94 | # or an aggregate expression (averages, sums, etc) that 95 | # has been computed over the objects that are related to the objects in the QuerySet. 96 | annotation_columns = list(queryset_values.query.annotation_select) 97 | if annotation_columns: 98 | field_names += annotation_columns 99 | 100 | writer = csv.DictWriter(file_obj, field_names, **csv_kwargs) 101 | 102 | header_map = dict((field, field) for field in field_names) 103 | 104 | use_verbose_names = kwargs.get('use_verbose_names', True) 105 | 106 | if use_verbose_names: 107 | for field in queryset.model._meta.get_fields(): 108 | if field.name in field_names: 109 | try: 110 | header_map[field.name] = field.verbose_name 111 | except AttributeError: 112 | header_map[field.name] = field.name 113 | 114 | kwargs.setdefault('field_names', field_names) 115 | header_map.update(kwargs.get('field_header_map', {})) 116 | 117 | yield writer.writerow(header_map) 118 | 119 | if extra_field: 120 | model = queryset_values.model 121 | for item in queryset_values: 122 | item = _sanitize_related_item(item, model, **kwargs) 123 | item = _sanitize_item(item, **kwargs) 124 | yield writer.writerow(item) 125 | else: 126 | for item in queryset_values: 127 | item = _sanitize_item(item, **kwargs) 128 | yield writer.writerow(item) 129 | 130 | 131 | def _sanitize_item(item, **kwargs): 132 | def _serialize_value(value): 133 | if isinstance(value, datetime.datetime): 134 | return value.isoformat() 135 | else: 136 | return str(value) 137 | 138 | obj = {} 139 | field_names = kwargs.get('field_names', []) 140 | exclude = kwargs.get('exclude', []) 141 | extra_field = kwargs.get('extra_field', []) 142 | field_serializer_map = kwargs.get('field_serializer_map', {}) 143 | 144 | for key, val in item.items(): 145 | if key in exclude: 146 | continue 147 | if key in field_names or key in extra_field: 148 | if key in extra_field: 149 | obj[key] = val 150 | continue 151 | if val is not None: 152 | serializer = field_serializer_map.get(key, _serialize_value) 153 | newval = serializer(val) 154 | if not isinstance(newval, str): 155 | newval = str(newval) 156 | obj[key] = newval 157 | return obj 158 | 159 | 160 | def _sanitize_related_item(item, model, **kwargs): 161 | obj = model.objects.get(id=item['id']) 162 | extra_field = kwargs.get('extra_field', []) 163 | field_serializer_map = kwargs.get('field_serializer_map', {}) 164 | 165 | for field_name in extra_field: 166 | serializer = field_serializer_map.get(field_name) 167 | item[field_name] = serializer(obj) 168 | return item 169 | -------------------------------------------------------------------------------- /django_export_csv/mixin.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | import datetime 4 | import unicodecsv as csv 5 | import codecs 6 | 7 | from django.http import StreamingHttpResponse 8 | 9 | from django_export_csv import clean_filename, attach_datestamp, generate_filename, Echo, get_uncontain_field_names 10 | 11 | 12 | class QueryCsvMixin(object): 13 | filename = None 14 | add_datestamp = False 15 | use_verbose_names = True 16 | fields = [] 17 | exclude = [] 18 | field_order = [] 19 | field_header_map = {} 20 | field_serializer_map = {} 21 | extra_field = [] 22 | 23 | def render_csv_response(self, queryset): 24 | """ 25 | making a CSV streaming http response, take a queryset 26 | """ 27 | if self.filename: 28 | filename = clean_filename(self.filename) 29 | if self.add_datestamp: 30 | filename = attach_datestamp(filename) 31 | else: 32 | filename = generate_filename(queryset, self.add_datestamp) 33 | 34 | response_args = {'content_type': 'text/csv'} 35 | 36 | response = StreamingHttpResponse( 37 | self._iter_csv(queryset, Echo()), **response_args) 38 | 39 | # support chinese filename 40 | response['Content-Disposition'] = b'attachment; filename=' + filename.encode(encoding='utf-8') + b';' 41 | response['Cache-Control'] = 'no-cache' 42 | 43 | return response 44 | 45 | def _iter_csv(self, queryset, file_obj): 46 | """ 47 | Writes CSV data to a file object based on the 48 | contents of the queryset and yields each row. 49 | """ 50 | csv_kwargs = {'encoding': 'utf-8'} 51 | 52 | # add BOM to support MS Excel (for Windows only) 53 | yield file_obj.write(codecs.BOM_UTF8) 54 | 55 | if type(queryset).__name__ == 'ValuesQuerySet': 56 | queryset_values = queryset 57 | else: 58 | queryset_values = queryset.values() 59 | 60 | if self.fields: 61 | uncontain_field_names = get_uncontain_field_names( 62 | self.fields, queryset_values.field_names 63 | ) 64 | if uncontain_field_names: 65 | raise Exception(','.join(uncontain_field_names) + " aren't in default field names") 66 | 67 | field_names = self.fields 68 | elif self.exclude: 69 | field_names = [ 70 | f.name 71 | for f in queryset.model._meta.get_fields() 72 | if not (f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)) 73 | and f.name not in self.exclude 74 | ] 75 | else: 76 | field_names = [f.name for f in queryset.model._meta.get_fields() 77 | if not (f.is_relation 78 | or f.one_to_one 79 | or (f.many_to_one and f.related_model))] 80 | 81 | field_names += self.extra_field 82 | 83 | field_names = [field_name for field_name in field_names if field_name in self.field_order] + \ 84 | [field_name for field_name in field_names if field_name not in self.field_order] 85 | 86 | # support extra 87 | # Sometimes, the Django query syntax by itself can't easily express a complex WHERE clause. 88 | # For these edge cases, Django provides the extra() QuerySet modifier 89 | # a hook for injecting specific clauses into the SQL generated by a QuerySet. 90 | extra_columns = list(queryset_values.query.extra_select) 91 | if extra_columns: 92 | field_names += extra_columns 93 | 94 | # support annotate 95 | # Annotates each object in the QuerySet with the provided list of query expressions. 96 | # An expression may be a simple value, a reference to a field on the model (or any related models), 97 | # or an aggregate expression (averages, sums, etc) that 98 | # has been computed over the objects that are related to the objects in the QuerySet. 99 | annotation_columns = list(queryset_values.query.annotation_select) 100 | if annotation_columns: 101 | field_names += annotation_columns 102 | 103 | writer = csv.DictWriter(file_obj, field_names, **csv_kwargs) 104 | 105 | header_map = dict((field, field) for field in field_names) 106 | if self.use_verbose_names: 107 | for field in queryset.model._meta.get_fields(): 108 | if field.name in field_names: 109 | try: 110 | header_map[field.name] = field.verbose_name 111 | except AttributeError: 112 | header_map[field.name] = field.name 113 | 114 | header_map.update(self.field_header_map) 115 | 116 | yield writer.writerow(header_map) 117 | 118 | if self.extra_field: 119 | model = queryset_values.model 120 | for item in queryset_values: 121 | item = self._sanitize_related_item(self.field_serializer_map, item, model) 122 | item = self._sanitize_item(self.field_serializer_map, item, field_names) 123 | yield writer.writerow(item) 124 | else: 125 | for item in queryset_values: 126 | item = self._sanitize_item(self.field_serializer_map, item, field_names) 127 | yield writer.writerow(item) 128 | 129 | def _sanitize_item(self, field_serializer_map, item, field_names): 130 | def _serialize_value(value): 131 | if isinstance(value, datetime.datetime): 132 | return value.isoformat() 133 | else: 134 | return str(value) 135 | 136 | obj = {} 137 | for key, val in item.items(): 138 | if key in self.exclude: 139 | continue 140 | if key in field_names or key in self.extra_field: 141 | if key in self.extra_field: 142 | obj[key] = val 143 | continue 144 | if val is not None: 145 | serializer = field_serializer_map.get(key, _serialize_value) 146 | newval = serializer(val) 147 | if not isinstance(newval, str): 148 | newval = str(newval) 149 | obj[key] = newval 150 | return obj 151 | 152 | def _sanitize_related_item(self, field_serializer_map, item, model): 153 | obj = model.objects.get(id=item['id']) 154 | for field_name in self.extra_field: 155 | serializer = field_serializer_map.get(field_name) 156 | item[field_name] = serializer(obj) 157 | return item 158 | -------------------------------------------------------------------------------- /django_export_csv/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | import datetime 4 | 5 | from django.core.exceptions import ValidationError 6 | 7 | 8 | def clean_filename(filename): 9 | if '.' in filename: 10 | if not filename.endswith('.csv'): 11 | raise ValidationError('file extension should be .csv') 12 | else: 13 | filename = "%s.csv" % filename 14 | return filename 15 | 16 | 17 | def attach_datestamp(filename): 18 | if filename != clean_filename(filename): 19 | raise ValidationError('filename must be cleaned first') 20 | 21 | date_string = datetime.date.today().strftime("%Y%m%d") 22 | return '%s_%s.csv' % (filename[:-4], date_string) 23 | 24 | 25 | def generate_filename(queryset, add_datestamp=False): 26 | filename = queryset.model._meta.model_name 27 | filename = clean_filename(filename) 28 | if add_datestamp: 29 | filename = attach_datestamp(filename) 30 | return filename 31 | 32 | 33 | def get_uncontain_field_names(list1, list2): 34 | return [i for i in list1 if i not in list2] 35 | 36 | 37 | class Echo(object): 38 | """ 39 | An file-like object that implements just the write method. 40 | """ 41 | 42 | def write(self, value): 43 | return value 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.8.8, <2.0 2 | unicodecsv==0.14.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from setuptools.command.test import test as TestCommand 3 | import sys 4 | 5 | 6 | class Tox(TestCommand): 7 | def finalize_options(self): 8 | TestCommand.finalize_options(self) 9 | self.test_args = [] 10 | self.test_suite = True 11 | 12 | def run_tests(self): 13 | # import here, cause outside the eggs aren't loaded 14 | import tox 15 | errcode = tox.cmdline(self.test_args) 16 | sys.exit(errcode) 17 | 18 | setup( 19 | name='django-export-csv', 20 | version='0.1.3.1', 21 | keywords=['django', 'csv', 'export', 'queryset'], 22 | description='A CSV exporter for Django, this tool create a shortcut to render a queryset to a CSV steaming HTTP response.', 23 | license='MIT License', 24 | long_description=open('README.md').read(), 25 | author='oddcc', 26 | author_email='skycc71640@gmail.com', 27 | url='https://github.com/oddcc/django-export-csv', 28 | install_requires=[ 29 | 'django>=1.8.8, <2.0', 30 | 'unicodecsv>=0.14.1', 31 | ], 32 | classifiers=[ 33 | "Development Status :: 4 - Beta", 34 | "Intended Audience :: Developers", 35 | "Programming Language :: Python", 36 | "Environment :: Plugins", 37 | "Framework :: Django", 38 | "License :: OSI Approved :: GNU General Public License (GPL)" 39 | ], 40 | packages=find_packages(exclude=('tests',)), 41 | tests_require=[ 42 | 'tox', 43 | ], 44 | cmdclass={'test': Tox}, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DebugNinjaSlayer/django-export-csv/cbd88c51c1e8fbd265fd27f6bf8d8fa80456166f/tests/__init__.py -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DebugNinjaSlayer/django-export-csv/cbd88c51c1e8fbd265fd27f6bf8d8fa80456166f/tests/core/__init__.py -------------------------------------------------------------------------------- /tests/core/data_init.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | import datetime 4 | 5 | from django.db import models 6 | 7 | from tests.core.models import College, Student 8 | 9 | 10 | def create_student_and_get_queryset(): 11 | college1, _ = College.objects.get_or_create(name="College 1st") 12 | college2, _ = College.objects.get_or_create(name="College 2nd") 13 | 14 | Student.objects.get_or_create( 15 | name='Jim', age=18, is_graduated=False, birthday=datetime.date(1998, 6, 6), college=college1 16 | ) 17 | Student.objects.get_or_create( 18 | name='Bing', age=22, is_graduated=True, birthday=datetime.date(1994, 2, 6), college=college1 19 | ) 20 | Student.objects.get_or_create( 21 | name='Monica', age=25, is_graduated=True, birthday=datetime.date(1991, 2, 6), college=college2 22 | ) 23 | 24 | return Student.objects.extra(select={'is_young': 'birthday < "1995-07-06"'}) 25 | 26 | 27 | def create_college_and_get_queryset(): 28 | college1, _ = College.objects.get_or_create(name="College 1st") 29 | college2, _ = College.objects.get_or_create(name="College 2nd") 30 | 31 | Student.objects.get_or_create( 32 | name='Jim', age=18, is_graduated=False, birthday=datetime.date(1998, 6, 6), college=college1 33 | ) 34 | Student.objects.get_or_create( 35 | name='Bing', age=22, is_graduated=True, birthday=datetime.date(1994, 2, 6), college=college1 36 | ) 37 | Student.objects.get_or_create( 38 | name='Monica', age=25, is_graduated=True, birthday=datetime.date(1991, 2, 6), college=college2 39 | ) 40 | 41 | return College.objects.annotate(student_count=models.Count('students')) 42 | -------------------------------------------------------------------------------- /tests/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.1 on 2018-03-29 01:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='College', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=30, verbose_name="College's name")), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Student', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('name', models.CharField(max_length=30, verbose_name="Student's name")), 29 | ('age', models.IntegerField(verbose_name="Student's age")), 30 | ('is_graduated', models.BooleanField(default=False, verbose_name='Graduated')), 31 | ('birthday', models.DateTimeField(verbose_name='Birthday')), 32 | ('college', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='students', to='core.College', verbose_name="Students's college")), 33 | ], 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /tests/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DebugNinjaSlayer/django-export-csv/cbd88c51c1e8fbd265fd27f6bf8d8fa80456166f/tests/core/migrations/__init__.py -------------------------------------------------------------------------------- /tests/core/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | from django.db import models 4 | 5 | 6 | class Student(models.Model): 7 | name = models.CharField(max_length=30, verbose_name="Student's name") 8 | age = models.IntegerField(verbose_name="Student's age") 9 | is_graduated = models.BooleanField(default=False, verbose_name="Graduated") 10 | birthday = models.DateTimeField(verbose_name="Birthday") 11 | college = models.ForeignKey('College', verbose_name="Students's college", related_name='students') 12 | 13 | def __str__(self): 14 | return self.name 15 | 16 | 17 | class College(models.Model): 18 | name = models.CharField(max_length=30, verbose_name="College's name") 19 | -------------------------------------------------------------------------------- /tests/core/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import os 3 | 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | SECRET_KEY = '+^jd4@x8l1353bu8)_*p_ii0l7q32+-6je!4*r7sx56y-&=!_8' 7 | 8 | DEBUG = True 9 | 10 | INSTALLED_APPS = [ 11 | 'django.contrib.contenttypes', 12 | 13 | 'tests.core', 14 | 'django_export_csv', 15 | ] 16 | 17 | ROOT_URLCONF = 'tests.core.urls' 18 | 19 | DATABASES = { 20 | 'default': { 21 | 'ENGINE': 'django.db.backends.sqlite3', 22 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 23 | } 24 | } -------------------------------------------------------------------------------- /tests/core/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .tests_request import * 2 | from .tests_utils import * -------------------------------------------------------------------------------- /tests/core/tests/tests_request.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | from django.test import TestCase 4 | 5 | 6 | class ResponseTests(TestCase): 7 | def test_cbv_export(self): 8 | response = self.client.get('/') 9 | self.assertEqual(response.status_code, 200) 10 | self.assertTrue(response.has_header("Content-Disposition")) 11 | self.assertEqual(response['Content-Type'], 'text/csv') 12 | 13 | response = self.client.get('/college') 14 | self.assertEqual(response.status_code, 200) 15 | self.assertTrue(response.has_header("Content-Disposition")) 16 | self.assertEqual(response['Content-Type'], 'text/csv') 17 | 18 | def test_fbc_export(self): 19 | response = self.client.get('/function') 20 | self.assertEqual(response.status_code, 200) 21 | self.assertTrue(response.has_header("Content-Disposition")) 22 | self.assertEqual(response['Content-Type'], 'text/csv') 23 | -------------------------------------------------------------------------------- /tests/core/tests/tests_utils.py: -------------------------------------------------------------------------------- 1 | #-*-coding:utf-8 -*- 2 | from __future__ import unicode_literals 3 | from __future__ import absolute_import 4 | from django.test import TestCase 5 | from django.core.exceptions import ValidationError 6 | 7 | from tests.core.data_init import create_student_and_get_queryset 8 | from django_export_csv import utils 9 | 10 | 11 | class CleanFilenameTests(TestCase): 12 | def _assertCleanedFilenameEquals(self, filename, expected_filename): 13 | cleaned_filename = utils.clean_filename(filename) 14 | self.assertEqual(cleaned_filename, expected_filename) 15 | 16 | def test_filename_without_dots(self): 17 | self._assertCleanedFilenameEquals('testfile', 'testfile.csv') 18 | 19 | def test_filename_without_dots_cn(self): 20 | self._assertCleanedFilenameEquals('测试文件', '测试文件.csv') 21 | 22 | def test_filename_no_csv(self): 23 | self.assertRaises(ValidationError, utils.clean_filename, 'test.file') 24 | 25 | def test_filename_no_csv_cn(self): 26 | self.assertRaises(ValidationError, utils.clean_filename, '测试.file') 27 | 28 | def test_filename_not_end_of_csv(self): 29 | self.assertRaises(ValidationError, utils.clean_filename, 'test.csv.file') 30 | 31 | def test_filename_not_end_of_csv_cn(self): 32 | self.assertRaises(ValidationError, utils.clean_filename, '测试.csv.file') 33 | 34 | def test_csv_filename(self): 35 | self._assertCleanedFilenameEquals('testfile.csv', 'testfile.csv') 36 | 37 | def test_csv_filename_cn(self): 38 | self._assertCleanedFilenameEquals('测试文件.csv', '测试文件.csv') 39 | 40 | 41 | class AttachDatestampTests(TestCase): 42 | def test_filename_should_be_cleaned_first(self): 43 | self.assertRaises(ValidationError, utils.attach_datestamp, 'testfile') 44 | self.assertRaises(ValidationError, utils.attach_datestamp, 'test.file') 45 | 46 | def test_filename_should_be_cleaned_first_cn(self): 47 | self.assertRaises(ValidationError, utils.attach_datestamp, '测试文件') 48 | self.assertRaises(ValidationError, utils.attach_datestamp, '测试.file') 49 | 50 | def test_attach_datestamp(self): 51 | filename = utils.attach_datestamp('testfile.csv') 52 | self.assertRegexpMatches(filename, r'testfile_[0-9]{8}.csv') 53 | 54 | def test_attach_datestamp_cn(self): 55 | filename = utils.attach_datestamp('测试文件.csv') 56 | self.assertRegexpMatches(filename, r'测试文件_[0-9]{8}.csv') 57 | 58 | 59 | class GenerateFileTests(TestCase): 60 | def test_can_generate_filename_from_model(self): 61 | queryset = create_student_and_get_queryset() 62 | self.assertEqual(utils.generate_filename(queryset), 'student.csv') 63 | self.assertRegexpMatches(utils.generate_filename(queryset, True), r'student_[0-9]{8}.csv') 64 | -------------------------------------------------------------------------------- /tests/core/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | from django.conf.urls import url 4 | 5 | from tests.core import views 6 | 7 | urlpatterns = [ 8 | url(r'^$', views.StudentListView.as_view()), 9 | url(r'^college$', views.CollegeListView.as_view()), 10 | url(r'^function$', views.student_list_view), 11 | ] 12 | -------------------------------------------------------------------------------- /tests/core/views.py: -------------------------------------------------------------------------------- 1 | #-*-coding:utf-8 -*- 2 | from __future__ import unicode_literals 3 | from __future__ import absolute_import 4 | from django.views.generic import ListView 5 | 6 | from tests.core.data_init import create_student_and_get_queryset, create_college_and_get_queryset 7 | from django_export_csv import QueryCsvMixin, render_csv_response 8 | 9 | 10 | def boolean_serializer(value): 11 | if value: 12 | return 'Y' 13 | else: 14 | return 'N' 15 | 16 | 17 | def college_serializer(obj): 18 | return obj.college.name 19 | 20 | 21 | class StudentListView(QueryCsvMixin, ListView): 22 | filename = 'export_student_list' 23 | add_datestamp = True 24 | use_verbose_names = True 25 | exclude = ['id'] 26 | field_order = ['name', 'is_graduated'] 27 | field_header_map = {'is_graduated': 'Graduated'} 28 | field_serializer_map = {'is_graduated': boolean_serializer, 'college': college_serializer} 29 | extra_field = ['college'] 30 | queryset = create_student_and_get_queryset() 31 | 32 | def get(self, *args, **kwargs): 33 | return self.render_csv_response(self.get_queryset()) 34 | 35 | 36 | class CollegeListView(QueryCsvMixin, ListView): 37 | filename = 'export_college_list' 38 | add_datestamp = True 39 | use_verbose_names = True 40 | queryset = create_college_and_get_queryset() 41 | 42 | def get(self, *args, **kwargs): 43 | return self.render_csv_response(self.get_queryset()) 44 | 45 | 46 | def student_list_view(request): 47 | filename = 'export_student_list' 48 | add_datestamp = True 49 | use_verbose_names = True 50 | exclude = ['id'] 51 | field_order = ['name', 'is_graduated'] 52 | field_header_map = {'is_graduated': 'Graduated'} 53 | field_serializer_map = {'is_graduated': boolean_serializer, 'college': college_serializer} 54 | extra_field = ['college'] 55 | 56 | if request.method == 'GET': 57 | queryset = create_student_and_get_queryset() 58 | return render_csv_response( 59 | queryset, filename=filename, add_datestamp=add_datestamp, use_verbose_names=use_verbose_names, 60 | exclude_field=exclude, field_order=field_order, field_header_map=field_header_map, 61 | field_serializer_map=field_serializer_map, extra_field=extra_field 62 | ) 63 | -------------------------------------------------------------------------------- /tests/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DebugNinjaSlayer/django-export-csv/cbd88c51c1e8fbd265fd27f6bf8d8fa80456166f/tests/db.sqlite3 -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import unicode_literals 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.path.pardir)) 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError: 13 | # The above import may fail for some other reason. Ensure that the 14 | # issue is really that Django is missing to avoid masking other 15 | # exceptions on Python 2. 16 | try: 17 | import django 18 | except ImportError: 19 | raise ImportError( 20 | "Couldn't import Django. Are you sure it's installed and " 21 | "available on your PYTHONPATH environment variable? Did you " 22 | "forget to activate a virtual environment?" 23 | ) 24 | raise 25 | execute_from_command_line(sys.argv) 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = 4 | py{27,33,34,35}-django{18}, 5 | py{27,34,35}-django{19,110}, 6 | py{27,33,34,35,36}-django{111}, 7 | 8 | [testenv] 9 | commands=python {toxinidir}/tests/manage.py test core 10 | install_command = pip install {opts} {packages} --trusted-host pypi.python.org 11 | deps= 12 | django18: Django>=1.8,<1.9 13 | django19: Django>=1.9,<1.10 14 | django110: Django>=1.10,<1.11 15 | django111: Django>=1.11,<1.12 --------------------------------------------------------------------------------