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

Add basic views for managing and issueing cards

parent 295bfa8d
No related branches found
No related tags found
No related merge requests found
Showing
with 400 additions and 46 deletions
from django import forms
from django_select2.forms import ModelSelect2Widget
from aleksis.apps.kort.models import Card
class CardForm(forms.ModelForm):
class Meta:
model = Card
fields = ["person", "valid_until"]
widgets = {
"person": ModelSelect2Widget(
search_fields=[
"first_name__icontains",
"last_name__icontains",
"short_name__icontains",
],
attrs={"data-minimum-input-length": 0, "class": "browser-default"},
),
}
......@@ -3,14 +3,27 @@ from django.utils.translation import ugettext_lazy as _
MENUS = {
"NAV_MENU_CORE": [
{
"name": _("Kort"),
"url": "test_pdf",
"name": _("Student ID Cards"),
"url": "#",
"root": True,
"icon": "credit_card",
"validators": [
"menu_generator.validators.is_authenticated",
"aleksis.core.util.core_helpers.has_person",
],
"submenu": [],
"submenu": [
{
"name": _("All Cards"),
"url": "cards",
"icon": "list",
"validators": [
(
"aleksis.core.util.predicates.permission_validator",
"core.view_cards_rule",
)
],
},
],
}
]
}
# Generated by Django 3.2.12 on 2022-03-08 19:23
import django.contrib.sites.managers
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('sites', '0002_alter_domain_unique'),
('kort', '0002_card_printer'),
]
operations = [
migrations.RemoveField(
model_name='card',
name='print_finished_at',
),
migrations.RemoveField(
model_name='card',
name='print_started_at',
),
migrations.RemoveField(
model_name='card',
name='printed_with',
),
migrations.CreateModel(
name='CardLayout',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('extended_data', models.JSONField(default=dict, editable=False)),
('name', models.CharField(max_length=255, verbose_name='Name')),
('template', models.TextField(verbose_name='Template')),
('css', models.TextField(blank=True, verbose_name='Custom CSS')),
('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
],
options={
'abstract': False,
},
managers=[
('objects', django.contrib.sites.managers.CurrentSiteManager()),
],
),
]
# Generated by Django 3.2.12 on 2022-03-08 19:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kort', '0003_auto_20220308_2023'),
]
operations = [
migrations.AlterField(
model_name='card',
name='chip_number',
field=models.CharField(blank=True, max_length=255, verbose_name='Chip Number'),
),
]
from django.core.exceptions import ValidationError
from django.db import models
from django.template import Context, Template
from django.utils import timezone
from django.utils.translation import gettext as _
from aleksis.core.mixins import ExtensibleModel
from aleksis.core.models import Person
from django.template import Template, Context
from django.core.exceptions import ValidationError
class CardPrinterStatus(models.TextChoices):
ONLINE = "online", _("Online")
......@@ -42,20 +44,15 @@ class CardLayout(ExtensibleModel):
template = models.TextField(verbose_name=_("Template"))
css = models.TextField(verbose_name=_("Custom CSS"), blank=True)
def get_template(self) -> Template:
return Template(self.template)
def render(self, card: "Card"):
t = self.get_template()
context = card.get_context()
return t.render(Context(context))
def validate_template(self):
try:
t = Template(self.template)
......@@ -64,36 +61,23 @@ class CardLayout(ExtensibleModel):
raise ValidationError(_("Template is invalid: {}").format(e))
class Card(ExtensibleModel):
person = models.ForeignKey(
Person, models.CASCADE, verbose_name=_("Person"), related_name="cards"
)
chip_number = models.IntegerField(verbose_name=_("Chip Number"))
chip_number = models.CharField(verbose_name=_("Chip Number"), blank=True, max_length=255)
valid_until = models.DateField(verbose_name=_("Valid until"))
deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False)
# Print status
printed_with = models.ForeignKey(
CardPrinter,
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_("Printed with"),
)
print_started_at = models.DateTimeField(verbose_name=_("Printed at"), blank=True, null=True)
print_finished_at = models.DateTimeField(verbose_name=_("Printed at"), blank=True, null=True)
@property
def print_status(self) -> PrintStatus:
if self.print_finished_at:
return PrintStatus.FINISHED
elif self.print_started_at:
return PrintStatus.IN_PROGRESS
else:
return PrintStatus.REGISTERED
def is_valid(self):
return (
self.valid_until <= timezone.now().date() and not self.deactivated and self.chip_number
)
def deactivate(self):
self.deactivated = True
self.save()
def get_context(self):
return {
......
# TBD
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django_tables2 import A, BooleanColumn, Column, RelatedLinkColumn, Table
class CardTable(Table):
"""Table to list cards."""
class Meta:
attrs = {"class": "highlight"}
person = RelatedLinkColumn()
chip_number = Column(verbose_name=_("Chip number"))
current_status = Column(verbose_name=_("Current status"), accessor=A("pk"))
valid_until = Column(verbose_name=_("Valid until"))
deactivated = BooleanColumn(verbose_name=_("Deactivated"))
actions = Column(verbose_name=_("Actions"), accessor=A("pk"))
def render_current_status(self, value, record):
return render_to_string(
"components/materialize-chips.html",
dict(
content=_("Valid") if record.is_valid else _("Not valid"),
classes="white-text " + ("green" if record.is_valid else "red"),
),
)
def render_actions(self, value, record):
return render_to_string("kort/card/actions.html", dict(pk=value, card=record))
{% load i18n %}
<!-- Modal Structure -->
<div id="deactivate-modal-{{ card.pk }}" class="modal">
<div class="modal-content">
<h4>{% trans "Do you really want to deactivate the following card?" %}</h4>
{% include "kort/card/short.html" %}
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">
<i class="material-icons left">close</i>
{% trans "Close" %}
</a>
<a href="{% url "deactivate_card" card.pk %}" class="modal-close waves-effect waves-light orange btn">
<i class="material-icons left">timer_off</i>
{% trans "Deactivate" %}
</a>
</div>
</div>
<div id="delete-modal-{{ card.pk }}" class="modal">
<div class="modal-content">
<h4>{% trans "Do you really want to delete the following card?" %}</h4>
{% include "kort/card/short.html" %}
<figure class="alert warning">
<i class="material-icons left">warning</i>
{% blocktrans %}
Please pay attention that a deletion of a card is irreversible and should be only used to clean up misprints.
If you just want to make a card unusable because a student has lost his card or left the school,
please deactivate the card instead.
{% endblocktrans %}
</figure>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat">
<i class="material-icons left">close</i>
{% trans "Close" %}
</a>
<a href="{% url "delete_card" card.pk %}" class="modal-close waves-effect waves-light red btn">
<i class="material-icons left">delete</i>
{% trans "Delete" %}
</a>
</div>
</div>
<a class="btn-flat waves-effect waves-orange orange-text modal-trigger" href="#deactivate-modal-{{ card.pk }}">
<i class="material-icons left">timer_off</i>
{% trans "Deactivate" %}
</a>
<a class="btn-flat waves-effect waves-red red-text modal-trigger" href="#delete-modal-{{ card.pk }}">
<i class="material-icons left">delete</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{% endblocktrans %}{% endblock %}
{% block no_page_title %}{% endblock %}
{% block content %}
<p class="flow-text">{% trans "Do you really want to delete the following card?" %}</p>
{% include "kort/card/short.html" %}
<figure class="alert warning">
<i class="material-icons left">warning</i>
{% blocktrans %}
Please pay attention that a deletion of a card is irreversible and should be only used to clean up misprints.
If you just want to make a card unusable because a student has lost his card or left the school,
please deactivate the card instead.
{% endblocktrans %}
</figure>
<form method="post" action="">
{% csrf_token %}
<a href="{% url "cards" %}" class="modal-close waves-effect waves-green btn">
<i class="material-icons left">arrow_back</i>
{% trans "Go back" %}
</a>
<button type="submit" name="delete" class="modal-close waves-effect waves-light red btn">
<i class="material-icons left">delete</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 extra_head %}
{{ form.media.css }}
{% include_css "select2-materialize" %}
{% endblock %}
{% block browser_title %}{% blocktrans %}Edit card{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans %}Edit 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 %}Cards{% endblocktrans %}{% endblock %}
{% block page_title %}{% blocktrans %}Cards{% endblocktrans %}{% endblock %}
{% block content %}
<script>
</script>
{% has_perm 'core.create_card_rule' user person as can_create_person %}
{% if can_create_person %}
<a class="btn green waves-effect waves-light" href="{% url 'create_card' %}">
<i class="material-icons left">add</i>
{% trans "Issue new card" %}
</a>
{% endif %}
{# <h2>{% trans "Filter persons" %}</h2>#}
{# <form method="get">#}
{# {% form form=persons_filter.form %}{% endform %}#}
{# {% trans "Search" as caption %}#}
{# {% include "core/partials/save_button.html" with caption=caption icon="search" %}#}
{# <button type="reset" class="btn red waves-effect waves-light">#}
{# <i class="material-icons left">clear</i>#}
{# {% trans "Clear" %}#}
{# </button>#}
{# </form>#}
{# <h2>{% trans "Selected persons" %}</h2>#}
{% render_table table %}
{% endblock %}
{% load i18n %}
<table>
<tr>
<th>{% trans "Person" %}</th>
<td>{{ card.person }}</td>
</tr>
<tr>
<th>{% trans "Chip number" %}</th>
<td>{{ card.chip_number }}</td>
</tr>
<tr>
<th>{% trans "Valid until" %}</th>
<td>{{ card.valid_until }}</td>
</tr>
</table>
\ No newline at end of file
from io import BytesIO
from django import template
from django.utils.safestring import mark_safe
from io import BytesIO
import barcode
from django.utils.safestring import mark_safe
register = template.Library()
......@@ -11,8 +12,10 @@ register = template.Library()
def generate_barcode(uid):
rv = BytesIO()
writer = barcode.writer.SVGWriter()
code = barcode.get('code128', uid, writer=writer)
code.write(rv, options={"module_height": 5, "module_width": 0.3, "text_distance": 2, "font_size": 6})
code = barcode.get("code128", uid, writer=writer)
code.write(
rv, options={"module_height": 5, "module_width": 0.3, "text_distance": 2, "font_size": 6}
)
rv.seek(0)
# get rid of the first bit of boilerplate
......@@ -23,4 +26,3 @@ def generate_barcode(uid):
# read the svg tag into a string
svg = rv.read()
return mark_safe(svg.decode("utf-8"))
......@@ -4,4 +4,8 @@ from . import views
urlpatterns = [
path("test", views.TestPDFView.as_view(), name="test_pdf"),
path("cards/", views.CardListView.as_view(), name="cards"),
path("cards/create/", views.CardCreateView.as_view(), name="create_card"),
path("cards/<int:pk>/deactivate/", views.CardDeactivateView.as_view(), name="deactivate_card"),
path("cards/<int:pk>/delete/", views.CardDeleteView.as_view(), name="delete_card"),
]
from aleksis.core.views import RenderPDFView
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.shortcuts import redirect, render
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views import View
from django.views.generic.detail import SingleObjectMixin
@login_required
def empty(request: HttpRequest) -> HttpResponse:
context = {}
from django_tables2 import SingleTableView
from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin
return render(request, "kort/empty.html", context)
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.core.views import RenderPDFView
class TestPDFView(RenderPDFView):
......@@ -21,4 +27,48 @@ class TestPDFView(RenderPDFView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Test PDF"
return context
\ No newline at end of file
return context
class CardListView(PermissionRequiredMixin, RevisionMixin, SingleTableView):
"""List view for all cards."""
permission_required = "core.view_cards_rule"
template_name = "kort/card/list.html"
model = Card
table_class = CardTable
class CardCreateView(PermissionRequiredMixin, RevisionMixin, AdvancedCreateView):
"""View used to create a card."""
permission_required = "core.create_card_rule"
context_object_name = "application"
template_name = "kort/card/create.html"
form_class = CardForm
success_message = _("The card has been created successfully.")
success_url = reverse_lazy("cards")
class CardDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
"""View used to delete a card."""
permission_required = "core.delete_card_rule"
success_url = reverse_lazy("cards")
template_name = "kort/card/delete.html"
model = Card
success_message = _("The card has been deleted successfully.")
class CardDeactivateView(PermissionRequiredMixin, RevisionMixin, SingleObjectMixin, View):
"""View used to deactivate 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()
self.object.deactivate()
messages.success(request, _("The card has been deactivated successfully."))
return redirect(self.success_url)
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