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

Add basic printer management including API views for submitting printer status

parent d30d9733
No related branches found
No related tags found
1 merge request!3Setup printer stuff
Pipeline #59935 passed
Showing
with 519 additions and 26 deletions
from django.contrib import admin
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 aleksis.apps.kort.models import CardPrinter
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 CardPrinterSerializer(serializers.ModelSerializer):
class Meta:
model = CardPrinter
fields = (
"id",
"name",
"description",
"location",
"status",
"status_label",
"status_color",
"status_icon",
"status_text",
"last_seen_at",
)
class CardPrinterStatusSerializer(serializers.ModelSerializer):
class Meta:
model = CardPrinter
fields = ("status", "status_text")
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
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")}
......@@ -2,7 +2,7 @@ from django import forms
from django_select2.forms import ModelSelect2Widget
from aleksis.apps.kort.models import Card
from aleksis.apps.kort.models import Card, CardPrinter
class CardForm(forms.ModelForm):
......@@ -19,3 +19,9 @@ class CardForm(forms.ModelForm):
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
}
class CardPrinterForm(forms.ModelForm):
class Meta:
model = CardPrinter
fields = ["name", "location", "description"]
......@@ -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",
)
],
},
],
}
]
......
from typing import Union
from typing import Any, Union
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.db import models
......@@ -19,6 +20,31 @@ 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")
......@@ -47,6 +73,7 @@ class CardPrinter(ExtensibleModel):
verbose_name=_("OAuth2 application"),
blank=True,
null=True,
related_name="card_printers",
)
def save(self, *args, **kwargs):
......@@ -55,6 +82,7 @@ class CardPrinter(ExtensibleModel):
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 +92,35 @@ 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"
class Meta:
verbose_name = _("Card printer")
verbose_name_plural = _("Card printers")
......
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,
)
class CardTable(Table):
......@@ -27,3 +35,28 @@ class CardTable(Table):
def render_actions(self, value, record):
return render_to_string("kort/card/actions.html", dict(pk=value, card=record))
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"))
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))
{% 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>
{% 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>
{% 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 %}
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load i18n rules material_form any_js %}
{% load render_table from django_tables2 %}
{% block browser_title %}{% blocktrans %}Card printers{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans %}Card printers{% endblocktrans %}{% endblock %}
{% block content %}
{% has_perm 'core.create_cardprinter_rule' user person as can_create_person %}
{% if can_create_person %}
<a class="btn green waves-effect waves-light" href="{% url 'create_card_printer' %}">
<i class="material-icons left iconify" data-icon="mdi:plus"></i>
{% trans "Register new card printer" %}
</a>
{% endif %}
{% render_table table %}
{% endblock %}
{% load i18n %}
<table>
<tr>
<th>{% trans "Name" %}</th>
<td>{{ printer.name }}</td>
</tr>
<tr>
<th>{% trans "Location" %}</th>
<td>{{ printer.location|default:"–" }}</td>
</tr>
<tr>
<th>{% trans "Status" %}</th>
<td>{% include "kort/printer/status.html" %}</td>
</tr>
</table>
\ No newline at end of file
{% load i18n %}
<span class="chip white-text {{ printer.status_color }}">
<i class="material-icons left iconify" data-icon="{{ printer.status_icon }}"></i>
{{ printer.status_label }}
</span>
\ No newline at end of file
from django.urls import path
from . import views
from . import api, views
urlpatterns = [
path("test", views.TestPDFView.as_view(), name="test_pdf"),
......@@ -14,4 +14,24 @@ urlpatterns = [
),
path("cards/<int:pk>/deactivate/", views.CardDeactivateView.as_view(), name="deactivate_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"),
path("printers/<int:pk>/", views.CardPrinterDetailView.as_view(), name="card_printer"),
path("printers/<int:pk>/edit/", views.CardPrinterEditView.as_view(), name="edit_card_printer"),
path(
"printers/<int:pk>/delete/",
views.CardPrinterDeleteView.as_view(),
name="delete_card_printer",
),
path(
"printers/<int:pk>/config/",
views.CardPrinterConfigView.as_view(),
name="card_printer_config",
),
path("api/v1/printers/", api.CardPrinterDetails.as_view(), name="api_card_printer"),
path(
"api/v1/printers/<int:pk>/status/",
api.CardPrinterUpdateStatus.as_view(),
name="api_card_printer_status",
),
]
import json
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
......@@ -10,10 +12,10 @@ from django_tables2 import SingleTableView
from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin
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.apps.kort.forms import CardForm, CardPrinterForm
from aleksis.apps.kort.models import Card, CardPrinter
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
from aleksis.core.views import RenderPDFView
......@@ -110,3 +112,67 @@ class CardGeneratePDFView(PermissionRequiredMixin, RevisionMixin, SingleObjectMi
button_url=redirect_url,
button_icon="credit_card",
)
class CardPrinterListView(PermissionRequiredMixin, RevisionMixin, SingleTableView):
"""List view for all card printers."""
permission_required = "core.view_cardprinters_rule"
template_name = "kort/printer/list.html"
model = CardPrinter
table_class = CardPrinterTable
class CardPrinterCreateView(PermissionRequiredMixin, RevisionMixin, AdvancedCreateView):
"""View used to create a card printer."""
permission_required = "core.create_cardprinter_rule"
template_name = "kort/printer/create.html"
form_class = CardPrinterForm
model = CardPrinter
success_message = _("The card printer has been created successfully.")
def get_success_url(self):
return reverse("card_printer", args=[self.object.pk])
class CardPrinterEditView(PermissionRequiredMixin, RevisionMixin, AdvancedEditView):
"""View used to edit a card printer."""
permission_required = "core.edit_cardprinter_rule"
template_name = "kort/printer/edit.html"
form_class = CardPrinterForm
model = CardPrinter
success_message = _("The card printer has been changed successfully.")
def get_success_url(self):
return reverse("card_printer", args=[self.object.pk])
class CardPrinterDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
"""View used to delete a card printer."""
permission_required = "core.delete_cardprinter_rule"
success_url = reverse_lazy("card_printers")
template_name = "kort/printer/delete.html"
model = CardPrinter
success_message = _("The card printer has been deleted successfully.")
class CardPrinterDetailView(PermissionRequiredMixin, RevisionMixin, DetailView):
permission_required = "core.view_cardprinter_rule"
model = CardPrinter
template_name = "kort/printer/detail.html"
class CardPrinterConfigView(PermissionRequiredMixin, RevisionMixin, SingleObjectMixin, View):
permission_required = "core.view_cardprinter_rule"
model = CardPrinter
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
self.object = self.get_object()
response = HttpResponse(
json.dumps(self.object.generate_config()), content_type="application/json"
)
response["Content-Disposition"] = f'attachment; filename="{self.object.config_filename}"'
return response
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