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