Skip to content

Commit bdac56e

Browse files
committed
Merge branch 'main' into release
2 parents c4a89f1 + b4d22a8 commit bdac56e

19 files changed

+500
-483
lines changed

.github/workflows/ci.yml

+12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ jobs:
1717
steps:
1818
- name: Check out repository
1919
uses: actions/checkout@v2
20+
- name: Install platform dependencies
21+
run: |
22+
sudo apt -y update
23+
sudo apt -y install --no-install-recommends \
24+
texlive-latex-base \
25+
texlive-latex-recommended \
26+
texlive-plain-generic \
27+
lmodern
28+
- name: Install pandoc
29+
run: |
30+
wget https://github.com/jgm/pandoc/releases/download/2.17.1.1/pandoc-2.17.1.1-1-amd64.deb
31+
sudo dpkg -i pandoc-2.17.1.1-1-amd64.deb
2032
- uses: actions/setup-python@v2
2133
with:
2234
python-version: 3.9.16

Aptfile

Whitespace-only changes.

Dockerfile

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,47 @@
1-
FROM python:3.9-bullseye
1+
FROM python:3.9-bookworm
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+
texlive-plain-generic \
24+
lmodern
25+
26+
RUN case $(uname -m) in \
27+
"x86_64") ARCH=amd64 ;; \
28+
"aarch64") ARCH=arm64 ;; \
29+
esac \
30+
&& wget --quiet https://github.com/jgm/pandoc/releases/download/2.17.1.1/pandoc-2.17.1.1-1-${ARCH}.deb \
31+
&& dpkg -i pandoc-2.17.1.1-1-${ARCH}.deb
32+
433
RUN mkdir /code
534
WORKDIR /code
35+
636
COPY dev-requirements.txt /code/
737
COPY base-requirements.txt /code/
8-
RUN pip install -r dev-requirements.txt
38+
39+
RUN pip --no-cache-dir --disable-pip-version-check install --upgrade pip setuptools wheel
40+
41+
RUN --mount=type=cache,target=/root/.cache/pip \
42+
set -x \
43+
&& pip --disable-pip-version-check \
44+
install \
45+
-r dev-requirements.txt
46+
947
COPY . /code/

base-requirements.txt

+3-4
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==2.3.0

pydotorg/settings/base.py

-1
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

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
REFERENCE_DOCX = os.path.join(dirname, "reference.docx")
13+
14+
15+
def _clean_split(text, separator="\n"):
16+
return [
17+
t.replace("-", "").strip()
18+
for t in text.split("\n")
19+
if t.replace("-", "").strip()
20+
]
21+
22+
23+
def _contract_context(contract, **context):
24+
start_date = contract.sponsorship.start_date
25+
context.update(
26+
{
27+
"contract": contract,
28+
"start_date": start_date,
29+
"start_day_english_suffix": format(start_date, "S"),
30+
"sponsor": contract.sponsorship.sponsor,
31+
"sponsorship": contract.sponsorship,
32+
"benefits": _clean_split(contract.benefits_list.raw),
33+
"legal_clauses": _clean_split(contract.legal_clauses.raw),
34+
"renewal": True if contract.sponsorship.renewal else False,
35+
}
36+
)
37+
previous_effective = contract.sponsorship.previous_effective_date
38+
context["previous_effective"] = previous_effective if previous_effective else "UNKNOWN"
39+
context["previous_effective_english_suffix"] = format(previous_effective, "S") if previous_effective else "UNKNOWN"
40+
return context
41+
42+
43+
def render_markdown_from_template(contract, **context):
44+
template = "sponsors/admin/contracts/sponsorship-agreement.md"
45+
context = _contract_context(contract, **context)
46+
return render_to_string(template, context)
47+
48+
49+
def render_contract_to_pdf_response(request, contract, **context):
50+
response = HttpResponse(
51+
render_contract_to_pdf_file(contract, **context), content_type="application/pdf"
52+
)
53+
return response
54+
55+
56+
def render_contract_to_pdf_file(contract, **context):
57+
with tempfile.NamedTemporaryFile() as docx_file:
58+
with tempfile.NamedTemporaryFile(suffix=".pdf") as pdf_file:
59+
markdown = render_markdown_from_template(contract, **context)
60+
pdf = pypandoc.convert_text(
61+
markdown, "pdf", outputfile=pdf_file.name, format="md"
62+
)
63+
return pdf_file.read()
64+
65+
66+
def render_contract_to_docx_response(request, contract, **context):
67+
response = HttpResponse(
68+
render_contract_to_docx_file(contract, **context),
69+
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
70+
)
71+
response[
72+
"Content-Disposition"
73+
] = f"attachment; filename={'sponsorship-renewal' if contract.sponsorship.renewal else 'sponsorship-contract'}-{contract.sponsorship.sponsor.name.replace(' ', '-').replace('.', '')}.docx"
74+
return response
75+
76+
77+
def render_contract_to_docx_file(contract, **context):
78+
markdown = render_markdown_from_template(contract, **context)
79+
with tempfile.NamedTemporaryFile() as docx_file:
80+
docx = pypandoc.convert_text(
81+
markdown,
82+
"docx",
83+
outputfile=docx_file.name,
84+
format="md",
85+
filters=[DOCXPAGEBREAK_FILTER],
86+
extra_args=[f"--reference-doc", REFERENCE_DOCX],
87+
)
88+
return docx_file.read()

sponsors/pandoc_filters/__init__.py

Whitespace-only changes.

sponsors/pandoc_filters/pagebreak.py

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
elem = self.pagebreak
66+
# elif elem.text == r"\newsection":
67+
# if (doc.format == "docx"):
68+
# pf.debug("Section Break")
69+
# elem = self.sectionbreak
70+
# else:
71+
# elem = []
72+
elif elem.text == r"\toc":
73+
if (doc.format == "docx"):
74+
pf.debug("Table of Contents")
75+
para = [pf.Para(pf.Str("Table"), pf.Space(), pf.Str("of"), pf.Space(), pf.Str("Contents"))]
76+
div = pf.Div(*para, attributes={"custom-style": "TOC Heading"})
77+
elem = [div, self.toc]
78+
else:
79+
elem = []
80+
return elem
81+
82+
83+
def main(doc=None):
84+
dp = DocxPagebreak()
85+
return pf.run_filter(dp.action, doc=doc)
86+
87+
88+
if __name__ == "__main__":
89+
main()

sponsors/pdf.py

-78
This file was deleted.

sponsors/reference.docx

11.5 KB
Binary file not shown.

sponsors/tests/test_contracts.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
16+
# DOCX unit test
17+
def test_render_response_with_docx_attachment(self):
18+
request = Mock(HttpRequest)
19+
self.contract.sponsorship.renewal = False
20+
response = render_contract_to_docx_response(request, self.contract)
21+
22+
self.assertEqual(response.get("Content-Disposition"), "attachment; filename=sponsorship-contract-Sponsor.docx")
23+
self.assertEqual(
24+
response.get("Content-Type"),
25+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
26+
)
27+
28+
29+
# DOCX unit test
30+
def test_render_renewal_response_with_docx_attachment(self):
31+
request = Mock(HttpRequest)
32+
self.contract.sponsorship.renewal = True
33+
response = render_contract_to_docx_response(request, self.contract)
34+
35+
self.assertEqual(response.get("Content-Disposition"), "attachment; filename=sponsorship-renewal-Sponsor.docx")
36+
self.assertEqual(
37+
response.get("Content-Type"),
38+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
39+
)

0 commit comments

Comments
 (0)