Skip to content

Commit

Permalink
Add Gotenberg as option in addition to wkhtmltopdf (#336)
Browse files Browse the repository at this point in the history
  • Loading branch information
Syfaro authored Nov 15, 2024
1 parent 8c664e1 commit 324bf13
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 25 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/django.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ jobs:
# Maps tcp port 5432 on service container to the host
- 5432:5432

gotenberg:
image: gotenberg/gotenberg:8
options: >-
--health-cmd "curl --fail http://localhost:3000/health"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 3000:3000

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
Expand Down
3 changes: 3 additions & 0 deletions fm_eventmanager/settings.py.docker
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,6 @@ ENVIRONMENT_NAME = os.getenv('ENVIRONMENT_NAME', "Production Server")
ENVIRONMENT_COLOR = os.getenv('ENVIRONMENT_COLOR', "#FF0000")
ENVIRONMENT_TEXT_COLOR = os.getenv('ENVIRONMENT_TEXT_COLOR', "#00FF00")
ENVIRONMENT_FLOAT = os.getenv('ENVIRONMENT_FLOAT', False)

PRINT_RENDERER = os.getenv('PRINT_RENDERER', 'wkhtmltopdf')
GOTENBERG_HOST = os.getenv('GOTENBERG_HOST', None)
2 changes: 2 additions & 0 deletions fm_eventmanager/settings.py.travis
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,5 @@ ENVIRONMENT_NAME = "Production Server"
ENVIRONMENT_COLOR = "#FF0000"
ENVIRONMENT_TEXT_COLOR = "#00FF00"
ENVIRONMENT_FLOAT = True

GOTENBERG_HOST = "http://localhost:3000"
61 changes: 58 additions & 3 deletions registration/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
from django.contrib import admin, auth, messages
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.signing import TimestampSigner
from django.db import transaction
from django.db.models import JSONField, Max
from django.db.models import Max
from django.forms import NumberInput, widgets
from django.http import HttpResponseRedirect
from django.shortcuts import render
Expand Down Expand Up @@ -544,7 +545,7 @@ class EventAdmin(admin.ModelAdmin):
"venue",
"charity",
"donations",
("codeOfConduct", "badgeTheme"),
("codeOfConduct", "badgeTheme", "defaultBadgeTemplate"),
)
},
),
Expand Down Expand Up @@ -945,7 +946,17 @@ def get_attendee_age(attendee):


def print_badges(modeladmin, request, queryset):
pdf_path = generate_badge_labels(queryset, request)
if getattr(settings, "PRINT_RENDERER", "wkhtmltopdf") == "gotenberg":
signer = TimestampSigner()
data = signer.sign_object({
"badge_ids": [badge.id for badge in queryset],
})

pdf_path = reverse("registration:pdf") + f"?data={data}"
else:
pdf_name = generate_badge_labels(queryset, request)
pdf_path = reverse("registration:pdf") + f"?file={pdf_name}"


response = HttpResponseRedirect(reverse("registration:print"))
url_params = {"file": pdf_path, "next": request.get_full_path()}
Expand Down Expand Up @@ -1615,3 +1626,47 @@ def headers_highlighted(self, instance):


admin.site.register(PaymentWebhookNotification, PaymentWebhookAdmin)


class BadgeTemplateAdmin(admin.ModelAdmin):
list_display = (
"name",
"paperWidth",
"paperHeight",
"marginTop",
"marginBottom",
"marginLeft",
"marginRight",
"landscape",
"scale"
)

fieldsets = (
(
None,
{
"fields": ("name", "template"),
}
),
(
"Paper Setup",
{
"fields": (
"landscape",
"scale",
("paperWidth", "paperHeight"),
)
}
),
(
"Margins And Padding",
{
"fields": (
("marginTop", "marginBottom"),
("marginLeft", "marginRight"),
),
}
),
)

admin.site.register(BadgeTemplate, BadgeTemplateAdmin)
35 changes: 35 additions & 0 deletions registration/migrations/0103_auto_20241109_1543.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 3.2.25 on 2024-11-09 20:43

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('registration', '0102_event_dealer_wifi_partner_price'),
]

operations = [
migrations.CreateModel(
name='BadgeTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('template', models.TextField()),
('paperWidth', models.CharField(max_length=10, null=True)),
('paperHeight', models.CharField(max_length=10, null=True)),
('marginTop', models.CharField(max_length=10, null=True)),
('marginBottom', models.CharField(max_length=10, null=True)),
('marginLeft', models.CharField(max_length=10, null=True)),
('marginRight', models.CharField(max_length=10, null=True)),
('landscape', models.BooleanField(default=True)),
('scale', models.FloatField(default=1.0)),
],
),
migrations.AddField(
model_name='event',
name='defaultBadgeTemplate',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='registration.badgetemplate'),
),
]
22 changes: 22 additions & 0 deletions registration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@ def __str__(self):
return self.name


class BadgeTemplate(models.Model):
name = models.CharField(max_length=100)
template = models.TextField()
paperWidth = models.CharField(max_length=10, null=True, verbose_name="Paper Width")
paperHeight = models.CharField(max_length=10, null=True, verbose_name="Paper Height")
marginTop = models.CharField(max_length=10, null=True, verbose_name = "Margin Top")
marginBottom = models.CharField(max_length=10, null=True, verbose_name = "Margin Bottom")
marginLeft = models.CharField(max_length=10, null=True, verbose_name="Margin Left")
marginRight = models.CharField(max_length=10, null=True, verbose_name = "Margin Right")
landscape = models.BooleanField(default=True)
scale = models.FloatField(default=1.0)

