From bb4aee45c19154951eb13292ae50e5602874158d Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Sun, 27 Mar 2022 16:54:51 +0200
Subject: [PATCH] Implement API for getting and updating print jobs with all
 connected changes

---
 aleksis/apps/kort/api.py                      | 65 ++++++++++++++++++-
 aleksis/apps/kort/forms.py                    |  4 +-
 ...9_cardprinter_generate_number_on_server.py | 18 +++++
 .../migrations/0010_auto_20220326_2123.py     | 25 +++++++
 aleksis/apps/kort/models.py                   | 23 ++++++-
 .../kort/printer/detail_content.html          |  8 ++-
 aleksis/apps/kort/urls.py                     | 10 +++
 7 files changed, 147 insertions(+), 6 deletions(-)
 create mode 100644 aleksis/apps/kort/migrations/0009_cardprinter_generate_number_on_server.py
 create mode 100644 aleksis/apps/kort/migrations/0010_auto_20220326_2123.py

diff --git a/aleksis/apps/kort/api.py b/aleksis/apps/kort/api.py
index cce086a..ded44c5 100644
--- a/aleksis/apps/kort/api.py
+++ b/aleksis/apps/kort/api.py
@@ -1,11 +1,14 @@
 from django.contrib import admin
+from django.shortcuts import get_object_or_404
 from django.utils import timezone
 
 from oauth2_provider.contrib.rest_framework import TokenHasScope
 from rest_framework import generics, permissions, serializers
 from rest_framework.permissions import BasePermission
+from rest_framework.response import Response
+from rest_framework.views import APIView
 
-from aleksis.apps.kort.models import CardPrinter
+from aleksis.apps.kort.models import Card, CardPrinter, CardPrintJob
 
 admin.autodiscover()
 
@@ -20,6 +23,16 @@ class CorrectPrinterPermission(BasePermission):
         return False
 
 
+class CorrectJobPrinterPermission(BasePermission):
+    """Check whether the OAuth2 application belongs to the job's printer."""
+
+    def has_object_permission(self, request, view, obj) -> bool:
+        token = request.auth
+        if token.application == obj.printer.oauth2_application:
+            return True
+        return False
+
+
 class CardPrinterSerializer(serializers.ModelSerializer):
     class Meta:
         model = CardPrinter
@@ -35,6 +48,7 @@ class CardPrinterSerializer(serializers.ModelSerializer):
             "status_text",
             "last_seen_at",
             "cups_printer",
+            "generate_number_on_server",
         )
 
 
@@ -44,6 +58,26 @@ class CardPrinterStatusSerializer(serializers.ModelSerializer):
         fields = ("status", "status_text")
 
 
+class CardSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Card
+        fields = ("id", "chip_number", "valid_until", "deactivated", "person", "pdf_file")
+
+
+class CardPrintJobSerializer(serializers.ModelSerializer):
+    card = CardSerializer()
+
+    class Meta:
+        model = CardPrintJob
+        fields = ("id", "printer", "card", "status", "status_text")
+
+
+class CardPrintJobStatusSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = CardPrintJob
+        fields = ("id", "status", "status_text")
+
+
 class CardPrinterDetails(generics.RetrieveAPIView):
     """Show details about the card printer."""
 
@@ -71,3 +105,32 @@ class CardPrinterUpdateStatus(generics.UpdateAPIView):
         instance.last_seen_at = timezone.now()
         instance.save()
         return r
+
+
+class GetNextPrintJob(APIView):
+    """Get the next print job."""
+
+    permission_classes = [permissions.IsAuthenticated, TokenHasScope, CorrectPrinterPermission]
+    required_scopes = ["card_printer"]
+    serializer_class = CardPrinterSerializer
+    queryset = CardPrinter.objects.all()
+
+    def get_object(self, pk):
+        return get_object_or_404(CardPrinter, pk=pk)
+
+    def get(self, request, pk, *args, **kwargs):
+        printer = self.get_object(pk)
+        job = printer.get_next_print_job()
+        if not job:
+            return Response({"status": "no_job"})
+        serializer = CardPrintJobSerializer(job)
+        return Response(serializer.data)
+
+
+class CardPrintJobUpdateStatusView(generics.UpdateAPIView):
+    """Update the status of the card printer."""
+
+    permission_classes = [permissions.IsAuthenticated, TokenHasScope, CorrectJobPrinterPermission]
+    required_scopes = ["card_printer"]
+    serializer_class = CardPrintJobStatusSerializer
+    queryset = CardPrintJob.objects.all()
diff --git a/aleksis/apps/kort/forms.py b/aleksis/apps/kort/forms.py
index 1bb2c98..e0ba41c 100644
--- a/aleksis/apps/kort/forms.py
+++ b/aleksis/apps/kort/forms.py
@@ -26,12 +26,12 @@ class CardForm(forms.ModelForm):
 class CardPrinterForm(forms.ModelForm):
     layout = Layout(
         Fieldset(_("Generic attributes"), "name", "location", "description"),
-        Fieldset(_("Printer settings"), "cups_printer"),
+        Fieldset(_("Printer settings"), "cups_printer", "generate_number_on_server"),
     )
 
     class Meta:
         model = CardPrinter
-        fields = ["name", "location", "description", "cups_printer"]
+        fields = ["name", "location", "description", "cups_printer", "generate_number_on_server"]
 
 
 class PrinterSelectForm(forms.Form):
