From 10680259b0843d3688dbae241f8abb5c366ea4c5 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 20 Jul 2012 14:17:59 -0400 Subject: test_template_to_temp_file tries to clean up after itself. --- wkhtmltopdf/tests.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 5eb733f..0249cb5 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -1,13 +1,21 @@ +from __future__ import absolute_import + +import os + from django.test import TestCase -from utils import template_to_temp_file +from .utils import template_to_temp_file + class TestUtils(TestCase): def test_template_to_temp_file(self): """Should render a template to a temporary file.""" title = 'A test template.' temp_file = template_to_temp_file('sample.html', {'title': title}) - with open(temp_file, 'r') as f: - saved_content = f.read() - self.assertTrue(title in saved_content) - + try: + with open(temp_file, 'r') as f: + saved_content = f.read() + self.assertTrue(title in saved_content) + finally: + if os.path.exists(temp_file): + os.remove(temp_file) -- cgit v1.2.3 From df83b5a8045d1fe69b3ae4d5ffdc5f98069862dc Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 20 Jul 2012 14:30:13 -0400 Subject: test_wkhtmltopdf tests that wkhtmltopdf generates a PDF. --- wkhtmltopdf/tests.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 0249cb5..f50d8f2 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -4,10 +4,22 @@ import os from django.test import TestCase -from .utils import template_to_temp_file +from .utils import template_to_temp_file, wkhtmltopdf class TestUtils(TestCase): + def test_wkhtmltopdf(self): + """Should run wkhtmltopdf to generate a PDF""" + title = 'A test template.' + temp_file = template_to_temp_file('sample.html', {'title': title}) + pdf_output = None + try: + pdf_output = wkhtmltopdf(pages=[temp_file]) + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + self.assertTrue(pdf_output.startswith('%PDF'), pdf_output) + def test_template_to_temp_file(self): """Should render a template to a temporary file.""" title = 'A test template.' -- cgit v1.2.3 From eae470eaaa1d3c95f8e58c5296ed28a01bcd74aa Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 20 Jul 2012 14:41:30 -0400 Subject: wkhtmltopdf() uses subprocess.check_output() instead of a custom Popen call. --- wkhtmltopdf/subprocess.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ wkhtmltopdf/utils.py | 16 +++++----------- 2 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 wkhtmltopdf/subprocess.py (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/subprocess.py b/wkhtmltopdf/subprocess.py new file mode 100644 index 0000000..d0abacd --- /dev/null +++ b/wkhtmltopdf/subprocess.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import + +from subprocess import * + + +# Provide Python 2.7's check_output() function. +try: + check_output +except NameError: + def check_output(*popenargs, **kwargs): + r"""Run command with arguments and return its output as a byte string. + + If the exit code was non-zero it raises a CalledProcessError. The + CalledProcessError object will have the return code in the returncode + attribute and output in the output attribute. + + The arguments are the same as for the Popen constructor. Example: + + >>> check_output(["ls", "-l", "/dev/null"]) + 'crw-rw-rw- 1 root root 1, 3 Oct 18 2007 /dev/null\n' + + The stdout argument is not allowed as it is used internally. + To capture standard error in the result, use stderr=STDOUT. + + >>> check_output(["/bin/sh", "-c", + ... "ls -l non_existent_file ; exit 0"], + ... stderr=STDOUT) + 'ls: non_existent_file: No such file or directory\n' + """ + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be overridden.') + process = Popen(stdout=PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + error = CalledProcessError(retcode, cmd) + # Add the output attribute to CalledProcessError, which + # doesn't exist until Python 2.7. + error.output = output + raise error + return output diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index cf01f43..7994a39 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -1,11 +1,14 @@ +from __future__ import absolute_import + from os import fdopen -from subprocess import Popen, PIPE, CalledProcessError from tempfile import mkstemp from django.conf import settings from django.template import loader from django.utils.encoding import smart_str +from .subprocess import check_output + WKHTMLTOPDF_CMD = getattr(settings, 'WKHTMLTOPDF_CMD', 'wkhtmltopdf') def wkhtmltopdf(pages, output=None, **kwargs): @@ -45,17 +48,8 @@ def wkhtmltopdf(pages, output=None, **kwargs): kwargs['quiet'] = '' args = '%s %s %s %s' % (WKHTMLTOPDF_CMD, _extra_args(**kwargs), ' '.join(pages), output or '-') + return check_output(args, shell=True) - process = Popen(args, stdout=PIPE, shell=True) - stdoutdata, stderrdata = process.communicate() - - if process.returncode != 0: - raise CalledProcessError(process.returncode, args) - - if output is None: - output = stdoutdata - - return output def template_to_temp_file(template_name, dictionary=None, context_instance=None): """ -- cgit v1.2.3 From a377496b5a9bfab824b52a7f2e69a64f194930b8 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 20 Jul 2012 15:52:03 -0400 Subject: Reliable command-line argument parsing for wkhtmltopdf(). The API for wkhtmltopdf has changed. Long arguments that take no parameters now use True and not the empty string. In addition, argument-parameters may now be Unicode. --- wkhtmltopdf/tests.py | 38 ++++++++++++++++++++++++++++++++++-- wkhtmltopdf/utils.py | 55 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 20 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index f50d8f2..3e8359f 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -1,24 +1,58 @@ +# -*- coding: utf-8 -*- + from __future__ import absolute_import +from StringIO import StringIO import os +import sys from django.test import TestCase -from .utils import template_to_temp_file, wkhtmltopdf +from .subprocess import CalledProcessError +from .utils import _options_to_args, template_to_temp_file, wkhtmltopdf class TestUtils(TestCase): + def setUp(self): + # Clear standard error + self._stderr = sys.stderr + sys.stderr = open(os.devnull, 'w') + + def tearDown(self): + sys.stderr = self._stderr + + def test_options_to_args(self): + self.assertEqual(_options_to_args(), []) + self.assertEqual(_options_to_args(heart=u'♥', verbose=True, + file_name='file-name'), + ['--file-name', 'file-name', + '--heart', u'♥', + '--verbose']) + def test_wkhtmltopdf(self): """Should run wkhtmltopdf to generate a PDF""" title = 'A test template.' temp_file = template_to_temp_file('sample.html', {'title': title}) pdf_output = None try: + # Standard call pdf_output = wkhtmltopdf(pages=[temp_file]) + self.assertTrue(pdf_output.startswith('%PDF'), pdf_output) + + # Single page + pdf_output = wkhtmltopdf(pages=temp_file) + self.assertTrue(pdf_output.startswith('%PDF'), pdf_output) + + # Unicode + pdf_output = wkhtmltopdf(pages=[temp_file], title=u'♥') + self.assertTrue(pdf_output.startswith('%PDF'), pdf_output) + + # Invalid arguments + self.assertRaises(CalledProcessError, + wkhtmltopdf, pages=[]) finally: if os.path.exists(temp_file): os.remove(temp_file) - self.assertTrue(pdf_output.startswith('%PDF'), pdf_output) def test_template_to_temp_file(self): """Should render a template to a temporary file.""" diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index 7994a39..aab4152 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -1,6 +1,8 @@ from __future__ import absolute_import +from itertools import chain from os import fdopen +import sys from tempfile import mkstemp from django.conf import settings @@ -11,12 +13,24 @@ from .subprocess import check_output WKHTMLTOPDF_CMD = getattr(settings, 'WKHTMLTOPDF_CMD', 'wkhtmltopdf') + +def _options_to_args(**options): + """Converts ``options`` into a string of command-line arguments.""" + flags = [] + for name in sorted(options): + value = options[name] + flags.append('--' + name.replace('_', '-')) + if value is not True: + flags.append(unicode(value)) + return flags + + def wkhtmltopdf(pages, output=None, **kwargs): """ Converts html to PDF using http://code.google.com/p/wkhtmltopdf/. pages: List of file paths or URLs of the html to be converted. - output: Optional output file path. + output: Optional output file path. If None, the output is returned. **kwargs: Passed to wkhtmltopdf via _extra_args() (See https://github.com/antialize/wkhtmltopdf/blob/master/README_WKHTMLTOPDF for acceptable args.) @@ -24,31 +38,36 @@ def wkhtmltopdf(pages, output=None, **kwargs): {'footer_html': 'http://example.com/foot.html'} becomes '--footer-html http://example.com/foot.html' - Where there is no value passed, use a blank string. e.g.: - {'disable_javascript': ''} + Where there is no value passed, use True. e.g.: + {'disable_javascript': True} becomes: - '--disable-javascript ' + '--disable-javascript' example usage: - wkhtmltopdf(html_path="~/example.html", + wkhtmltopdf(pages=['/tmp/example.html'], dpi=300, - orientation="Landscape", - disable_javascript="") + orientation='Landscape', + disable_javascript=True) """ + if isinstance(pages, basestring): + # Support a single page. + pages = [pages] - def _extra_args(**kwargs): - """Converts kwargs into a string of flags to be passed to wkhtmltopdf.""" - flags = '' - for k, v in kwargs.items(): - flags += ' --%s %s' % (k.replace('_', '-'), v) - return flags + if output is None: + # Standard output. + output = '-' - if not isinstance(pages, list): - pages = [pages] + # Default options: + options = { + 'quiet': True, + } + options.update(kwargs) - kwargs['quiet'] = '' - args = '%s %s %s %s' % (WKHTMLTOPDF_CMD, _extra_args(**kwargs), ' '.join(pages), output or '-') - return check_output(args, shell=True) + args = list(chain([WKHTMLTOPDF_CMD], + _options_to_args(**options), + list(pages), + [output])) + return check_output(args, stderr=sys.stderr) def template_to_temp_file(template_name, dictionary=None, context_instance=None): -- cgit v1.2.3 From 6f7d08e0ec7b3c46ea6e9e6aae7fbc9327df29b4 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 20 Jul 2012 15:54:02 -0400 Subject: settings.WKHTMLTOPDF_CMD is loaded on-the-fly, not at the module level. This allows overriding this configuration option at run-time. --- wkhtmltopdf/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index aab4152..ce32621 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -11,8 +11,6 @@ from django.utils.encoding import smart_str from .subprocess import check_output -WKHTMLTOPDF_CMD = getattr(settings, 'WKHTMLTOPDF_CMD', 'wkhtmltopdf') - def _options_to_args(**options): """Converts ``options`` into a string of command-line arguments.""" @@ -63,7 +61,8 @@ def wkhtmltopdf(pages, output=None, **kwargs): } options.update(kwargs) - args = list(chain([WKHTMLTOPDF_CMD], + cmd = getattr(settings, 'WKHTMLTOPDF_CMD', 'wkhtmltopdf') + args = list(chain([cmd], _options_to_args(**options), list(pages), [output])) -- cgit v1.2.3 From 803aba8859109e2c17e4bdf53126c058a2b60918 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 20 Jul 2012 15:58:55 -0400 Subject: settings.WKHTMLTOPDF_CMD_OPTIONS sets default command-line options. --- wkhtmltopdf/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index ce32621..7fe432a 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +from copy import copy from itertools import chain from os import fdopen import sys @@ -56,9 +57,11 @@ def wkhtmltopdf(pages, output=None, **kwargs): output = '-' # Default options: - options = { - 'quiet': True, - } + options = getattr(settings, 'WKHTMLTOPDF_CMD_OPTIONS', None) + if options is None: + options = {'quiet': True} + else: + options = copy(options) options.update(kwargs) cmd = getattr(settings, 'WKHTMLTOPDF_CMD', 'wkhtmltopdf') -- cgit v1.2.3 From bd8903e7787a7bc0738432d9b75f9d1eac446061 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 20 Jul 2012 16:32:58 -0400 Subject: Use warnings.warn instead of raising PendingDeprecationWarnings. --- wkhtmltopdf/tests.py | 22 ++++++++++++++++++++++ wkhtmltopdf/views.py | 13 +++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 3e8359f..8e6b9b2 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -5,11 +5,13 @@ from __future__ import absolute_import from StringIO import StringIO import os import sys +import warnings from django.test import TestCase from .subprocess import CalledProcessError from .utils import _options_to_args, template_to_temp_file, wkhtmltopdf +from .views import PdfResponse, PdfTemplateView class TestUtils(TestCase): @@ -65,3 +67,23 @@ class TestUtils(TestCase): finally: if os.path.exists(temp_file): os.remove(temp_file) + + +class TestViews(TestCase): + def test_deprecated(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + PdfTemplateView() + self.assertEqual(len(w), 1) + self.assertEqual(w[0].category, PendingDeprecationWarning) + self.assertTrue( + 'PDFTemplateView' in str(w[0].message), + "'PDFTemplateView' not in {!r}".format(w[0].message)) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + PdfResponse(None, None) + self.assertEqual(len(w), 1) + self.assertEqual(w[0].category, PendingDeprecationWarning) + self.assertTrue( + 'PDFResponse' in str(w[0].message), + "'PDFResponse' not in {!r}".format(w[0].message)) diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index 164f94f..8da86f5 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -1,5 +1,6 @@ import os from re import compile +import warnings from django.conf import settings from django.contrib.sites.models import Site @@ -21,8 +22,8 @@ class PDFResponse(HttpResponse): class PdfResponse(PDFResponse): def __init__(self, content, filename): - warning = '''PdfResponse is deprecated in favour of PDFResponse. It will be removed in version 1.''' - raise PendingDeprecationWarning(warning) + warnings.warn('PdfResponse is deprecated in favour of PDFResponse. It will be removed in version 1.', + PendingDeprecationWarning, 2) super(PdfResponse, self).__init__(content, filename) @@ -90,7 +91,7 @@ class PDFTemplateView(TemplateView): class PdfTemplateView(PDFTemplateView): #TODO: Remove this in v1.0 - def as_view(cls, **initkwargs): - warning = '''PdfTemplateView is deprecated in favour of PDFTemplateView. It will be removed in version 1.''' - raise PendingDeprecationWarning(warning) - return super(PdfTemplateView, cls).as_view(**initkwargs) + def __init__(self, *args, **kwargs): + warnings.warn('PdfTemplateView is deprecated in favour of PDFTemplateView. It will be removed in version 1.', + PendingDeprecationWarning, 2) + super(PdfTemplateView, self).__init__(*args, **kwargs) -- cgit v1.2.3 From 8c5b30925654309da9e79bf8bb65e010068bd903 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 20 Jul 2012 16:34:23 -0400 Subject: Remove extraneous StringIO import. --- wkhtmltopdf/tests.py | 1 - 1 file changed, 1 deletion(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 8e6b9b2..5f90c33 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -2,7 +2,6 @@ from __future__ import absolute_import -from StringIO import StringIO import os import sys import warnings -- cgit v1.2.3 From 4a27fe9f16ecd4bc6f94ad89046767450316490c Mon Sep 17 00:00:00 2001 From: Simon Law Date: Mon, 23 Jul 2012 15:29:39 -0400 Subject: PDFResponse is more robust: * Now matches HttpResponse in function signature. * Modern Django content_type/mimetype handling. * Sanitizes and quotes filenames in Content-Disposition header. * Tests. --- wkhtmltopdf/tests.py | 42 +++++++++++++++++++++++++++++++++++++++++- wkhtmltopdf/utils.py | 30 ++++++++++++++++++++++++++++++ wkhtmltopdf/views.py | 16 +++++++++++----- 3 files changed, 82 insertions(+), 6 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 5f90c33..6b49378 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -10,7 +10,7 @@ from django.test import TestCase from .subprocess import CalledProcessError from .utils import _options_to_args, template_to_temp_file, wkhtmltopdf -from .views import PdfResponse, PdfTemplateView +from .views import PDFResponse, PdfResponse, PdfTemplateView class TestUtils(TestCase): @@ -69,7 +69,47 @@ class TestUtils(TestCase): class TestViews(TestCase): + def test_pdf_response(self): + """Should generate the correct HttpResonse object and mimetype""" + # 404 + response = PDFResponse(content='', status=404) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.content, '') + self.assertEqual(response['Content-Type'], 'application/pdf') + self.assertFalse(response.has_header('Content-Disposition')) + + content = '%PDF-1.4\n%%EOF' + # Without filename + response = PDFResponse(content=content) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, content) + self.assertEqual(response['Content-Type'], 'application/pdf') + self.assertFalse(response.has_header('Content-Disposition')) + + # With filename + response = PDFResponse(content=content, filename="nospace.pdf") + self.assertEqual(response['Content-Disposition'], + 'attachment; filename="nospace.pdf"') + response = PDFResponse(content=content, filename="one space.pdf") + self.assertEqual(response['Content-Disposition'], + 'attachment; filename="one space.pdf"') + response = PDFResponse(content=content, filename="4'5\".pdf") + self.assertEqual(response['Content-Disposition'], + 'attachment; filename="4\'5.pdf"') + response = PDFResponse(content=content, filename=u"♥.pdf") + self.assertEqual(response['Content-Disposition'], + 'attachment; filename="?.pdf"') + + # Content-Type + response = PDFResponse(content=content, + content_type='application/x-pdf') + self.assertEqual(response['Content-Type'], 'application/x-pdf') + response = PDFResponse(content=content, + mimetype='application/x-pdf') + self.assertEqual(response['Content-Type'], 'application/x-pdf') + def test_deprecated(self): + """Should warn when using deprecated views.""" with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') PdfTemplateView() diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index 7fe432a..fb8f0aa 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -81,3 +81,33 @@ def template_to_temp_file(template_name, dictionary=None, context_instance=None) f.write(smart_str(loader.render_to_string(template_name, dictionary=dictionary, context_instance=context_instance))) return tempfile_path + +def content_disposition_filename(filename): + """ + Sanitize a file name to be used in the Content-Disposition HTTP + header. + + Even if the standard is quite permissive in terms of + characters, there are a lot of edge cases that are not supported by + different browsers. + + See http://greenbytes.de/tech/tc2231/#attmultinstances for more details. + """ + filename = filename.replace(';', '').replace('"', '') + return http_quote(filename) + + +def http_quote(string): + """ + Given a unicode string, will do its dandiest to give you back a + valid ascii charset string you can use in, say, http headers and the + like. + """ + if isinstance(string, unicode): + try: + import unidecode + string = unidecode.unidecode(string) + except ImportError: + string = string.encode('ascii', 'replace') + # Wrap in double-quotes for ; , and the like + return '"{!s}"'.format(string.replace('\\', '\\\\').replace('"', '\\"')) diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index 8da86f5..539a669 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import os from re import compile import warnings @@ -8,23 +10,27 @@ from django.template.context import RequestContext from django.template.response import HttpResponse from django.views.generic import TemplateView -from wkhtmltopdf.utils import template_to_temp_file, wkhtmltopdf +from .utils import (content_disposition_filename, + template_to_temp_file, wkhtmltopdf) class PDFResponse(HttpResponse): - def __init__(self, content, *args, **kwargs): + def __init__(self, content, mimetype=None, status=200, + content_type='application/pdf', *args, **kwargs): filename = kwargs.pop('filename', None) - super(PDFResponse, self).__init__(content, 'application/pdf', *args, **kwargs) + super(PDFResponse, self).__init__(content, mimetype, status, + content_type, *args, **kwargs) if filename: + filename = content_disposition_filename(filename) header_content = 'attachment; filename={0}'.format(filename) - self.__setitem__('Content-Disposition', header_content) + self['Content-Disposition'] = header_content class PdfResponse(PDFResponse): def __init__(self, content, filename): warnings.warn('PdfResponse is deprecated in favour of PDFResponse. It will be removed in version 1.', PendingDeprecationWarning, 2) - super(PdfResponse, self).__init__(content, filename) + super(PdfResponse, self).__init__(content, filename=filename) class PDFTemplateView(TemplateView): -- cgit v1.2.3 From 8e53c5bc5db462f6e39404c73ac96ec0cbb6a6c7 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Tue, 24 Jul 2012 14:57:06 -0400 Subject: PDFTemplateResponse and PDFTemplateView now match Django's implementations PDFTemplateResponse is like TemplateResponse in that it does dynamic rendering of a template on the fly. PDFTemplateView has a much smaller implementation, relying on PDFTemplateResponse to do the rendering for it. It also knows about the standard TemplateResponse when it needs to render the HTML version. --- wkhtmltopdf/tests.py | 116 +++++++++++++++++++++++++++++++++- wkhtmltopdf/utils.py | 55 ++++++++++++++++ wkhtmltopdf/views.py | 174 ++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 307 insertions(+), 38 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 6b49378..0497dd3 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -7,10 +7,13 @@ import sys import warnings from django.test import TestCase +from django.test.client import RequestFactory from .subprocess import CalledProcessError -from .utils import _options_to_args, template_to_temp_file, wkhtmltopdf -from .views import PDFResponse, PdfResponse, PdfTemplateView +from .utils import (override_settings, + _options_to_args, template_to_temp_file, wkhtmltopdf) +from .views import (PDFResponse, PdfResponse, PDFTemplateResponse, + PDFTemplateView, PdfTemplateView) class TestUtils(TestCase): @@ -70,7 +73,7 @@ class TestUtils(TestCase): class TestViews(TestCase): def test_pdf_response(self): - """Should generate the correct HttpResonse object and mimetype""" + """Should generate the correct HttpResponse object and mimetype""" # 404 response = PDFResponse(content='', status=404) self.assertEqual(response.status_code, 404) @@ -108,6 +111,113 @@ class TestViews(TestCase): mimetype='application/x-pdf') self.assertEqual(response['Content-Type'], 'application/x-pdf') + def test_pdf_template_response(self): + """Test PDFTemplateResponse.""" + from django.conf import settings + + with override_settings( + MEDIA_URL='/media/', + STATIC_URL='/static/', + TEMPLATE_LOADERS=['django.template.loaders.filesystem.Loader'], + TEMPLATE_DIRS=[os.path.join(os.path.dirname(__file__), + 'testproject', 'templates')], + WKHTMLTOPDF_DEBUG=False, + ): + # Setup sample.html + template = 'sample.html' + context = {'title': 'Heading'} + request = RequestFactory().get('/') + response = PDFTemplateResponse(request=request, + template=template, + context=context) + self.assertEqual(response._request, request) + self.assertEqual(response.template_name, template) + self.assertEqual(response.context_data, context) + self.assertEqual(response.filename, None) + self.assertEqual(response.header_template, None) + self.assertEqual(response.footer_template, None) + self.assertEqual(response.cmd_options, {}) + self.assertFalse(response.has_header('Content-Disposition')) + + # Render to temporary file + tempfile = response.render_to_temporary_file(template) + tempfile.seek(0) + html_content = tempfile.read() + self.assertTrue(html_content.startswith('')) + self.assertTrue('

{title}

'.format(**context) + in html_content) + + pdf_content = response.rendered_content + self.assertTrue(pdf_content.startswith('%PDF-')) + self.assertTrue(pdf_content.endswith('%%EOF\n')) + + # Header + filename = 'output.pdf' + footer_template = 'footer.html' + cmd_options = {'title': 'Test PDF'} + response = PDFTemplateResponse(request=request, + template=template, + context=context, + filename=filename, + footer_template=footer_template, + cmd_options=cmd_options) + self.assertEqual(response.filename, filename) + self.assertEqual(response.header_template, None) + self.assertEqual(response.footer_template, footer_template) + self.assertEqual(response.cmd_options, cmd_options) + self.assertTrue(response.has_header('Content-Disposition')) + + tempfile = response.render_to_temporary_file(footer_template) + tempfile.seek(0) + footer_content = tempfile.read() + self.assertTrue('MEDIA_URL = {}'.format(settings.MEDIA_URL) + in footer_content) + self.assertTrue('STATIC_URL = {}'.format(settings.STATIC_URL) + in footer_content) + + pdf_content = response.rendered_content + self.assertTrue('\0'.join('{title}'.format(**cmd_options)) + in pdf_content) + + def test_pdf_template_view(self): + """Test PDFTemplateView.""" + with override_settings( + MEDIA_URL='/media/', + STATIC_URL='/static/', + TEMPLATE_LOADERS=['django.template.loaders.filesystem.Loader'], + TEMPLATE_DIRS=[os.path.join(os.path.dirname(__file__), + 'testproject', 'templates')], + WKHTMLTOPDF_DEBUG=False, + ): + # Setup sample.html + template = 'sample.html' + filename = 'output.pdf' + view = PDFTemplateView.as_view(filename=filename, + template_name=template) + + # As PDF + request = RequestFactory().get('/') + response = view(request) + self.assertEqual(response.status_code, 200) + response.render() + self.assertEqual(response['Content-Disposition'], + 'attachment; filename="{}"'.format(filename)) + self.assertTrue(response.content.startswith('%PDF-')) + self.assertTrue(response.content.endswith('%%EOF\n')) + + # As HTML + request = RequestFactory().get('/?as=html') + response = view(request) + self.assertEqual(response.status_code, 200) + response.render() + self.assertFalse(response.has_header('Content-Disposition')) + self.assertTrue(response.content.startswith('')) + + # POST + request = RequestFactory().post('/') + response = view(request) + self.assertEqual(response.status_code, 405) + def test_deprecated(self): """Should warn when using deprecated views.""" with warnings.catch_warnings(record=True) as w: diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index fb8f0aa..d0475c4 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -5,6 +5,7 @@ from itertools import chain from os import fdopen import sys from tempfile import mkstemp +import warnings from django.conf import settings from django.template import loader @@ -76,6 +77,8 @@ def template_to_temp_file(template_name, dictionary=None, context_instance=None) """ Renders a template to a temp file, and returns the path of the file. """ + warnings.warn('template_to_temp_file is deprecated in favour of PDFResponse. It will be removed in version 1.', + PendingDeprecationWarning, 2) file_descriptor, tempfile_path = mkstemp(suffix='.html') with fdopen(file_descriptor, 'wt') as f: f.write(smart_str(loader.render_to_string(template_name, dictionary=dictionary, context_instance=context_instance))) @@ -111,3 +114,55 @@ def http_quote(string): string = string.encode('ascii', 'replace') # Wrap in double-quotes for ; , and the like return '"{!s}"'.format(string.replace('\\', '\\\\').replace('"', '\\"')) + + +try: + # From Django 1.4 + from django.conf import override_settings +except ImportError: + class override_settings(object): + """ + Acts as either a decorator, or a context manager. If it's a decorator it + takes a function and returns a wrapped function. If it's a contextmanager + it's used with the ``with`` statement. In either event entering/exiting + are called before and after, respectively, the function/block is executed. + """ + def __init__(self, **kwargs): + self.options = kwargs + self.wrapped = settings._wrapped + + def __enter__(self): + self.enable() + + def __exit__(self, exc_type, exc_value, traceback): + self.disable() + + def __call__(self, test_func): + from django.test import TransactionTestCase + if isinstance(test_func, type) and issubclass(test_func, TransactionTestCase): + original_pre_setup = test_func._pre_setup + original_post_teardown = test_func._post_teardown + def _pre_setup(innerself): + self.enable() + original_pre_setup(innerself) + def _post_teardown(innerself): + original_post_teardown(innerself) + self.disable() + test_func._pre_setup = _pre_setup + test_func._post_teardown = _post_teardown + return test_func + else: + @wraps(test_func) + def inner(*args, **kwargs): + with self: + return test_func(*args, **kwargs) + return inner + + def enable(self): + override = copy(settings._wrapped) + for key, new_value in self.options.items(): + setattr(override, key, new_value) + settings._wrapped = override + + def disable(self): + settings._wrapped = self.wrapped diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index 539a669..0e91b2a 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -2,28 +2,40 @@ from __future__ import absolute_import import os from re import compile +from tempfile import NamedTemporaryFile import warnings from django.conf import settings from django.contrib.sites.models import Site +from django.http import HttpResponse from django.template.context import RequestContext -from django.template.response import HttpResponse +from django.template.response import TemplateResponse from django.views.generic import TemplateView -from .utils import (content_disposition_filename, - template_to_temp_file, wkhtmltopdf) +from .utils import (content_disposition_filename, wkhtmltopdf) class PDFResponse(HttpResponse): def __init__(self, content, mimetype=None, status=200, - content_type='application/pdf', *args, **kwargs): - filename = kwargs.pop('filename', None) - super(PDFResponse, self).__init__(content, mimetype, status, - content_type, *args, **kwargs) + content_type=None, filename=None, *args, **kwargs): + + if content_type is None: + content_type = 'application/pdf' + + super(PDFResponse, self).__init__(content=content, + mimetype=mimetype, + status=status, + content_type=content_type) + self.set_filename(filename) + + def set_filename(self, filename): + self.filename = filename if filename: filename = content_disposition_filename(filename) header_content = 'attachment; filename={0}'.format(filename) self['Content-Disposition'] = header_content + else: + del self['Content-Disposition'] class PdfResponse(PDFResponse): @@ -33,6 +45,94 @@ class PdfResponse(PDFResponse): super(PdfResponse, self).__init__(content, filename=filename) +class PDFTemplateResponse(TemplateResponse, PDFResponse): + """Renders a Template into a PDF using wkhtmltopdf""" + + def __init__(self, request, template, context=None, mimetype=None, + status=None, content_type=None, current_app=None, + filename=None, header_template=None, footer_template=None, + cmd_options=None, *args, **kwargs): + + super(PDFTemplateResponse, self).__init__(request=request, + template=template, + context=context, + mimetype=mimetype, + status=status, + content_type=content_type, + current_app=None, + *args, **kwargs) + self.set_filename(filename) + + self.header_template = header_template + self.footer_template = footer_template + + if cmd_options is None: + cmd_options = {} + self.cmd_options = cmd_options + + def render_to_temporary_file(self, template_name, mode='w+b', bufsize=-1, + suffix='', prefix='tmp', dir=None, + delete=True): + template = self.resolve_template(template_name) + context = self.resolve_context(self.context_data) + content = template.render(context) + tempfile = NamedTemporaryFile(mode=mode, bufsize=bufsize, + suffix=suffix, prefix=prefix, + dir=dir, delete=delete) + try: + tempfile.write(content) + tempfile.flush() + return tempfile + except: + # Clean-up tempfile if an Exception is raised. + tempfile.close() + raise + + @property + def rendered_content(self): + """Returns the freshly rendered content for the template and context + described by the PDFResponse. + + This *does not* set the final content of the response. To set the + response content, you must either call render(), or set the + content explicitly using the value of this property. + """ + debug = getattr(settings, 'WKHTMLTOPDF_DEBUG', False) + + cmd_options = self.cmd_options.copy() + + input_file = header_file = footer_file = None + + try: + input_file = self.render_to_temporary_file( + template_name=self.template_name, + prefix='wkhtmltopdf', suffix='.html', + delete=(not debug) + ) + + if self.header_template: + header_file = self.render_to_temporary_file( + template_name=self.header_template, + prefix='wkhtmltopdf', suffix='.html', + delete=(not debug) + ) + cmd_options.setdefault('header_html', header_file.name) + + if self.footer_template: + footer_file = self.render_to_temporary_file( + template_name=self.footer_template, + prefix='wkhtmltopdf', suffix='.html', + delete=(not debug) + ) + cmd_options.setdefault('footer_html', footer_file.name) + + return wkhtmltopdf(pages=[input_file.name], **cmd_options) + finally: + # Clean up temporary files + for f in filter(None, (input_file, header_file, footer_file)): + f.close() + + class PDFTemplateView(TemplateView): filename = 'rendered_pdf.pdf' footer_template = None @@ -42,28 +142,20 @@ class PDFTemplateView(TemplateView): margin_left = 0 margin_right = 0 margin_top = 0 - response = PDFResponse - _tmp_files = None - - def __init__(self, *args, **kwargs): - self._tmp_files = [] - super(PDFTemplateView, self).__init__(*args, **kwargs) - - def get(self, request, context_instance=None, *args, **kwargs): - if request.GET.get('as', '') == 'html': - return super(PDFTemplateView, self).get(request, *args, **kwargs) - - if context_instance: - self.context_instance = context_instance - else: - self.context_instance = RequestContext(request, self.get_context_data(**kwargs)) - - page_path = template_to_temp_file(self.get_template_names(), self.get_context_data(), self.context_instance) - pdf_kwargs = self.get_pdf_kwargs() - output = wkhtmltopdf(page_path, **pdf_kwargs) - if self._tmp_files: - map(os.remove, self._tmp_files) - return self.response(output, filename=self.get_filename()) + response_class = PDFTemplateResponse + html_response_class = TemplateResponse + + def get(self, request, *args, **kwargs): + response_class = self.response_class + try: + if request.GET.get('as', '') == 'html': + # Use the html_response_class if HTML was requested. + self.response_class = self.html_response_class + return super(PDFTemplateView, self).get(request, + *args, **kwargs) + finally: + # Remove self.response_class + self.response_class = response_class def get_filename(self): return self.filename @@ -76,12 +168,6 @@ class PDFTemplateView(TemplateView): 'margin_top': self.margin_top, 'orientation': self.orientation, } - if self.header_template: - kwargs['header_html'] = template_to_temp_file(self.header_template, self.get_context_data(), self.context_instance) - self._tmp_files.append(kwargs['header_html']) - if self.footer_template: - kwargs['footer_html'] = template_to_temp_file(self.footer_template, self.get_context_data(), self.context_instance) - self._tmp_files.append(kwargs['footer_html']) return kwargs def get_context_data(self, **kwargs): @@ -95,6 +181,24 @@ class PDFTemplateView(TemplateView): return context + def render_to_response(self, context, **response_kwargs): + """ + Returns a PDF response with a template rendered with the given context. + """ + filename = response_kwargs.pop('filename', self.get_filename()) + cmd_options = response_kwargs.pop('cmd_options', self.get_pdf_kwargs()) + + if issubclass(self.response_class, PDFTemplateResponse): + return super(PDFTemplateView, self).render_to_response( + context=context, filename=filename, cmd_options=cmd_options, + **response_kwargs + ) + else: + return super(PDFTemplateView, self).render_to_response( + context=context, + **response_kwargs + ) + class PdfTemplateView(PDFTemplateView): #TODO: Remove this in v1.0 def __init__(self, *args, **kwargs): -- cgit v1.2.3 From bde096a028c2705b8f56fb5fdcbbaed4b318862d Mon Sep 17 00:00:00 2001 From: Simon Law Date: Tue, 24 Jul 2012 15:14:12 -0400 Subject: Move testproject to wkhtmltopdf._testproject. Avoid polluting the global library namespace with testproject. Instead, since wkhtmltopdf tests use it, we install it within under a "private" name. --HG-- rename : testproject/__init__.py => wkhtmltopdf/_testproject/__init__.py rename : testproject/manage.py => wkhtmltopdf/_testproject/manage.py rename : testproject/requirements.txt => wkhtmltopdf/_testproject/requirements.txt rename : testproject/settings.py => wkhtmltopdf/_testproject/settings.py rename : testproject/templates/footer.html => wkhtmltopdf/_testproject/templates/footer.html rename : testproject/templates/sample.html => wkhtmltopdf/_testproject/templates/sample.html rename : testproject/urls.py => wkhtmltopdf/_testproject/urls.py --- wkhtmltopdf/_testproject/__init__.py | 0 wkhtmltopdf/_testproject/manage.py | 14 +++ wkhtmltopdf/_testproject/requirements.txt | 1 + wkhtmltopdf/_testproject/settings.py | 152 +++++++++++++++++++++++++ wkhtmltopdf/_testproject/templates/footer.html | 2 + wkhtmltopdf/_testproject/templates/sample.html | 7 ++ wkhtmltopdf/_testproject/urls.py | 17 +++ wkhtmltopdf/tests.py | 4 +- 8 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 wkhtmltopdf/_testproject/__init__.py create mode 100644 wkhtmltopdf/_testproject/manage.py create mode 100644 wkhtmltopdf/_testproject/requirements.txt create mode 100644 wkhtmltopdf/_testproject/settings.py create mode 100644 wkhtmltopdf/_testproject/templates/footer.html create mode 100644 wkhtmltopdf/_testproject/templates/sample.html create mode 100644 wkhtmltopdf/_testproject/urls.py (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/_testproject/__init__.py b/wkhtmltopdf/_testproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wkhtmltopdf/_testproject/manage.py b/wkhtmltopdf/_testproject/manage.py new file mode 100644 index 0000000..3e4eedc --- /dev/null +++ b/wkhtmltopdf/_testproject/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +import imp +try: + imp.find_module('settings') # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) + sys.exit(1) + +import settings + +if __name__ == "__main__": + execute_manager(settings) diff --git a/wkhtmltopdf/_testproject/requirements.txt b/wkhtmltopdf/_testproject/requirements.txt new file mode 100644 index 0000000..74c18fb --- /dev/null +++ b/wkhtmltopdf/_testproject/requirements.txt @@ -0,0 +1 @@ +Django==1.3.1 diff --git a/wkhtmltopdf/_testproject/settings.py b/wkhtmltopdf/_testproject/settings.py new file mode 100644 index 0000000..0454766 --- /dev/null +++ b/wkhtmltopdf/_testproject/settings.py @@ -0,0 +1,152 @@ +import os + +PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) + +# Django settings for testproject project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'test.db', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '$)v)0$h)^c5!h2wms8*wn1c!7)7dp@qb87h7q)zecp2@$pnv=g' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'testproject.urls' + +TEMPLATE_DIRS = ( + os.path.join(PROJECT_PATH, 'templates') + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', + + 'wkhtmltopdf', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} diff --git a/wkhtmltopdf/_testproject/templates/footer.html b/wkhtmltopdf/_testproject/templates/footer.html new file mode 100644 index 0000000..3ae09cb --- /dev/null +++ b/wkhtmltopdf/_testproject/templates/footer.html @@ -0,0 +1,2 @@ +MEDIA_URL = {{ MEDIA_URL }} +STATIC_URL = {{ STATIC_URL }} diff --git a/wkhtmltopdf/_testproject/templates/sample.html b/wkhtmltopdf/_testproject/templates/sample.html new file mode 100644 index 0000000..3dfbdbb --- /dev/null +++ b/wkhtmltopdf/_testproject/templates/sample.html @@ -0,0 +1,7 @@ + + + + +

{{ title }}

+ + diff --git a/wkhtmltopdf/_testproject/urls.py b/wkhtmltopdf/_testproject/urls.py new file mode 100644 index 0000000..039e61a --- /dev/null +++ b/wkhtmltopdf/_testproject/urls.py @@ -0,0 +1,17 @@ +from django.conf.urls.defaults import patterns, include, url + +# Uncomment the next two lines to enable the admin: +# from django.contrib import admin +# admin.autodiscover() + +urlpatterns = patterns('', + # Examples: + # url(r'^$', 'testproject.views.home', name='home'), + # url(r'^testproject/', include('testproject.foo.urls')), + + # Uncomment the admin/doc line below to enable admin documentation: + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + # url(r'^admin/', include(admin.site.urls)), +) diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 0497dd3..19fde76 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -120,7 +120,7 @@ class TestViews(TestCase): STATIC_URL='/static/', TEMPLATE_LOADERS=['django.template.loaders.filesystem.Loader'], TEMPLATE_DIRS=[os.path.join(os.path.dirname(__file__), - 'testproject', 'templates')], + '_testproject', 'templates')], WKHTMLTOPDF_DEBUG=False, ): # Setup sample.html @@ -186,7 +186,7 @@ class TestViews(TestCase): STATIC_URL='/static/', TEMPLATE_LOADERS=['django.template.loaders.filesystem.Loader'], TEMPLATE_DIRS=[os.path.join(os.path.dirname(__file__), - 'testproject', 'templates')], + '_testproject', 'templates')], WKHTMLTOPDF_DEBUG=False, ): # Setup sample.html -- cgit v1.2.3 From a0da923c093f79e2205529c5f12fad620f3159a7 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Tue, 24 Jul 2012 16:18:39 -0400 Subject: PDFTemplateView.cmd_options contains all the options to pass to wkhtmltopdf Before, command-line arguments were class-based. Unfortunately, this means that you cannot add new command-line arguments without subclassing. Instead, PDFTemplateView.cmd_options is a dictionary of all command-line arguments. PDFTemplateView.as_view(cmd_options={...}) now works as expected. !!!! WARNING !!!! cmd_options is now empty, leaving wkhtmltopdf with its default behaviour. Explicitly add the options you want. Existing subclasses of PDFTemplateView will now break, but a PendingDeprecationWarning will be issued. Margins will now be wkhtmltopdf's default of 10mm. PdfTemplateView contains a compatibility shim with the old default values for margins and orientation. --- wkhtmltopdf/tests.py | 26 +++++++++++++++++++++++ wkhtmltopdf/utils.py | 8 +++++++ wkhtmltopdf/views.py | 60 +++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 79 insertions(+), 15 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 19fde76..1802440 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -218,6 +218,24 @@ class TestViews(TestCase): response = view(request) self.assertEqual(response.status_code, 405) + def test_get_cmd_options(self): + # Default cmd_options + view = PDFTemplateView() + self.assertEqual(view.cmd_options, PDFTemplateView.cmd_options) + self.assertEqual(PDFTemplateView.cmd_options, {}) + + # Instantiate with new cmd_options + cmd_options = {'orientation': 'landscape'} + view = PDFTemplateView(cmd_options=cmd_options) + self.assertEqual(view.cmd_options, cmd_options) + self.assertEqual(PDFTemplateView.cmd_options, {}) + + # Update local instance of cmd_options + view = PDFTemplateView() + view.cmd_options.update(cmd_options) + self.assertEqual(view.cmd_options, cmd_options) + self.assertEqual(PDFTemplateView.cmd_options, {}) + def test_deprecated(self): """Should warn when using deprecated views.""" with warnings.catch_warnings(record=True) as w: @@ -236,3 +254,11 @@ class TestViews(TestCase): self.assertTrue( 'PDFResponse' in str(w[0].message), "'PDFResponse' not in {!r}".format(w[0].message)) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + PDFTemplateView().get_pdf_kwargs() + self.assertEqual(len(w), 1) + self.assertEqual(w[0].category, PendingDeprecationWarning) + self.assertTrue( + 'get_pdf_kwargs()' in str(w[0].message), + "'get_pdf_kwargs()' not in {!r}".format(w[0].message)) diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index d0475c4..03a5f82 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -19,6 +19,8 @@ def _options_to_args(**options): flags = [] for name in sorted(options): value = options[name] + if value is None: + continue flags.append('--' + name.replace('_', '-')) if value is not True: flags.append(unicode(value)) @@ -38,11 +40,17 @@ def wkhtmltopdf(pages, output=None, **kwargs): {'footer_html': 'http://example.com/foot.html'} becomes '--footer-html http://example.com/foot.html' + Where there is no value passed, use True. e.g.: {'disable_javascript': True} becomes: '--disable-javascript' + To disable a default option, use None. e.g: + {'quiet': None'} + becomes: + '' + example usage: wkhtmltopdf(pages=['/tmp/example.html'], dpi=300, diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index 0e91b2a..bbb6e7d 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -137,14 +137,23 @@ class PDFTemplateView(TemplateView): filename = 'rendered_pdf.pdf' footer_template = None header_template = None - orientation = 'portrait' - margin_bottom = 0 - margin_left = 0 - margin_right = 0 - margin_top = 0 + response_class = PDFTemplateResponse html_response_class = TemplateResponse + # Command-line options to pass to wkhtmltopdf + cmd_options = { + # 'orientation': 'portrait', + # 'collate': True, + # 'quiet': None, + } + + def __init__(self, *args, **kwargs): + super(PDFTemplateView, self).__init__(*args, **kwargs) + + # Copy self.cmd_options to prevent clobbering the class-level object. + self.cmd_options = self.cmd_options.copy() + def get(self, request, *args, **kwargs): response_class = self.response_class try: @@ -160,15 +169,13 @@ class PDFTemplateView(TemplateView): def get_filename(self): return self.filename + def get_cmd_options(self): + return self.cmd_options + def get_pdf_kwargs(self): - kwargs = { - 'margin_bottom': self.margin_bottom, - 'margin_left': self.margin_left, - 'margin_right': self.margin_right, - 'margin_top': self.margin_top, - 'orientation': self.orientation, - } - return kwargs + warnings.warn('PDFTemplateView.get_pdf_kwargs() is deprecated in favour of get_cmd_options(). It will be removed in version 1.', + PendingDeprecationWarning, 2) + return self.get_cmd_options() def get_context_data(self, **kwargs): context = super(PDFTemplateView, self).get_context_data(**kwargs) @@ -185,10 +192,14 @@ class PDFTemplateView(TemplateView): """ Returns a PDF response with a template rendered with the given context. """ - filename = response_kwargs.pop('filename', self.get_filename()) - cmd_options = response_kwargs.pop('cmd_options', self.get_pdf_kwargs()) + filename = response_kwargs.pop('filename', None) + cmd_options = response_kwargs.pop('cmd_options', None) if issubclass(self.response_class, PDFTemplateResponse): + if filename is None: + filename = self.get_filename() + if cmd_options is None: + cmd_options = self.get_cmd_options() return super(PDFTemplateView, self).render_to_response( context=context, filename=filename, cmd_options=cmd_options, **response_kwargs @@ -201,7 +212,26 @@ class PDFTemplateView(TemplateView): class PdfTemplateView(PDFTemplateView): #TODO: Remove this in v1.0 + orientation = 'portrait' + margin_bottom = 0 + margin_left = 0 + margin_right = 0 + margin_top = 0 + def __init__(self, *args, **kwargs): warnings.warn('PdfTemplateView is deprecated in favour of PDFTemplateView. It will be removed in version 1.', PendingDeprecationWarning, 2) super(PdfTemplateView, self).__init__(*args, **kwargs) + + def get_cmd_options(self): + return self.get_pdf_kwargs() + + def get_pdf_kwargs(self): + kwargs = { + 'margin_bottom': self.margin_bottom, + 'margin_left': self.margin_left, + 'margin_right': self.margin_right, + 'margin_top': self.margin_top, + 'orientation': self.orientation, + } + return kwargs -- cgit v1.2.3 From 566738a2f151918c00c522b3f2c742f2faf185dc Mon Sep 17 00:00:00 2001 From: Simon Law Date: Tue, 24 Jul 2012 16:28:06 -0400 Subject: PDFTemplateView actually renders headers and footers now. --- wkhtmltopdf/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index bbb6e7d..bd1da13 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -201,7 +201,10 @@ class PDFTemplateView(TemplateView): if cmd_options is None: cmd_options = self.get_cmd_options() return super(PDFTemplateView, self).render_to_response( - context=context, filename=filename, cmd_options=cmd_options, + context=context, filename=filename, + header_template=self.header_template, + footer_template=self.footer_template, + cmd_options=cmd_options, **response_kwargs ) else: -- cgit v1.2.3 From a6f0a53702a940bf055dadb3bf558aea49c6d862 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Tue, 24 Jul 2012 16:29:30 -0400 Subject: Cleanups and documentation. --- wkhtmltopdf/views.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index bd1da13..c337b6c 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -1,6 +1,5 @@ from __future__ import absolute_import -import os from re import compile from tempfile import NamedTemporaryFile import warnings @@ -8,7 +7,6 @@ import warnings from django.conf import settings from django.contrib.sites.models import Site from django.http import HttpResponse -from django.template.context import RequestContext from django.template.response import TemplateResponse from django.views.generic import TemplateView @@ -16,6 +14,8 @@ from .utils import (content_disposition_filename, wkhtmltopdf) class PDFResponse(HttpResponse): + """HttpResponse that sets the headers for PDF output.""" + def __init__(self, content, mimetype=None, status=200, content_type=None, filename=None, *args, **kwargs): @@ -134,10 +134,17 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): class PDFTemplateView(TemplateView): + """Class-based view for HTML templates rendered to PDF.""" + + # Filename for downloaded PDF. If None, the response is inline. filename = 'rendered_pdf.pdf' - footer_template = None + + # Filenames for the content, header, and footer templates. + template_name = None header_template = None + footer_template = None + # TemplateResponse classes for PDF and HTML response_class = PDFTemplateResponse html_response_class = TemplateResponse -- cgit v1.2.3 From 5a1847309e7fa431c98565805d88a21a40d01406 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Wed, 25 Jul 2012 12:51:26 -0400 Subject: MEDIA_URL and STATIC_URL overrides PDFTemplateResponse.get_override_settings() MEDIA_URL and STATIC_URL used to be set only in get_context_data(), but there are apps such as staticfiles and Django Compressor where this won't work well. Instead, they need to be overridden at the settings level, not at the context level. This allows template context processors to populate a RequestContext with the right values. In addition, MEDIA_URL and STATIC_URL are now overridden as file:// URLs, based on MEDIA_ROOT and STATIC_ROOT. This allows developers to access these views in runserver, against their current codebase. It also means faster access for wkhtmltopdf, since the files are stored locally. --- wkhtmltopdf/tests.py | 52 ++++++++++++++++++++++++++++++++++++++----- wkhtmltopdf/utils.py | 6 +++++ wkhtmltopdf/views.py | 63 ++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 98 insertions(+), 23 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 1802440..764c7f2 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -117,7 +117,13 @@ class TestViews(TestCase): with override_settings( MEDIA_URL='/media/', + MEDIA_ROOT='/tmp/media', STATIC_URL='/static/', + STATIC_ROOT='/tmp/static', + TEMPLATE_CONTEXT_PROCESSORS=[ + 'django.core.context_processors.media', + 'django.core.context_processors.static', + ], TEMPLATE_LOADERS=['django.template.loaders.filesystem.Loader'], TEMPLATE_DIRS=[os.path.join(os.path.dirname(__file__), '_testproject', 'templates')], @@ -151,7 +157,7 @@ class TestViews(TestCase): self.assertTrue(pdf_content.startswith('%PDF-')) self.assertTrue(pdf_content.endswith('%%EOF\n')) - # Header + # Footer filename = 'output.pdf' footer_template = 'footer.html' cmd_options = {'title': 'Test PDF'} @@ -170,20 +176,53 @@ class TestViews(TestCase): tempfile = response.render_to_temporary_file(footer_template) tempfile.seek(0) footer_content = tempfile.read() - self.assertTrue('MEDIA_URL = {}'.format(settings.MEDIA_URL) - in footer_content) - self.assertTrue('STATIC_URL = {}'.format(settings.STATIC_URL) - in footer_content) + + media_url = 'MEDIA_URL = file://{}/'.format(settings.MEDIA_ROOT) + self.assertTrue( + media_url in footer_content, + "{!r} not in {!r}".format(media_url, footer_content) + ) + + static_url = 'STATIC_URL = file://{}/'.format(settings.STATIC_ROOT) + self.assertTrue( + static_url in footer_content, + "{!r} not in {!r}".format(static_url, footer_content) + ) pdf_content = response.rendered_content self.assertTrue('\0'.join('{title}'.format(**cmd_options)) in pdf_content) + # Override settings + response = PDFTemplateResponse(request=request, + template=template, + context=context, + filename=filename, + footer_template=footer_template, + cmd_options=cmd_options, + override_settings={ + 'STATIC_URL': 'file:///tmp/s/' + }) + tempfile = response.render_to_temporary_file(footer_template) + tempfile.seek(0) + footer_content = tempfile.read() + + static_url = 'STATIC_URL = {}'.format('file:///tmp/s/') + self.assertTrue( + static_url in footer_content, + "{!r} not in {!r}".format(static_url, footer_content) + ) + self.assertEqual(settings.STATIC_URL, '/static/') + def test_pdf_template_view(self): """Test PDFTemplateView.""" with override_settings( MEDIA_URL='/media/', STATIC_URL='/static/', + TEMPLATE_CONTEXT_PROCESSORS=[ + 'django.core.context_processors.media', + 'django.core.context_processors.static', + ], TEMPLATE_LOADERS=['django.template.loaders.filesystem.Loader'], TEMPLATE_DIRS=[os.path.join(os.path.dirname(__file__), '_testproject', 'templates')], @@ -193,7 +232,8 @@ class TestViews(TestCase): template = 'sample.html' filename = 'output.pdf' view = PDFTemplateView.as_view(filename=filename, - template_name=template) + template_name=template, + footer_template='footer.html') # As PDF request = RequestFactory().get('/') diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index 03a5f82..69da74f 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -5,6 +5,7 @@ from itertools import chain from os import fdopen import sys from tempfile import mkstemp +import urllib import warnings from django.conf import settings @@ -124,6 +125,11 @@ def http_quote(string): return '"{!s}"'.format(string.replace('\\', '\\\\').replace('"', '\\"')) +def pathname2fileurl(pathname): + """Returns a file:// URL for pathname. Handles OS-specific conversions.""" + return 'file://' + urllib.pathname2url(pathname) + + try: # From Django 1.4 from django.conf import override_settings diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index c337b6c..4bceb27 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -5,12 +5,12 @@ from tempfile import NamedTemporaryFile import warnings from django.conf import settings -from django.contrib.sites.models import Site from django.http import HttpResponse from django.template.response import TemplateResponse from django.views.generic import TemplateView -from .utils import (content_disposition_filename, wkhtmltopdf) +from .utils import (content_disposition_filename, override_settings, + pathname2fileurl, wkhtmltopdf) class PDFResponse(HttpResponse): @@ -51,7 +51,8 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): def __init__(self, request, template, context=None, mimetype=None, status=None, content_type=None, current_app=None, filename=None, header_template=None, footer_template=None, - cmd_options=None, *args, **kwargs): + cmd_options=None, override_settings=None, + *args, **kwargs): super(PDFTemplateResponse, self).__init__(request=request, template=template, @@ -70,12 +71,23 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): cmd_options = {} self.cmd_options = cmd_options + self.override_settings = override_settings + def render_to_temporary_file(self, template_name, mode='w+b', bufsize=-1, suffix='', prefix='tmp', dir=None, delete=True): template = self.resolve_template(template_name) - context = self.resolve_context(self.context_data) - content = template.render(context) + + # Since many things require a sensible settings.MEDIA_URL and + # settings.STATIC_URL, including TEMPLATE_CONTEXT_PROCESSORS; + # the settings themselves need to be overridden when rendering. + # + # This allows django-wkhtmltopdf to play nicely with the + # staticfiles app, for instance. + with override_settings(**self.get_override_settings()): + context = self.resolve_context(self.context_data) + content = template.render(context) + tempfile = NamedTemporaryFile(mode=mode, bufsize=bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete) @@ -132,6 +144,31 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): for f in filter(None, (input_file, header_file, footer_file)): f.close() + def get_override_settings(self): + """Returns a dictionary of settings to override for response_class""" + overrides = { + 'MEDIA_ROOT': settings.MEDIA_ROOT, + 'MEDIA_URL': settings.MEDIA_URL, + 'STATIC_ROOT': settings.STATIC_ROOT, + 'STATIC_URL': settings.STATIC_URL, + } + if self.override_settings is not None: + overrides.update(self.override_settings) + + has_scheme = compile(r'^[^:/]+://') + + # If MEDIA_URL doesn't have a scheme, we transform it into a + # file:// URL based on MEDIA_ROOT. + urls = [('MEDIA_URL', 'MEDIA_ROOT'), + ('STATIC_URL', 'STATIC_ROOT')] + for url, root in urls: + if not has_scheme.match(overrides[url]): + overrides[url] = pathname2fileurl(overrides[root]) + if not overrides[url].endswith('/'): + overrides[url] += '/' + + return overrides + class PDFTemplateView(TemplateView): """Class-based view for HTML templates rendered to PDF.""" @@ -184,34 +221,26 @@ class PDFTemplateView(TemplateView): PendingDeprecationWarning, 2) return self.get_cmd_options() - def get_context_data(self, **kwargs): - context = super(PDFTemplateView, self).get_context_data(**kwargs) - - match_full_url = compile(r'^https?://') - if not match_full_url.match(settings.STATIC_URL): - context['STATIC_URL'] = 'http://' + Site.objects.get_current().domain + settings.STATIC_URL - if not match_full_url.match(settings.MEDIA_URL): - context['MEDIA_URL'] = 'http://' + Site.objects.get_current().domain + settings.MEDIA_URL - - return context - def render_to_response(self, context, **response_kwargs): """ Returns a PDF response with a template rendered with the given context. """ filename = response_kwargs.pop('filename', None) cmd_options = response_kwargs.pop('cmd_options', None) + override_settings = response_kwargs.pop('override_settings', None) if issubclass(self.response_class, PDFTemplateResponse): if filename is None: filename = self.get_filename() + if cmd_options is None: cmd_options = self.get_cmd_options() + return super(PDFTemplateView, self).render_to_response( context=context, filename=filename, header_template=self.header_template, footer_template=self.footer_template, - cmd_options=cmd_options, + cmd_options=cmd_options, override_settings=override_settings, **response_kwargs ) else: -- cgit v1.2.3 From 68215e393d65eb8999fd3c26566cd7a331fe4300 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Wed, 25 Jul 2012 13:16:51 -0400 Subject: settings.WKHTMLTOPDF_ENV can override environment variables. This is most usefully set to {'DISPLAY': ':1'} in production. This allows wkhtmltopdf access to a specific X headless server, since the server will not be running under X. --- wkhtmltopdf/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index 69da74f..3172c42 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from copy import copy from itertools import chain from os import fdopen +import os import sys from tempfile import mkstemp import urllib @@ -74,12 +75,16 @@ def wkhtmltopdf(pages, output=None, **kwargs): options = copy(options) options.update(kwargs) + env = getattr(settings, 'WKHTMLTOPDF_ENV', None) + if env is not None: + env = dict(os.environ, **env) + cmd = getattr(settings, 'WKHTMLTOPDF_CMD', 'wkhtmltopdf') args = list(chain([cmd], _options_to_args(**options), list(pages), [output])) - return check_output(args, stderr=sys.stderr) + return check_output(args, stderr=sys.stderr, env=env) def template_to_temp_file(template_name, dictionary=None, context_instance=None): -- cgit v1.2.3 From 22d2721030ccc81dff23093321290b6ffda6d5fd Mon Sep 17 00:00:00 2001 From: Simon Law Date: Wed, 25 Jul 2012 13:17:35 -0400 Subject: PDFTemplateResponse.convert_to_pdf() calls wkhtmltopdf. Method to make it easier to override spawning of the wkhtmltopdf subprocess. --- wkhtmltopdf/views.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index 4bceb27..ad69874 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -100,6 +100,18 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): tempfile.close() raise + def convert_to_pdf(self, filename, + header_filename=None, footer_filename=None): + cmd_options = self.cmd_options.copy() + # Clobber header_html and footer_html only if filenames are + # provided. These keys may be in self.cmd_options as hardcoded + # static files. + if header_filename is not None: + cmd_options['header_html'] = header_filename + if footer_filename is not None: + cmd_options['footer_html'] = footer_filename + return wkhtmltopdf(pages=[filename], **cmd_options) + @property def rendered_content(self): """Returns the freshly rendered content for the template and context @@ -111,9 +123,8 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): """ debug = getattr(settings, 'WKHTMLTOPDF_DEBUG', False) - cmd_options = self.cmd_options.copy() - input_file = header_file = footer_file = None + header_filename = footer_filename = None try: input_file = self.render_to_temporary_file( @@ -128,7 +139,7 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): prefix='wkhtmltopdf', suffix='.html', delete=(not debug) ) - cmd_options.setdefault('header_html', header_file.name) + header_filename = header_file.name if self.footer_template: footer_file = self.render_to_temporary_file( @@ -136,9 +147,11 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): prefix='wkhtmltopdf', suffix='.html', delete=(not debug) ) - cmd_options.setdefault('footer_html', footer_file.name) + footer_filename = footer_file.name - return wkhtmltopdf(pages=[input_file.name], **cmd_options) + return self.convert_to_pdf(filename=input_file.name, + header_filename=header_filename, + footer_filename=footer_filename) finally: # Clean up temporary files for f in filter(None, (input_file, header_file, footer_file)): -- cgit v1.2.3 From 18132dc19a5bbfe094269d4d0152f3586115306f Mon Sep 17 00:00:00 2001 From: Simon Law Date: Wed, 25 Jul 2012 13:33:03 -0400 Subject: settings.WKHTMLTOPDF_DEBUG defaults to settings.DEBUG. --- wkhtmltopdf/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index ad69874..fff98c8 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -121,7 +121,7 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): response content, you must either call render(), or set the content explicitly using the value of this property. """ - debug = getattr(settings, 'WKHTMLTOPDF_DEBUG', False) + debug = getattr(settings, 'WKHTMLTOPDF_DEBUG', settings.DEBUG) input_file = header_file = footer_file = None header_filename = footer_filename = None -- cgit v1.2.3 From fd3c890e2ecd2b5448ee7ca65503c2b31ac92ddb Mon Sep 17 00:00:00 2001 From: Simon Law Date: Thu, 26 Jul 2012 12:46:58 -0400 Subject: Python 2.6 compatibility fixes. Implicit position arguments for str.format() are a 2.7ism. --- wkhtmltopdf/tests.py | 20 ++++++++++---------- wkhtmltopdf/utils.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) (limited to 'wkhtmltopdf') diff --git a/wkhtmltopdf/tests.py b/wkhtmltopdf/tests.py index 764c7f2..3bdc481 100644 --- a/wkhtmltopdf/tests.py +++ b/wkhtmltopdf/tests.py @@ -177,16 +177,16 @@ class TestViews(TestCase): tempfile.seek(0) footer_content = tempfile.read() - media_url = 'MEDIA_URL = file://{}/'.format(settings.MEDIA_ROOT) + media_url = 'MEDIA_URL = file://{0}/'.format(settings.MEDIA_ROOT) self.assertTrue( media_url in footer_content, - "{!r} not in {!r}".format(media_url, footer_content) + "{0!r} not in {1!r}".format(media_url, footer_content) ) - static_url = 'STATIC_URL = file://{}/'.format(settings.STATIC_ROOT) + static_url = 'STATIC_URL = file://{0}/'.format(settings.STATIC_ROOT) self.assertTrue( static_url in footer_content, - "{!r} not in {!r}".format(static_url, footer_content) + "{0!r} not in {1!r}".format(static_url, footer_content) ) pdf_content = response.rendered_content @@ -207,10 +207,10 @@ class TestViews(TestCase): tempfile.seek(0) footer_content = tempfile.read() - static_url = 'STATIC_URL = {}'.format('file:///tmp/s/') + static_url = 'STATIC_URL = {0}'.format('file:///tmp/s/') self.assertTrue( static_url in footer_content, - "{!r} not in {!r}".format(static_url, footer_content) + "{0!r} not in {1!r}".format(static_url, footer_content) ) self.assertEqual(settings.STATIC_URL, '/static/') @@ -241,7 +241,7 @@ class TestViews(TestCase): self.assertEqual(response.status_code, 200) response.render() self.assertEqual(response['Content-Disposition'], - 'attachment; filename="{}"'.format(filename)) + 'attachment; filename="{0}"'.format(filename)) self.assertTrue(response.content.startswith('%PDF-')) self.assertTrue(response.content.endswith('%%EOF\n')) @@ -285,7 +285,7 @@ class TestViews(TestCase): self.assertEqual(w[0].category, PendingDeprecationWarning) self.assertTrue( 'PDFTemplateView' in str(w[0].message), - "'PDFTemplateView' not in {!r}".format(w[0].message)) + "'PDFTemplateView' not in {0!r}".format(w[0].message)) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') PdfResponse(None, None) @@ -293,7 +293,7 @@ class TestViews(TestCase): self.assertEqual(w[0].category, PendingDeprecationWarning) self.assertTrue( 'PDFResponse' in str(w[0].message), - "'PDFResponse' not in {!r}".format(w[0].message)) + "'PDFResponse' not in {0!r}".format(w[0].message)) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') PDFTemplateView().get_pdf_kwargs() @@ -301,4 +301,4 @@ class TestViews(TestCase): self.assertEqual(w[0].category, PendingDeprecationWarning) self.assertTrue( 'get_pdf_kwargs()' in str(w[0].message), - "'get_pdf_kwargs()' not in {!r}".format(w[0].message)) + "'get_pdf_kwargs()' not in {0!r}".format(w[0].message)) diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index 3172c42..29e4ef6 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -127,7 +127,7 @@ def http_quote(string): except ImportError: string = string.encode('ascii', 'replace') # Wrap in double-quotes for ; , and the like - return '"{!s}"'.format(string.replace('\\', '\\\\').replace('"', '\\"')) + return '"{0!s}"'.format(string.replace('\\', '\\\\').replace('"', '\\"')) def pathname2fileurl(pathname): -- cgit v1.2.3