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

Implement PDF rendering of cards and respective UI views

parent 05d13827
No related branches found
No related tags found
No related merge requests found
Pipeline #59019 failed
Showing
with 230 additions and 49 deletions
from django.contrib import admin
from aleksis.apps.kort.models import Card, CardLayout, CardPrinter
admin.site.register(Card)
admin.site.register(CardPrinter)
admin.site.register(CardLayout)
# Generated by Django 3.2.12 on 2022-03-10 16:48
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kort', '0004_alter_card_chip_number'),
]
operations = [
migrations.AddField(
model_name='card',
name='pdf_file',
field=models.FileField(blank=True, null=True, upload_to='cards/', validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='PDF file'),
),
]
from typing import Union
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.db import models from django.db import models
from django.template import Context, Template from django.template import Context, Template
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from celery.result import AsyncResult
from aleksis.core.mixins import ExtensibleModel from aleksis.core.mixins import ExtensibleModel
from aleksis.core.models import Person from aleksis.core.models import Person
...@@ -69,6 +74,14 @@ class Card(ExtensibleModel): ...@@ -69,6 +74,14 @@ class Card(ExtensibleModel):
valid_until = models.DateField(verbose_name=_("Valid until")) valid_until = models.DateField(verbose_name=_("Valid until"))
deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False) deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False)
pdf_file = models.FileField(
verbose_name=_("PDF file"),
blank=True,
null=True,
upload_to="cards/",
validators=[FileExtensionValidator(["pdf"])],
)
@property @property
def is_valid(self): def is_valid(self):
return ( return (
...@@ -86,6 +99,18 @@ class Card(ExtensibleModel): ...@@ -86,6 +99,18 @@ class Card(ExtensibleModel):
"valid_until": self.valid_until, "valid_until": self.valid_until,
} }
def generate_pdf(self) -> Union[bool, AsyncResult]:
from .tasks import generate_card_pdf
if self.pdf_file:
return True
return generate_card_pdf.delay(self.pk)
def __str__(self):
if self.chip_number:
return f"{self.person} ({self.chip_number})"
return f"{self.person}"
class Meta: class Meta:
verbose_name = _("Card") verbose_name = _("Card")
verbose_name_plural = _("Cards") verbose_name_plural = _("Cards")
from aleksis.core.settings import JS_URL
YARN_INSTALLED_APPS = ["pdfobject"]
ANY_JS = {
"pdfobject": {"js_url": JS_URL + "/pdfobject/pdfobject.min.js"},
}
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_tables2 import A, BooleanColumn, Column, RelatedLinkColumn, Table from django_tables2 import A, BooleanColumn, Column, LinkColumn, RelatedLinkColumn, Table
class CardTable(Table): class CardTable(Table):
...@@ -11,7 +11,7 @@ class CardTable(Table): ...@@ -11,7 +11,7 @@ class CardTable(Table):
attrs = {"class": "highlight"} attrs = {"class": "highlight"}
person = RelatedLinkColumn() person = RelatedLinkColumn()
chip_number = Column(verbose_name=_("Chip number")) chip_number = LinkColumn("card", verbose_name=_("Chip number"), args=[A("pk")])
current_status = Column(verbose_name=_("Current status"), accessor=A("pk")) current_status = Column(verbose_name=_("Current status"), accessor=A("pk"))
valid_until = Column(verbose_name=_("Valid until")) valid_until = Column(verbose_name=_("Valid until"))
deactivated = BooleanColumn(verbose_name=_("Deactivated")) deactivated = BooleanColumn(verbose_name=_("Deactivated"))
...@@ -19,10 +19,9 @@ class CardTable(Table): ...@@ -19,10 +19,9 @@ class CardTable(Table):
def render_current_status(self, value, record): def render_current_status(self, value, record):
return render_to_string( return render_to_string(
"components/materialize-chips.html", "kort/card/status.html",
dict( dict(
content=_("Valid") if record.is_valid else _("Not valid"), card=record,
classes="white-text " + ("green" if record.is_valid else "red"),
), ),
) )
......
from celery.result import allow_join_result
from celery.states import SUCCESS
from aleksis.apps.kort.models import Card
from aleksis.core.celery import app
from aleksis.core.util.pdf import generate_pdf_from_template
@app.task
def generate_card_pdf(card_pk: int):
card = Card.objects.get(pk=card_pk)
context = card.get_context()
file_object, result = generate_pdf_from_template("kort/pdf.html", context)
with allow_join_result():
result.wait()
file_object.refresh_from_db()
if result.status == SUCCESS and file_object.file:
card.pdf_file.save("card.pdf", file_object.file.file)
card.save()
{% load i18n %} {% load i18n %}
<!-- Modal Structure -->
<div id="deactivate-modal-{{ card.pk }}" class="modal"> <div id="detail-modal-{{ card.pk }}" class="modal">
<div class="modal-content"> <div class="modal-content">
<h4>{% trans "Do you really want to deactivate the following card?" %}</h4> <h4>{% blocktrans with person=card.person %}Card of {{ person }}{% endblocktrans %}</h4>
{% include "kort/card/short.html" %} {% include "kort/card/detail_content.html" %}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat"> <a href="#!" class="modal-close waves-effect waves-green btn-flat">
<i class="material-icons left">close</i> <i class="material-icons left">close</i>
{% trans "Close" %} {% trans "Close" %}
</a> </a>
<a href="{% url "deactivate_card" card.pk %}" class="modal-close waves-effect waves-light orange btn">
<i class="material-icons left">timer_off</i>
{% trans "Deactivate" %}
</a>
</div>
</div>
<div id="delete-modal-{{ card.pk }}" class="modal">
<div class="modal-content">
<h4>{% trans "Do you really want to delete the following card?" %}</h4>
{% include "kort/card/short.html" %}
<figure class="alert warning">
<i class="material-icons left">warning</i>
{% blocktrans %}
Please pay attention that a deletion of a card is irreversible and should be only used to clean up misprints.
If you just want to make a card unusable because a student has lost his card or left the school,
please deactivate the card instead.
{% endblocktrans %}
</figure>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">
<i class="material-icons left">close</i>
{% trans "Close" %}
</a>
<a href="{% url "delete_card" card.pk %}" class="modal-close waves-effect waves-light red btn">
<i class="material-icons left">delete</i>
{% trans "Delete" %}
</a>
</div> </div>
</div> </div>
<a class="btn-flat waves-effect waves-orange orange-text modal-trigger" href="#deactivate-modal-{{ card.pk }}"> <a class="btn-flat waves-effect waves-green green-text modal-trigger" href="#detail-modal-{{ card.pk }}">
<i class="material-icons left">timer_off</i> <i class="material-icons left">slideshow</i>
{% trans "Deactivate" %} {% trans "Show" %}
</a> </a>
<a class="btn-flat waves-effect waves-red red-text modal-trigger" href="#delete-modal-{{ card.pk }}"> {% if not card.deactivated %}
<div id="deactivate-modal-{{ card.pk }}" class="modal">
<div class="modal-content">
<h4>{% trans "Do you really want to deactivate the following card?" %}</h4>
{% include "kort/card/short.html" %}
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">
<i class="material-icons left">close</i>
{% trans "Close" %}
</a>
<a href="{% url "deactivate_card" card.pk %}" class="modal-close waves-effect waves-light orange btn">
<i class="material-icons left">timer_off</i>
{% trans "Deactivate" %}
</a>
</div>
</div>
<a class="btn-flat waves-effect waves-orange orange-text modal-trigger" href="#deactivate-modal-{{ card.pk }}">
<i class="material-icons left">timer_off</i>
{% trans "Deactivate" %}
</a>
{% endif %}
<a class="btn-flat waves-effect waves-red red-text modal-trigger" href="{% url "delete_card" card.pk %}">
<i class="material-icons left">delete</i> <i class="material-icons left">delete</i>
{% trans "Delete" %} {% trans "Delete" %}
</a> </a>
\ No newline at end of file
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load material_form i18n any_js %}
{% block browser_title %}{% blocktrans %}Card{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans with person=object.person %}Card of {{ person }}{% endblocktrans %}{% endblock %}
{% block content %}
{% include_js "pdfobject" %}
{% include "kort/card/detail_content.html" with card=object %}
{% endblock %}
{% load i18n %}
<div class="row">
<div class="col s12 m12 l6">
<div class="card">
<div class="card-content">
<div class="card-title">{% trans "Card details" %}</div>
<table>
<tr>
<th>{% trans "Person" %}</th>
<td>{{ card.person }}</td>
</tr>
<tr>
<th>{% trans "Chip number" %}</th>
<td><code>{{ card.chip_number|default:_("not set yet") }}</code></td>
</tr>
<tr>
<th>{% trans "Valid until" %}</th>
<td>{{ card.valid_until }}</td>
</tr>
<tr>
<th>{% trans "Status" %}</th>
<td>
{% include "kort/card/status.html" %}
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col s12 m12 l6">
{% if card.pdf_file %}
<div id="card-pdf-{{ card.pk }}" style="height: 500px;"></div>
<script>PDFObject.embed("{{ card.pdf_file.url }}", "#card-pdf-{{ card.pk }}");</script>
{% else %}
<div class="row center-via-flex">
<a class="btn waves-effect waves-light" href="{% url "generate_card_pdf" card.pk %}">
<i class="material-icons left">picture_as_pdf</i>
{% trans "Generate card as PDF" %}
</a>
</div>
{% endif %}
</div>
</div>
\ No newline at end of file
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load i18n rules material_form %} {% load i18n rules material_form any_js %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block browser_title %}{% blocktrans %}Cards{% endblocktrans %}{% endblock %} {% block browser_title %}{% blocktrans %}Cards{% endblocktrans %}{% endblock %}
...@@ -36,5 +36,6 @@ ...@@ -36,5 +36,6 @@
{# </form>#} {# </form>#}
{# <h2>{% trans "Selected persons" %}</h2>#} {# <h2>{% trans "Selected persons" %}</h2>#}
{% include_js "pdfobject" %}
{% render_table table %} {% render_table table %}
{% endblock %} {% endblock %}
{% load i18n %}
{% if card.is_valid %}
<span class="badge new green white-text">{% trans "Valid" %}</span>
{% else %}
<span class="badge new green white-text">{% trans "Not valid" %}</span>
{% endif %}
\ No newline at end of file
...@@ -181,7 +181,7 @@ ...@@ -181,7 +181,7 @@
Name/Surname/Nom Name/Surname/Nom
</div> </div>
<div class="info-value"> <div class="info-value">
Mustermann {{ person.last_name }}
</div> </div>
</div> </div>
<div class="info-item"> <div class="info-item">
...@@ -189,7 +189,7 @@ ...@@ -189,7 +189,7 @@
Vornamen/Given names/Prénoms Vornamen/Given names/Prénoms
</div> </div>
<div class="info-value"> <div class="info-value">
Max {{ person.first_name }} {{ person.additional_name }}
</div> </div>
</div> </div>
<div class="info-item"> <div class="info-item">
...@@ -197,7 +197,7 @@ ...@@ -197,7 +197,7 @@
Wohnort/Residence/Résidence Wohnort/Residence/Résidence
</div> </div>
<div class="info-value"> <div class="info-value">
Musterstraße 1, 12345 Musterstadt {{ person.street }} {{ person.housenumber }}, {{ person.postal_code }} {{ person.place }}
</div> </div>
</div> </div>
<div class="info-item"> <div class="info-item">
...@@ -205,14 +205,14 @@ ...@@ -205,14 +205,14 @@
Geburtsdatum/Date of birth/Date de naissance Geburtsdatum/Date of birth/Date de naissance
</div> </div>
<div class="info-value"> <div class="info-value">
01.01.2007 {{ person.date_of_birth|date:"SHORT_DATE_FORMAT" }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="front-footer"> <div class="front-footer">
{% generate_barcode "55846268859" %} {% generate_barcode chip_number %}
<div class="signature"> <div class="signature">
Lübeck, den {% now "SHORT_DATE_FORMAT" %} Lübeck, den {% now "SHORT_DATE_FORMAT" %}
<img src="{% static "kort/signature.png" %}"> <img src="{% static "kort/signature.png" %}">
......
...@@ -6,6 +6,12 @@ urlpatterns = [ ...@@ -6,6 +6,12 @@ urlpatterns = [
path("test", views.TestPDFView.as_view(), name="test_pdf"), path("test", views.TestPDFView.as_view(), name="test_pdf"),
path("cards/", views.CardListView.as_view(), name="cards"), path("cards/", views.CardListView.as_view(), name="cards"),
path("cards/create/", views.CardCreateView.as_view(), name="create_card"), path("cards/create/", views.CardCreateView.as_view(), name="create_card"),
path("cards/<int:pk>/", views.CardDetailView.as_view(), name="card"),
path(
"cards/<int:pk>/generate_pdf/",
views.CardGeneratePDFView.as_view(),
name="generate_card_pdf",
),
path("cards/<int:pk>/deactivate/", views.CardDeactivateView.as_view(), name="deactivate_card"), path("cards/<int:pk>/deactivate/", views.CardDeactivateView.as_view(), name="deactivate_card"),
path("cards/<int:pk>/delete/", views.CardDeleteView.as_view(), name="delete_card"), path("cards/<int:pk>/delete/", views.CardDeleteView.as_view(), name="delete_card"),
] ]
from django.contrib import messages from django.contrib import messages
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.urls import reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views import View from django.views import View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import DetailView, SingleObjectMixin
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from reversion.views import RevisionMixin from reversion.views import RevisionMixin
...@@ -14,6 +14,7 @@ from aleksis.apps.kort.forms import CardForm ...@@ -14,6 +14,7 @@ from aleksis.apps.kort.forms import CardForm
from aleksis.apps.kort.models import Card from aleksis.apps.kort.models import Card
from aleksis.apps.kort.tables import CardTable from aleksis.apps.kort.tables import CardTable
from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView
from aleksis.core.util.celery_progress import render_progress_page
from aleksis.core.views import RenderPDFView from aleksis.core.views import RenderPDFView
...@@ -72,3 +73,40 @@ class CardDeactivateView(PermissionRequiredMixin, RevisionMixin, SingleObjectMix ...@@ -72,3 +73,40 @@ class CardDeactivateView(PermissionRequiredMixin, RevisionMixin, SingleObjectMix
self.object.deactivate() self.object.deactivate()
messages.success(request, _("The card has been deactivated successfully.")) messages.success(request, _("The card has been deactivated successfully."))
return redirect(self.success_url) return redirect(self.success_url)
class CardDetailView(PermissionRequiredMixin, RevisionMixin, DetailView):
permission_required = "core.view_card_rule"
model = Card
template_name = "kort/card/detail.html"
class CardGeneratePDFView(PermissionRequiredMixin, RevisionMixin, SingleObjectMixin, View):
permission_required = "views.view_card_rule"
model = Card
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
self.object = self.get_object()
if not self.object.chip_number:
messages.error(request, _("The chip number is missing."))
return redirect("cards")
redirect_url = reverse("card", args=[self.object.pk])
result = self.object.generate_pdf()
if result == True:
return redirect(redirect_url)
return render_progress_page(
request,
result,
title=_("Progress: Generate card layout as PDF file"),
progress_title=_("Generating PDF file …"),
success_message=_("The PDF file with the card layout has been generated successfully."),
error_message=_("There was a problem while generating the PDF file."),
redirect_on_success_url=redirect_url,
button_title=_("Show card"),
button_url=redirect_url,
button_icon="credit_card",
)
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