Compare commits

...

8 Commits

12 changed files with 337 additions and 45 deletions

View File

@ -15,6 +15,33 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/cards": {
"post": {
"description": "Generate cards based on the provided data",
"consumes": [
"application/json"
],
"produces": [
"application/pdf"
],
"tags": [
"cards"
],
"summary": "Generate runner cards",
"parameters": [
{
"description": "Card data",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CardRequest"
}
}
],
"responses": {}
}
},
"/contracts": {
"post": {
"description": "Generate a contract based on the provided data",
@ -35,7 +62,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Contract"
"$ref": "#/definitions/models.ContractRequest"
}
}
],
@ -44,7 +71,42 @@ const docTemplate = `{
}
},
"definitions": {
"models.Contract": {
"models.Card": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"runner": {
"$ref": "#/definitions/models.Runner"
}
}
},
"models.CardRequest": {
"type": "object",
"properties": {
"card": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Card"
}
},
"locale": {
"type": "string",
"enum": [
"en",
"de"
]
}
}
},
"models.ContractRequest": {
"type": "object",
"properties": {
"locale": {

View File

@ -6,6 +6,33 @@
"contact": {}
},
"paths": {
"/cards": {
"post": {
"description": "Generate cards based on the provided data",
"consumes": [
"application/json"
],
"produces": [
"application/pdf"
],
"tags": [
"cards"
],
"summary": "Generate runner cards",
"parameters": [
{
"description": "Card data",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.CardRequest"
}
}
],
"responses": {}
}
},
"/contracts": {
"post": {
"description": "Generate a contract based on the provided data",
@ -26,7 +53,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Contract"
"$ref": "#/definitions/models.ContractRequest"
}
}
],
@ -35,7 +62,42 @@
}
},
"definitions": {
"models.Contract": {
"models.Card": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"runner": {
"$ref": "#/definitions/models.Runner"
}
}
},
"models.CardRequest": {
"type": "object",
"properties": {
"card": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Card"
}
},
"locale": {
"type": "string",
"enum": [
"en",
"de"
]
}
}
},
"models.ContractRequest": {
"type": "object",
"properties": {
"locale": {

View File

@ -1,5 +1,28 @@
definitions:
models.Contract:
models.Card:
properties:
code:
type: string
enabled:
type: boolean
id:
type: integer
runner:
$ref: '#/definitions/models.Runner'
type: object
models.CardRequest:
properties:
card:
items:
$ref: '#/definitions/models.Card'
type: array
locale:
enum:
- en
- de
type: string
type: object
models.ContractRequest:
properties:
locale:
enum:
@ -39,6 +62,24 @@ info:
for pdf generation.
title: LfK Document Server API
paths:
/cards:
post:
consumes:
- application/json
description: Generate cards based on the provided data
parameters:
- description: Card data
in: body
name: data
required: true
schema:
$ref: '#/definitions/models.CardRequest'
produces:
- application/pdf
responses: {}
summary: Generate runner cards
tags:
- cards
/contracts:
post:
consumes:
@ -50,7 +91,7 @@ paths:
name: data
required: true
schema:
$ref: '#/definitions/models.Contract'
$ref: '#/definitions/models.ContractRequest'
produces:
- application/pdf
responses: {}

110
handlers/card.go Normal file
View File

@ -0,0 +1,110 @@
package handlers
import (
"log"
"slices"
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"git.odit.services/lfk/document-server/templates"
"github.com/gofiber/fiber/v2"
)
// GenerateCard godoc
// @Summary Generate runner cards
// @Description Generate cards based on the provided data
// @Tags cards
// @Accept json
// @Param data body models.CardRequest true "Card data"
// @Produce application/pdf
// @Router /cards [post]
func GenerateCard(c *fiber.Ctx) error {
cardRequest := new(models.CardRequest)
if err := c.BodyParser(cardRequest); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
if !slices.Contains([]string{"en", "de"}, cardRequest.Locale) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid locale",
})
}
generator := services.DefaultTemplater{}
templateString, err := templates.GetTemplate(cardRequest.Locale, "card")
if err != nil {
log.Println(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found",
})
}
template, err := generator.StringToTemplate(templateString)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
genConfig := &models.CardTemplateOptions{
CardSegments: splitCardSegments(cardRequest.Cards),
EventName: "Event name",
CardSubtitle: "Card subtitle",
BarcodeFormat: "ean13",
BarcodePrefix: "",
}
result, err := generator.Execute(template, genConfig)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
c.Set(fiber.HeaderContentType, "text/html")
converter := services.GotenbergConverter{BaseUrl: "http://localhost:3001"}
pdf, err := converter.ToPdf(result, "a4", false)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
c.Set(fiber.HeaderContentType, "application/pdf")
return c.Send(pdf)
}
func invertCardArrayItemPairs(cards []models.Card) []models.Card {
inverted := make([]models.Card, 0)
for i := 0; i < len(cards); i += 2 {
if i+1 < len(cards) {
inverted = append(inverted, cards[i+1])
}
inverted = append(inverted, cards[i])
}
return inverted
}
func splitCardSegments(cards []models.Card) []models.CardTemplateSegment {
cardSegments := make([]models.CardTemplateSegment, 0)
const currentCards = 0
for i := 0; i < len(cards); i += 10 {
segmentLength := 10
if len(cards)-i < 10 {
segmentLength = len(cards) - i
}
segment := cards[i : i+segmentLength]
if segmentLength%2 != 0 {
segment = append(segment, models.Card{
ID: 0,
Enabled: false,
Runner: models.Runner{},
Code: "",
})
}
cardSegments = append(cardSegments, models.CardTemplateSegment{
Cards: segment,
CardsSwapped: invertCardArrayItemPairs(segment),
})
}
return cardSegments
}

View File

@ -59,7 +59,7 @@ func GenerateContract(c *fiber.Ctx) error {
BarcodePrefix: "1",
}
result, err := generator.Execute(template)
result, err := generator.Execute(template, genConfig)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
@ -78,9 +78,9 @@ func GenerateContract(c *fiber.Ctx) error {
return c.Send(pdf)
}
func repeatRunnerArrayItems(runners []Runner, duplicates int) []Runner {
func repeatRunnerArrayItems(runners []models.Runner, duplicates int) []models.Runner {
var duplicatedRunners []models.Runner
for _, runner := range contract.Runners {
for _, runner := range runners {
for i := 0; i < duplicates; i++ {
duplicatedRunners = append(duplicatedRunners, runner)
}

View File

@ -35,6 +35,7 @@ func main() {
return c.SendString("Hello, World!")
})
v1.Post("/contracts", handlers.GenerateContract)
v1.Post("/cards", handlers.GenerateCard)
app.Use(handlers.NotFoundHandler)
docs.SwaggerInfo.BasePath = "/"

View File

@ -1,7 +1,7 @@
package models
type CardRequest struct {
Cards []Card `json:"card"`
Cards []Card `json:"cards"`
Locale string `json:"locale" enums:"en,de"`
}
@ -12,10 +12,15 @@ type Card struct {
Code string `json:"code"`
}
type ContractTemplateOptions struct {
Cards []Card `json:"cards"`
EventName string `json:"event_name"`
CardSubtitle string `json:"card_subtitle"`
BarcodeFormat string `json:"barcode_format"`
BarcodePrefix string `json:"barcode_prefix"`
type CardTemplateOptions struct {
CardSegments []CardTemplateSegment `json:"card_segments"`
EventName string `json:"event_name"`
CardSubtitle string `json:"card_subtitle"`
BarcodeFormat string `json:"barcode_format"`
BarcodePrefix string `json:"barcode_prefix"`
}
type CardTemplateSegment struct {
Cards []Card `json:"cards"`
CardsSwapped []Card `json:"cards_swapped"`
}

View File

@ -6,7 +6,6 @@ import (
"errors"
"html/template"
"image/png"
"strconv"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/code128"
@ -22,22 +21,20 @@ type Templater interface {
type DefaultTemplater struct {
}
func idToEan13(id int, prefix string) (string, error) {
idStr := strconv.Itoa(id)
if len(idStr) > 12 {
func idToEan13(id string, prefix string) (string, error) {
if len(id) > 12 {
return "", errors.New("id too long")
}
for len(idStr) < 11 {
idStr = "0" + idStr
for len(id) < 11 {
id = "0" + id
}
idStr = prefix + idStr
id = prefix + id
return idStr, nil
return id, nil
}
func (t *DefaultTemplater) GenerateBarcode(code int, format string, prefix string) (string, error) {
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
var generatedCode barcode.Barcode
var err error
@ -53,13 +50,13 @@ func (t *DefaultTemplater) GenerateBarcode(code int, format string, prefix strin
}
break
case "code128":
generatedCode, err = code128.Encode(prefix + strconv.Itoa(code))
generatedCode, err = code128.Encode(prefix + code)
if err != nil {
return "", err
}
break
case "qr":
generatedCode, err = qr.Encode(prefix+strconv.Itoa(code), qr.M, qr.Numeric)
generatedCode, err = qr.Encode(prefix+code, qr.M, qr.AlphaNumeric)
if err != nil {
return "", err
}
@ -84,7 +81,8 @@ func (t *DefaultTemplater) GenerateBarcode(code int, format string, prefix strin
func (t *DefaultTemplater) StringToTemplate(templateString string) (*template.Template, error) {
return template.New("template").Funcs(template.FuncMap{
"barcode": t.GenerateBarcode,
"barcode": t.GenerateBarcode,
"sponsorLogo": func(id int) string { return "a" },
}).Parse(templateString)
}

View File

@ -28,10 +28,12 @@
</head>
<body class="A4 landscape">
{{ range .CardSegments }}
<div class="sheet">
<div class="columns is-multiline">
{{ range .cards }}
{{ range .Cards }}
<div class="column is-half runnercard">
{{ if ne .Code "" }}
<p class="title is-5" style="text-align: center; padding-bottom: 0; margin-top: -0.75rem;">{{ $.EventName }}</p>
<p style="text-align: center; margin-top: -1.5rem; font-size: small;">{{ $.CardSubtitle }}</p>
<p style="font-size: small;">Mit Unterstützung von:</p>
@ -43,29 +45,36 @@
</div>
<div class="column is-half">
<!--BARCODE HERE-->
<img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 2cm;"
<img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 5rem;"
src="data:image/png;base64,{{ barcode .Code $.BarcodeFormat $.BarcodePrefix }}" />
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
</div>
</div>
<p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
<p>{{ .Runner.Group.Name }}</p>
{{ end}}
</div>
{{/each}}
{{ end }}
</div>
</div>
<div class="sheet">
<div class="columns is-multiline">
{{#each cards_swapped}}
{{ range .CardsSwapped }}
<div class="column is-half runnercard" style="justify-content: center; align-items: center; text-align: center;">
{{ if ne .Code "" }}
<!--SPONSOR LOGO FIRST-->
<div style="height: 2cm; padding: 0 0 2.25cm 0">
<div style="height: 2cm; padding: 0 0 1cm 0">
<img style="object-fit: cover; max-height: 2cm;" src="data:image/png;base64,{{ sponsorLogo .ID }}" />
</div>
<img style="object-fit: cover; max-height: 2.5cm; position: relative;" src="data:image/png;base64,{{ barcode .Code $.BarcodeFormat $.BarcodePrefix }}" />
<img style="object-fit: cover; max-height: 6rem; position: relative;"
src="data:image/png;base64,{{ barcode .Code $.BarcodeFormat $.BarcodePrefix }}" />
<p style="font-size: 1rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
{{ end }}
</div>
{{/each}}
{{ end }}
</div>
</div>
{{ end}}
</body>
</html>

View File

@ -28,9 +28,10 @@
</head>
<body class="A4 landscape">
{{ range .CardSegments }}
<div class="sheet">
<div class="columns is-multiline">
{{#each cards}}
{{ range .Cards }}
<div class="column is-half runnercard">
<p class="title is-5" style="text-align: center; padding-bottom: 0; margin-top: -0.75rem;">{{ $.EventName }}</p>
<p style="text-align: center; margin-top: -1.5rem; font-size: small;">{{ $.CardSubtitle }}</p>
@ -43,29 +44,32 @@
</div>
<div class="column is-half">
<!--BARCODE HERE-->
<img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 2cm;"
<img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 5rem;"
src="data:image/png;base64,{{ barcode .Code $.BarcodeFormat $.BarcodePrefix }}" />
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
</div>
</div>
<p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
<p>{{ .Runner.Group.Name }}</p>
</div>
{{/each}}
{{ end }}
</div>
</div>
<div class="sheet">
<div class="columns is-multiline">
{{#each cards_swapped}}
{{ range .CardsSwapped }}
<div class="column is-half runnercard" style="justify-content: center; align-items: center; text-align: center;">
<!--SPONSOR LOGO FIRST-->
<div style="height: 2cm; padding: 0 0 2.25cm 0">
<div style="height: 2cm; padding: 0 0 1cm 0">
<img style="object-fit: cover; max-height: 2cm;" src="data:image/png;base64,{{ sponsorLogo .ID }}" />
</div>
<img style="object-fit: cover; max-height: 2.5cm; position: relative;" src="data:image/png;base64,{{ barcode .Code $.BarcodeFormat $.BarcodePrefix }}" />
<img style="object-fit: cover; max-height: 6rem; position: relative;" src="data:image/png;base64,{{ barcode .Code $.BarcodeFormat $.BarcodePrefix }}" />
<p style="font-size: 1rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
</div>
{{/each}}
{{ end }}
</div>
</div>
{{ end}}
</body>
</html>

View File

@ -56,7 +56,7 @@
</div>
<div class="column">
<img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 2cm;"
src="data:image/png;base64,{{ barcode .ID $.BarcodeFormat $.BarcodePrefix }}" />
src="data:image/png;base64,{{ barcode (printf "%d" .ID) $.BarcodeFormat $.BarcodePrefix }}" />
</div>
</div>
<div class="columns" style="padding-top: 1rem;">

View File

@ -55,7 +55,7 @@
</div>
<div class="column">
<img style="vertical-align: revert; margin-top: auto; object-fit: cover; max-height: 2cm;"
src="data:image/png;base64,{{ barcode .ID $.BarcodeFormat $.BarcodePrefix }}"/>
src="data:image/png;base64,{{ barcode (printf "%d" .ID) $.BarcodeFormat $.BarcodePrefix }}"/>
</div>
</div>
<div class="columns" style="padding-top: 1rem;">