Skip to content

Commit cca7b3a

Browse files
committed
Move to pandoc for rendering sponsorship contracts
1 parent 7ba7d36 commit cca7b3a

File tree

15 files changed

+439
-434
lines changed

15 files changed

+439
-434
lines changed

Aptfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pandoc
2+
texlive-latex-base
3+
texlive-latex-recommended
4+
texlive-fonts-recommended
5+
lmodern

Dockerfile

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,39 @@
11
FROM python:3.9-bullseye
22
ENV PYTHONUNBUFFERED=1
33
ENV PYTHONDONTWRITEBYTECODE=1
4+
5+
# By default, Docker has special steps to avoid keeping APT caches in the layers, which
6+
# is good, but in our case, we're going to mount a special cache volume (kept between
7+
# builds), so we WANT the cache to persist.
8+
RUN set -eux; \
9+
rm -f /etc/apt/apt.conf.d/docker-clean; \
10+
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache;
11+
12+
# Install System level build requirements, this is done before
13+
# everything else because these are rarely ever going to change.
14+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
15+
--mount=type=cache,target=/var/lib/apt,sharing=locked \
16+
set -x \
17+
&& apt-get update \
18+
&& apt-get install --no-install-recommends -y \
19+
pandoc \
20+
texlive-latex-base \
21+
texlive-latex-recommended \
22+
texlive-fonts-recommended \
23+
lmodern
24+
425
RUN mkdir /code
526
WORKDIR /code
27+
628
COPY dev-requirements.txt /code/
729
COPY base-requirements.txt /code/
8-
RUN pip install -r dev-requirements.txt
30+
31+
RUN pip --no-cache-dir --disable-pip-version-check install --upgrade pip setuptools wheel
32+
33+
RUN --mount=type=cache,target=/root/.cache/pip \
34+
set -x \
35+
&& pip --disable-pip-version-check \
36+
install \
37+
-r dev-requirements.txt
38+
939
COPY . /code/

base-requirements.txt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,11 @@ django-filter==2.4.0
4444
django-ordered-model==3.4.3
4545
django-widget-tweaks==1.4.8
4646
django-countries==7.2.1
47-
xhtml2pdf==0.2.5
48-
django-easy-pdf3==0.1.2
4947
num2words==0.5.10
5048
django-polymorphic==3.0.0
5149
sorl-thumbnail==12.7.0
52-
docxtpl==0.12.0
53-
reportlab==3.6.6
5450
django-extensions==3.1.4
5551
django-import-export==2.7.1
52+
53+
pypandoc==1.12
54+
panflute==1.12

pydotorg/settings/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@
173173
'ordered_model',
174174
'widget_tweaks',
175175
'django_countries',
176-
'easy_pdf',
177176
'sorl.thumbnail',
178177

179178
'banners',

sponsors/contracts.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import os
2+
import tempfile
3+
4+
from django.http import HttpResponse
5+
from django.template.loader import render_to_string
6+
from django.utils.dateformat import format
7+
8+
import pypandoc
9+
10+
dirname = os.path.dirname(__file__)
11+
DOCXPAGEBREAK_FILTER = os.path.join(dirname, "pandoc_filters/pagebreak.py")
12+
13+
14+
def _clean_split(text, separator="\n"):
15+
return [
16+
t.replace("-", "").strip()
17+
for t in text.split("\n")
18+
if t.replace("-", "").strip()
19+
]
20+
21+
22+
def _contract_context(contract, **context):
23+
start_date = contract.sponsorship.start_date
24+
context.update(
25+
{
26+
"contract": contract,
27+
"start_date": start_date,
28+
"start_day_english_suffix": format(start_date, "S"),
29+
"sponsor": contract.sponsorship.sponsor,
30+
"sponsorship": contract.sponsorship,
31+
"benefits": _clean_split(contract.benefits_list.raw),
32+
"legal_clauses": _clean_split(contract.legal_clauses.raw),
33+
}
34+
)
35+
return context
36+
37+
38+
def render_contract_to_pdf_response(request, contract, **context):
39+
response = HttpResponse(
40+
render_contract_to_pdf_file(contract, **context), content_type="application/pdf"
41+
)
42+
return response
43+
44+
45+
def render_contract_to_pdf_file(contract, **context):
46+
with tempfile.NamedTemporaryFile() as docx_file:
47+
with tempfile.NamedTemporaryFile(suffix=".pdf") as pdf_file:
48+
docx_file.write(render_contract_to_docx_file(contract, **context))
49+
pdf = pypandoc.convert_file(
50+
docx_file.name, "pdf", outputfile=pdf_file.name, format="docx"
51+
)
52+
return pdf_file.read()
53+
54+
55+
def render_contract_to_docx_response(request, contract, **context):
56+
response = HttpResponse(
57+
render_contract_to_docx_file(contract, **context),
58+
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
59+
)
60+
response[
61+
"Content-Disposition"
62+
] = f"attachment; filename=sponsorship-contract-{contract.sponsorship.sponsor.name.replace(' ', '-')}.docx"
63+
return response
64+
65+
66+
def render_contract_to_docx_file(contract, **context):
67+
template = "sponsors/admin/contracts/sponsorship-agreement.md"
68+
reference = "sponsors/admin/contracts/reference.docx"
69+
context = _contract_context(contract, **context)
70+
markdown = render_to_string(template, context)
71+
with tempfile.NamedTemporaryFile() as docx_file:
72+
docx = pypandoc.convert_text(
73+
markdown,
74+
"docx",
75+
outputfile=docx_file.name,
76+
format="md",
77+
filters=[DOCXPAGEBREAK_FILTER],
78+
)
79+
return docx_file.read()

