Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ python:
- "2.6"
- "2.7"
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"
- DJANGO=1.8 RUNNER="python" SUCCESS="echo DONE"
- DJANGO=1.5
- DJANGO=1.6
- DJANGO=1.7
- DJANGO=1.8
- DJANGO=1.9
matrix:
exclude:
- python: "2.6"
env: DJANGO=1.7 RUNNER="python" SUCCESS="echo DONE"
env: DJANGO=1.7
- python: "2.6"
env: DJANGO=1.8 RUNNER="python" SUCCESS="echo DONE"
env: DJANGO=1.8
- python: "2.6"
env: DJANGO=1.9
install:
- pip install -q Django==$DJANGO
- pip install -r dev_requirements.txt
- python setup.py install
script:
- $RUNNER test_app/manage.py test djqscsv_tests
- coverage run --source=djqscsv test_app/manage.py test djqscsv_tests
- flake8 --exclude=migrations djqscsv/ test_app/
after_success:
- $SUCCESS
- coveralls
38 changes: 38 additions & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
contributing
------------

in order for a pull request to be merged, please make sure it meets the following criteria:

1. all unit tests pass for all supported versions of python and django.
2. every newly added line of code is exercised by at least one unit test.
3. all code is compliant with PEP8 style conventions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these requirements it may be good to point to the instructions further down in the document, and to also mention that Travis will be checking all three automatically. Maybe link to or include a screenshot of how Travis reports errors and how a contributor could use the Travis results to fixup their PR.



development setup
-----------------
to setup a development environment, run the following commands::

$ pip install -r dev_requirements.txt


unit testing
------------

to run the unit tests, run the following commands::

$ cd test_app
$ python manage.py test


demo testing
------------

to ensure the app behaves as expected, run the following::

$ cd test_app
$ python manage.py runserver

then, visit ``http://localhost:8000/`` in your browser and confirm it produces a valid CSV.