def __str__(self):
return str(self.name)


class Event(LookupTable):
dealerRegStart = models.DateTimeField(
verbose_name="Dealer Registration Start",
Expand Down Expand Up @@ -195,6 +211,12 @@ class Event(LookupTable):
blank=True,
on_delete=models.SET_NULL,
)
defaultBadgeTemplate = models.ForeignKey(
BadgeTemplate,
null=True,
on_delete=models.SET_NULL,
verbose_name="Badge Template",
)
newStaffDiscount = models.ForeignKey(
Discount,
null=True,
Expand Down
2 changes: 1 addition & 1 deletion registration/templates/registration/printing.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<body>
<a href="{{ next }}">Go Back</a>
<script>
var w = window.open("{% url 'registration:pdf' %}" + "/?file={{ file }}", "", "width=400,height=400");
var w = window.open("{{ file }}", "", "width=400,height=400");

if (!w || w.closed || typeof w.closed == 'undefined') {
// Didn't popup
Expand Down
100 changes: 100 additions & 0 deletions registration/tests/test_registration_printing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from datetime import timedelta

from django.conf import settings
from django.contrib.auth.models import User
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import timezone

from registration.models import (
Attendee,
Badge,
BadgeTemplate,
Event,
Order,
OrderItem,
PriceLevel,
)
from registration.tests.common import DEFAULT_EVENT_ARGS, TEST_ATTENDEE_ARGS

now = timezone.now()
ten_days = timedelta(days=10)


class TestRegistrationPrinting(TestCase):
def setUp(self):
self.admin_user = User.objects.create_superuser("admin", "admin@host", "admin")
self.admin_user.save()

self.badge_template = BadgeTemplate(
template="<script>window.badgeReady = true;</script>"
)
self.badge_template.save()

self.event = Event(
defaultBadgeTemplate=self.badge_template, **DEFAULT_EVENT_ARGS
)
self.event.save()

self.priceLevel = PriceLevel(
name="Attendee",
description="Hello",
basePrice=1.00,
startDate=now - ten_days,
endDate=now + ten_days,
public=True,
)
self.priceLevel.save()

self.order = Order(
total=100,
billingType=Order.CREDIT,
status=Order.COMPLETED,
reference="CREDIT_ORDER_1",
)
self.order.save()

self.attendee = Attendee(**TEST_ATTENDEE_ARGS)
self.attendee.save()

self.badge = Badge(event=self.event, attendee=self.attendee)
self.badge.save()

self.order_item = OrderItem(
order=self.order,
badge=self.badge,
priceLevel=self.priceLevel,
)
self.order_item.save()

self.client = Client()

def _badge_generates_pdf(self) -> dict:
self.assertTrue(self.client.login(username="admin", password="admin"))

response = self.client.get(
reverse("registration:onsite_print_badges") + f"?id={self.badge.pk}"
)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIsNotNone(data["file"])

self.client.logout()

response = self.client.get(data["file"])
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["content-type"], "application/pdf")

return data

def test_print_wkhtmltopdf(self):
settings.PRINT_RENDERER = "wkhtmltopdf"
data = self._badge_generates_pdf()
# wkhtmltopdf responses return a direct path to the file
self.assertIn("?file=", data["file"])

def test_print_gotenberg(self):
settings.PRINT_RENDERER = "gotenberg"
data = self._badge_generates_pdf()
# gotenberg responses return a signed data parameter with badge IDs
self.assertIn("?data=", data["file"])
35 changes: 26 additions & 9 deletions registration/views/onsite_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import permission_required
from django.contrib.messages import get_messages
from django.core.signing import TimestampSigner
from django.db.models import Q, Sum
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.utils.http import urlencode
from django.views.decorators.csrf import csrf_exempt

from registration import admin, mqtt, payments, printing
from registration import admin, mqtt, payments
from registration.admin import TWOPLACES
from registration.models import (
Badge,
Expand Down Expand Up @@ -342,20 +344,35 @@ def get_messages_list(request):
@staff_member_required
def onsite_print_badges(request):
badge_list = request.GET.getlist("id")
queryset = Badge.objects.filter(id__in=badge_list)
pdf_path = admin.generate_badge_labels(queryset, request)
# Async notify the frontend to refresh the cart
logger.info("Refreshing admin cart")
admin_push_cart_refresh(request)

file_url = reverse("registration:print") + "?file={0}".format(pdf_path)
if getattr(settings, "PRINT_RENDERER", "wkhtmltopdf") == "gotenberg":
terminal = get_active_terminal(request)

signer = TimestampSigner()
data = signer.sign_object({
"badge_ids": [int(badge_id) for badge_id in badge_list],
"terminal": terminal.name if terminal else None,
})

pdf_path = reverse("registration:pdf") + f"?data={data}"
else:
queryset = Badge.objects.filter(id__in=badge_list)
pdf_name = admin.generate_badge_labels(queryset, request)

pdf_path = reverse("registration:pdf") + f"?file={pdf_name}"

# Async notify the frontend to refresh the cart
logger.info("Refreshing admin cart")
admin_push_cart_refresh(request)

print_url = reverse("registration:print") + "?" + urlencode({"file": pdf_path})

return JsonResponse(
{
"success": True,
"file": pdf_path,
"next": request.get_full_path(),
"url": file_url,
"file": pdf_path,
"url": print_url,
}
)

Expand Down
Loading

0 comments on commit 324bf13

Please sign in to comment.