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/utils.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) (limited to 'wkhtmltopdf/utils.py') 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/utils.py | 55 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 18 deletions(-) (limited to 'wkhtmltopdf/utils.py') 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/utils.py') 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/utils.py') 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 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/utils.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'wkhtmltopdf/utils.py') 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('"', '\\"')) -- 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/utils.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) (limited to 'wkhtmltopdf/utils.py') 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 -- 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/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'wkhtmltopdf/utils.py') 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, -- 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/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'wkhtmltopdf/utils.py') 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 -- 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/utils.py') 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 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/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'wkhtmltopdf/utils.py') 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