Compare commits

..

No commits in common. "d706ae164547f70f2b8f3cc19ff20963de98fa48" and "1426ca5fa6a6f91b25157db40faaf1bf49d80973" have entirely different histories.

50 changed files with 1026 additions and 10669 deletions

View File

@ -1,15 +1,15 @@
FROM alpine:3.18 FROM alpine
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript font-droid RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript ttf-opensans
RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn
ADD backend /zetikettes ADD backend /root/zetikettes
RUN mkdir -p /usr/share/fonts/TTF \ RUN mkdir -p /usr/share/fonts/TTF \
&& cp /zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \ && cp /root/zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \
&& fc-cache -fv && fc-cache -fv
# the script will look for templates in /data # the script will look for templates in /data
WORKDIR /zetikettes/zetikettes WORKDIR /root/zetikettes/zetikettes
CMD /usr/bin/gunicorn zetikettes.wsgi -b 0.0.0.0:8000 --timeout 600 --forwarded-allow-ips="*" CMD /usr/bin/gunicorn zetikettes.wsgi -b 0.0.0.0:8000 --timeout 600 --forwarded-allow-ips="*"

View File

@ -1,48 +0,0 @@
LIBPREFIX ?= /var/lib/zetikettes
image-name = pol/zetikettes
uuid = $(shell id -u):$(shell id -g)
.PHONY: image
image: ## build the docker images
docker compose build
.PHONY: initial-db
initial-db: image ## create and populate a database
mkdir -p $(LIBPREFIX)/data
docker run --rm \
--user $(uuid) \
-v $(LIBPREFIX)/data:/data \
$(image-name) \
/zetikettes/zetikettes/manage.py migrate
docker run --rm \
--user $(uuid) \
-v $(LIBPREFIX)/data:/data \
$(image-name) \
/zetikettes/zetikettes/manage.py loaddata initial_db
cp templates/*.svg $(LIBPREFIX)/data/
.PHONY: superuser
superuser: image ## create a superuser in the django admin
docker run --rm \
--user $(uuid) \
-v $(LIBPREFIX)/data:/data \
-it \
$(image-name) \
/zetikettes/zetikettes/manage.py createsuperuser
.PHONY: staticfiles
staticfiles: image ## install all static files
cp -r frontend $(LIBPREFIX)/
docker run --rm \
--user $(uuid) \
-v $(LIBPREFIX):/libdir \
-w /libdir \
$(image-name) \
/zetikettes/zetikettes/manage.py collectstatic
.PHONY: help
help: ## Show this help
@echo Noteworthy targets:
@egrep '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
.DEFAULT_GOAL := help

View File

@ -1,53 +1,64 @@
zetikettes 2.0 zetikettes
============== ==========
ouaich. tavu. ouaich.
**NOTE**: release 2.0 broke compatibility with previous "releases". Maxime has all the recent data to repopulate a new database.
Initial setup Initial setup
------------- -------------
### Database setup and population
Populate an empty database (if none already exists):
```
sudo make initial-db
```
This will setup an initial database under `/var/lib/zetikettes/data`
Optionally, you may need credentials to access the admin page:
```
make superuser
```
### System service
``` ```
docker build -t zetikettes .
sudo mkdir -p /etc/docker/compose/zetikettes sudo mkdir -p /etc/docker/compose/zetikettes
sudo cp compose.yml /etc/docker/compose/zetikettes/ sudo cp docker-compose.yml /etc/docker/compose/zetikettes/
sudo systemctl enable --now docker-compose@zetikettes sudo systemctl enable --now docker-compose@zetikettes
``` ```
If not already present in `/etc/systemd/system/`, the `docker-compose@` service file is provided.
### www static files
```
make staticfiles
```
This will install frontend and django admin static files under `/var/lib/zetikettes`
### Nginx
Example configuration is provided in `nginx_locations`.
Nginx is configured to: Nginx is configured to:
- redirect `/zetikettes/srv/` to `localhost:8000` - redirect /zetikettes/srv/ to localhost:8000
- redirect `/zetikettes/` to `/var/lib/zetikettes/frontend` - redirect /zetikettes/ to /var/lib/zetikettes/static
- redirect `/zetikettes/srv/static` to `/var/lib/zetikettes/www_static`
Conf is in /etc/nginx/sites-available/default
Test
----
```
docker run --rm -it -v $PWD/templates:/data zetikettes /bin/bash /root/zetikettes/old/mkjam.sh
```
This should produce a .pdf in `templates/`. Open it to check that
layout & fonts are correct.
Run
---
```
docker-compose up
```
Notes for deploying
-------------------
.h3 Initialize empty database
```
python manage.py migrate
```
.h3 Prepare static files
```
python manage.py collectstatic
```
The files will be in `www_static/` and need to be moved to `/var/lib/zetikettes/www_static`
.h3 Change host settings
If not deploying on `aerith.ponteilla.net`, you'll need to edit `backend/zetikettes/zetikettes/settings.py` to change a couple things in there.
Change available templates
--------------------------
1. go to /zetikettes/admin
1. add the newtikette
1. still no need to restart the container (magic!)
2. profit.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
[]

View File

@ -20,9 +20,6 @@ def parse_args():
default=sys.stdout, default=sys.stdout,
type=argparse.FileType('w'), type=argparse.FileType('w'),
help='output path (default: stdout)') help='output path (default: stdout)')
parser.add_argument('--landscape',
action='store_true',
help='input sticker is in landscape orientation')
parser.add_argument('sticker', parser.add_argument('sticker',
type=argparse.FileType('r'), type=argparse.FileType('r'),
default=sys.stdin, default=sys.stdin,
@ -32,7 +29,7 @@ def parse_args():
return parser.parse_args() return parser.parse_args()
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE ,landscape=False): def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
with open(template) as tpl: with open(template) as tpl:
tpl_data = tpl.read() tpl_data = tpl.read()
@ -41,11 +38,19 @@ def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE ,landscape=False):
lines = lines[1:] lines = lines[1:]
sticker_data = ''.join(lines) sticker_data = ''.join(lines)
rotate = "translate(102, 0) rotate(90)" if not landscape else ""
subs = { subs = {
'sticker': sticker_data, 'left0': sticker_data,
'rotate': rotate, 'left1': sticker_data,
'left2': sticker_data,
'left3': sticker_data,
'left4': sticker_data,
'left5': sticker_data,
'right0': sticker_data,
'right1': sticker_data,
'right2': sticker_data,
'right3': sticker_data,
'right4': sticker_data,
'right5': sticker_data,
} }
out.write(Template(tpl_data).substitute(subs)) out.write(Template(tpl_data).substitute(subs))

View File

@ -22,18 +22,25 @@ def parse_args():
parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre') parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre')
parser.add_argument('--fruit', '-f', required=False, help='Quantité de fruits') parser.add_argument('--fruit', '-f', required=False, help='Quantité de fruits')
parser.add_argument('--size', '-s', required=False, help='Masse de produit') parser.add_argument('--size', '-s', required=False, help='Masse de produit')
parser.add_argument('--landscape',
action='store_true',
help='input sticker is in landscape orientation')
parser.add_argument('sticker', help='path to the sticker template') parser.add_argument('sticker', help='path to the sticker template')
return parser.parse_args() return parser.parse_args()
def makesticker(sticker: str, out: typing.IO, subs: dict): def makesticker(sticker: str, out: typing.IO, subs: dict, landscape=False):
with open(sticker) as fin: with open(sticker) as fin:
lines = fin.readlines() lines = fin.readlines()
if lines[0].startswith('<?xml'): if lines[0].startswith('<?xml'):
lines = lines[1:] lines = lines[1:]
sticker_data = ''.join(lines) sticker_data = ''.join(lines)
if landscape:
# Rotate the sticker 90 degrees
sticker_data = "<g transform=\"rotate(-90,0,0) translate(-102,0)\">{}</g>".format(sticker_data)
out.write(Template(sticker_data).substitute(subs)) out.write(Template(sticker_data).substitute(subs))
@ -43,8 +50,8 @@ if __name__ == "__main__":
'dluo': args.pop('dluo'), 'dluo': args.pop('dluo'),
'lot': args.pop('lot'), 'lot': args.pop('lot'),
'teneur': args.pop('teneur'), 'teneur': args.pop('teneur'),
'f': args.pop('fruit'), 'fruit': args.pop('fruit'),
'q': args.pop('quantite'), 'qty': args.pop('quantite'),
'size': args.pop('size'), 'size': args.pop('size'),
} }
args['subs'] = subs args['subs'] = subs

25
backend/old/mkjam.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
DEFAULT_DPI=300
LARGE='370' # Large container (370g)
SMALL='220' # Small container (220g)
SIZE=$LARGE
DLUO='décembre 2023'
LOT='0722-2'
TENEUR='50' # %
FRUIT='80' # g
STICKER='Gelée Extra - Cassis.svg'
DATADIR=/data
PDF="`basename \"${STICKER}\" .svg` - ${LOT} (${SIZE}g).pdf"
here=$(dirname $(readlink -f $0))
cd $DATADIR
$here/makesticker.py --landscape --dluo "$DLUO" --lot "$LOT" --teneur "$TENEUR" --fruit "$FRUIT" --quantite "$SIZE" -o out.svg "$STICKER" && \
$here/makeplanche.py -o pout.svg -t $here/planche.svg.in out.svg && \
rm out.svg && \
inkscape --export-type="png" --export-dpi=$DEFAULT_DPI pout.svg && \
rm pout.svg && \
convert pout.png "$PDF"

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 210 297"
height="297mm"
width="210mm">
<g transform="translate(3, 4.5)">
<g transform="translate(-42,144) rotate(-90,144,0)">
<g transform="translate( 0,0)">$right0</g>
<g transform="translate( 48,0)">$right1</g>
<g transform="translate( 96,0)">$right2</g>
<g transform="translate(144,0)">$right3</g>
<g transform="translate(192,0)">$right4</g>
<g transform="translate(240,0)">$right5</g>
</g>
<g transform="translate(-42,144) rotate(90,144,0)">
<g transform="translate( 0,0)">$left0</g>
<g transform="translate( 48,0)">$left1</g>
<g transform="translate( 96,0)">$left2</g>
<g transform="translate(144,0)">$left3</g>
<g transform="translate(192,0)">$left4</g>
<g transform="translate(240,0)">$left5</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 896 B

269
backend/old/web.py Normal file
View File

@ -0,0 +1,269 @@
import argparse
from http.server import BaseHTTPRequestHandler, HTTPServer
import io
import json
import logging
import os
import re
import string
import subprocess
import tempfile
import time
import traceback
from makesticker import makesticker
from makeplanche import makeplanche
# defaults
PORT = 8000
OUT_DIR = tempfile.gettempdir()
DATA_DIR = '/data'
TEMPLATE_DIR = DATA_DIR
DEFAULT_DPI = 300
LEMOTDEPASSE = 'je veux ajouter une étiquette'
TIKETTES = os.path.join(DATA_DIR, 'tikettes.json')
config = {}
def tikettes_path():
return config.get('tikettes', TIKETTES)
def template_dir():
return config.get('template_dir', TEMPLATE_DIR)
def inkscapize(svg_in, pdf_out):
png_out = tempfile.mktemp(suffix='.png')
try:
cmd = ['inkscape', '--export-type=png', f'--export-filename={png_out}',
f'--export-dpi={DEFAULT_DPI}', svg_in]
subprocess.check_call(cmd)
cmd = ['convert', png_out, pdf_out]
subprocess.check_call(cmd)
finally:
if os.path.exists(png_out):
os.unlink(png_out)
def handle_newtikette(formdata):
with open(tikettes_path()) as f:
tikettes = json.load(f)
sticker = formdata['title'] + '.svg'
with open(os.path.join(template_dir(), sticker), 'w') as f:
f.write(formdata['sticker'])
known_subs = {
"dluo": "germinal 9999",
"fruit": "80",
"teneur": "50",
"lot": "0000-0",
"qty": "370",
"vol": "50",
}
newtikette = {
'title': formdata['title'],
'sticker': sticker,
'landscape': 'landscape' in formdata,
'subs': {k: v for k, v in known_subs.items() if k in formdata},
}
logging.info(f'adding newtikette: {newtikette}')
tikettes.append(newtikette)
with open(tikettes_path(), 'w') as f:
json.dump(tikettes, f, indent=2)
return json.dumps({'status': 'ok', 'message': 'newtikette added'})
def handle_generate(request):
request = json.loads(request)
# fill in sticker details
sticker_out = tempfile.mktemp(suffix='.svg')
try:
with open(sticker_out, 'w') as stickout:
landscape = request.get('landscape', False)
print(f'landscape: {landscape}')
makesticker(os.path.join(template_dir(), request['sticker']), stickout,
request['subs'], landscape=landscape)
# make sticker sheet
planche_out = tempfile.mktemp(suffix='.svg')
with open(sticker_out, 'r') as stickin:
with open(planche_out, 'w') as planchout:
makeplanche(stickin, planchout)
# process to printable pdf
pdf_out = tempfile.mktemp(dir=OUT_DIR, suffix='.pdf')
inkscapize(planche_out, pdf_out)
response = {'status': 'ok', 'file': os.path.basename(pdf_out),
'message': 'this is the way'}
return json.dumps(response)
finally:
if os.path.exists(sticker_out):
os.unlink(sticker_out)
if 'planche_out' in locals() and os.path.exists(planche_out):
os.unlink(planche_out)
def parse_multipart(data, boundary):
logging.debug(f'parsing data with boundary: {boundary}')
f = io.StringIO(data)
out = []
if boundary not in f.readline():
return out
while True:
headers = []
while True:
h = f.readline().strip()
try:
k, v = re.findall(r'^([a-zA-Z-]+): (.*)$', h)[0]
except IndexError:
break
headers.append((k, v))
metadata = None
otherheaders = []
logging.debug(headers)
for k, v in headers:
if k == 'Content-Disposition':
parts = v.split('; ')
if parts[0] != 'form-data':
continue
d = dict(re.findall(r'([^=]+)="(.*)"$', p)[0] for p in parts[1:])
metadata = d
else:
otherheaders.append((k, v))
value = io.StringIO()
while True:
l = f.readline()
if not l:
break
if boundary in l:
# value.write(l.split(boundary)[0])
break
value.write(l)
if metadata is None:
break
logging.debug(f'({metadata}, {value.getvalue()[:-2]}, {otherheaders})')
out.append((metadata, value.getvalue()[:-2], otherheaders))
return out
class MyServer(BaseHTTPRequestHandler):
def do_GET(self):
logging.info(f'GET {self.path}')
if self.path == '/list':
self.send_response(200)
self.send_header("Content-type", "application/json")
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
with open(tikettes_path()) as f:
tikettes = json.load(f)
resp = {'status': 'ok', 'message': 'this is the way',
'tikettes': tikettes}
self.wfile.write(json.dumps(resp).encode())
return
match = re.match(r'/data/(\w+\.pdf)', self.path)
if match is not None:
pdf_path = os.path.join(OUT_DIR, match.groups()[0])
if not os.path.exists(pdf_path):
self.send_response(404)
self.end_headers()
return
self.send_response(200)
self.send_header("Content-type", "application/pdf")
self.end_headers()
with open(pdf_path, 'rb') as f:
self.wfile.write(f.read())
return
self.send_response(200)
self.send_header("Content-type", "text/html; charset=UTF-8")
self.end_headers()
self.wfile.write(b"<html><head><title>Zetikettes</title></head>")
self.wfile.write(b"<body>")
self.wfile.write("<p>This is not the way.</p>".encode())
self.wfile.write(b"</body></html>")
def do_OPTIONS(self):
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
def do_POST(self):
logging.info(f'POST {self.path}')
try:
length = int(self.headers['content-length'])
req = self.rfile.read(length).decode()
if self.path == '/newtikette':
boundary = re.findall(r'boundary=(.*)$', self.headers['content-type'])[0]
formdata = {p['name']: v for p, v, _ in parse_multipart(req, boundary)}
logging.info({k: v[:100] for k, v in formdata.items()})
if formdata.get('lemotdepasse', None) != LEMOTDEPASSE:
logging.warning(f'wrong lemotdepasse')
self.send_response(403)
self.send_header("Content-type", "application/json")
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps({'status': 'notok', 'message': 'bad lemotdepasse'}).encode())
return
resp = handle_newtikette(formdata).encode()
else:
resp = handle_generate(req).encode()
except:
self.send_response(500)
self.send_header("Content-type", "text/plain")
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(traceback.format_exc().encode())
raise
self.send_response(200)
self.send_header("Content-type", "application/json")
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(resp)
def parse_args():
parser = argparse.ArgumentParser(description='Zetikette backend')
parser.add_argument('--port', type=int, default=PORT, help=f'default: {PORT}')
parser.add_argument('--data-dir', default=DATA_DIR, help=f'default: {DATA_DIR}')
return parser.parse_args()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
args = parse_args()
config.update({
'tikettes': os.path.join(args.data_dir, 'tikettes.json'),
'template_dir': args.data_dir,
})
webServer = HTTPServer(('', args.port), MyServer)
print(f"Server started on port {args.port}")
try:
webServer.serve_forever()
except KeyboardInterrupt:
pass
webServer.server_close()
print("Server stopped.")

View File

@ -1,152 +0,0 @@
[
{
"model": "tikette.tisub",
"pk": 2,
"fields": {
"name": "dluo",
"descritpion": "DLUO",
"default": "décembre 1992",
"type": "YM"
}
},
{
"model": "tikette.tisub",
"pk": 3,
"fields": {
"name": "lot",
"descritpion": "№ de lot",
"default": "1234-5",
"type": "ST"
}
},
{
"model": "tikette.tisub",
"pk": 4,
"fields": {
"name": "q",
"descritpion": "Poids net (g)",
"default": "370",
"type": "ST"
}
},
{
"model": "tikette.tisub",
"pk": 5,
"fields": {
"name": "teneur",
"descritpion": "Teneur en fruits (%)",
"default": "50",
"type": "ST"
}
},
{
"model": "tikette.tisub",
"pk": 6,
"fields": {
"name": "f",
"descritpion": "Quantité de fruits pour 100g (g)",
"default": "60",
"type": "ST"
}
},
{
"model": "tikette.tikategory",
"pk": 1,
"fields": {
"name": "Pesto",
"landscape": true,
"prototempalte": "Pesto.svg",
"subs": [
2,
3,
4
]
}
},
{
"model": "tikette.tikategory",
"pk": 2,
"fields": {
"name": "Confiture",
"landscape": true,
"prototempalte": "Confiture.svg",
"subs": [
2,
3,
4,
5,
6
]
}
},
{
"model": "tikette.tikategory",
"pk": 3,
"fields": {
"name": "Chocolat",
"landscape": false,
"prototempalte": "Chocolat.svg",
"subs": [
2,
3,
4
]
}
},
{
"model": "tikette.tikategory",
"pk": 4,
"fields": {
"name": "Aromate",
"landscape": false,
"prototempalte": "Aromate.svg",
"subs": [
2,
3,
4
]
}
},
{
"model": "tikette.tikategory",
"pk": 5,
"fields": {
"name": "Sel",
"landscape": true,
"prototempalte": "Sel.svg",
"subs": [
2,
3,
4
]
}
},
{
"model": "tikette.tikategory",
"pk": 6,
"fields": {
"name": "Sirop",
"landscape": false,
"prototempalte": "Sirop.svg",
"subs": [
2,
3,
4
]
}
},
{
"model": "tikette.tikategory",
"pk": 7,
"fields": {
"name": "Tisane",
"landscape": false,
"prototempalte": "Tisane.svg",
"subs": [
2,
3,
4
]
}
}
]

View File

@ -1,9 +1,8 @@
from django.contrib import admin from django.contrib import admin
from .models import Tikette, Tizer, Tikategory, Tisub from .models import Tikette, Tikategory, Tisub
admin.site.register(Tikette) admin.site.register(Tikette)
admin.site.register(Tizer)
admin.site.register(Tikategory) admin.site.register(Tikategory)
admin.site.register(Tisub) admin.site.register(Tisub)

View File

@ -7,8 +7,6 @@ from .makeplanche import makeplanche
OUT_DIR = tempfile.gettempdir() OUT_DIR = tempfile.gettempdir()
DEFAULT_DPI = 300 DEFAULT_DPI = 300
DESIGNATION_FONTSIZE = 42.6667
def inkscapize(svg_in, pdf_out): def inkscapize(svg_in, pdf_out):
png_out = tempfile.mktemp(suffix='.png') png_out = tempfile.mktemp(suffix='.png')
@ -24,31 +22,31 @@ def inkscapize(svg_in, pdf_out):
os.unlink(png_out) os.unlink(png_out)
def generate(template, subs, out_dir, landscape=False): def generate(request, out_dir):
""" Generate a sticker sheet. """ Generate a sticker sheet.
template: file name for the sticker template request: dict-like with the following fields:
subs: dict-like with key-value template subtitution fields sticker: mandatory. filename for the sticker
out_dir: you get it subs: mandatory. dict-like with key-value template subtitution fields
landscape: optional. defaults to False landscape: optional. defaults to False
"""
# default designation font size out_dir: you get it
subs['designation_fontsize'] = subs.get('designation_fontsize', """
DESIGNATION_FONTSIZE)
# fill in sticker details # fill in sticker details
sticker_out = tempfile.mktemp(suffix='.svg') sticker_out = tempfile.mktemp(suffix='.svg')
try: try:
with open(sticker_out, 'w') as stickout: with open(sticker_out, 'w') as stickout:
makesticker(os.path.join(out_dir, template), stickout, subs) landscape = request.get('landscape', False)
makesticker(os.path.join(out_dir, request['sticker']), stickout,
request['subs'], landscape=landscape)
# make sticker sheet # make sticker sheet
planche_out = tempfile.mktemp(suffix='.svg') planche_out = tempfile.mktemp(suffix='.svg')
with open(sticker_out, 'r') as stickin: with open(sticker_out, 'r') as stickin:
with open(planche_out, 'w') as planchout: with open(planche_out, 'w') as planchout:
makeplanche(stickin, planchout, landscape=landscape) makeplanche(stickin, planchout)
# process to printable pdf # process to printable pdf
pdf_out = tempfile.mktemp(dir=out_dir, suffix='.pdf') pdf_out = tempfile.mktemp(dir=out_dir, suffix='.pdf')

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
from string import Template
import sys
DEFAULT_SHEET_TEMPLATE = Path(__file__).parent / 'planche.svg.in'
def parse_args():
parser = argparse.ArgumentParser(
description='Make a sheet with 12 stickers')
parser.add_argument('--template',
'-t',
default=DEFAULT_SHEET_TEMPLATE,
help='path to the sheet template')
parser.add_argument('--out',
'-o',
default=sys.stdout,
type=argparse.FileType('w'),
help='output path (default: stdout)')
parser.add_argument('sticker',
type=argparse.FileType('r'),
default=sys.stdin,
nargs='?',
help='path to the sticker SVG (default: stdin)')
return parser.parse_args()
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
with open(template) as tpl:
tpl_data = tpl.read()
lines = sticker.readlines()
if lines[0].startswith('<?xml'):
lines = lines[1:]
sticker_data = ''.join(lines)
subs = {
'left0': sticker_data,
'left1': sticker_data,
'left2': sticker_data,
'left3': sticker_data,
'left4': sticker_data,
'left5': sticker_data,
'right0': sticker_data,
'right1': sticker_data,
'right2': sticker_data,
'right3': sticker_data,
'right4': sticker_data,
'right5': sticker_data,
}
out.write(Template(tpl_data).substitute(subs))
if __name__ == "__main__":
makeplanche(**vars(parse_args()))

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
import argparse
from string import Template
import sys
import typing
def parse_args():
parser = argparse.ArgumentParser(description='Fill in sticker details')
parser.add_argument('--out',
'-o',
default=sys.stdout,
type=argparse.FileType('w'),
help='output path (default: stdout)')
parser.add_argument('--dluo',
'-d',
required=True,
help='Date Limite d\'Utilisation Optimale')
parser.add_argument('--lot', '-l', required=True, help='Numéro de lot')
parser.add_argument('--quantite', '-q', required=False, help='Quantité (volume ou masse)')
parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre')
parser.add_argument('--fruit', '-f', required=False, help='Quantité de fruits')
parser.add_argument('--size', '-s', required=False, help='Masse de produit')
parser.add_argument('--landscape',
action='store_true',
help='input sticker is in landscape orientation')
parser.add_argument('sticker', help='path to the sticker template')
return parser.parse_args()
def makesticker(sticker: str, out: typing.IO, subs: dict, landscape=False):
with open(sticker) as fin:
lines = fin.readlines()
if lines[0].startswith('<?xml'):
lines = lines[1:]
sticker_data = ''.join(lines)
if landscape:
# Rotate the sticker 90 degrees
sticker_data = "<g transform=\"rotate(-90,0,0) translate(-102,0)\">{}</g>".format(sticker_data)
out.write(Template(sticker_data).substitute(subs))
if __name__ == "__main__":
args = vars(parse_args())
subs = {
'dluo': args.pop('dluo'),
'lot': args.pop('lot'),
'teneur': args.pop('teneur'),
'fruit': args.pop('fruit'),
'qty': args.pop('quantite'),
'size': args.pop('size'),
}
args['subs'] = subs
makesticker(**args)

View File

@ -1,7 +1,7 @@
# Generated by Django 5.2.4 on 2025-08-05 12:57 # Generated by Django 4.2.2 on 2023-07-03 14:37
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -18,7 +18,6 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)), ('name', models.CharField(max_length=50)),
('landscape', models.BooleanField()), ('landscape', models.BooleanField()),
('prototempalte', models.FileField(upload_to='')),
], ],
options={ options={
'verbose_name_plural': 'tikategoriez', 'verbose_name_plural': 'tikategoriez',
@ -31,41 +30,22 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=50)), ('name', models.CharField(max_length=50)),
('descritpion', models.TextField()), ('descritpion', models.TextField()),
('default', models.TextField()), ('default', models.TextField()),
('type', models.CharField(choices=[('ST', 'Short Text'), ('LT', 'Long Text'), ('C', 'Color'), ('YM', 'Year Month'), ('B', 'Boolean')], default='ST', max_length=2)),
], ],
options={ options={
'verbose_name_plural': 'tisubz', 'verbose_name_plural': 'tisubz',
}, },
), ),
migrations.CreateModel(
name='Tizer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.CharField()),
],
options={
'verbose_name_plural': 'tizerz',
},
),
migrations.CreateModel( migrations.CreateModel(
name='Tikette', name='Tikette',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)), ('title', models.CharField(max_length=100)),
('designation', models.CharField(max_length=100)), ('svg', models.FileField(upload_to='')),
('ingredients', models.TextField()),
('description', models.TextField()),
('color', models.CharField(max_length=6)),
('ab', models.CharField(choices=[('inline', 'Visible'), ('none', 'Invisible')], max_length=7)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tikette.tikategory')), ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tikette.tikategory')),
('subs', models.ManyToManyField(to='tikette.tisub')),
], ],
options={ options={
'verbose_name_plural': 'tikettz', 'verbose_name_plural': 'tikettz',
}, },
), ),
migrations.AddField(
model_name='tikategory',
name='subs',
field=models.ManyToManyField(to='tikette.tisub'),
),
] ]

View File

@ -1,18 +1,9 @@
from django.db import models from django.db import models
class Tisub(models.Model): class Tisub(models.Model):
class Type(models.TextChoices):
SHORT_TEXT = "ST"
LONG_TEXT = "LT"
COLOR = "C"
YEAR_MONTH = "YM"
BOOLEAN = "B"
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
descritpion = models.TextField() descritpion = models.TextField()
default = models.TextField() default = models.TextField()
# everything is text. type is for UX (e.g. color picker)
type = models.CharField(max_length=2, choices=Type, default=Type.SHORT_TEXT)
def __str__(self): def __str__(self):
return self.name return self.name
@ -25,10 +16,6 @@ class Tikategory(models.Model):
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
landscape = models.BooleanField() landscape = models.BooleanField()
subs = models.ManyToManyField(Tisub) subs = models.ManyToManyField(Tisub)
# For now we'll hardcode the following:xi
# [designation, ingredients, description, color, AB, designation_fontsize]
#protosubs = models.ManyToManyField(Tisub, related_name="protosubs")
prototempalte = models.FileField()
def __str__(self): def __str__(self):
return self.name return self.name
@ -38,19 +25,9 @@ class Tikategory(models.Model):
class Tikette(models.Model): class Tikette(models.Model):
class AbVisibility(models.TextChoices):
VISIBLE = "inline"
INVISIBLE = "none"
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
category = models.ForeignKey(Tikategory, on_delete=models.CASCADE) category = models.ForeignKey(Tikategory, on_delete=models.CASCADE)
svg = models.FileField()
designation = models.CharField(max_length=100)
ingredients = models.TextField()
description = models.TextField()
color = models.CharField(max_length=6)
ab = models.CharField(max_length=7, choices=AbVisibility)
# designation_fontsize is hardcoded to 42.6667 in planche/generate.py
def __str__(self): def __str__(self):
return self.title return self.title
@ -58,12 +35,4 @@ class Tikette(models.Model):
class Meta: class Meta:
verbose_name_plural = "tikettz" verbose_name_plural = "tikettz"
# Create your models here.
class Tizer(models.Model):
email = models.CharField()
def __str__(self):
return self.email
class Meta:
verbose_name_plural = "tizerz"

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 210 297"
height="297mm"
width="210mm">
<g transform="translate(3, 4.5)">
<g transform="translate(-42,144) rotate(-90,144,0)">
<g transform="translate( 0,0)">$right0</g>
<g transform="translate( 48,0)">$right1</g>
<g transform="translate( 96,0)">$right2</g>
<g transform="translate(144,0)">$right3</g>
<g transform="translate(192,0)">$right4</g>
<g transform="translate(240,0)">$right5</g>
</g>
<g transform="translate(-42,144) rotate(90,144,0)">
<g transform="translate( 0,0)">$left0</g>
<g transform="translate( 48,0)">$left1</g>
<g transform="translate( 96,0)">$left2</g>
<g transform="translate(144,0)">$left3</g>
<g transform="translate(192,0)">$left4</g>
<g transform="translate(240,0)">$left5</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 896 B

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 210 297"
height="297mm"
width="210mm">
<g transform="translate(3, 4.5)">
<g transform="translate(0, 0) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 48) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 96) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 144) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 192) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 240) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="rotate(180, 102, 144)">
<g transform="translate(0, 0) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 48) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 96) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 144) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 192) ${rotate} scale(0.26458)">${sticker}</g>
<g transform="translate(0, 240) ${rotate} scale(0.26458)">${sticker}</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -5,83 +5,27 @@ from django.http import JsonResponse
from django.shortcuts import render from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from .planche import generate as stickersheet from . import generate as stickersheet
from .models import Tikette, Tikategory from .models import Tikette, Tikategory
CORS={'access-control-allow-origin': '*'} CORS={'access-control-allow-origin': '*'}
def index(request):
def quirk_bold_allergens(ingredients):
out = []
for ing in (x.strip() for x in ingredients.split(',')):
if ing.startswith('*'):
out.append(f'<tspan style="font-weight:bold">{ing[1:]}</tspan>')
else:
out.append(ing)
return ", ".join(out)
def get_list(request):
tikettes = [{ tikettes = [{
'id': x.id,
'title': x.title, 'title': x.title,
'category': x.category.name, 'category': x.category.name,
'prototempalte': x.category.prototempalte.name, 'sticker': x.svg.name,
'landscape': x.category.landscape, 'landscape': x.category.landscape,
'designation': x.designation,
'ingredients': quirk_bold_allergens(x.ingredients),
'description': x.description,
'ab': x.ab,
'color': x.color,
'subs': {x.name: x.default for x in x.category.subs.all()}, 'subs': {x.name: x.default for x in x.category.subs.all()},
} for x in Tikette.objects.all()] } for x in Tikette.objects.all()]
return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS) return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS)
def get_categories(request):
tikats = [{
'id': x.id,
'name': x.name,
} for x in Tikategory.objects.all()]
return JsonResponse({'status': 'ok', 'tikats': tikats}, headers=CORS)
@csrf_exempt @csrf_exempt
def generate(request): def generate(request):
if request.method != "POST": if request.method != "POST":
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'}) return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
payload = json.loads(request.body) payload = json.loads(request.body)
pdfpath = stickersheet.generate(payload, out_dir=settings.TIKETTE_OUT_DIR)
subs = dict(payload['subs'])
for key in ('designation', 'ingredients', 'description', 'color', 'AB'):
subs[key] = payload[key]
pdfpath = stickersheet.generate(template=payload['template'], subs=subs,
out_dir=settings.TIKETTE_OUT_DIR,
landscape=payload['landscape'])
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS) return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)
@csrf_exempt
def newtikette(request):
if request.method != "POST":
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
payload = json.loads(request.body)
tikette = Tikette(**payload)
tikette.save()
return JsonResponse({'status': 'ok'}, headers=CORS)
@csrf_exempt
def deletetikette(request):
if request.method != "POST":
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
payload = json.loads(request.body)
Tikette.objects.get(id=payload["id"]).delete()
return JsonResponse({'status': 'ok'}, headers=CORS)

View File

@ -24,9 +24,6 @@ import tikette.views
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('list', tikette.views.get_list), path('list', tikette.views.index),
path('categories', tikette.views.get_categories), path('', tikette.views.generate),
path('generate', tikette.views.generate),
path('newtikette', tikette.views.newtikette),
path('deletetikette', tikette.views.deletetikette),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -3,8 +3,7 @@ version: '3.1'
services: services:
zetikettes: zetikettes:
build: . image: zetikettes
image: pol/zetikettes
restart: always restart: always
ports: ports:
- 127.0.0.1:8000:8000 - 127.0.0.1:8000:8000

View File

@ -1,14 +0,0 @@
[Unit]
Description=%i service with docker compose
PartOf=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/etc/docker/compose/%i
ExecStart=/usr/bin/docker-compose up -d --remove-orphans
ExecStop=/usr/bin/docker-compose down
[Install]
WantedBy=multi-user.target

View File

@ -1 +1 @@
const backend_api = 'http://localhost:8000/' const backend_api = 'http://jenova.ponteilla.net:8001/'

View File

@ -3,19 +3,15 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zětikwett's</title> <title>zetikettes 0.2</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!--Import Google Icon Font-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Compiled and minified CSS --> <!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link rel="stylesheet" href="placeholder.css">
<!-- Compiled and minified JavaScript --> <!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script src="jscolor.min.js"></script>
<script src="config.js"></script> <script src="config.js"></script>
<script src="zetikettes.js"></script> <script src="zetikettes.js"></script>
@ -32,70 +28,15 @@ main {
</style> </style>
</head> </head>
<body> <body>
<nav class="light-blue"> <header class="container center orange-text">
<div class="nav-wrapper container"> <h1>Zétikwett's</h1>
<a href="#" class="brand-logo">Zětikwett's</a> </header>
<ul id="nav-mobile" class="right hide-on-med-and-down">
<li>
<a class="modal-trigger btn orange" href="#newproduct">
<i class="material-icons left">add</i>Nouveau produit
</a>
</li>
</ul>
</div>
</nav>
<main class="container row"> <main class="container row">
<p></p>
<div class="col m6 offset-m3 s12"> <div class="col m6 offset-m3 s12">
<a class="modal-trigger btn orange hide-on-large-only" href="#newproduct"><i class="material-icons left">add</i>Nouveau produit</a> <ul class="collapsible" id="appbody"></ul>
<ul class="collapsible" id="appbody">
<div class="shimmer">
<li><div class="collapsible-header"><h6 class="faux-text short"></h6></div></li>
<li><div class="collapsible-header"><h6 class="faux-text"></h6></div></li>
<li><div class="collapsible-header"><h6 class="faux-text shorter"></h6></div></li>
</div>
</ul>
</div>
<!-- Modal Structure -->
<div id="newproduct" class="modal">
<div class="modal-content">
<div class="input-field">
<input id="new-name" type="text">
<label>Nom interne du produit</label>
</div>
<div class="input-field">
<select id="new-type">
</select>
<label>Type de produit</label>
</div>
<div class="input-field">
<input id="new-designation" type="text">
<label>Désignation</label>
</div>
<div class="input-field">
<textarea id="new-ingredients" class="materialize-textarea"></textarea>
<label>Ingrédients</label>
</div>
<div class="input-field">
<textarea id="new-description" class="materialize-textarea"></textarea>
<label>Description</label>
</div>
<div class="input-field">
<input value="#97A1CC" data-jscolor="{previewSize: 0}" id="new-color" type="text">
<label>Couleur</label>
</div>
<label>
<input type="checkbox" id="new-organic">
<span>Bio</span>
</label>
<div class="modal-footer">
<button type="submit" class="modal-close waves-effect waves-green btn" id="new-add">Ajouter</button>
</div>
</div>
</div> </div>
</main> </main>
<footer class="page-footer orange"> <footer class="page-footer">
<div class="container row"> <div class="container row">
<span class="col right">displayed with recycled electrons.</span> <span class="col right">displayed with recycled electrons.</span>
</div> </div>

File diff suppressed because one or more lines are too long

View File

@ -1,47 +0,0 @@
/*
* Read the blog post here:
* https://letsbuildui.dev/articles/how-to-build-a-skeleton-loading-placeholder
*/
.faux-text {
background: #dddddd;
border-radius: 4px;
height: 20px;
margin-bottom: 5px;
width: 100%;
}
.faux-text.short {
width: 75%;
}
.faux-text.shorter {
width: 55%;
}
.shimmer {
overflow: hidden;
position: relative;
}
.shimmer::before {
content: "";
position: absolute;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.4) 50%,
rgba(255, 255, 255, 0) 100%
);
height: 100%;
width: 100%;
z-index: 1;
animation: shimmer 1s infinite;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}