sponsors/pandoc_filters/__init__.py

Whitespace-only changes.

sponsors/pandoc_filters/pagebreak.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
# ------------------------------------------------------------------------------
5+
# Source: https://github.com/pandocker/pandoc-docx-pagebreak-py/
6+
# Revision: c8cddccebb78af75168da000a3d6ac09349bef73
7+
# ------------------------------------------------------------------------------
8+
# MIT License
9+
#
10+
# Copyright (c) 2018 pandocker
11+
#
12+
# Permission is hereby granted, free of charge, to any person obtaining a copy
13+
# of this software and associated documentation files (the "Software"), to deal
14+
# in the Software without restriction, including without limitation the rights
15+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16+
# copies of the Software, and to permit persons to whom the Software is
17+
# furnished to do so, subject to the following conditions:
18+
#
19+
# The above copyright notice and this permission notice shall be included in all
20+
# copies or substantial portions of the Software.
21+
#
22+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28+
# SOFTWARE.
29+
# ------------------------------------------------------------------------------
30+
31+
""" pandoc-docx-pagebreakpy
32+
Pandoc filter to insert pagebreak as openxml RawBlock
33+
Only for docx output
34+
35+
Trying to port pandoc-doc-pagebreak
36+
- https://github.com/alexstoick/pandoc-docx-pagebreak
37+
"""
38+
39+
import panflute as pf
40+
41+
42+
class DocxPagebreak(object):
43+
pagebreak = pf.RawBlock("<w:p><w:r><w:br w:type=\"page\" /></w:r></w:p>", format="openxml")
44+
sectionbreak = pf.RawBlock("<w:p><w:pPr><w:sectPr><w:type w:val=\"nextPage\" /></w:sectPr></w:pPr></w:p>",
45+
format="openxml")
46+
toc = pf.RawBlock(r"""
47+
<w:sdt>
48+
<w:sdtContent xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
49+
<w:p>
50+
<w:r>
51+
<w:fldChar w:fldCharType="begin" w:dirty="true" />
52+
<w:instrText xml:space="preserve">TOC \o "1-3" \h \z \u</w:instrText>
53+
<w:fldChar w:fldCharType="separate" />
54+
<w:fldChar w:fldCharType="end" />
55+
</w:r>
56+
</w:p>
57+
</w:sdtContent>
58+
</w:sdt>
59+
""", format="openxml")
60+
61+
def action(self, elem, doc):
62+
if isinstance(elem, pf.RawBlock):
63+
if elem.text == r"\newpage":
64+
if (doc.format == "docx"):
65+
pf.debug("Page Break")
66+
elem = self.pagebreak
67+
# elif elem.text == r"\newsection":
68+
# if (doc.format == "docx"):
69+
# pf.debug("Section Break")
70+
# elem = self.sectionbreak
71+
# else:
72+
# elem = []
73+
elif elem.text == r"\toc":
74+
if (doc.format == "docx"):
75+
pf.debug("Table of Contents")
76+
para = [pf.Para(pf.Str("Table"), pf.Space(), pf.Str("of"), pf.Space(), pf.Str("Contents"))]
77+
div = pf.Div(*para, attributes={"custom-style": "TOC Heading"})
78+
elem = [div, self.toc]
79+
else:
80+
elem = []
81+
return elem
82+
83+
84+
def main(doc=None):
85+
dp = DocxPagebreak()
86+
return pf.run_filter(dp.action, doc=doc)
87+
88+
89+
if __name__ == "__main__":
90+
main()

sponsors/pdf.py

Lines changed: 0 additions & 70 deletions
This file was deleted.

sponsors/tests/test_contracts.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from datetime import date
2+
from model_bakery import baker
3+
from unittest.mock import patch, Mock
4+
5+
from django.http import HttpRequest
6+
from django.test import TestCase
7+
from django.utils.dateformat import format
8+
9+
from sponsors.contracts import render_contract_to_docx_response
10+
11+
12+
class TestRenderContract(TestCase):
13+
def setUp(self):
14+
self.contract = baker.make_recipe("sponsors.tests.empty_contract", sponsorship__start_date=date.today())
15+
self.context = {
16+
"contract": self.contract,
17+
"start_date": self.contract.sponsorship.start_date,
18+
"start_day_english_suffix": format(self.contract.sponsorship.start_date, "S"),
19+
"sponsor": self.contract.sponsorship.sponsor,
20+
"sponsorship": self.contract.sponsorship,
21+
"benefits": [],
22+
"legal_clauses": [],
23+
}
24+
25+
# DOCX unit test
26+
def test_render_response_with_docx_attachment(self):
27+
request = Mock(HttpRequest)
28+
response = render_contract_to_docx_response(request, self.contract)
29+
30+
self.assertEqual(response.get("Content-Disposition"), "attachment; filename=sponsorship-contract-Sponsor.docx")
31+
self.assertEqual(
32+
response.get("Content-Type"),
33+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
34+
)

0 commit comments

Comments
 (0)