Skip to content
Snippets Groups Projects
Verified Commit b5a7c868 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch 'master' into feature/vue/about

parents 4b61591e e8d7b1b4
No related branches found
No related tags found
1 merge request!1095[Vue] About page
......@@ -14,16 +14,22 @@ Added
* Notification drawer in top nav bar
* GraphQL queries and mutations for core data management
* [Dev] Provide plain PDF template without header/footer for special layouts.
* Show also group ownerships on person detail page
Changed
~~~~~~~
* Rewrite of frontend using Vuetify
* [Dev] Provide function to generate PDF files from fully-rendered templates.
* OIDC scope "profile" now exposes the avatar instead of the official photo
Fixed
~~~~~
* The logo in the PDF files was displayed at the wrong position.
* Sometimes the PDF files were not generated correctly
and images were displayed only partially.
* Error message in permission form was misleading.
Removed
......
......@@ -30,6 +30,7 @@ RUN apt-get -y update && \
eatmydata apt-get install -y --no-install-recommends \
build-essential \
chromium \
chromium-driver \
curl \
dumb-init \
gettext \
......
@page {
padding: 0;
margin: 0;
}
table.small-print, td.small-print, th.small-print {
font-size: 10pt;
}
tr {
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
}
td, th {
padding: 1px;
}
td.rotate, th.rotate {
text-align: center;
transform: rotate(-90deg);
}
......@@ -67,9 +67,10 @@ header .row, header .col {
}
#print-logo {
padding: 2mm;
height: 22mm;
width: auto;
margin-block: 0;
padding: 2mm 2mm 2mm 0;
}
.page-break {
......
{% load static i18n any_js sass_tags %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
{% include "core/partials/meta.html" %}
<title>
{% block no_browser_title %}
{% block browser_title %}{% endblock %} —
{% endblock %}
{{ SITE_PREFERENCES.general__title }}
</title>
{% include_css "material-design-icons" %}
{% include_css "Roboto100" %}
{% include_css "Roboto300" %}
{% include_css "Roboto400" %}
{% include_css "Roboto500" %}
{% include_css "Roboto700" %}
{% include_css "Roboto900" %}
{% include_css "paper-css" %}
<link rel="stylesheet" href="{% sass_src 'public/style.scss' %}"/>
<link rel="stylesheet" href="{% static "print-simple.css" %}"/>
{% block size %}
<style>
@page {
size: {{ width }}mm {{ height }}mm;
}
@media print {
html, body {
width: {{ width }}mm;
}
}
.sheet {
width: {{ width }}mm;
height: {{ height|add:-1 }}.83mm;
}
</style>
{% endblock %}
{% block extra_head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
......@@ -241,18 +241,36 @@
{% endif %}
{% has_perm 'core.view_person_groups_rule' user person as can_view_groups %}
{% if can_view_groups and groups %}
{% if can_view_groups %}
<div class="col s12 m6 l4">
<h2>{% blocktrans %}Groups{% endblocktrans %}</h2>
<div class="card-panel">
<div class="collection">
{% for group in groups %}
<a href="{{ group.get_absolute_url }}" class="collection-item">
{{ group.name }} ({{ group.school_term }})
</a>
{% endfor %}
{% if groups.count %}
<div>
<h2>{% blocktrans %}Groups{% endblocktrans %}</h2>
<div class="card-panel">
<div class="collection">
{% for group in groups %}
<a href="{{ group.get_absolute_url }}" class="collection-item">
{{ group.name }} ({{ group.school_term }})
</a>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
{% if person.owner_of_recursive.count %}
<div>
<h2>{% blocktrans %}Group ownership{% endblocktrans %}</h2>
<div class="card-panel">
<div class="collection">
{% for group in person.owner_of_recursive.all %}
<a href="{{ group.get_absolute_url }}" class="collection-item">
{{ group.name }} ({{ group.school_term }})
</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
{% endif %}
</div>
......
import base64
import os
import subprocess # noqa
from datetime import timedelta
from tempfile import TemporaryDirectory
from typing import Optional, Tuple, Union
from typing import Callable, Optional, Tuple, Union
from urllib.parse import urljoin
from django.conf import settings
......@@ -19,6 +20,7 @@ from django.utils.translation import gettext as _
from celery.result import AsyncResult
from celery_progress.backend import ProgressRecorder
from selenium import webdriver
from aleksis.core.celery import app
from aleksis.core.models import PDFFile
......@@ -26,6 +28,29 @@ from aleksis.core.util.celery_progress import recorded_task, render_progress_pag
from aleksis.core.util.core_helpers import process_custom_context_processors
def _generate_pdf_with_chromium(temp_dir, pdf_path, html_url, lang):
"""Generate a PDF file from a HTML file."""
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--kiosk-printing")
chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--disable-setuid-sandbox")
chrome_options.add_argument("--dbus-stub")
chrome_options.add_argument("--temp-profile")
chrome_options.add_argument(f"--lang={lang}")
driver = webdriver.Chrome(options=chrome_options)
driver.get(html_url)
pdf = driver.execute_cdp_cmd(
"Page.printToPDF", {"printBackground": True, "preferCSSPageSize": True}
)
driver.close()
with open(pdf_path, "wb") as f:
f.write(base64.b64decode(pdf["data"]))
@recorded_task
def generate_pdf(
file_pk: int, html_url: str, recorder: ProgressRecorder, lang: Optional[str] = None
......@@ -40,26 +65,7 @@ def generate_pdf(
pdf_path = os.path.join(temp_dir, "print.pdf")
lang = lang or get_language()
# Run PDF generation using a headless Chromium
cmd = [
"chromium",
"--headless",
"--no-sandbox",
"--run-all-compositor-stages-before-draw",
"--temp-profile",
"--disable-dev-shm-usage",
"--disable-gpu",
"--disable-setuid-sandbox",
"--dbus-stub",
f"--home-dir={temp_dir}",
f"--lang={lang}",
f"--print-to-pdf={pdf_path}",
html_url,
]
res = subprocess.run(cmd) # noqa
# Let the task fail on a non-success return code
res.check_returncode()
_generate_pdf_with_chromium(temp_dir, pdf_path, html_url, lang)
# Upload PDF file to media storage
with open(pdf_path, "rb") as f:
......@@ -69,10 +75,8 @@ def generate_pdf(
recorder.set_progress(1, 1)
def generate_pdf_from_template(
template_name: str, context: Optional[dict] = None, request: Optional[HttpRequest] = None
) -> Tuple[PDFFile, AsyncResult]:
"""Start a PDF generation task and return the matching file object and Celery result."""
def process_context_for_pdf(context: Optional[dict] = None, request: Optional[HttpRequest] = None):
context = context or {}
if not request:
processed_context = process_custom_context_processors(
settings.NON_REQUEST_CONTEXT_PROCESSORS
......@@ -80,11 +84,14 @@ def generate_pdf_from_template(
processed_context.update(context)
else:
processed_context = context
html_template = render_to_string(template_name, processed_context, request)
return processed_context
file_object = PDFFile.objects.create(
html_file=ContentFile(html_template.encode(), name="source.html")
)
def generate_pdf_from_html(
html: str, request: Optional[HttpRequest] = None
) -> Tuple[PDFFile, AsyncResult]:
"""Start a PDF generation task and return the matching file object and Celery result."""
file_object = PDFFile.objects.create(html_file=ContentFile(html.encode(), name="source.html"))
# As this method may be run in background and there is no request available,
# we have to use a predefined URL from settings then
......@@ -98,6 +105,23 @@ def generate_pdf_from_template(
return file_object, result
def generate_pdf_from_template(
template_name: str,
context: Optional[dict] = None,
request: Optional[HttpRequest] = None,
render_method: Optional[Callable] = None,
) -> Tuple[PDFFile, AsyncResult]:
"""Start a PDF generation task and return the matching file object and Celery result."""
processed_context = process_context_for_pdf(context, request)
if render_method:
html_template = render_method(processed_context, request)
else:
html_template = render_to_string(template_name, processed_context, request)
return generate_pdf_from_html(html_template, request)
def render_pdf(
request: Union[HttpRequest, None], template_name: str, context: dict = None
) -> HttpResponse:
......
......@@ -8,7 +8,7 @@ Indexable = indexes.Indexable # noqa
class SearchIndex(BaseSearchIndex):
"""Base class for search indexes on AlekSIS models.
It provides a default document field caleld text and exects
It provides a default document field called text and exects
the related model in the model attribute.
"""
......
......@@ -53,6 +53,7 @@ Install some packages from the Debian package system.
yarnpkg \
python3-virtualenv \
chromium \
chromium-driver \
redis-server \
postgresql \
locales-all \
......
......@@ -47,7 +47,7 @@ Install native dependencies
Some system libraries are required to install AlekSIS. On Debian, for example, this would be done with::
sudo apt install build-essential libpq-dev libpq5 libssl-dev python3-dev python3-pip python3-venv yarnpkg gettext chromium
sudo apt install build-essential libpq-dev libpq5 libssl-dev python3-dev python3-pip python3-venv yarnpkg gettext chromium chromium-driver
Get Poetry
----------
......
......@@ -60,7 +60,7 @@ django-any-js = "^1.1"
django-menu-generator-ng = "^1.2.3"
django-tables2 = "^2.1"
django-phonenumber-field = {version = "^6.1", extras = ["phonenumbers"]}
django-sass-processor = "1.0"
django-sass-processor = "1.2.1"
libsass = "^0.21.0"
colour = "^0.1.5"
dynaconf = {version = "^3.1", extras = ["yaml", "toml", "ini"]}
......@@ -106,7 +106,7 @@ django-cachalot = "^2.3.2"
django-prometheus = "^2.1.0"
django-model-utils = "^4.0.0"
bs4 = "^0.0.1"
django-invitations = "^1.9.3"
django-invitations = "^2.0.0"
django-cleavejs = "^0.1.0"
django-allauth = "^0.51.0"
django-uwsgi-ng = "^1.1.0"
......@@ -128,8 +128,9 @@ pycountry = "^22.0.0"
django-ical = "^1.8.3"
django-iconify = "^0.3"
customidenticon = "^0.1.5"
graphene-django = "^2.15.0"
graphene-django = "^3.0.0"
django-webpack-loader = "^1.6.0"
selenium = "^4.4.3"
[tool.poetry.extras]
ldap = ["django-auth-ldap"]
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment