Skip to content

Commit

Permalink
Merge pull request #1357 from digitalfabrik/bugfix/pdf-export
Browse files Browse the repository at this point in the history
Fix PDF export
  • Loading branch information
timobrembeck authored Apr 18, 2022
2 parents 920cb60 + 0d6fa42 commit 2e1f38b
Show file tree
Hide file tree
Showing 39 changed files with 362 additions and 1,754 deletions.
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ integreat_cms/locale/de/LC_MESSAGES/django.mo
# Compressed JS & CSS Files
integreat_cms/static/dist/

# File cache
integreat_cms/cache/

# Django stuff:
*.log
*.pot
Expand All @@ -60,6 +57,9 @@ webpack-stats.json
# Media Library
integreat_cms/media/

# PDF files
integreat_cms/pdf/

# XLIFF files folder
integreat_cms/xliff/upload
integreat_cms/xliff/download
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ UNRELEASED
* [ [#1328](https://github.com/digitalfabrik/integreat-cms/issues/1328) ] Fix missing entries in broken link checker
* [ [#1289](https://github.com/digitalfabrik/integreat-cms/issues/1289) ] Prevent submitting feedback for a non-existent imprint
* [ [#1359](https://github.com/digitalfabrik/integreat-cms/issues/1359) ] Cascade delete imprint feedback when imprint is deleted
* [ [#1350](https://github.com/digitalfabrik/integreat-cms/issues/1350) ] Fix font support of PDF export
* [ [#1349](https://github.com/digitalfabrik/integreat-cms/issues/1349) ] Fix network error when downloading PDF files


2022.4.0
Expand Down
4 changes: 2 additions & 2 deletions example-configs/integreat-cms.ini
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ ALLOWED_HOSTS =
STATIC_ROOT = /var/www/integreat-cms/static
# The directory for media files [optional, defaults to "media" in the application directory]
MEDIA_ROOT = /var/www/integreat-cms/media
# The directory for PDF files [optional, defaults to "pdf" in the application directory]
PDF_ROOT = /var/www/integreat-cms/pdf
# The directory for xliff files [optional, defaults to "xliff" in the application directory]
XLIFF_ROOT = /var/www/integreat-cms/xliff
# Enable the possibility to upload legacy file formats [optional, defaults to False]
Expand All @@ -74,8 +76,6 @@ DB_PORT = <port>
REDIS_CACHE = True
# Set this if you want to connect to redis via socket [optional, defaults to None]
REDIS_UNIX_SOCKET = /var/run/redis/redis-server.sock
# The location of the file-based cache (e.g. for PDF files) [optional, defaults to "cache" in the application directory]
FILE_CACHE = /var/cache/integreat-cms

[email]
# Sender email [optional, defaults to "[email protected]"]
Expand Down
4 changes: 2 additions & 2 deletions integreat_cms/api/v3/pdf_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def pdf_export(request, region_slug, language_slug):
:raises ~django.http.Http404: HTTP status 404 if the requested page translation cannot be found.
:return: The requested pages as PDF document (inline)
:rtype: ~django.http.HttpResponse
:return: The redirect to the generated PDF document
:rtype: ~django.http.HttpResponseRedirect
"""
region = request.region
# Request unrestricted queryset because pdf generator performs further operations (e.g. aggregation) on the queryset
Expand Down
6 changes: 3 additions & 3 deletions integreat_cms/cms/templates/pages/page_pdf.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<table>
<tr>
<td id="first-footer">
<pdf:pagenumber>
<pdf:pagenumber/>
</td>
<td id="second-footer">
{{ region.full_name }}
Expand All @@ -69,9 +69,9 @@ <h1 id="title_page">{{ language.table_of_contents }}</h1>
<!-- according to xhtml2pdf documentation all custom tags like <pdf:toc/>, <pdf:nextpage>, etc.
should be wrapped inside block tags like <div> to avoid problems -->
<div class="toc">
<pdf:toc />
<pdf:toc/>
</div>
<div><pdf:nextpage></div>
<div><pdf:nextpage/></div>
{% endif %}

{% for page, info in annotated_pages %}
Expand Down
80 changes: 41 additions & 39 deletions integreat_cms/cms/utils/pdf_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.cache import caches
from django.db.models import Min
from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template.loader import get_template
from django.utils.translation import ugettext as _
from django.views.decorators.cache import never_cache
Expand All @@ -18,6 +20,8 @@

logger = logging.getLogger(__name__)

pdf_storage = FileSystemStorage(location=settings.PDF_ROOT, base_url=settings.PDF_URL)


@never_cache
# pylint: disable=too-many-locals
Expand All @@ -36,8 +40,8 @@ def generate_pdf(region, language_slug, pages):
:param pages: at least on page to render as PDF document
:type pages: ~treebeard.ns_tree.NS_NodeQuerySet
:return: PDF document wrapped in a HtmlResponse
:rtype: ~django.http.HttpResponse
:return: Redirection to PDF document
:rtype: ~django.http.HttpResponseRedirect
"""
# first all necessary data for hashing are collected, starting at region slug
# region last_updated field taking into account, to keep track of maybe edited region icons
Expand All @@ -55,12 +59,7 @@ def generate_pdf(region, language_slug, pages):
# finally combine all list entries to a single hash key
pdf_key_string = "_".join(map(str, pdf_key_list))
# compute the hash value based on the hash key
pdf_hash = hashlib.sha256(bytes(pdf_key_string, "utf-8")).hexdigest()
cache = caches["pdf"]
cached_response = cache.get(pdf_hash, "has_expired")
if cached_response != "has_expired":
# if django cache already contains a response object
return cached_response
pdf_hash = hashlib.sha256(bytes(pdf_key_string, "utf-8")).hexdigest()[:10]
amount_pages = pages.count()
if amount_pages == 0:
return HttpResponse(
Expand All @@ -81,36 +80,39 @@ def generate_pdf(region, language_slug, pages):
# In any other case, take the region name
title = region.name
language = Language.objects.get(slug=language_slug)
filename = f"Integreat - {language.translated_name} - {title}.pdf"
# Convert queryset to annotated list which can be rendered better
annotated_pages = Page.get_annotated_list_qs(pages)
context = {
"right_to_left": language.text_direction == text_directions.RIGHT_TO_LEFT,
"region": region,
"annotated_pages": annotated_pages,
"language": language,
"amount_pages": amount_pages,
"prevent_italics": ["ar", "fa"],
}
response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = f'filename="{filename}"'
html = get_template("pages/page_pdf.html").render(context)
pisa_status = pisa.CreatePDF(
html, dest=response, link_callback=link_callback, encoding="UTF-8"
)
# pylint: disable=no-member
if pisa_status.err:
logger.error(
"The following PDF could not be rendered: %r, %r, %r",
region,
language,
pages,
)
return HttpResponse(
_("The PDF could not be successfully generated."), status=500
)
cache.set(pdf_hash, response, 60 * 60 * 24)
return response
filename = f"{pdf_hash}/Integreat - {language.translated_name} - {title}.pdf"
# Only generate new pdf if not already exists
if not pdf_storage.exists(filename):
# Convert queryset to annotated list which can be rendered better
annotated_pages = Page.get_annotated_list_qs(pages)
context = {
"right_to_left": language.text_direction == text_directions.RIGHT_TO_LEFT,
"region": region,
"annotated_pages": annotated_pages,
"language": language,
"amount_pages": amount_pages,
"prevent_italics": ["ar", "fa"],
}
html = get_template("pages/page_pdf.html").render(context)
# Save empty file
pdf_storage.save(filename, ContentFile(""))
# Write PDF content into file
with pdf_storage.open(filename, "w+b") as pdf_file:
pisa_status = pisa.CreatePDF(
html, dest=pdf_file, link_callback=link_callback, encoding="UTF-8"
)
# pylint: disable=no-member
if pisa_status.err:
logger.error(
"The following PDF could not be rendered: %r, %r, %r",
region,
language,
pages,
)
return HttpResponse(
_("The PDF could not be successfully generated."), status=500
)
return redirect(pdf_storage.url(filename))


# pylint: disable=unused-argument
Expand Down
10 changes: 2 additions & 8 deletions integreat_cms/cms/views/pages/page_bulk_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,12 @@ def post(self, request, *args, **kwargs):
:return: The redirect
:rtype: ~django.http.HttpResponseRedirect
"""
# Generate PDF document wrapped in a HtmlResponse object
response = generate_pdf(
# Generate PDF document and redirect to it
return generate_pdf(
request.region,
kwargs.get("language_slug"),
self.get_queryset(),
)
if response.status_code == 200:
# Offer PDF document for download
response["Content-Disposition"] = (
response["Content-Disposition"] + "; attachment"
)
return response


# pylint: disable=too-many-ancestors
Expand Down
10 changes: 9 additions & 1 deletion integreat_cms/core/middleware/access_control_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ class AccessControlMiddleware:
"""

#: The namespaces that are whitelisted and don't require access control
whitelist = ["api", "public", "sitemap", "i18n", "media_files", "xliff_files"]
whitelist = [
"api",
"public",
"sitemap",
"i18n",
"media_files",
"pdf_files",
"xliff_files",
]

def __init__(self, get_response):
"""
Expand Down
19 changes: 11 additions & 8 deletions integreat_cms/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,18 +643,10 @@

#: Configuration for caches (see :setting:`django:CACHES` and :doc:`django:topics/cache`).
#: Use a ``LocMemCache`` for development and a ``RedisCache`` whenever available.
#: Additionally, a ``FileBasedCache`` is used for PDF caching.
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
},
"pdf": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": os.path.join(
os.environ.get("INTEGREAT_CMS_FILE_CACHE", os.path.join(BASE_DIR, "cache")),
"pdf",
),
},
}

# Use RedisCache when activated
Expand Down Expand Up @@ -754,6 +746,17 @@
]


##############
# PDF EXPORT #
##############

#: The directory where PDF files are stored
PDF_ROOT = os.environ.get("INTEGREAT_CMS_PDF_ROOT", os.path.join(BASE_DIR, "pdf"))

#: The URL path where PDF files are served for download
PDF_URL = "/pdf/"


#######################
# XLIFF SERIALIZATION #
#######################
Expand Down
9 changes: 9 additions & 0 deletions integreat_cms/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@
)
),
),
path(
"",
include(
(
static(settings.PDF_URL, document_root=settings.PDF_ROOT),
"pdf_files",
)
),
),
path(
"",
include(
Expand Down
12 changes: 6 additions & 6 deletions integreat_cms/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-15 14:48+0000\n"
"POT-Creation-Date: 2022-04-15 20:11+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Integreat <[email protected]>\n"
"Language-Team: Integreat <[email protected]>\n"
Expand Down Expand Up @@ -3169,7 +3169,7 @@ msgstr "Feedback"

#: cms/templates/_base.html:181
#: cms/templates/push_notifications/push_notification_list.html:9
#: core/settings.py:718
#: core/settings.py:710
msgid "News"
msgstr "Nachrichten"

Expand Down Expand Up @@ -5711,12 +5711,12 @@ msgstr "Unbekannt"
msgid "CW"
msgstr "KW"

#: cms/utils/pdf_utils.py:67
#: cms/utils/pdf_utils.py:66
msgid "No valid pages selected for PDF generation."
msgstr ""
"Es wurden keine gültigen Seiten zur Erzeugung der PDF-Datei ausgewählt."

#: cms/utils/pdf_utils.py:110
#: cms/utils/pdf_utils.py:113
msgid "The PDF could not be successfully generated."
msgstr "PDF-Datei konnte nicht erfolgreich erzeugt werden."

Expand Down Expand Up @@ -6404,11 +6404,11 @@ msgstr ""
msgid "Success: The user {user} cannot publish this page anymore."
msgstr "Erfolg: Der Nutzer {user} kann diese Seite nicht mehr veröffentlichen"

#: cms/views/pages/page_bulk_actions.py:109
#: cms/views/pages/page_bulk_actions.py:103
msgid "XLIFF file for translation to {} successfully created."
msgstr "XLIFF Datei für die Übersetzung nach {} wurde erfolgreich erstellt."

#: cms/views/pages/page_bulk_actions.py:113
#: cms/views/pages/page_bulk_actions.py:107
msgid "If the download does not start automatically, please click {}here{}."
msgstr ""
"Wenn der Download nicht automatisch startet, klicken Sie bitte {}hier{}."
Expand Down
Loading

0 comments on commit 2e1f38b

Please sign in to comment.