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

Merge branch '11-allow-editing-layout' into 'main'

Resolve "Allow editing layout"

Closes #11

See merge request !5
parents 29da3623 8e3f8149
No related branches found
No related tags found
1 merge request!5Resolve "Allow editing layout"
Pipeline #81637 failed
Showing
with 303 additions and 8 deletions
from django import forms from django import forms
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_ace import AceWidget
from django_select2.forms import ModelSelect2Widget 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 CardForm(forms.ModelForm):
class Meta: class Meta:
model = Card model = Card
fields = ["person", "valid_until"] fields = ["person", "valid_until", "layout"]
widgets = { widgets = {
"person": ModelSelect2Widget( "person": ModelSelect2Widget(
search_fields=[ search_fields=[
...@@ -22,6 +24,10 @@ class CardForm(forms.ModelForm): ...@@ -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): class CardPrinterForm(forms.ModelForm):
layout = Layout( layout = Layout(
...@@ -53,3 +59,27 @@ class PrinterSelectForm(forms.Form): ...@@ -53,3 +59,27 @@ class PrinterSelectForm(forms.Form):
self.fields["printer"].queryset = printers self.fields["printer"].queryset = printers
if printers.count() == 1: if printers.count() == 1:
self.fields["printer"].initial = printers.first() 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
)
...@@ -34,6 +34,17 @@ MENUS = { ...@@ -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",
)
],
},
], ],
} }
] ]
......
# 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()),
],
),
]
# 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'),
),
]
...@@ -16,6 +16,7 @@ from model_utils.models import TimeStampedModel ...@@ -16,6 +16,7 @@ from model_utils.models import TimeStampedModel
from aleksis.core.mixins import ExtensibleModel from aleksis.core.mixins import ExtensibleModel
from aleksis.core.models import OAuthApplication, Person from aleksis.core.models import OAuthApplication, Person
from aleksis.core.util.pdf import process_context_for_pdf
class CardPrinterStatus(models.TextChoices): class CardPrinterStatus(models.TextChoices):
...@@ -172,19 +173,63 @@ class CardPrinter(ExtensibleModel): ...@@ -172,19 +173,63 @@ class CardPrinter(ExtensibleModel):
verbose_name_plural = _("Card printers") 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): 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")) name = models.CharField(max_length=255, verbose_name=_("Name"))
template = models.TextField(verbose_name=_("Template")) template = models.TextField(verbose_name=_("Template"))
css = models.TextField(verbose_name=_("Custom CSS"), blank=True) 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: 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"): def render(self, card: "Card"):
t = self.get_template() t = self.get_template()
context = card.get_context() 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): def validate_template(self):
try: try:
...@@ -193,6 +238,13 @@ class CardLayout(ExtensibleModel): ...@@ -193,6 +238,13 @@ class CardLayout(ExtensibleModel):
except Exception as e: except Exception as e:
raise ValidationError(_("Template is invalid: {}").format(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): class Card(ExtensibleModel):
person = models.ForeignKey( person = models.ForeignKey(
...@@ -202,6 +254,9 @@ class Card(ExtensibleModel): ...@@ -202,6 +254,9 @@ class Card(ExtensibleModel):
valid_until = models.DateField(verbose_name=_("Valid until")) valid_until = models.DateField(verbose_name=_("Valid until"))
deactivated = models.BooleanField(verbose_name=_("Deactivated"), default=False) 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( pdf_file = models.FileField(
verbose_name=_("PDF file"), verbose_name=_("PDF file"),
blank=True, blank=True,
...@@ -224,6 +279,7 @@ class Card(ExtensibleModel): ...@@ -224,6 +279,7 @@ class Card(ExtensibleModel):
"person": self.person, "person": self.person,
"chip_number": self.chip_number, "chip_number": self.chip_number,
"valid_until": self.valid_until, "valid_until": self.valid_until,
"media_files": self.layout.media_files.all(),
} }
def generate_pdf(self) -> Union[bool, AsyncResult]: def generate_pdf(self) -> Union[bool, AsyncResult]:
...@@ -234,6 +290,8 @@ class Card(ExtensibleModel): ...@@ -234,6 +290,8 @@ class Card(ExtensibleModel):
return generate_card_pdf.delay(self.pk) return generate_card_pdf.delay(self.pk)
def print_card(self, printer: CardPrinter): 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 = CardPrintJob(card=self, printer=printer)
job.save() job.save()
if not self.chip_number and printer.generate_number_on_server: if not self.chip_number and printer.generate_number_on_server:
......
...@@ -5,3 +5,5 @@ YARN_INSTALLED_APPS = ["pdfobject"] ...@@ -5,3 +5,5 @@ YARN_INSTALLED_APPS = ["pdfobject"]
ANY_JS = { ANY_JS = {
"pdfobject": {"js_url": JS_URL + "/pdfobject/pdfobject.min.js"}, "pdfobject": {"js_url": JS_URL + "/pdfobject/pdfobject.min.js"},
} }
INSTALLED_APPS = ["django_ace"]
aleksis/apps/kort/static/kort/background.png

32.7 KiB

Image diff could not be displayed: it is too large. Options to address this: view the blob.
aleksis/apps/kort/static/kort/logo-background.png

133 KiB

aleksis/apps/kort/static/kort/logo.jpg

41.9 KiB

aleksis/apps/kort/static/kort/logo.png

123 KiB

aleksis/apps/kort/static/kort/signature.png

12.1 KiB

aleksis/apps/kort/static/kort/stamp.png

62.9 KiB

aleksis/apps/kort/static/kort/test.png

32.7 KiB

...@@ -66,3 +66,19 @@ class CardPrinterTable(Table): ...@@ -66,3 +66,19 @@ class CardPrinterTable(Table):
def render_actions(self, value, record): def render_actions(self, value, record):
return render_to_string("kort/printer/actions.html", dict(pk=value, printer=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))
...@@ -3,15 +3,15 @@ from celery.states import SUCCESS ...@@ -3,15 +3,15 @@ from celery.states import SUCCESS
from aleksis.apps.kort.models import Card from aleksis.apps.kort.models import Card
from aleksis.core.celery import app 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 @app.task
def generate_card_pdf(card_pk: int): def generate_card_pdf(card_pk: int):
card = Card.objects.get(pk=card_pk) card = Card.objects.get(pk=card_pk)
context = card.get_context() html = card.layout.render(card)
file_object, result = generate_pdf_from_template("kort/pdf.html", context) file_object, result = generate_pdf_from_html(html)
with allow_join_result(): with allow_join_result():
result.wait() result.wait()
......
...@@ -25,6 +25,13 @@ ...@@ -25,6 +25,13 @@
</th> </th>
<td>{{ card.valid_until }}</td> <td>{{ card.valid_until }}</td>
</tr> </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> <tr>
<th> <th>
<i class="material-icons left iconify" data-icon="mdi:checkbox-blank-badge-outline"></i> <i class="material-icons left iconify" data-icon="mdi:checkbox-blank-badge-outline"></i>
......
{% 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
{# -*- 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 %}
{# -*- 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
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