Skip to content

Commit 0d4bfab

Browse files
rlskoeseracdhablms
committed
Updates for compatibility with Django 4.0+
* Revise list of python versions for tox * Use updated django url/path syntax * Update for django 4.0+ compatibility * Address indentation flagged by flake8 * Update tox.ini * Allow a wider range of django versions * Add basic github actions unit test with build matrix * Remove import error checks for older versions of django * Exclude incompatible python/django combinations from build matrix * Add python 3.13 to test matrix * Update python & Django classifiers to match tested versions * Configure custom export action for custom export test case --------- Co-authored-by: Chris Adams <[email protected]> Co-authored-by: Ben Silverman <[email protected]>
1 parent aa890c8 commit 0d4bfab

File tree

10 files changed

+129
-30
lines changed

10 files changed

+129
-30
lines changed

.github/workflows/unit_tests.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: unit tests
2+
3+
on:
4+
push: # run on every push or PR to any branch
5+
pull_request:
6+
schedule: # run automatically on main branch each Tuesday at 11am
7+
- cron: "0 16 * * 2"
8+
9+
jobs:
10+
python-unit:
11+
name: Python unit tests
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
16+
django: ["4.0", "4.1", "4.2", "5.0", "5.1"]
17+
exclude:
18+
# django 5.0 and 5.1 require python 3.10 minimum
19+
- python: "3.9"
20+
django: 5.0
21+
- python: "3.9"
22+
django: 5.1
23+
# django 4.0 only goes up to python 3.10
24+
- python: "3.11"
25+
django: 4.0
26+
- python: "3.12"
27+
django: 4.0
28+
- python: "3.13"
29+
django: 4.0
30+
# django 4.1 only goes up to python 3.11
31+
- python: "3.12"
32+
django: 4.1
33+
- python: "3.13"
34+
django: 4.1
35+
steps:
36+
- name: Checkout repository
37+
uses: actions/checkout@v4
38+
39+
- name: Setup Python
40+
uses: actions/setup-python@v5
41+
with:
42+
python-version: ${{ matrix.python }}
43+
44+
# Base python cache on the hash of test requirements file
45+
# if the file changes, the cache is invalidated.
46+
- name: Cache pip
47+
uses: actions/cache@v4
48+
with:
49+
path: ~/.cache/pip
50+
key: pip-${{ hashFiles('requirements-test.txt') }}
51+
restore-keys: |
52+
pip-${{ hashFiles('requirements-test.txt') }}
53+
54+
- name: Install package with dependencies
55+
run: |
56+
pip install -q Django==${{ matrix.django }}
57+
pip install -r requirements-test.txt
58+
59+
- name: Run python tests
60+
run: python tests/run_tests.py

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
django
1+
django>=4.0,<6.0
22
xlsxwriter

setup.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
setup(
1414
name='django-tabular-export',
15-
version='1.1.0',
15+
version='1.2.0',
1616
description="""Simple spreadsheet exports from Django""",
1717
long_description=readme,
1818
author='Chris Adams',
@@ -32,14 +32,19 @@
3232
classifiers=[
3333
'Development Status :: 5 - Production/Stable',
3434
'Framework :: Django',
35-
'Framework :: Django :: 1.8',
36-
'Framework :: Django :: 1.9',
35+
'Framework :: Django :: 4.0',
36+
'Framework :: Django :: 4.1',
37+
'Framework :: Django :: 4.2',
38+
'Framework :: Django :: 5.0',
39+
'Framework :: Django :: 5.1',
3740
'Intended Audience :: Developers',
3841
'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication',
3942
'Natural Language :: English',
40-
'Programming Language :: Python :: 2',
41-
'Programming Language :: Python :: 2.7',
4243
'Programming Language :: Python :: 3',
43-
'Programming Language :: Python :: 3.5',
44+
'Programming Language :: Python :: 3.9',
45+
'Programming Language :: Python :: 3.10',
46+
'Programming Language :: Python :: 3.11',
47+
'Programming Language :: Python :: 3.12',
48+
'Programming Language :: Python :: 3.13',
4449
],
4550
)

