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

Merge branch 'feature/printers' into 'main'

Setup printer stuff

See merge request !3
parents d30d9733 6518b0f2
No related branches found
No related tags found
1 merge request!3Setup printer stuff
Pipeline #71634 canceled
Showing
with 821 additions and 25 deletions
from django.contrib import admin
from django.shortcuts import get_object_or_404
from django.utils import timezone
from celery.result import allow_join_result
from celery.states import SUCCESS
from oauth2_provider.contrib.rest_framework import TokenHasScope
from rest_framework import generics, permissions, serializers
from rest_framework.exceptions import APIException, ValidationError
from rest_framework.permissions import BasePermission
from rest_framework.response import Response
from rest_framework.views import APIView
from aleksis.apps.kort.models import Card, CardPrinter, CardPrintJob
admin.autodiscover()
class CorrectPrinterPermission(BasePermission):
"""Check whether the OAuth2 application belongs to the printer."""
def has_object_permission(self, request, view, obj) -> bool:
token = request.auth
if token.application == obj.oauth2_application:
return True
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
fields = (
"id",
"name",
"description",
"location",
"status",
"status_label",
"status_color",
"status_icon",
"status_text",
"last_seen_at",
"cups_printer",
"generate_number_on_server",
"card_detector",
)
class CardPrinterStatusSerializer(serializers.ModelSerializer):
class Meta:
model = CardPrinter
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 CardChipNumberSerializer(serializers.ModelSerializer):
class Meta:
model = Card
fields = ("chip_number",)
class CardPrinterDetails(generics.RetrieveAPIView):
"""Show details about the card printer."""
permission_classes = [permissions.IsAuthenticated, TokenHasScope, CorrectPrinterPermission]
required_scopes = ["card_printer"]
serializer_class = CardPrinterSerializer
queryset = CardPrinter.objects.all()
def get_object(self):
token = self.request.auth
return token.application.card_printers.all().first()
class CardPrinterUpdateStatus(generics.UpdateAPIView):
"""Update the status of the card printer."""
permission_classes = [permissions.IsAuthenticated, TokenHasScope, CorrectPrinterPermission]
required_scopes = ["card_printer"]
serializer_class = CardPrinterStatusSerializer
queryset = CardPrinter.objects.all()
def update(self, request, *args, **kwargs):
r = super().update(request, *args, **kwargs)
instance = self.get_object()
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()
class CardPrintJobSetChipNumberView(generics.UpdateAPIView):
"""Update the status of the card printer."""
permission_classes = [permissions.IsAuthenticated, TokenHasScope, CorrectJobPrinterPermission]
required_scopes = ["card_printer"]
serializer_class = CardChipNumberSerializer
queryset = CardPrintJob.objects.all()
def update(self, request, *args, **kwargs):
instance = self.get_object()
card = instance.card
if card.chip_number:
raise ValidationError
serializer = self.get_serializer(card, data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
result = instance.card.generate_pdf()
with allow_join_result():
result.wait()
card.refresh_from_db()
if result.status == SUCCESS and card.pdf_file:
serializer = CardPrintJobSerializer(instance)
instance.refresh_from_db()
return Response(serializer.data)
else:
card.chip_number = None
card.save()
raise APIException("Error while generating PDF file")
from django.apps import apps
from django.db import models
from django.db.models import functions
from django.utils.translation import gettext as _
from aleksis.core.util.apps import AppConfig
......@@ -25,18 +22,4 @@ class DefaultConfig(AppConfig):
@classmethod
def get_all_scopes(cls) -> dict[str, str]:
"""Return all OAuth scopes and their descriptions for this app."""
Card = apps.get_model("kort", "Card")
label_prefix = _("Access and manage printer status and print jobs")
scopes = dict(
Card.objects.annotate(
scope=functions.Concat(
models.Value(f"{Card.SCOPE_PREFIX}_"),
models.F("pk"),
output_field=models.CharField(),
),
label=functions.Concat(models.Value(f"{label_prefix}: "), models.F("name")),
)
.values_list("scope", "label")
.distinct()
)
return scopes
return {"card_printer": _("Access and manage printer status and print jobs")}
from django import forms
from django.utils.translation import gettext as _
from django_select2.forms import ModelSelect2Widget
from material import Fieldset, Layout
from aleksis.apps.kort.models import Card
from aleksis.apps.kort.models import Card, CardPrinter
class CardForm(forms.ModelForm):
......@@ -19,3 +21,35 @@ class CardForm(forms.ModelForm):
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
}
class CardPrinterForm(forms.ModelForm):
layout = Layout(
Fieldset(_("Generic attributes"), "name", "location", "description"),
Fieldset(
_("Printer settings"), "cups_printer", "generate_number_on_server", "card_detector"
),
)
class Meta:
model = CardPrinter
fields = [
"name",
"location",
"description",
"cups_printer",
"generate_number_on_server",
"card_detector",
]
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()
......@@ -23,6 +23,17 @@ MENUS = {
)
],
},
{
"name": _("Card Printers"),
"url": "card_printers",
"svg_icon": "mdi:printer-outline",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"core.view_cardprinters_rule",
)
],
},
],
}
]
......
# 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()),
],
),
]
# Generated by Django 3.2.12 on 2022-03-19 19:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('kort', '0007_auto_20220315_1957'),
]
operations = [
migrations.AddField(
model_name='cardprinter',
name='cups_printer',
field=models.CharField(blank=True, max_length=255, verbose_name='CUPS printer'),
),
migrations.AlterField(
model_name='cardprintjob',
name='printer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='kort.cardprinter', verbose_name='Printer'),
),
migrations.AlterField(
model_name='cardprintjob',
name='status',
field=models.CharField(choices=[('registered', 'Registered'), ('in_progress', 'In progress'), ('finished', 'Finished'), ('failed', 'Failed')], default='registered', max_length=255, verbose_name='Status'),
),
]
# 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'),
),
]
# 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'),
),
]
# Generated by Django 3.2.13 on 2022-04-11 14:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kort', '0010_auto_20220326_2123'),
]
operations = [
migrations.AddField(
model_name='cardprinter',
name='card_detector',
field=models.CharField(blank=True, max_length=255, verbose_name='Card detector'),
),
]
from typing import Union
import uuid
from datetime import timedelta
from typing import Any, Optional, Union
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.db import models
from django.db.models import Q
from django.template import Context, Template
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
......@@ -19,11 +24,37 @@ class CardPrinterStatus(models.TextChoices):
WITH_ERRORS = "with_errors", _("With errors")
NOT_REGISTERED = "not_registered", _("Not registered")
@classmethod
def get_color(cls, value):
_colors = {
CardPrinterStatus.ONLINE.value: "green",
CardPrinterStatus.OFFLINE.value: "red",
CardPrinterStatus.WITH_ERRORS.value: "orange",
CardPrinterStatus.NOT_REGISTERED.value: "grey",
}
return _colors.get(value)
@classmethod
def get_icon(cls, value):
_icons = {
CardPrinterStatus.ONLINE.value: "mdi:printer-check",
CardPrinterStatus.OFFLINE.value: "mdi:printer-off",
CardPrinterStatus.WITH_ERRORS.value: "mdi:printer-alert",
CardPrinterStatus.NOT_REGISTERED.value: "mdi:printer-search",
}
return _icons.get(value)
@classmethod
def get_label(cls, value):
_labels = {x: y for x, y in cls.choices}
return _labels.get(value)
class PrintStatus(models.TextChoices):
REGISTERED = "registered", _("Registered")
IN_PROGRESS = "in_progress", _("In progress")
FINISHED = "finished", _("Finished")
FAILED = "failed", _("Failed")
class CardPrinter(ExtensibleModel):
......@@ -47,14 +78,23 @@ class CardPrinter(ExtensibleModel):
verbose_name=_("OAuth2 application"),
blank=True,
null=True,
related_name="card_printers",
)
# 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")
)
card_detector = models.CharField(max_length=255, verbose_name=_("Card detector"), blank=True)
def save(self, *args, **kwargs):
if not self.oauth2_application:
application = OAuthApplication(
client_type=OAuthApplication.CLIENT_CONFIDENTIAL,
authorization_grant_type=OAuthApplication.GRANT_AUTHORIZATION_CODE,
name=f"Card printer: {self.name}",
redirect_uris="urn:ietf:wg:oauth:2.0:oob",
)
application.save()
self.oauth2_application = application
......@@ -64,6 +104,73 @@ class CardPrinter(ExtensibleModel):
def __str__(self):
return self.name
@property
def status_label(self) -> str:
"""Return the verbose name of the status."""
return CardPrinterStatus.get_label(self.status)
@property
def status_color(self) -> str:
"""Return a color for the status."""
return CardPrinterStatus.get_color(self.status)
@property
def status_icon(self) -> str:
"""Return an iconify icon for the status."""
return CardPrinterStatus.get_icon(self.status)
def generate_config(self) -> dict[str, Any]:
"""Generate the configuration for the printer client."""
config = {
"base_url": settings.BASE_URL,
"client_id": self.oauth2_application.client_id,
"client_secret": self.oauth2_application.client_secret,
}
return config
@property
def config_filename(self) -> str:
"""Return the filename for the printer client configuration."""
return f"card-printer-config-{self.pk}.json"
def check_online_status(self):
if (
self.status
not in (CardPrinterStatus.NOT_REGISTERED.value, CardPrinterStatus.OFFLINE.value)
and self.last_seen_at
):
if self.last_seen_at < timezone.now() - timedelta(minutes=1):
self.status = CardPrinterStatus.OFFLINE.value
self.save()
@classmethod
def check_online_status_for_all(cls):
for printer in cls.objects.all():
printer.check_online_status()
def get_next_print_job(self) -> Optional["CardPrintJob"]:
if not self.generate_number_on_server:
print(
self.jobs.filter(card__pdf_file=""),
self.jobs.filter(card__pdf_file="").exclude(card__chip_number=""),
)
self.jobs.filter(
(Q(card__pdf_file="") & ~Q(card__chip_number=""))
| Q(status=PrintStatus.IN_PROGRESS)
).update(status=PrintStatus.FAILED)
Card.objects.filter(
jobs__in=self.jobs.filter(status=PrintStatus.FAILED), chip_number=""
).update(chip_number="")
else:
self.jobs.filter(status=PrintStatus.IN_PROGRESS).update(status=PrintStatus.FAILED)
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")
......@@ -130,6 +237,19 @@ 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 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})"
......@@ -138,3 +258,24 @@ class Card(ExtensibleModel):
class Meta:
verbose_name = _("Card")
verbose_name_plural = _("Cards")
class CardPrintJob(TimeStampedModel, 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"), related_name="jobs"
)
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")
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django_tables2 import A, BooleanColumn, Column, LinkColumn, RelatedLinkColumn, Table
from django_tables2 import (
A,
BooleanColumn,
Column,
DateTimeColumn,
LinkColumn,
RelatedLinkColumn,
Table,
)
from aleksis.apps.kort.forms import PrinterSelectForm
class CardTable(Table):
......@@ -26,4 +36,33 @@ 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):
"""Table to list card printers."""
class Meta:
attrs = {"class": "highlight"}
name = LinkColumn("card_printer", verbose_name=_("Printer name"), args=[A("pk")])
location = Column(verbose_name=_("Printer location"))
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):
return render_to_string(
"kort/printer/status.html",
dict(
printer=record,
),
)
def render_actions(self, value, record):
return render_to_string("kort/printer/actions.html", dict(pk=value, printer=record))
......@@ -39,9 +39,12 @@
</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>
<a href="{{ card.pdf_file.url }}" class="btn waves-effect waves-light">
<i class="material-icons left iconify" data-icon="mdi:file-pdf-box"></i>
{% trans "Show card as PDF" %}
</a>
{% else %}
<div class="row center-via-flex">
<a class="btn waves-effect waves-light" href="{% url "generate_card_pdf" card.pk %}">
......
{% 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>
......@@ -2,5 +2,5 @@
{% 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>
<span class="badge new red white-text">{% trans "Not valid" %}</span>
{% endif %}
\ No newline at end of file
{% load i18n %}
<div id="detail-modal-{{ printer.pk }}" class="modal">
<div class="modal-content">
<h4>{{ printer.name }}</h4>
{% include "kort/printer/detail_content.html" %}
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">
<i class="material-icons left iconify" data-icon="mdi:close"></i>
{% trans "Close" %}
</a>
</div>
</div>
<a class="btn-flat waves-effect waves-green green-text modal-trigger" href="#detail-modal-{{ printer.pk }}">
<i class="material-icons left iconify" data-icon="mdi:play-box-outline"></i>
{% trans "Show" %}
</a>
<a class="btn-flat waves-effect waves-orange orange-text modal-trigger" href="{% url "edit_card_printer" printer.pk %}">
<i class="material-icons left iconify" data-icon="mdi:pencil-outline"></i>
{% trans "Edit" %}
</a>
<a class="btn-flat waves-effect waves-red red-text modal-trigger" href="{% url "delete_card_printer" printer.pk %}">
<i class="material-icons left iconify" data-icon="mdi:delete-outline"></i>
{% trans "Delete" %}
</a>
\ No newline at end of file
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load material_form i18n any_js %}
{% block extra_head %}
{{ form.media.css }}
{% include_css "select2-materialize" %}
{% endblock %}
{% block browser_title %}{% blocktrans %}Create card{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans %}Create card{% endblocktrans %}{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% form form=form %}{% endform %}
{% include "core/partials/save_button.html" %}
</form>
{% include_js "select2-materialize" %}
{{ form.media.js }}
{% endblock %}
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load i18n rules material_form %}
{% load render_table from django_tables2 %}
{% block browser_title %}{% blocktrans %}Delete Card Printer{% endblocktrans %}{% endblock %}
{% block no_page_title %}{% endblock %}
{% block content %}
<p class="flow-text">{% trans "Do you really want to delete the following card printer?" %}</p>
{% include "kort/printer/short.html" with printer=object %}
<figure class="alert warning">
<i class="material-icons left iconify" data-icon="mdi:alert-octagon-outline"></i>
{% blocktrans %}
Please pay attention that this also will deactivate the access for the print client and you would have to
reconfigure the client if you want to use it further.
{% endblocktrans %}
</figure>
<form method="post" action="">
{% csrf_token %}
<a href="{% url "card_printers" %}" class="modal-close waves-effect waves-green btn">
<i class="material-icons left iconify" data-icon="mdi:arrow-left"></i>
{% trans "Go back" %}
</a>
<button type="submit" name="delete" class="modal-close waves-effect waves-light red btn">
<i class="material-icons left iconify" data-icon="mdi:delete-outline"></i>
{% trans "Delete" %}
</button>
</form>
{% endblock %}
\ No newline at end of file
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load material_form i18n any_js %}
{% block browser_title %}{% blocktrans %}Card Printer{% endblocktrans %}{% endblock %}
{% block page_title %}{{ object.name }}{% endblock %}
{% block content %}
<a class="btn waves-effect waves-light secondary margin-bottom" href="{% url "card_printers" %}">
<i class="material-icons left iconify" data-icon="mdi:arrow-left"></i>
{% trans "Back to all printers" %}
</a>
<a class="btn waves-effect waves-light margin-bottom orange modal-trigger"
href="{% url "edit_card_printer" object.pk %}">
<i class="material-icons left iconify" data-icon="mdi:pencil-outline"></i>
{% trans "Edit" %}
</a>
<a class="btn waves-effect waves-light margin-bottom red modal-trigger" href="{% url "delete_card_printer" object.pk %}">
<i class="material-icons left iconify" data-icon="mdi:delete-outline"></i>
{% trans "Delete" %}
</a>
{% include "kort/printer/detail_content.html" with printer=object %}
{% endblock %}
{% load i18n %}
<div class="row no-margin">
<div class="col s12 m12 l6 no-padding">
<div class="card">
<div class="card-content">
<div class="card-title">{% trans "Printer details" %}</div>
<table>
<tr>
<th>
<i class="material-icons left iconify" data-icon="mdi:printer-outline"></i>
{% trans "Name" %}</th>
<td>{{ printer.name }}</td>
</tr>
<tr>
<th>
<i class="material-icons left iconify" data-icon="mdi:map-marker-outline"></i>
{% trans "Location" %}
</th>
<td>{{ printer.location|default:"–" }}</td>
</tr>
<tr>
<th>
<i class="material-icons left iconify" data-icon="mdi:card-text-outline"></i>
{% trans "Description" %}
</th>
<td>{{ printer.description|default:"–" }}</td>
</tr>
<tr>
<th>
<i class="material-icons left iconify" data-icon="mdi:clock-check-outline"></i>
{% trans "Last seen at" %}
</th>
<td>{{ printer.last_seen_at|default:_("never seen yet") }}</td>
</tr>
<tr>
<th {% if printer.status_text %}rowspan="2"{% endif %}>
<i class="material-icons left iconify" data-icon="mdi:checkbox-blank-badge-outline"></i>
{% trans "Status" %}
</th>
<td>
{% include "kort/printer/status.html" %}
</td>
</tr>
{% if printer.status_text %}
<tr>
<td>
{{ printer.status_text }}
</td>
</tr>
{% endif %}
</table>
</div>
</div>
</div>
<div class="col s12 m12 l6">
<!-- Jobs here -->
{% if printer.status == "not_registered" %}
<div class="card">
<div class="card-content">
<div class="card-title">{% trans "Setup printer client" %}</div>
<p>
{% blocktrans %}
To enable printing, you have to register the print client on the device
which the printer is connected to.
{% endblocktrans %}
</p>
<h6>{% trans "1. Download print client" %}</h6>
<h6>{% trans "2. Download configuration file" %}</h6>
<a class="btn waves-effect waves-light" href="{% url "card_printer_config" printer.pk %}">
<i class="material-icons left iconify" data-icon="mdi:download-outline"></i>
{% trans "Download configuration" %}
</a>
<h6>{% trans "3. Setup client" %}</h6>
<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 "Created at" %}
</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.created }}
</td>
<td>
{{ job.status }} {% if job.status_text %}({{ job.status_text }}){% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endif %}
</div>
</div>
\ No newline at end of file
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load material_form i18n any_js %}
{% block extra_head %}
{{ form.media.css }}
{% include_css "select2-materialize" %}
{% endblock %}
{% block browser_title %}{% blocktrans %}Edit card printer{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans %}Edit card printer{% endblocktrans %}{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% form form=form %}{% endform %}
{% include "core/partials/save_button.html" %}
</form>
{% include_js "select2-materialize" %}
{{ form.media.js }}
{% endblock %}
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