From fbf54be1da1cc90490336a57df6678a2f833a5f7 Mon Sep 17 00:00:00 2001 From: Jonathan Liuti Date: Wed, 25 Nov 2015 13:29:18 +0100 Subject: Extracted logic from views - fixed tests. The logic was coupled with the views which made things difficult to reuse if you wanted to use the pdf generation somehwere else than in a view. With this patch, the logic has been moved to `utils.py` and should be more easy to reuse. Tests have been adapted and made compatible with django > 1.7 --- .travis.yml | 7 ++++ wkhtmltopdf/tests/tests.py | 30 +++++++-------- wkhtmltopdf/utils.py | 87 ++++++++++++++++++++++++++++++++++++++++++- wkhtmltopdf/views.py | 92 +++++----------------------------------------- 4 files changed, 116 insertions(+), 100 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8dae083..0c64cd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,15 @@ env: - DJANGO="Django>=1.4,<1.5" - DJANGO="Django>=1.5,<1.6" - DJANGO="Django>=1.6,<1.7" + - DJANGO="Django>=1.7,<1.8" + - DJANGO="Django>=1.8,<1.9" + matrix: exclude: + - python: "2.6" + env: DJANGO="Django>=1.7,<1.8" + - python: "2.6" + env: DJANGO="Django>=1.8,<1.9" - python: "3.3" env: DJANGO="Django>=1.4,<1.5" - python: "3.4" diff --git a/wkhtmltopdf/tests/tests.py b/wkhtmltopdf/tests/tests.py index 48a8b19..c15d88f 100644 --- a/wkhtmltopdf/tests/tests.py +++ b/wkhtmltopdf/tests/tests.py @@ -6,6 +6,7 @@ import os import sys from django.conf import settings +from django.template import loader, RequestContext from django.test import TestCase from django.test.client import RequestFactory from django.utils import six @@ -13,7 +14,7 @@ from django.utils.encoding import smart_str from wkhtmltopdf.subprocess import CalledProcessError from wkhtmltopdf.utils import (_options_to_args, make_absolute_paths, - wkhtmltopdf) + wkhtmltopdf, render_to_temporary_file) from wkhtmltopdf.views import PDFResponse, PDFTemplateView, PDFTemplateResponse @@ -50,10 +51,8 @@ class TestUtils(TestCase): def test_wkhtmltopdf(self): """Should run wkhtmltopdf to generate a PDF""" title = 'A test template.' - response = PDFTemplateResponse(self.factory.get('/'), - None, - context={'title': title}) - temp_file = response.render_to_temporary_file('sample.html') + template = loader.get_template('sample.html') + temp_file = render_to_temporary_file(template, context={'title': title}) try: # Standard call pdf_output = wkhtmltopdf(pages=[temp_file.name]) @@ -76,23 +75,20 @@ class TestUtils(TestCase): def test_wkhtmltopdf_with_unicode_content(self): """A wkhtmltopdf call should render unicode content properly""" title = u'♥' - response = PDFTemplateResponse(self.factory.get('/'), - None, - context={'title': title}) - temp_file = response.render_to_temporary_file('unicode.html') + template = loader.get_template('unicode.html') + temp_file = render_to_temporary_file(template, context={'title': title}) try: pdf_output = wkhtmltopdf(pages=[temp_file.name]) self.assertTrue(pdf_output.startswith(b'%PDF'), pdf_output) finally: temp_file.close() - def test_PDFTemplateResponse_render_to_temporary_file(self): + def test_render_to_temporary_file(self): """Should render a template to a temporary file.""" title = 'A test template.' - response = PDFTemplateResponse(self.factory.get('/'), - None, - context={'title': title}) - temp_file = response.render_to_temporary_file('sample.html') + + template = loader.get_template('sample.html') + temp_file = render_to_temporary_file(template, context={'title': title}) temp_file.seek(0) saved_content = smart_str(temp_file.read()) self.assertTrue(title in saved_content) @@ -179,7 +175,8 @@ class TestViews(TestCase): self.assertFalse(response.has_header('Content-Disposition')) # Render to temporary file - tempfile = response.render_to_temporary_file(self.template) + template = loader.get_template(self.template) + tempfile = render_to_temporary_file(template, context=context) tempfile.seek(0) html_content = smart_str(tempfile.read()) self.assertTrue(html_content.startswith('')) @@ -205,7 +202,8 @@ class TestViews(TestCase): self.assertEqual(response.cmd_options, cmd_options) self.assertTrue(response.has_header('Content-Disposition')) - tempfile = response.render_to_temporary_file(self.footer_template) + footer_template = loader.get_template(self.footer_template) + tempfile = render_to_temporary_file(footer_template, context=RequestContext(request, context)) tempfile.seek(0) footer_content = smart_str(tempfile.read()) footer_content = make_absolute_paths(footer_content) diff --git a/wkhtmltopdf/utils.py b/wkhtmltopdf/utils.py index f9d7ac1..87b214c 100644 --- a/wkhtmltopdf/utils.py +++ b/wkhtmltopdf/utils.py @@ -6,6 +6,10 @@ import os import re import sys import shlex +from tempfile import NamedTemporaryFile + +from django.template.context import Context, RequestContext +from django.utils.encoding import smart_text try: from urllib.request import pathname2url @@ -103,6 +107,59 @@ def wkhtmltopdf(pages, output=None, **kwargs): return check_output(ck_args, **ck_kwargs) +def convert_to_pdf(filename, header_filename=None, footer_filename=None, cmd_options=None): + # Clobber header_html and footer_html only if filenames are + # provided. These keys may be in self.cmd_options as hardcoded + # static files. + cmd_options = cmd_options if cmd_options else {} + + 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) + +def render_pdf_from_template(input_template, header_template, footer_template, context, cmd_options=None): + debug = getattr(settings, 'WKHTMLTOPDF_DEBUG', settings.DEBUG) + cmd_options = cmd_options if cmd_options else {} + + input_file = header_file = footer_file = None + header_filename = footer_filename = None + + try: + input_file = render_to_temporary_file( + template=input_template, + context=context, + prefix='wkhtmltopdf', suffix='.html', + delete=(not debug) + ) + + if header_template: + header_file = render_to_temporary_file( + template=header_template, + context=context, + prefix='wkhtmltopdf', suffix='.html', + delete=(not debug) + ) + header_filename = header_file.name + + if footer_template: + footer_file = render_to_temporary_file( + template=footer_template, + context=context, + prefix='wkhtmltopdf', suffix='.html', + delete=(not debug) + ) + footer_filename = footer_file.name + + return convert_to_pdf(filename=input_file.name, + header_filename=header_filename, + footer_filename=footer_filename, + cmd_options=cmd_options) + finally: + # Clean up temporary files + for f in filter(None, (input_file, header_file, footer_file)): + f.close() def content_disposition_filename(filename): """ @@ -144,7 +201,6 @@ def pathname2fileurl(pathname): def make_absolute_paths(content): """Convert all MEDIA files into a file://URL paths in order to correctly get it displayed in PDFs.""" - overrides = [ { 'root': settings.MEDIA_ROOT, @@ -173,3 +229,32 @@ def make_absolute_paths(content): occur[len(x['url']):]) return content + +def render_to_temporary_file(template, context, mode='w+b', bufsize=-1, + suffix='.html', prefix='tmp', dir=None, + delete=True): + # make sure the context is a context object + if not isinstance(context, (Context, RequestContext)): + context = Context(context) + + content = smart_text(template.render(context)) + content = make_absolute_paths(content) + + try: + # Python3 has 'buffering' arg instead of 'bufsize' + tempfile = NamedTemporaryFile(mode=mode, buffering=bufsize, + suffix=suffix, prefix=prefix, + dir=dir, delete=delete) + except TypeError: + tempfile = NamedTemporaryFile(mode=mode, bufsize=bufsize, + suffix=suffix, prefix=prefix, + dir=dir, delete=delete) + + try: + tempfile.write(content.encode('utf-8')) + tempfile.flush() + return tempfile + except: + # Clean-up tempfile if an Exception is raised. + tempfile.close() + raise diff --git a/wkhtmltopdf/views.py b/wkhtmltopdf/views.py index d5f189a..f26c9ae 100644 --- a/wkhtmltopdf/views.py +++ b/wkhtmltopdf/views.py @@ -1,15 +1,10 @@ from __future__ import absolute_import -from tempfile import NamedTemporaryFile - -from django.conf import settings from django.http import HttpResponse from django.template.response import TemplateResponse from django.views.generic import TemplateView -from django.utils.encoding import smart_text -from .utils import (content_disposition_filename, make_absolute_paths, - wkhtmltopdf) +from .utils import (content_disposition_filename, render_pdf_from_template) class PDFResponse(HttpResponse): @@ -65,47 +60,6 @@ class PDFTemplateResponse(TemplateResponse, PDFResponse): cmd_options = {} self.cmd_options = cmd_options - def render_to_temporary_file(self, template_name, mode='w+b', bufsize=-1, - suffix='.html', prefix='tmp', dir=None, - delete=True): - template = self.resolve_template(template_name) - - context = self.resolve_context(self.context_data) - - content = smart_text(template.render(context)) - content = make_absolute_paths(content) - - try: - # Python3 has 'buffering' arg instead of 'bufsize' - tempfile = NamedTemporaryFile(mode=mode, buffering=bufsize, - suffix=suffix, prefix=prefix, - dir=dir, delete=delete) - except TypeError: - tempfile = NamedTemporaryFile(mode=mode, bufsize=bufsize, - suffix=suffix, prefix=prefix, - dir=dir, delete=delete) - - try: - tempfile.write(content.encode('utf-8')) - tempfile.flush() - return tempfile - except: - # Clean-up tempfile if an Exception is raised. - 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 @@ -115,42 +69,14 @@ 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', settings.DEBUG) - - input_file = header_file = footer_file = None - header_filename = footer_filename = 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) - ) - header_filename = 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) - ) - footer_filename = footer_file.name - - 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)): - f.close() - + cmd_options = self.cmd_options.copy() + return render_pdf_from_template( + self.resolve_template(self.template_name), + self.resolve_template(self.header_template), + self.resolve_template(self.footer_template), + context=self.resolve_context(self.context_data), + cmd_options=cmd_options + ) class PDFTemplateView(TemplateView): """Class-based view for HTML templates rendered to PDF.""" -- cgit v1.2.3