diff --git a/aleksis/apps/kort/migrations/0009_cardprinter_generate_number_on_server.py b/aleksis/apps/kort/migrations/0009_cardprinter_generate_number_on_server.py
new file mode 100644
index 0000000..e099be6
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0009_cardprinter_generate_number_on_server.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.12 on 2022-03-26 16:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('kort', '0008_auto_20220319_2018'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='cardprinter',
+            name='generate_number_on_server',
+            field=models.BooleanField(default=True, verbose_name='Generate card number on server'),
+        ),
+    ]
diff --git a/aleksis/apps/kort/migrations/0010_auto_20220326_2123.py b/aleksis/apps/kort/migrations/0010_auto_20220326_2123.py
new file mode 100644
index 0000000..028d25c
--- /dev/null
+++ b/aleksis/apps/kort/migrations/0010_auto_20220326_2123.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.2.12 on 2022-03-26 20:23
+
+from django.db import migrations
+import django.utils.timezone
+import model_utils.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('kort', '0009_cardprinter_generate_number_on_server'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='cardprintjob',
+            name='created',
+            field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
+        ),
+        migrations.AddField(
+            model_name='cardprintjob',
+            name='modified',
+            field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
+        ),
+    ]
diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py
index c69670e..a6281f3 100644
--- a/aleksis/apps/kort/models.py
+++ b/aleksis/apps/kort/models.py
@@ -1,5 +1,6 @@
+import uuid
 from datetime import timedelta
-from typing import Any, Union
+from typing import Any, Optional, Union
 
 from django.conf import settings
 from django.core.exceptions import ValidationError
@@ -10,6 +11,7 @@ from django.utils import timezone
 from django.utils.translation import gettext as _
 
 from celery.result import AsyncResult
+from model_utils.models import TimeStampedModel
 
 from aleksis.core.mixins import ExtensibleModel
 from aleksis.core.models import OAuthApplication, Person
@@ -80,6 +82,9 @@ class CardPrinter(ExtensibleModel):
 
     # Settings
     cups_printer = models.CharField(max_length=255, verbose_name=_("CUPS printer"), blank=True)
+    generate_number_on_server = models.BooleanField(
+        default=True, verbose_name=_("Generate card number on server")
+    )
 
     def save(self, *args, **kwargs):
         if not self.oauth2_application:
@@ -141,6 +146,14 @@ class CardPrinter(ExtensibleModel):
         for printer in cls.objects.all():
             printer.check_online_status()
 
+    def get_next_print_job(self) -> Optional["CardPrintJob"]:
+        jobs = self.jobs.order_by("created").filter(status=PrintStatus.REGISTERED)
+        if self.generate_number_on_server:
+            jobs = jobs.filter(card__pdf_file__isnull=False)
+        if jobs.exists():
+            return jobs.first()
+        return None
+
     class Meta:
         verbose_name = _("Card printer")
         verbose_name_plural = _("Card printers")
@@ -210,10 +223,16 @@ class Card(ExtensibleModel):
     def print_card(self, printer: CardPrinter):
         job = CardPrintJob(card=self, printer=printer)
         job.save()
+        if not self.chip_number and printer.generate_number_on_server:
+            self.chip_number = str(self.generate_number())
+            self.save()
         if self.chip_number:
             self.generate_pdf()
         return job
 
+    def generate_number(self) -> int:
+        return uuid.uuid1().int >> 32
+
     def __str__(self):
         if self.chip_number:
             return f"{self.person} ({self.chip_number})"
@@ -224,7 +243,7 @@ class Card(ExtensibleModel):
         verbose_name_plural = _("Cards")
 
 
-class CardPrintJob(ExtensibleModel):
+class CardPrintJob(TimeStampedModel, ExtensibleModel):
     printer = models.ForeignKey(
         CardPrinter, on_delete=models.CASCADE, verbose_name=_("Printer"), related_name="jobs"
     )
diff --git a/aleksis/apps/kort/templates/kort/printer/detail_content.html b/aleksis/apps/kort/templates/kort/printer/detail_content.html
index eaefc0d..9618a4b 100644
--- a/aleksis/apps/kort/templates/kort/printer/detail_content.html
+++ b/aleksis/apps/kort/templates/kort/printer/detail_content.html
@@ -83,6 +83,9 @@
               <th>
                 {% trans "Card" %}
               </th>
+              <th>
+                {% trans "Created at" %}
+              </th>
               <th>
                 {% trans "Status" %}
               </th>
@@ -95,7 +98,10 @@
                   </a>
                 </td>
                 <td>
-                  {{ job.status }}
+                  {{ job.created }}
+                </td>
+                <td>
+                  {{ job.status }} {% if job.status_text %}({{ job.status_text }}){% endif %}
                 </td>
               </tr>
             {% endfor %}
diff --git a/aleksis/apps/kort/urls.py b/aleksis/apps/kort/urls.py
index 9aa6a22..a0c9f5d 100644
--- a/aleksis/apps/kort/urls.py
+++ b/aleksis/apps/kort/urls.py
@@ -35,4 +35,14 @@ urlpatterns = [
         api.CardPrinterUpdateStatus.as_view(),
         name="api_card_printer_status",
     ),
+    path(
+        "api/v1/printers/<int:pk>/jobs/next/",
+        api.GetNextPrintJob.as_view(),
+        name="api_get_next_print_job",
+    ),
+    path(
+        "api/v1/jobs/<int:pk>/status/",
+        api.CardPrintJobUpdateStatusView.as_view(),
+        name="api_update_job_status",
+    ),
 ]
-- 
GitLab