View File

@ -1,16 +1,15 @@
const params = { const params = {
'dluo': 'DLUO', 'dluo': 'DLUO',
'lot': 'Nº de lot', 'lot': 'Nº de lot',
'q': 'Poids net (g)', 'qty': 'Poids net (g)',
'vol': 'Volume net (cL)',
'teneur': 'Teneur en fruits (%)', 'teneur': 'Teneur en fruits (%)',
'f': 'Quantité de fruits pour 100g (g)', 'fruit': 'Quantité de fruits pour 100g (g)',
} }
var tikats; function loadAll(zetikettes) {
function addProduct(tikette) {
const zett = tikette;
const appbody = $("#appbody"); const appbody = $("#appbody");
for (let zett of zetikettes) {
const block = $('<div class="section">'); const block = $('<div class="section">');
for (let sub in zett.subs) { for (let sub in zett.subs) {
block.append($(`<div class="input-field"><label class="active">${params[sub]}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`)); block.append($(`<div class="input-field"><label class="active">${params[sub]}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`));
@ -25,12 +24,7 @@ function addProduct(tikette) {
.toArray() .toArray()
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {}); .reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
const req = { const req = {
template: zett.prototempalte, sticker: zett.sticker,
designation: zett.designation,
description: zett.description,
ingredients: zett.ingredients,
color: zett.color,
AB: zett.ab, // mind the capitalization here
subs, subs,
landscape: zett.landscape, landscape: zett.landscape,
}; };
@ -38,13 +32,14 @@ function addProduct(tikette) {
loader.show(); loader.show();
$('.btn').addClass("disabled"); $('.btn').addClass("disabled");
$.post(backend_api + 'generate', JSON.stringify(req)) $.post(backend_api, JSON.stringify(req))
.then(data => { .then(data => {
console.log(data);
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`); const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
action.append(pdfbtn); action.append(pdfbtn);
}) })
.catch(err => { .catch(err => {
console.error(err); console.log(err);
}) })
.always(() => { .always(() => {
loader.hide(); loader.hide();
@ -53,70 +48,17 @@ function addProduct(tikette) {
}) })
.append(loader)); .append(loader));
const deleteAction = $('<a class="btn-flat grey-text"><b class="material-icons">delete</b>');
deleteAction.click(() => {
const req = {
id: zett.id,
};
$.post(backend_api + 'deletetikette', JSON.stringify(req)).then(reload);
return false;
});
appbody appbody
.append($('<li>') .append($('<li>')
.append($('<div class="collapsible-header valign-wrapper">') .append($(`<div class="collapsible-header"><h6 class="blue-text">${zett.title}</h6></div>`))
.append(`<h6 class="blue-text">${zett.title}</h6>`)
.append($('<span class="badge">')
.append(deleteAction)))
.append($('<div class="collapsible-body">') .append($('<div class="collapsible-body">')
.append(block) .append(block)
.append(action))); .append(action)));
} }
function setCategories() {
const katsel = $('#new-type');
katsel.empty();
for (let kat of tikats) {
katsel.append($(`<option value="${kat.id}">${kat.name}</option>`));
}
}
function loadAll(zetikettes) {
const appbody = $("#appbody");
appbody.empty();
for (let zett of zetikettes) {
addProduct(zett);
}
setCategories();
$('.collapsible').collapsible(); $('.collapsible').collapsible();
$('.modal').modal();
$('select').formSelect();
konami(); konami();
$('#new-add').off('click').click(() => {
const title = $("#new-name").val();
const category_id = $("#new-type").val();
const designation = $("#new-designation").val();
const ingredients = $("#new-ingredients").val();
const description = $("#new-description").val();
const color = $("#new-color").val().substring(1);
const ab = $("#new-organic").is(":checked") ? 'visible' : 'none';
const req = {
title,
category_id,
designation,
ingredients,
description,
color,
ab,
};
$.post(backend_api + 'newtikette', JSON.stringify(req)).then(reload);
});
} }
function konami() { function konami() {
@ -134,22 +76,16 @@ function konami() {
}); });
} }
async function reload() { $(document).ready(async () => {
try { try {
const resp = await $.ajax({ const resp = await $.ajax({
url: backend_api + 'list', url: backend_api + 'list',
timeout: 1000, timeout: 1000,
}); });
tikats = (await $.ajax({
url: backend_api + 'categories',
timeout: 1000,
})).tikats.sort((a, b) => a.name > b.name ? 1 : -1);
loadAll(resp.tikettes.sort((a, b) => (a.title < b.title) ? -1 : 1)); loadAll(resp.tikettes.sort((a, b) => (a.title < b.title) ? -1 : 1));
} catch(e) { } catch(e) {
const appbody = $("#appbody"); const appbody = $("#appbody");
appbody.append(`<li>Could not reach backend server`); appbody.append(`<li>Could not reach backend server`);
throw e; throw e;
} }
} });
$(document).ready(reload);

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.3 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.3 MiB

After

Width:  |  Height:  |  Size: 5.3 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

16
zetikettes.service Normal file
View File

@ -0,0 +1,16 @@
[Unit]
Description=Zetikettes backend service
After=docker.service
Requires=docker.service
[Service]
ExecStart=/usr/bin/docker run --rm --name %n \
-p 127.0.0.1:8000:8000 \
-v /var/lib/zetikettes/data:/data \
zetikettes
Restart=on-failure
ExecStartPre=-/usr/bin/docker exec %n stop
ExecStartPre=-/usr/bin/docker rm %n
[Install]
WantedBy=multi-user.target