tabular_export/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from functools import wraps
1616

17-
from django.utils.encoding import force_text
17+
from django.utils.encoding import force_str
1818
from django.utils.translation import gettext_lazy as _
1919

2020
from .core import export_to_csv_response, export_to_excel_response, flatten_queryset
@@ -32,7 +32,7 @@ def outer(f):
3232
@wraps(f)
3333
def inner(modeladmin, request, queryset, filename=None, *args, **kwargs):
3434
if filename is None:
35-
filename = '%s.%s' % (force_text(modeladmin.model._meta.verbose_name_plural), suffix)
35+
filename = '%s.%s' % (force_str(modeladmin.model._meta.verbose_name_plural), suffix)
3636
return f(modeladmin, request, queryset, filename=filename, *args, **kwargs)
3737
return inner
3838
return outer

tabular_export/core.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,21 @@
1616
If your Django settings module sets ``TABULAR_RESPONSE_DEBUG`` to ``True`` the data will be dumped as an HTML
1717
table and will not be delivered as a download.
1818
"""
19+
1920
from __future__ import absolute_import, division, print_function, unicode_literals
2021

2122
import csv
2223
import datetime
2324
import sys
2425
from functools import wraps
2526
from itertools import chain
27+
from urllib.parse import quote as urlquote
2628

2729
import xlsxwriter
2830
from django.conf import settings
2931
from django.http import HttpResponse, StreamingHttpResponse
30-
from django.utils.encoding import force_text
31-
from django.utils.http import urlquote
32-
from django.views.decorators.cache import never_cache
32+
from django.utils.encoding import force_str
33+
from django.utils.cache import add_never_cache_headers
3334

3435

3536
def get_field_names_from_queryset(qs):
@@ -90,7 +91,7 @@ def convert_value_to_unicode(v):
9091
elif hasattr(v, 'isoformat'):
9192
return v.isoformat()
9293
else:
93-
return force_text(v)
94+
return force_str(v)
9495

9596

9697
def set_content_disposition(f):
@@ -112,8 +113,9 @@ def inner(filename, *args, **kwargs):
112113
if not getattr(settings, 'TABULAR_RESPONSE_DEBUG', False):
113114
return f(filename, *args, **kwargs)
114115
else:
115-
resp = never_cache(export_to_debug_html_response)(filename, *args, **kwargs)
116-
del resp['Content-Disposition'] # Don't trigger a download
116+
resp = export_to_debug_html_response(filename, *args, **kwargs)
117+
del resp["Content-Disposition"] # Don't trigger a download
118+
add_never_cache_headers(resp)
117119
return resp
118120

119121
return inner
@@ -175,7 +177,7 @@ def export_to_excel_response(filename, headers, rows):
175177
elif isinstance(col, datetime.date):
176178
worksheet.write_datetime(y, x, col, date_format)
177179
else:
178-
worksheet.write(y, x, force_text(col, strings_only=True))
180+
worksheet.write(y, x, force_str(col, strings_only=True))
179181

180182
workbook.close()
181183

tests/admin.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,15 @@ def tags_count(self, obj):
2323
def get_queryset(self, *args, **kwargs):
2424
return self.model.objects.all().annotate(tags_count=Count('tags', distinct=True))
2525

26+
def custom_export_to_csv_action(self, request, queryset):
27+
# Add a custom action with the extra verbose name "number of tags"
28+
return export_to_csv_action(
29+
self,
30+
request,
31+
queryset,
32+
extra_verbose_names={'tags_count': 'number of tags'},
33+
)
34+
35+
actions = (export_to_excel_action, export_to_csv_action, custom_export_to_csv_action)
2636

2737
admin.site.register(TestModel, TestModelAdmin)

tests/run_tests.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
import sys
7+
from uuid import uuid4
78

89
import django
910
from django.conf import settings
@@ -35,10 +36,27 @@
3536
'tests',
3637
],
3738
SITE_ID=1,
38-
MIDDLEWARE_CLASSES=('django.contrib.sessions.middleware.SessionMiddleware',
39-
'django.contrib.auth.middleware.AuthenticationMiddleware',
40-
'django.contrib.messages.middleware.MessageMiddleware'),
41-
ROOT_URLCONF='tests.urls')
39+
MIDDLEWARE=(
40+
'django.contrib.sessions.middleware.SessionMiddleware',
41+
'django.contrib.auth.middleware.AuthenticationMiddleware',
42+
'django.contrib.messages.middleware.MessageMiddleware'),
43+
ROOT_URLCONF='tests.urls',
44+
TEMPLATES=[
45+
{
46+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
47+
'OPTIONS': {
48+
'context_processors': [
49+
'django.contrib.auth.context_processors.auth',
50+
'django.template.context_processors.request',
51+
'django.contrib.messages.context_processors.messages',
52+
],
53+
'loaders': [
54+
'django.template.loaders.app_directories.Loader',
55+
],
56+
},
57+
},
58+
],
59+
SECRET_KEY=uuid4(),)
4260

4361
django.setup()
4462

tests/test_admin_actions.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# encoding: utf-8
22
from __future__ import absolute_import, division, print_function, unicode_literals
3+
import unittest
34

45
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
56
from django.contrib.auth.models import User
6-
from django.core.urlresolvers import reverse
7+
try:
8+
from django.urls import reverse
9+
except ImportError:
10+
from django.core.urlresolvers import reverse
711
from django.test.testcases import TestCase
812

913
from .models import TestModel
@@ -64,9 +68,9 @@ def test_export_to_csv_action(self):
6468

6569
content = list(i.decode('utf-8') for i in response.streaming_content)
6670
self.assertEqual(len(content), TestModel.objects.count() + 1)
67-
self.assertRegexpMatches(content[0], r'^ID,title,tags_count')
68-
self.assertRegexpMatches(content[1], r'^1,TEST ITEM 1,0\r\n')
69-
self.assertRegexpMatches(content[2], r'^2,TEST ITEM 2,0\r\n')
71+
self.assertRegex(content[0], r'^ID,title,tags_count')
72+
self.assertRegex(content[1], r'^1,TEST ITEM 1,0\r\n')
73+
self.assertRegex(content[2], r'^2,TEST ITEM 2,0\r\n')
7074

7175
def test_custom_export_to_csv_action(self):
7276
changelist_url = reverse('admin:tests_testmodel_changelist')
@@ -85,6 +89,6 @@ def test_custom_export_to_csv_action(self):
8589

8690
content = list(i.decode('utf-8') for i in response.streaming_content)
8791
self.assertEqual(len(content), TestModel.objects.count() + 1)
88-
self.assertRegexpMatches(content[0], r'^ID,title,number of tags')
89-
self.assertRegexpMatches(content[1], r'^1,TEST ITEM 1,0\r\n')
90-
self.assertRegexpMatches(content[2], r'^2,TEST ITEM 2,0\r\n')
92+
self.assertRegex(content[0], r'^ID,title,number of tags')
93+
self.assertRegex(content[1], r'^1,TEST ITEM 1,0\r\n')
94+
self.assertRegex(content[2], r'^2,TEST ITEM 2,0\r\n')

tests/urls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# encoding: utf-8
22
from __future__ import absolute_import, division, print_function, unicode_literals
33

4-
from django.conf.urls import include, url
4+
from django.urls import path
55
from django.contrib import admin
66

77
urlpatterns = [
8-
url(r'^admin/', include(admin.site.urls)),
8+
path("admin/", admin.site.urls),
99
]

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py27, py35, py36
2+
envlist = py39,py310,py311,py312
33

44
[testenv]
55
setenv =

0 commit comments

Comments
 (0)