From 6ba798c92369cff73d89b5e4168519b8a24e94da Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Tue, 31 May 2022 14:50:39 +0200 Subject: [PATCH] Use and manage card layouts --- aleksis/apps/kort/forms.py | 36 ++++++- aleksis/apps/kort/menus.py | 11 +++ .../migrations/0012_auto_20220529_1435.py | 50 ++++++++++ .../migrations/0013_auto_20220530_1939.py | 29 ++++++ aleksis/apps/kort/models.py | 62 +++++++++++- aleksis/apps/kort/settings.py | 2 + aleksis/apps/kort/tables.py | 16 ++++ aleksis/apps/kort/tasks.py | 6 +- .../templates/kort/card/detail_content.html | 7 ++ .../templates/kort/card_layout/actions.html | 26 +++++ .../templates/kort/card_layout/create.html | 34 +++++++ .../templates/kort/card_layout/delete.html | 32 +++++++ .../templates/kort/card_layout/detail.html | 27 ++++++ .../kort/card_layout/detail_content.html | 54 +++++++++++ .../kort/templates/kort/card_layout/edit.html | 34 +++++++ .../kort/card_layout/instructions.html | 28 ++++++ .../kort/templates/kort/card_layout/list.html | 22 +++++ .../templates/kort/card_layout/short.html | 7 ++ .../material/fields/django_ace_acewidget.html | 28 ++++++ aleksis/apps/kort/urls.py | 9 ++ aleksis/apps/kort/views.py | 95 ++++++++++++++++++- pyproject.toml | 1 + 22 files changed, 605 insertions(+), 11 deletions(-) create mode 100644 aleksis/apps/kort/migrations/0012_auto_20220529_1435.py create mode 100644 aleksis/apps/kort/migrations/0013_auto_20220530_1939.py create mode 100644 aleksis/apps/kort/templates/kort/card_layout/actions.html create mode 100644 aleksis/apps/kort/templates/kort/card_layout/create.html create mode 100644 aleksis/apps/kort/templates/kort/card_layout/delete.html create mode 100644 aleksis/apps/kort/templates/kort/card_layout/detail.html create mode 100644 aleksis/apps/kort/templates/kort/card_layout/detail_content.html create mode 100644 aleksis/apps/kort/templates/kort/card_layout/edit.html create mode 100644 aleksis/apps/kort/templates/kort/card_layout/instructions.html create mode 100644 aleksis/apps/kort/templates/kort/card_layout/list.html create mode 100644 aleksis/apps/kort/templates/kort/card_layout/short.html create mode 100644 aleksis/apps/kort/templates/material/fields/django_ace_acewidget.html diff --git a/aleksis/apps/kort/forms.py b/aleksis/apps/kort/forms.py index 6b947e7..39781a6 100644 --- a/aleksis/apps/kort/forms.py +++ b/aleksis/apps/kort/forms.py @@ -1,16 +1,18 @@ from django import forms from django.utils.translation import gettext as _ +from django_ace import AceWidget from django_select2.forms import ModelSelect2Widget -from material import Fieldset, Layout +from material import Fieldset, Layout, Row -from aleksis.apps.kort.models import Card, CardPrinter +from aleksis.apps.kort.models import Card, CardLayout, CardLayoutMediaFile, CardPrinter class CardForm(forms.ModelForm): class Meta: model = Card - fields = ["person", "valid_until"] + fields = ["person", "valid_until", "layout"] + widgets = { "person": ModelSelect2Widget( search_fields=[ @@ -22,6 +24,10 @@ class CardForm(forms.ModelForm): ), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["layout"].required = True + class CardPrinterForm(forms.ModelForm): layout = Layout( @@ -53,3 +59,27 @@ class PrinterSelectForm(forms.Form): self.fields["printer"].queryset = printers if printers.count() == 1: self.fields["printer"].initial = printers.first() + + +class CardLayoutMediaFileForm(forms.ModelForm): + layout = Layout(Row("media_file", "DELETE")) + + class Meta: + model = CardLayoutMediaFile + fields = ["media_file"] + + +class CardLayoutForm(forms.ModelForm): + layout = Layout(Row("name"), Row("width", "height"), Row("template"), "css") + + template = forms.CharField(widget=AceWidget(mode="django")) + css = forms.CharField(widget=AceWidget(mode="css")) + + class Meta: + model = CardLayout + fields = ["name", "template", "css", "width", "height"] + + +CardLayoutMediaFileFormSet = forms.inlineformset_factory( + CardLayout, CardLayoutMediaFile, form=CardLayoutMediaFileForm +) diff --git a/aleksis/apps/kort/menus.py b/aleksis/apps/kort/menus.py index 2116a83..2f6f644 100644 --- a/aleksis/apps/kort/menus.py +++ b/aleksis/apps/kort/menus.py @@ -34,6 +34,17 @@ MENUS = { ) ], }, + { + "name": _("Card Layouts"), + "url": "card_layouts", + "svg_icon": "mdi:card-account-details-star-outline", + "validators": [ + ( + "aleksis.core.util.predicates.permission_validator", + "core.view_cardlayouts_rule", + ) + ], + }, ], } ] diff --git a/aleksis/apps/kort/migrations/0012_auto_20220529_1435.py b/aleksis/apps/kort/migrations/0012_auto_20220529_1435.py new file mode 100644 index 0000000..da30961 --- /dev/null +++ b/aleksis/apps/kort/migrations/0012_auto_20220529_1435.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.13 on 2022-05-29 12:35 + +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', '0011_cardprinter_card_detector'), + ] + + operations = [ + migrations.AddField( + model_name='cardlayout', + name='height', + field=models.PositiveIntegerField(default=1, verbose_name='Height'), + preserve_default=False, + ), + migrations.AddField( + model_name='cardlayout', + name='width', + field=models.PositiveIntegerField(default=1, verbose_name='Width'), + preserve_default=False, + ), + migrations.AlterField( + model_name='cardprintjob', + name='card', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='kort.card', verbose_name='Card'), + ), + migrations.CreateModel( + name='CardLayoutMediaFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('media_file', models.FileField(upload_to='card_layouts/media/', verbose_name='Media file')), + ('card_layout', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='media_files', to='kort.cardlayout', verbose_name='Card layout')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ], + options={ + 'verbose_name': 'Media file for a card layout', + 'verbose_name_plural': 'Media files for card layouts', + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + ] diff --git a/aleksis/apps/kort/migrations/0013_auto_20220530_1939.py b/aleksis/apps/kort/migrations/0013_auto_20220530_1939.py new file mode 100644 index 0000000..d7fbd10 --- /dev/null +++ b/aleksis/apps/kort/migrations/0013_auto_20220530_1939.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.13 on 2022-05-30 17:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('kort', '0012_auto_20220529_1435'), + ] + + operations = [ + migrations.AddField( + model_name='card', + name='layout', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='kort.cardlayout', verbose_name='Card Layout'), + ), + migrations.AlterField( + model_name='cardlayout', + name='height', + field=models.PositiveIntegerField(help_text='in mm', verbose_name='Height'), + ), + migrations.AlterField( + model_name='cardlayout', + name='width', + field=models.PositiveIntegerField(help_text='in mm', verbose_name='Width'), + ), + ] diff --git a/aleksis/apps/kort/models.py b/aleksis/apps/kort/models.py index f419e58..83ea2c9 100644 --- a/aleksis/apps/kort/models.py +++ b/aleksis/apps/kort/models.py @@ -16,6 +16,7 @@ from model_utils.models import TimeStampedModel from aleksis.core.mixins import ExtensibleModel from aleksis.core.models import OAuthApplication, Person +from aleksis.core.util.pdf import process_context_for_pdf class CardPrinterStatus(models.TextChoices): @@ -172,19 +173,63 @@ class CardPrinter(ExtensibleModel): verbose_name_plural = _("Card printers") +class CardLayoutMediaFile(ExtensibleModel): + media_file = models.FileField(upload_to="card_layouts/media/", verbose_name=_("Media file")) + card_layout = models.ForeignKey( + "CardLayout", + on_delete=models.CASCADE, + related_name="media_files", + verbose_name=_("Card layout"), + ) + + def __str__(self): + return self.media_file.name + + class Meta: + verbose_name = _("Media file for a card layout") + verbose_name_plural = _("Media files for card layouts") + + class CardLayout(ExtensibleModel): + BASE_TEMPLATE = """ + {{% extends "core/base_simple_print.html" %}} + {{% load i18n static barcode %}} + + {{% block size %}} + {{% with width={width} height={height} %}} + {{{{ block.super }}}} + {{% endwith %}} + {{% endblock %}} + + {{% block extra_head %}} + <style> + {css} + </style> + {{% endblock %}} + + {{% block content %}} + {template} + {{% endblock %}} + """ name = models.CharField(max_length=255, verbose_name=_("Name")) template = models.TextField(verbose_name=_("Template")) css = models.TextField(verbose_name=_("Custom CSS"), blank=True) + width = models.PositiveIntegerField(verbose_name=_("Width"), help_text=_("in mm")) + height = models.PositiveIntegerField(verbose_name=_("Height"), help_text=_("in mm")) def get_template(self) -> Template: - return Template(self.template) + template = self.BASE_TEMPLATE.format( + width=self.width, height=self.height, css=self.css, template=self.template + ) + print(template) + return Template(template) def render(self, card: "Card"): t = self.get_template() context = card.get_context() + processed_context = process_context_for_pdf(context) - return t.render(Context(context)) + return t.render(Context(processed_context)) def validate_template(self): try: @@ -193,6 +238,13 @@ class CardLayout(ExtensibleModel): except Exception as e: raise ValidationError(_("Template is invalid: {}").format(e)) + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Card Layout") + verbose_name_plural = _("Card Layouts") + class Card(ExtensibleModel): person = models.ForeignKey( @@ -202,6 +254,9 @@ class Card(ExtensibleModel): valid_until = models.DateField(verbose_name=_("Valid until")) deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False) + layout = models.ForeignKey( + CardLayout, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_("Card Layout") + ) pdf_file = models.FileField( verbose_name=_("PDF file"), blank=True, @@ -224,6 +279,7 @@ class Card(ExtensibleModel): "person": self.person, "chip_number": self.chip_number, "valid_until": self.valid_until, + "media_files": self.layout.media_files.all(), } def generate_pdf(self) -> Union[bool, AsyncResult]: @@ -234,6 +290,8 @@ class Card(ExtensibleModel): return generate_card_pdf.delay(self.pk) def print_card(self, printer: CardPrinter): + if not self.layout: + raise ValueError(_("There is no layout provided for the card.")) job = CardPrintJob(card=self, printer=printer) job.save() if not self.chip_number and printer.generate_number_on_server: diff --git a/aleksis/apps/kort/settings.py b/aleksis/apps/kort/settings.py index 1cbd74e..279ef09 100644 --- a/aleksis/apps/kort/settings.py +++ b/aleksis/apps/kort/settings.py @@ -5,3 +5,5 @@ YARN_INSTALLED_APPS = ["pdfobject"] ANY_JS = { "pdfobject": {"js_url": JS_URL + "/pdfobject/pdfobject.min.js"}, } + +INSTALLED_APPS = ["django_ace"] diff --git a/aleksis/apps/kort/tables.py b/aleksis/apps/kort/tables.py index dedcab6..7a1f0e7 100644 --- a/aleksis/apps/kort/tables.py +++ b/aleksis/apps/kort/tables.py @@ -66,3 +66,19 @@ class CardPrinterTable(Table): def render_actions(self, value, record): return render_to_string("kort/printer/actions.html", dict(pk=value, printer=record)) + + +class CardLayoutTable(Table): + """Table to list card layouts.""" + + class Meta: + attrs = {"class": "highlight"} + + name = LinkColumn("card_layout", verbose_name=_("Layout name"), args=[A("pk")]) + width = Column() + height = Column() + + actions = Column(verbose_name=_("Actions"), accessor=A("pk")) + + def render_actions(self, value, record): + return render_to_string("kort/card_layout/actions.html", dict(pk=value, card_layout=record)) diff --git a/aleksis/apps/kort/tasks.py b/aleksis/apps/kort/tasks.py index 82beaa0..dc7efed 100644 --- a/aleksis/apps/kort/tasks.py +++ b/aleksis/apps/kort/tasks.py @@ -3,15 +3,15 @@ from celery.states import SUCCESS from aleksis.apps.kort.models import Card from aleksis.core.celery import app -from aleksis.core.util.pdf import generate_pdf_from_template +from aleksis.core.util.pdf import generate_pdf_from_html @app.task def generate_card_pdf(card_pk: int): card = Card.objects.get(pk=card_pk) - context = card.get_context() - file_object, result = generate_pdf_from_template("kort/pdf.html", context) + html = card.layout.render(card) + file_object, result = generate_pdf_from_html(html) with allow_join_result(): result.wait() diff --git a/aleksis/apps/kort/templates/kort/card/detail_content.html b/aleksis/apps/kort/templates/kort/card/detail_content.html index 23b6227..e87525f 100644 --- a/aleksis/apps/kort/templates/kort/card/detail_content.html +++ b/aleksis/apps/kort/templates/kort/card/detail_content.html @@ -25,6 +25,13 @@ </th> <td>{{ card.valid_until }}</td> </tr> + <tr> + <th> + <i class="material-icons left iconify" data-icon="mdi:card-account-details-star-outline"></i> + {% trans "Card Layout" %} + </th> + <td>{{ card.layout|default:"–" }}</td> + </tr> <tr> <th> <i class="material-icons left iconify" data-icon="mdi:checkbox-blank-badge-outline"></i> diff --git a/aleksis/apps/kort/templates/kort/card_layout/actions.html b/aleksis/apps/kort/templates/kort/card_layout/actions.html new file mode 100644 index 0000000..f8220b2 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/actions.html @@ -0,0 +1,26 @@ +{% load i18n %} + +<div id="detail-modal-{{ card_layout.pk }}" class="modal"> + <div class="modal-content"> + <h4>{{ card_layout.name }}</h4> + {% include "kort/card_layout/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-{{ card_layout.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_layout" card_layout.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_layout" card_layout.pk %}"> + <i class="material-icons left iconify" data-icon="mdi:delete-outline"></i> + {% trans "Delete" %} +</a> \ No newline at end of file diff --git a/aleksis/apps/kort/templates/kort/card_layout/create.html b/aleksis/apps/kort/templates/kort/card_layout/create.html new file mode 100644 index 0000000..8cd9a44 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/create.html @@ -0,0 +1,34 @@ +{# -*- 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 layout{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Create card layout{% endblocktrans %}{% endblock %} + + +{% block content %} + {% include "kort/card_layout/instructions.html" %} + + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {% form form=form %}{% endform %} + + <h5>{% trans "Media Files" %}</h5> + {{ formset.management_form }} + + {% for inline_form in formset.forms %} + {% form form=inline_form %}{% endform %} + {% endfor %} + + {% include "core/partials/save_button.html" %} + </form> + {% include_js "select2-materialize" %} + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/kort/templates/kort/card_layout/delete.html b/aleksis/apps/kort/templates/kort/card_layout/delete.html new file mode 100644 index 0000000..7e668c3 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/delete.html @@ -0,0 +1,32 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} + +{% load i18n rules material_form %} +{% load render_table from django_tables2 %} + +{% block browser_title %}{% blocktrans %}Delete Card Layout{% endblocktrans %}{% endblock %} +{% block no_page_title %}{% endblock %} + +{% block content %} + <p class="flow-text">{% trans "Do you really want to delete the following card layout?" %}</p> + {% include "kort/card_layout/short.html" with card_layout=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 prevent the successful finishing of all active print jobs + which use these template. + {% endblocktrans %} + </figure> + <form method="post" action=""> + {% csrf_token %} + <a href="{% url "card_layouts" %}" 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 diff --git a/aleksis/apps/kort/templates/kort/card_layout/detail.html b/aleksis/apps/kort/templates/kort/card_layout/detail.html new file mode 100644 index 0000000..5a7fb24 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/detail.html @@ -0,0 +1,27 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} + +{% load material_form i18n any_js %} + + +{% block browser_title %}{% blocktrans %}Card Layout{% endblocktrans %}{% endblock %} +{% block page_title %}{{ object.name }}{% endblock %} + + +{% block content %} + <a class="btn waves-effect waves-light secondary margin-bottom" href="{% url "card_layouts" %}"> + <i class="material-icons left iconify" data-icon="mdi:arrow-left"></i> + {% trans "Back to all layouts" %} + </a> + <a class="btn waves-effect waves-light margin-bottom orange modal-trigger" + href="{% url "edit_card_layout" 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_layout" object.pk %}"> + <i class="material-icons left iconify" data-icon="mdi:delete-outline"></i> + {% trans "Delete" %} + </a> + {% include "kort/card_layout/detail_content.html" with card_layout=object %} +{% endblock %} diff --git a/aleksis/apps/kort/templates/kort/card_layout/detail_content.html b/aleksis/apps/kort/templates/kort/card_layout/detail_content.html new file mode 100644 index 0000000..8d20468 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/detail_content.html @@ -0,0 +1,54 @@ +{% load i18n %} +<div class="row no-margin"> + <div class="col s12 m12 l12 xl6 no-padding"> + <div class="card"> + <div class="card-content"> + <div class="card-title">{% trans "Layout details" %}</div> + <table> + <tr> + <th> + <i class="material-icons left iconify" data-icon="mdi:label-outline"></i> + {% trans "Name" %}</th> + <td>{{ card_layout.name }}</td> + </tr> + <tr> + <th> + <i class="material-icons left iconify" data-icon="mdi:image-size-select-large"></i> + {% trans "Size (w × h)" %} + </th> + <td>{{ card_layout.width }} mm × {{ card_layout.height }} mm</td> + </tr> + <tr> + <th> + <i class="material-icons left iconify" data-icon="mdi:folder-multiple-image"></i> + {% trans "Media files" %} + </th> + <td> + <ul class="collection"> + {% for file in card_layout.media_files.all %} + <a class="collection-item" href="{{ file.media_file.url }}">{{ file.media_file.name }}</a> + {% empty %} + <li class="collection-item">{% trans "No media files uploaded." %}</li> + {% endfor %} + </ul> + </td> + </tr> + </table> + </div> + </div> + </div> + <div class="col s12 m12 l12 xl6"> + <div class="card"> + <div class="card-content"> + <div class="card-title">{% trans "Template" %}</div> + <pre>{{ card_layout.template|linebreaksbr }}</pre> + </div> + </div> + <div class="card"> + <div class="card-content"> + <div class="card-title">{% trans "Custom CSS" %}</div> + <pre>{{ card_layout.css|linebreaksbr }}</pre> + </div> + </div> + </div> +</div> \ No newline at end of file diff --git a/aleksis/apps/kort/templates/kort/card_layout/edit.html b/aleksis/apps/kort/templates/kort/card_layout/edit.html new file mode 100644 index 0000000..f58916c --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/edit.html @@ -0,0 +1,34 @@ +{# -*- 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 layout{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Edit card layout{% endblocktrans %}{% endblock %} + + +{% block content %} + {% include "kort/card_layout/instructions.html" %} + + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {% form form=form %}{% endform %} + + <h5>{% trans "Media Files" %}</h5> + {{ formset.management_form }} + + {% for inline_form in formset.forms %} + {% form form=inline_form %}{% endform %} + {% endfor %} + + {% include "core/partials/save_button.html" %} + </form> + {% include_js "select2-materialize" %} + {{ form.media.js }} +{% endblock %} diff --git a/aleksis/apps/kort/templates/kort/card_layout/instructions.html b/aleksis/apps/kort/templates/kort/card_layout/instructions.html new file mode 100644 index 0000000..93a6497 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/instructions.html @@ -0,0 +1,28 @@ +{% load i18n %} +<div class="card"> + <div class="card-content"> + <div class="card-title">{% trans "Instructions on templating the card" %}</div> + {% blocktrans %} + You will be able to use the following data in your individual template. The template has to been written in the + Django template language. + {% endblocktrans %} + <div class="collection"> + <div class="collection-item"> + <code>{% verbatim %}{{ chip_number }}{% endverbatim %}</code> - {% trans "The chip number as string" %} + </div> + <div class="collection-item"> + <code>{% verbatim %}{{ person.x }}{% endverbatim %}</code> + - {% trans "This will give you access to any attributes of the person object like name, personal data or contact data." %} + </div> + <div class="collection-item"> + <code>{% verbatim %}{{ valid_until }}{% endverbatim %}</code> + - {% trans "The date until when the card is valid" %} + </div> + <div class="collection-item"> + <code>{% verbatim %} + {{ media_files.0.media_file.url }}{% endverbatim %}</code> - + {% trans "This will give you access to the uploaded media files. Replace 0 by the number of your media file (starting with 0)." %} + </div> + </div> + </div> +</div> diff --git a/aleksis/apps/kort/templates/kort/card_layout/list.html b/aleksis/apps/kort/templates/kort/card_layout/list.html new file mode 100644 index 0000000..c12a4bd --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/list.html @@ -0,0 +1,22 @@ +{# -*- engine:django -*- #} + +{% extends "core/base.html" %} + +{% load i18n rules material_form any_js %} +{% load render_table from django_tables2 %} + +{% block browser_title %}{% blocktrans %}Card layouts{% endblocktrans %}{% endblock %} +{% block page_title %}{% blocktrans %}Card layouts{% endblocktrans %}{% endblock %} + +{% block content %} + {% has_perm 'core.create_cardlayout_rule' user person as can_create %} + + {% if can_create %} + <a class="btn green waves-effect waves-light" href="{% url 'create_card_layout' %}"> + <i class="material-icons left iconify" data-icon="mdi:plus"></i> + {% trans "Create new card layout" %} + </a> + {% endif %} + + {% render_table table %} +{% endblock %} diff --git a/aleksis/apps/kort/templates/kort/card_layout/short.html b/aleksis/apps/kort/templates/kort/card_layout/short.html new file mode 100644 index 0000000..d2bc3e4 --- /dev/null +++ b/aleksis/apps/kort/templates/kort/card_layout/short.html @@ -0,0 +1,7 @@ +{% load i18n %} +<table> + <tr> + <th>{% trans "Name" %}</th> + <td>{{ card_layout.name }}</td> + </tr> +</table> \ No newline at end of file diff --git a/aleksis/apps/kort/templates/material/fields/django_ace_acewidget.html b/aleksis/apps/kort/templates/material/fields/django_ace_acewidget.html new file mode 100644 index 0000000..73f5717 --- /dev/null +++ b/aleksis/apps/kort/templates/material/fields/django_ace_acewidget.html @@ -0,0 +1,28 @@ +{% load material_form material_form_internal %} +{% part bound_field.field %}<{{ field.widget.component|default:'dmc-textarea' }}> + <div class="row"> + <div{% attrs bound_field 'group' %} + id="id_{{ bound_field.html_name }}_container" + class="input-field col s12{% if field.required %} required{% endif %}{% if bound_field.errors %} has-error{% endif %}" + {% endattrs %}> + {% part field label %} + <label{% attrs bound_field 'label' %} + for="{{ bound_field.id_for_label }}" + {% if bound_field.value %}class="active"{% endif %} + {% endattrs %}>{{ bound_field.label }}</label> + {% endpart %} + <div class="margin-top-35"> + {% part field prefix %}{% endpart %}{% part field control %} + {{ bound_field }} + {% endpart %} + </div> + {% part field help_text %}{% if field.help_text %} + <div class="help-block">{{ bound_field.help_text|safe }}</div> + {% endif %} + {% endpart %}{% part field errors %} + {% if bound_field.errors %} + {% include 'material/field_errors.html' %} + {% endif %} + {% endpart %}{{ hidden_initial }} + </div> + </div></{{ field.widget.component|default:'dmc-textarea' }}>{% endpart %} diff --git a/aleksis/apps/kort/urls.py b/aleksis/apps/kort/urls.py index 1cc3abc..1d31213 100644 --- a/aleksis/apps/kort/urls.py +++ b/aleksis/apps/kort/urls.py @@ -29,6 +29,15 @@ urlpatterns = [ views.CardPrinterConfigView.as_view(), name="card_printer_config", ), + path("layouts/", views.CardLayoutListView.as_view(), name="card_layouts"), + path("layouts/create/", views.CardLayoutCreateView.as_view(), name="create_card_layout"), + path("layouts/<int:pk>/", views.CardLayoutDetailView.as_view(), name="card_layout"), + path("layouts/<int:pk>/edit/", views.CardLayoutEditView.as_view(), name="edit_card_layout"), + path( + "layouts/<int:pk>/delete/", + views.CardLayoutDeleteView.as_view(), + name="delete_card_layout", + ), path("api/v1/printers/", api.CardPrinterDetails.as_view(), name="api_card_printer"), path( "api/v1/printers/<int:pk>/status/", diff --git a/aleksis/apps/kort/views.py b/aleksis/apps/kort/views.py index 833ea29..787408a 100644 --- a/aleksis/apps/kort/views.py +++ b/aleksis/apps/kort/views.py @@ -13,9 +13,15 @@ from django_tables2 import SingleTableView from reversion.views import RevisionMixin from rules.contrib.views import PermissionRequiredMixin -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.apps.kort.forms import ( + CardForm, + CardLayoutForm, + CardLayoutMediaFileFormSet, + CardPrinterForm, + PrinterSelectForm, +) +from aleksis.apps.kort.models import Card, CardLayout, CardPrinter, PrintStatus +from aleksis.apps.kort.tables import CardLayoutTable, 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 @@ -215,6 +221,89 @@ class CardPrinterDetailView(PermissionRequiredMixin, RevisionMixin, DetailView): return context +class CardLayoutFormMixin: + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + self.formset = CardLayoutMediaFileFormSet( + self.request.POST or None, self.request.FILES or None, instance=self.object + ) + context["formset"] = self.formset + return context + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + context = self.get_context_data(**kwargs) + form = self.get_form() + if form.is_valid() and self.formset.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + self.object = form.save() + self.formset.instance = self.object + self.formset.save() + return super().form_valid(form) + + +class CardLayoutListView(PermissionRequiredMixin, RevisionMixin, SingleTableView): + """List view for all card layouts.""" + + permission_required = "core.view_cardlayouts_rule" + template_name = "kort/card_layout/list.html" + model = CardLayout + table_class = CardLayoutTable + + +class CardLayoutCreateView( + PermissionRequiredMixin, CardLayoutFormMixin, RevisionMixin, AdvancedCreateView +): + """View used to create a card layout.""" + + permission_required = "core.create_cardlayout_rule" + template_name = "kort/card_layout/create.html" + form_class = CardLayoutForm + model = CardLayout + success_message = _("The card layout has been created successfully.") + + def get_success_url(self): + return reverse("card_layout", args=[self.object.pk]) + + def get_object(self): + return None + + +class CardLayoutEditView( + PermissionRequiredMixin, CardLayoutFormMixin, RevisionMixin, AdvancedEditView +): + """View used to edit a card layout.""" + + permission_required = "core.edit_cardlayout_rule" + template_name = "kort/card_layout/edit.html" + form_class = CardLayoutForm + model = CardLayout + success_message = _("The card layout has been changed successfully.") + + def get_success_url(self): + return reverse("card_layout", args=[self.object.pk]) + + +class CardLayoutDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView): + """View used to delete a card layout.""" + + permission_required = "core.delete_cardlayout_rule" + success_url = reverse_lazy("card_layouts") + template_name = "kort/card_layout/delete.html" + model = CardLayout + success_message = _("The card layout has been deleted successfully.") + + +class CardLayoutDetailView(PermissionRequiredMixin, RevisionMixin, DetailView): + permission_required = "core.view_cardlayout_rule" + model = CardLayout + template_name = "kort/card_layout/detail.html" + + class CardPrinterConfigView(PermissionRequiredMixin, RevisionMixin, SingleObjectMixin, View): permission_required = "core.view_cardprinter_rule" model = CardPrinter diff --git a/pyproject.toml b/pyproject.toml index 95eb428..a70a628 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ secondary = true python = "^3.9" aleksis-core = "^2.8" python-barcode = "^0.14.0" +django-ace = "^1.0.12" [tool.poetry.dev-dependencies] aleksis-builddeps = "*" -- GitLab