From ab9c4e66136cbb64037635267a772604a32acd65 Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Tue, 15 Mar 2022 20:50:52 +0100 Subject: [PATCH] Add models for print jobs and allow triggering them --- aleksis/apps/kort/forms.py | 14 +++++ .../migrations/0007_auto_20220315_1957.py | 49 ++++++++++++++++++ aleksis/apps/kort/models.py | 27 ++++++++++ aleksis/apps/kort/tables.py | 8 ++- .../templates/kort/card/detail_content.html | 1 + .../kort/templates/kort/card/print_form.html | 18 +++++++ .../kort/printer/detail_content.html | 28 ++++++++++ aleksis/apps/kort/urls.py | 1 + aleksis/apps/kort/views.py | 51 +++++++++++++++++-- 9 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 aleksis/apps/kort/migrations/0007_auto_20220315_1957.py create mode 100644 aleksis/apps/kort/templates/kort/card/print_form.html diff --git a/aleksis/apps/kort/forms.py b/aleksis/apps/kort/forms.py index fdf0c52..b8b700e 100644 --- a/aleksis/apps/kort/forms.py +++ b/aleksis/apps/kort/forms.py @@ -1,6 +1,8 @@ from django import forms +from django.utils.translation import gettext as _ from django_select2.forms import ModelSelect2Widget +from material import Layout from aleksis.apps.kort.models import Card, CardPrinter @@ -25,3 +27,15 @@ class CardPrinterForm(forms.ModelForm): class Meta: model = CardPrinter fields = ["name", "location", "description"] + + +class PrinterSelectForm(forms.Form): + layout = Layout("printer") + printer = forms.ModelChoiceField(queryset=None, label=_("Card Printer"), required=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + printers = CardPrinter.objects.all() + self.fields["printer"].queryset = printers + if printers.count() == 1: + self.fields["printer"].initial = printers.first() diff --git a/aleksis/apps/kort/migrations/0007_auto_20220315_1957.py b/aleksis/apps/kort/migrations/0007_auto_20220315_1957.py new file mode 100644 index 0000000..5a5cef6 --- /dev/null +++ b/aleksis/apps/kort/migrations/0007_auto_20220315_1957.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.12 on 2022-03-15 18:57 + +from django.conf import settings +import django.contrib.sites.managers +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL), + ('sites', '0002_alter_domain_unique'), + ('kort', '0006_auto_20220310_2003'), + ] + + operations = [ + migrations.AlterField( + model_name='card', + name='pdf_file', + field=models.FileField(blank=True, default='', upload_to='cards/', validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='PDF file'), + preserve_default=False, + ), + migrations.AlterField( + model_name='cardprinter', + name='oauth2_application', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='card_printers', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL, verbose_name='OAuth2 application'), + ), + migrations.CreateModel( + name='CardPrintJob', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('status', models.CharField(choices=[('registered', 'Registered'), ('in_progress', 'In progress'), ('finished', 'Finished')], default='registered', max_length=255, verbose_name='Status')), + ('status_text', models.TextField(blank=True, verbose_name='Status text')), + ('card', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='kort.card', verbose_name='Card')), + ('printer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='kort.cardprinter', verbose_name='Printer')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ], + options={ + 'verbose_name': 'Card print job', + 'verbose_name_plural': 'Card print jobs', + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + ] diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py index 0dcbf56..eb0ed24 100644 --- a/aleksis/apps/kort/models.py +++ b/aleksis/apps/kort/models.py @@ -50,6 +50,7 @@ class PrintStatus(models.TextChoices): REGISTERED = "registered", _("Registered") IN_PROGRESS = "in_progress", _("In progress") FINISHED = "finished", _("Finished") + FAILED = "failed", _("Failed") class CardPrinter(ExtensibleModel): @@ -187,6 +188,13 @@ class Card(ExtensibleModel): return True return generate_card_pdf.delay(self.pk) + def print_card(self, printer: CardPrinter): + job = CardPrintJob(card=self, printer=printer) + job.save() + if self.chip_number: + self.generate_pdf() + return job + def __str__(self): if self.chip_number: return f"{self.person} ({self.chip_number})" @@ -195,3 +203,22 @@ class Card(ExtensibleModel): class Meta: verbose_name = _("Card") verbose_name_plural = _("Cards") + + +class CardPrintJob(ExtensibleModel): + printer = models.ForeignKey( + CardPrinter, on_delete=models.CASCADE, verbose_name=_("Printer"), related_name="jobs" + ) + card = models.ForeignKey(Card, on_delete=models.CASCADE, verbose_name=_("Card")) + + status = models.CharField( + max_length=255, + verbose_name=_("Status"), + choices=PrintStatus.choices, + default=PrintStatus.REGISTERED, + ) + status_text = models.TextField(verbose_name=_("Status text"), blank=True) + + class Meta: + verbose_name = _("Card print job") + verbose_name_plural = _("Card print jobs") diff --git a/aleksis/apps/kort/tables.py b/aleksis/apps/kort/tables.py index fabd05a..dedcab6 100644 --- a/aleksis/apps/kort/tables.py +++ b/aleksis/apps/kort/tables.py @@ -11,6 +11,8 @@ from django_tables2 import ( Table, ) +from aleksis.apps.kort.forms import PrinterSelectForm + class CardTable(Table): """Table to list cards.""" @@ -34,7 +36,9 @@ class CardTable(Table): ) def render_actions(self, value, record): - return render_to_string("kort/card/actions.html", dict(pk=value, card=record)) + return render_to_string( + "kort/card/actions.html", dict(pk=value, card=record, printer_form=PrinterSelectForm()) + ) class CardPrinterTable(Table): @@ -48,6 +52,8 @@ class CardPrinterTable(Table): current_status = Column(verbose_name=_("Current status"), accessor=A("pk")) last_seen_at = DateTimeColumn(verbose_name=_("Last seen at")) + jobs_count = Column(verbose_name=_("Running jobs")) + actions = Column(verbose_name=_("Actions"), accessor=A("pk")) def render_current_status(self, value, record): diff --git a/aleksis/apps/kort/templates/kort/card/detail_content.html b/aleksis/apps/kort/templates/kort/card/detail_content.html index 78f6036..490e51b 100644 --- a/aleksis/apps/kort/templates/kort/card/detail_content.html +++ b/aleksis/apps/kort/templates/kort/card/detail_content.html @@ -39,6 +39,7 @@ </div> </div> <div class="col s12 m12 l6"> + {% include "kort/card/print_form.html" %} {% 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> diff --git a/aleksis/apps/kort/templates/kort/card/print_form.html b/aleksis/apps/kort/templates/kort/card/print_form.html new file mode 100644 index 0000000..01fef69 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card/print_form.html @@ -0,0 +1,18 @@ +{% load material_form i18n %} +<form action="{% url "print_card" card.pk %}" method="get"> + <div class="card"> + <div class="card-content"> + <div class="card-title"><i class="material-icons left iconify" + data-icon="mdi:printer-outline"></i> {% trans "Print card" %}</div> + + {% csrf_token %} + {% form form=printer_form %}{% endform %} + </div> + <div class="card-action-light"> + <button type="submit" class="btn waves-effect waves-light"> + <i class="material-icons left iconify" data-icon="mdi:printer-outline"></i> + {% trans "Print card" %} + </button> + </div> + </div> +</form> diff --git a/aleksis/apps/kort/templates/kort/printer/detail_content.html b/aleksis/apps/kort/templates/kort/printer/detail_content.html index 84e8ab6..eaefc0d 100644 --- a/aleksis/apps/kort/templates/kort/printer/detail_content.html +++ b/aleksis/apps/kort/templates/kort/printer/detail_content.html @@ -74,6 +74,34 @@ <code>kort-client setup {{ printer.config_filename }}</code> </div> </div> + {% else %} + <div class="card"> + <div class="card-content"> + <div class="card-title">{% trans "Print jobs" %}</div> + <table> + <tr> + <th> + {% trans "Card" %} + </th> + <th> + {% trans "Status" %} + </th> + </tr> + {% for job in printer.jobs.all %} + <tr> + <td> + <a href="{% url "card" job.card.pk %}"> + {{ job.card }} + </a> + </td> + <td> + {{ job.status }} + </td> + </tr> + {% endfor %} + </table> + </div> + </div> {% endif %} </div> </div> \ No newline at end of file diff --git a/aleksis/apps/kort/urls.py b/aleksis/apps/kort/urls.py index df8dd26..9aa6a22 100644 --- a/aleksis/apps/kort/urls.py +++ b/aleksis/apps/kort/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ name="generate_card_pdf", ), path("cards/<int:pk>/deactivate/", views.CardDeactivateView.as_view(), name="deactivate_card"), + path("cards/<int:pk>/print/", views.CardPrintView.as_view(), name="print_card"), path("cards/<int:pk>/delete/", views.CardDeleteView.as_view(), name="delete_card"), path("printers/", views.CardPrinterListView.as_view(), name="card_printers"), path("printers/create/", views.CardPrinterCreateView.as_view(), name="create_card_printer"), diff --git a/aleksis/apps/kort/views.py b/aleksis/apps/kort/views.py index 4458872..aadaac6 100644 --- a/aleksis/apps/kort/views.py +++ b/aleksis/apps/kort/views.py @@ -1,8 +1,9 @@ import json from django.contrib import messages +from django.db.models import Count, Q from django.http import HttpRequest, HttpResponse -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils.translation import gettext as _ from django.views import View @@ -12,8 +13,8 @@ from django_tables2 import SingleTableView from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin -from aleksis.apps.kort.forms import CardForm, CardPrinterForm -from aleksis.apps.kort.models import Card, CardPrinter +from aleksis.apps.kort.forms import CardForm, CardPrinterForm, PrinterSelectForm +from aleksis.apps.kort.models import Card, CardPrinter, PrintStatus from aleksis.apps.kort.tables import CardPrinterTable, CardTable from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView from aleksis.core.util.celery_progress import render_progress_page @@ -33,7 +34,16 @@ class TestPDFView(RenderPDFView): return context -class CardListView(PermissionRequiredMixin, RevisionMixin, SingleTableView): +class PrinterSelectMixin: + def get_context_data(self, **kwargs): + print("Called and used?") + context = super().get_context_data(**kwargs) + context["printers"] = CardPrinter.objects.all() + context["printer_form"] = PrinterSelectForm() + return context + + +class CardListView(PermissionRequiredMixin, RevisionMixin, PrinterSelectMixin, SingleTableView): """List view for all cards.""" permission_required = "core.view_cards_rule" @@ -77,7 +87,31 @@ class CardDeactivateView(PermissionRequiredMixin, RevisionMixin, SingleObjectMix return redirect(self.success_url) -class CardDetailView(PermissionRequiredMixin, RevisionMixin, DetailView): +class CardPrintView(PermissionRequiredMixin, RevisionMixin, SingleObjectMixin, View): + """View used to create a print job for a card.""" + + permission_required = "core.delete_card_rule" + model = Card + success_url = reverse_lazy("cards") + + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + self.object = self.get_object() + + printer = self.request.GET.get("printer") + printer = get_object_or_404(CardPrinter, pk=printer) + + self.object.print_card(printer) + messages.success( + request, + _( + "The print job for the card {} on the printer {} has been created successfully." + ).format(self.object.person, printer.name), + ) + + return redirect(self.success_url) + + +class CardDetailView(PermissionRequiredMixin, RevisionMixin, PrinterSelectMixin, DetailView): permission_required = "core.view_card_rule" model = Card template_name = "kort/card/detail.html" @@ -122,6 +156,13 @@ class CardPrinterListView(PermissionRequiredMixin, RevisionMixin, SingleTableVie model = CardPrinter table_class = CardPrinterTable + def get_queryset(self): + return CardPrinter.objects.all().annotate( + jobs_count=Count( + "jobs", filter=~Q(jobs__status__in=[PrintStatus.FINISHED, PrintStatus.FAILED]) + ) + ) + class CardPrinterCreateView(PermissionRequiredMixin, RevisionMixin, AdvancedCreateView): """View used to create a card printer.""" -- GitLab