6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,9 @@ views.py::
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>`_.


development and contributions
-----------------------------

Please read the included ``CONTRIBUTING.rst`` file.
2 changes: 2 additions & 0 deletions dev_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
coveralls==0.3
coverage==3.6
flake8==2.5.1
django>=1.5
4 changes: 2 additions & 2 deletions djqscsv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from djqscsv import (render_to_csv_response, write_csv, # NOQA
generate_filename, CSVException) # NOQA
from .djqscsv import (render_to_csv_response, write_csv, # NOQA
generate_filename, CSVException) # NOQA
5 changes: 4 additions & 1 deletion djqscsv/_csql.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
This module may later be officially supported.
"""


def _identity(x):
return x


def _transform(dataset, arg):
if isinstance(arg, str):
field = arg
Expand Down Expand Up @@ -43,6 +45,7 @@ def EXCLUDE(dataset, *args):

def CONSTANT(value, display_name):
return (None, display_name, lambda x: value)



def AS(field, display_name):
return (field, display_name, _identity)
43 changes: 28 additions & 15 deletions djqscsv/djqscsv.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
from django.utils.text import slugify
from django.http import HttpResponse

from django.conf import settings
if not settings.configured:
# required to import ValuesQuerySet
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 """
Expand Down Expand Up @@ -74,24 +67,44 @@ def write_csv(queryset, file_obj, **kwargs):

# the CSV must always be built from a values queryset
# in order to introspect the necessary fields.
if isinstance(queryset, ValuesQuerySet):
# However, repeated calls to values can expose fields that were not
# present in the original qs. If using `values` as a way to
# scope field permissions, this is unacceptable. The solution
# is to make sure values is called *once*.

# perform an string check to avoid a non-existent class in certain
# versions
if type(queryset).__name__ == 'ValuesQuerySet':
values_qs = queryset
else:
values_qs = queryset.values()
# could be a non-values qs, or could be django 1.9+
iterable_class = getattr(queryset, '_iterable_class', object)
if iterable_class.__name__ == 'ValuesIterable':
values_qs = queryset
else:
values_qs = queryset.values()

try:
field_names = values_qs.field_names

field_names = values_qs.query.values_select
except AttributeError:
# in django1.5, empty querysets trigger
# this exception, but not django 1.6
raise CSVException("Empty queryset provided to exporter.")
try:
field_names = values_qs.field_names
except AttributeError:
# in django1.5, empty querysets trigger
# this exception, but not django 1.6
raise CSVException("Empty queryset provided to exporter.")

extra_columns = list(values_qs.query.extra_select)
if extra_columns:
field_names += extra_columns

aggregate_columns = list(values_qs.query.aggregate_select)
try:
aggregate_columns = list(values_qs.query.annotation_select)
except AttributeError:
# this gets a deprecation warning in django 1.9 but is
# required in django<=1.7
aggregate_columns = list(values_qs.query.aggregate_select)

if aggregate_columns:
field_names += aggregate_columns

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.3.2',
version='0.3.3',
description='A simple python module for writing querysets to csv',
long_description=open('README.rst').read(),
author=author,
Expand Down
2 changes: 1 addition & 1 deletion test_app/djqscsv_tests/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

import djqscsv.djqscsv as djqscsv # NOQA

from djqscsv._csql import SELECT, EXCLUDE, AS, CONSTANT
from djqscsv._csql import SELECT, EXCLUDE, AS, CONSTANT # NOQA
36 changes: 36 additions & 0 deletions test_app/djqscsv_tests/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-01-13 15:38
from __future__ import unicode_literals

import datetime
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Activity',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name=b'Name of Activity')),
],
),
migrations.CreateModel(
name='Person',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name="Person's name")),
('address', models.CharField(max_length=255)),
('info', models.TextField(verbose_name=b'Info on Person')),
('born', models.DateTimeField(default=datetime.datetime(2001, 1, 1, 1, 1))),
('hobby', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djqscsv_tests.Activity')),
],
),
]
Empty file.
7 changes: 4 additions & 3 deletions test_app/djqscsv_tests/models.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
from django.db import models

from django.utils.translation import ugettext as _

from django.utils.encoding import python_2_unicode_compatible
from datetime import datetime

SOME_TIME = datetime(2001, 01, 01, 01, 01)
SOME_TIME = datetime(2001, 1, 1, 1, 1)


class Activity(models.Model):
name = models.CharField(max_length=50, verbose_name="Name of Activity")


@python_2_unicode_compatible
class Person(models.Model):
name = models.CharField(max_length=50, verbose_name=_("Person's name"))
address = models.CharField(max_length=255)
info = models.TextField(verbose_name="Info on Person")
hobby = models.ForeignKey(Activity)
born = models.DateTimeField(default=SOME_TIME)

def __unicode__(self):
def __str__(self):
return self.name
4 changes: 2 additions & 2 deletions test_app/djqscsv_tests/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from test_csv_creation import *
from test_utilities import *
from djqscsv_tests.tests.test_csv_creation import * # NOQA
from djqscsv_tests.tests.test_utilities import * # NOQA
3 changes: 1 addition & 2 deletions test_app/djqscsv_tests/tests/test_csv_creation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from django.db.models import Count
from django.test import TestCase
from django.core.exceptions import ValidationError

from django import VERSION as DJANGO_VERSION

Expand All @@ -16,7 +15,6 @@
from django.utils import six

if six.PY3:
from functools import filter
from io import StringIO
else:
from StringIO import StringIO
Expand Down Expand Up @@ -285,6 +283,7 @@ def test_extra_select_header_map(self):
self.qs, csv_with_extra,
field_header_map={'Most Powerful': 'Sturdiest'})


class RenderToCSVResponseTests(CSVTestCase):

def test_render_to_csv_response_with_filename_and_datestamp(self):
Expand Down
16 changes: 9 additions & 7 deletions test_app/djqscsv_tests/tests/test_utilities.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
import datetime

from operator import attrgetter

from django.test import TestCase
from django.core.exceptions import ValidationError

from django.utils.encoding import python_2_unicode_compatible

from djqscsv_tests.context import djqscsv

from djqscsv_tests.util import create_people_and_get_queryset
Expand Down Expand Up @@ -81,9 +85,8 @@ def test_sanitize_date_with_formatter(self):
def test_sanitize_date_with_bad_formatter(self):
record = {'name': 'Tenar',
'created': datetime.datetime(1973, 5, 13)}
formatter = lambda d: d.day
with self.assertRaises(AttributeError):
djqscsv._sanitize_unicode_record(formatter, record)
djqscsv._sanitize_unicode_record(attrgetter('day'), record)


class AppendDatestampTests(TestCase):
Expand Down Expand Up @@ -120,15 +123,14 @@ def test_generate_filename(self):
class SafeUtf8EncodeTest(TestCase):
def test_safe_utf8_encode(self):

@python_2_unicode_compatible
class Foo(object):
def __unicode__(self):
def __str__(self):
return u'¯\_(ツ)_/¯'
def __str_(self):
return self.__unicode__().encode('utf-8')

for val in (u'¯\_(ツ)_/¯', 'plain', r'raw',
b'123', 11312312312313L, False,
datetime.datetime(2001, 01, 01),
b'123', 11312312312313, False,
datetime.datetime(2001, 1, 1),
4, None, [], set(), Foo):

first_pass = djqscsv._safe_utf8_stringify(val)
Expand Down
9 changes: 4 additions & 5 deletions test_app/djqscsv_tests/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from django.conf.urls import patterns, include, url
import views
from django.conf.urls import url
from djqscsv_tests import views

urlpatterns = patterns(
'',
url(r'^get_csv/', views.get_csv, name='get_csv'),
urlpatterns = (
url(r'^$', views.get_csv, name='get_csv'),
)
1 change: 1 addition & 0 deletions test_app/djqscsv_tests/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .models import Person, Activity


def create_people_and_get_queryset():
doing_magic, _ = Activity.objects.get_or_create(name="Doing Magic")
resting, _ = Activity.objects.get_or_create(name="Resting")
Expand Down
4 changes: 1 addition & 3 deletions test_app/djqscsv_tests/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

import djqscsv
from models import Person
from djqscsv_tests.context import djqscsv
from .util import create_people_and_get_queryset


Expand Down