diff --git a/aleksis/apps/kort/admin.py b/aleksis/apps/kort/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..9c4a2ff4f10e7707226344aa6e847941da73d984 --- /dev/null +++ b/aleksis/apps/kort/admin.py @@ -0,0 +1,7 @@ +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) diff --git a/aleksis/apps/kort/migrations/0005_card_pdf_file.py b/aleksis/apps/kort/migrations/0005_card_pdf_file.py new file mode 100644 index 0000000000000000000000000000000000000000..e78b54c530c0cf80ff17c3e28ac29cde73ca37fb --- /dev/null +++ b/aleksis/apps/kort/migrations/0005_card_pdf_file.py @@ -0,0 +1,19 @@ +# 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'), + ), + ] diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py index a81d6ed37fecd18dc24b1e58917f163579147a31..e9123d8ae81f12cc3a4ea1ffe987fbb543a49119 100644 --- a/aleksis/apps/kort/models.py +++ b/aleksis/apps/kort/models.py @@ -1,9 +1,14 @@ +from typing import Union + from django.core.exceptions import ValidationError +from django.core.validators import FileExtensionValidator from django.db import models from django.template import Context, Template from django.utils import timezone from django.utils.translation import gettext as _ +from celery.result import AsyncResult + from aleksis.core.mixins import ExtensibleModel from aleksis.core.models import Person @@ -69,6 +74,14 @@ class Card(ExtensibleModel): valid_until = models.DateField(verbose_name=_("Valid until")) 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 def is_valid(self): return ( @@ -86,6 +99,18 @@ class Card(ExtensibleModel): "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: verbose_name = _("Card") verbose_name_plural = _("Cards") diff --git a/aleksis/apps/kort/settings.py b/aleksis/apps/kort/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..1cbd74e3786c9c49e0f88b35a205ac4467d658d5 --- /dev/null +++ b/aleksis/apps/kort/settings.py @@ -0,0 +1,7 @@ +from aleksis.core.settings import JS_URL + +YARN_INSTALLED_APPS = ["pdfobject"] + +ANY_JS = { + "pdfobject": {"js_url": JS_URL + "/pdfobject/pdfobject.min.js"}, +} diff --git a/aleksis/apps/kort/tables.py b/aleksis/apps/kort/tables.py index 635102ccf613f0bca82da4ab8b39077bb36bfb3e..5b53edde95c511f73605f25b3adc094ee3b674e6 100644 --- a/aleksis/apps/kort/tables.py +++ b/aleksis/apps/kort/tables.py @@ -1,7 +1,7 @@ from django.template.loader import render_to_string 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): @@ -11,7 +11,7 @@ class CardTable(Table): attrs = {"class": "highlight"} 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")) valid_until = Column(verbose_name=_("Valid until")) deactivated = BooleanColumn(verbose_name=_("Deactivated")) @@ -19,10 +19,9 @@ class CardTable(Table): def render_current_status(self, value, record): return render_to_string( - "components/materialize-chips.html", + "kort/card/status.html", dict( - content=_("Valid") if record.is_valid else _("Not valid"), - classes="white-text " + ("green" if record.is_valid else "red"), + card=record, ), ) diff --git a/aleksis/apps/kort/tasks.py b/aleksis/apps/kort/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..82beaa0b3d7b51f95a82eaadecad6c4aa0d0f01d --- /dev/null +++ b/aleksis/apps/kort/tasks.py @@ -0,0 +1,21 @@ +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() diff --git a/aleksis/apps/kort/templates/kort/card/actions.html b/aleksis/apps/kort/templates/kort/card/actions.html index 1c39b5daffc6cc26d456c1d4c05c44f97214bde7..ce253a92178bb47677c286fdf3d106a9d23b72e7 100644 --- a/aleksis/apps/kort/templates/kort/card/actions.html +++ b/aleksis/apps/kort/templates/kort/card/actions.html @@ -1,50 +1,44 @@ {% load i18n %} -<!-- Modal Structure --> -<div id="deactivate-modal-{{ card.pk }}" class="modal"> + +<div id="detail-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" %} + <h4>{% blocktrans with person=card.person %}Card of {{ person }}{% endblocktrans %}</h4> + {% include "kort/card/detail_content.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> -<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> -<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 class="btn-flat waves-effect waves-green green-text modal-trigger" href="#detail-modal-{{ card.pk }}"> + <i class="material-icons left">slideshow</i> + {% trans "Show" %} </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> {% trans "Delete" %} </a> \ No newline at end of file diff --git a/aleksis/apps/kort/templates/kort/card/detail.html b/aleksis/apps/kort/templates/kort/card/detail.html new file mode 100644 index 0000000000000000000000000000000000000000..d5176cacc78154bf71b82091819a19fbf5f9a27f --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card/detail.html @@ -0,0 +1,15 @@ +{# -*- 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 %} diff --git a/aleksis/apps/kort/templates/kort/card/detail_content.html b/aleksis/apps/kort/templates/kort/card/detail_content.html new file mode 100644 index 0000000000000000000000000000000000000000..c0a4cb37836d6a09ea6b854ed0ec51200f94f0d2 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card/detail_content.html @@ -0,0 +1,43 @@ +{% 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 diff --git a/aleksis/apps/kort/templates/kort/card/list.html b/aleksis/apps/kort/templates/kort/card/list.html index 2a6524b30b7ea87dd108806c21ce27360e2011ea..874d37d17bc8ad80f6d954d22ea47e2ea33c4c4c 100644 --- a/aleksis/apps/kort/templates/kort/card/list.html +++ b/aleksis/apps/kort/templates/kort/card/list.html @@ -2,7 +2,7 @@ {% extends "core/base.html" %} -{% load i18n rules material_form %} +{% load i18n rules material_form any_js %} {% load render_table from django_tables2 %} {% block browser_title %}{% blocktrans %}Cards{% endblocktrans %}{% endblock %} @@ -36,5 +36,6 @@ {# </form>#} {# <h2>{% trans "Selected persons" %}</h2>#} + {% include_js "pdfobject" %} {% render_table table %} {% endblock %} diff --git a/aleksis/apps/kort/templates/kort/card/status.html b/aleksis/apps/kort/templates/kort/card/status.html new file mode 100644 index 0000000000000000000000000000000000000000..009baa0bcdea5fbd575d481ae9f2a0efcf9bd7af --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card/status.html @@ -0,0 +1,6 @@ +{% 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 diff --git a/aleksis/apps/kort/templates/kort/pdf.html b/aleksis/apps/kort/templates/kort/pdf.html index a1d51ba1d409bb86750611309afbaec0c2f9cc98..c01cdc31131162fef0d57f562d9e58c88ccb7c4b 100644 --- a/aleksis/apps/kort/templates/kort/pdf.html +++ b/aleksis/apps/kort/templates/kort/pdf.html @@ -181,7 +181,7 @@ Name/Surname/Nom </div> <div class="info-value"> - Mustermann + {{ person.last_name }} </div> </div> <div class="info-item"> @@ -189,7 +189,7 @@ Vornamen/Given names/Prénoms </div> <div class="info-value"> - Max + {{ person.first_name }} {{ person.additional_name }} </div> </div> <div class="info-item"> @@ -197,7 +197,7 @@ Wohnort/Residence/Résidence </div> <div class="info-value"> - Musterstraße 1, 12345 Musterstadt + {{ person.street }} {{ person.housenumber }}, {{ person.postal_code }} {{ person.place }} </div> </div> <div class="info-item"> @@ -205,14 +205,14 @@ Geburtsdatum/Date of birth/Date de naissance </div> <div class="info-value"> - 01.01.2007 + {{ person.date_of_birth|date:"SHORT_DATE_FORMAT" }} </div> </div> </div> </div> <div class="front-footer"> - {% generate_barcode "55846268859" %} + {% generate_barcode chip_number %} <div class="signature"> Lübeck, den {% now "SHORT_DATE_FORMAT" %} <img src="{% static "kort/signature.png" %}"> diff --git a/aleksis/apps/kort/urls.py b/aleksis/apps/kort/urls.py index 8aa00d73f70c3c65d37b862b93d9c9b75f52db55..983140bc22fee78ee81eddfa486a3ca36faffb92 100644 --- a/aleksis/apps/kort/urls.py +++ b/aleksis/apps/kort/urls.py @@ -6,6 +6,12 @@ urlpatterns = [ path("test", views.TestPDFView.as_view(), name="test_pdf"), path("cards/", views.CardListView.as_view(), name="cards"), 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>/delete/", views.CardDeleteView.as_view(), name="delete_card"), ] diff --git a/aleksis/apps/kort/views.py b/aleksis/apps/kort/views.py index bfe584c9663db438e57725eb1fb1488ec92a24a4..0c088a9fa89b4f0a19c905782c623f44c8234319 100644 --- a/aleksis/apps/kort/views.py +++ b/aleksis/apps/kort/views.py @@ -1,10 +1,10 @@ from django.contrib import messages from django.http import HttpRequest, HttpResponse 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.views import View -from django.views.generic.detail import SingleObjectMixin +from django.views.generic.detail import DetailView, SingleObjectMixin from django_tables2 import SingleTableView from reversion.views import RevisionMixin @@ -14,6 +14,7 @@ from aleksis.apps.kort.forms import CardForm from aleksis.apps.kort.models import Card from aleksis.apps.kort.tables import CardTable from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView +from aleksis.core.util.celery_progress import render_progress_page from aleksis.core.views import RenderPDFView @@ -72,3 +73,40 @@ class CardDeactivateView(PermissionRequiredMixin, RevisionMixin, SingleObjectMix self.object.deactivate() messages.success(request, _("The card has been deactivated successfully.")) 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", + )