Compare commits

..

3 Commits

Author SHA1 Message Date
3de3586fe0 Fǒnt update 2025-08-05 18:52:19 +02:00
9b6811c410 Mets des écaillěs 2025-08-05 17:38:29 +02:00
ebe9516557 Voici les Blěktřs pour schtroumpfer les bidules. 2025-07-27 19:26:27 +02:00
48 changed files with 1757 additions and 688 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) scale(0.26458)">$right0</g>
<g transform="translate( 48,0) scale(0.26458)">$right1</g>
<g transform="translate( 96,0) scale(0.26458)">$right2</g>
<g transform="translate(144,0) scale(0.26458)">$right3</g>
<g transform="translate(192,0) scale(0.26458)">$right4</g>
<g transform="translate(240,0) scale(0.26458)">$right5</g>
</g>
<g transform="translate(-42,144) rotate(90,144,0)">
<g transform="translate( 0,0) scale(0.26458)">$left0</g>
<g transform="translate( 48,0) scale(0.26458)">$left1</g>
<g transform="translate( 96,0) scale(0.26458)">$left2</g>
<g transform="translate(144,0) scale(0.26458)">$left3</g>
<g transform="translate(192,0) scale(0.26458)">$left4</g>
<g transform="translate(240,0) scale(0.26458)">$left5</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

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>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,122 +1,64 @@
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 addProduct(tikette) {
const zett = tikette;
const appbody = $("#appbody");
const block = $('<div class="section">');
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]}">`));
}
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
.hide();
const action = $('<div class="section">')
.append($('<a class="btn">generate<a>')
.click(() => {
const subs = block.find(':text')
.toArray()
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
const req = {
template: zett.prototempalte,
designation: zett.designation,
description: zett.description,
ingredients: zett.ingredients,
color: zett.color,
AB: zett.ab, // mind the capitalization here
subs,
landscape: zett.landscape,
};
loader.show();
$('.btn').addClass("disabled");
$.post(backend_api + 'generate', JSON.stringify(req))
.then(data => {
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
action.append(pdfbtn);
})
.catch(err => {
console.error(err);
})
.always(() => {
loader.hide();
$('.btn').removeClass('disabled');
});
})
.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
.append($('<li>')
.append($('<div class="collapsible-header valign-wrapper">')
.append(`<h6 class="blue-text">${zett.title}</h6>`)
.append($('<span class="badge">')
.append(deleteAction)))
.append($('<div class="collapsible-body">')
.append(block)
.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) { function loadAll(zetikettes) {
const appbody = $("#appbody"); const appbody = $("#appbody");
appbody.empty();
for (let zett of zetikettes) { for (let zett of zetikettes) {
addProduct(zett); const block = $('<div class="section">');
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]}">`));
}
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
.hide();
const action = $('<div class="section">')
.append($('<a class="btn">generate<a>')
.click(() => {
const subs = block.find(':text')
.toArray()
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
const req = {
sticker: zett.sticker,
subs,
landscape: zett.landscape,
};
loader.show();
$('.btn').addClass("disabled");
$.post(backend_api, JSON.stringify(req))
.then(data => {
console.log(data);
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
action.append(pdfbtn);
})
.catch(err => {
console.log(err);
})
.always(() => {
loader.hide();
$('.btn').removeClass('disabled');
});
})
.append(loader));
appbody
.append($('<li>')
.append($(`<div class="collapsible-header"><h6 class="blue-text">${zett.title}</h6></div>`))
.append($('<div class="collapsible-body">')
.append(block)
.append(action)));
} }
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);

33
product_definitions.csv Normal file
View File

@@ -0,0 +1,33 @@
aromate;Herbes de Provence;Thym*, Sarriette*, Origan*;Le classique des herbes aromatiques pour donner une touche méditerranéenne à vos plats.;6307a6;Aromate - Herbes de Provence - Thym Sarriette Origan.svg;True
aromate;Origan;Origan*;La fameuse herbe à pizza, mais qui peut aussi assaisonner vos soupes, plats mijotés, poissons ou charcuteries.;4800b9;Aromate - Origan.svg;True
aromate;Sarriette;Sarriette des montagnes*;Le pèbre d'aï s'utilise en cuisine dans les pommes de terre, champignons, ragoûts de légumes, riz...;00d40e;Aromate - Sarriette.svg;True
aromate;Sauge;Sauge officinale*;La plante qui sauve peut être utilisée en cuisine, dans vos viandes en sauce, légumes, vinaigrettes...;d00676;Aromate - Sauge.svg;True
aromate;Thym;Thym*;Le roi des aromates, à utiliser en branche ou effeuillé, pour agrémenter vos grillades, plats en sauce, poêlées...;0048ff;Aromate - Thym.svg;True
confiture;Confiture d'Abricot;Abricot*, Sucre de canne*;;ff5d00;Confiture - Abricot.svg;True
confiture;Confiture d'Abricot;Abricot, Sucre de canne*;;ff5d00;Confiture - Abricot - Non Bio.svg;False
confiture;Confiture d'Abricot Lavande;Abricot, Lavande*, Sucre de canne*;;9683ec;Confiture - Abricot Lavande - Non Bio.svg;False
confiture;Gelée Extra de Groseille;Groseille*, Sucre de canne*;;ec001a;Gelée Extra - Groseille.svg;True
confiture;Gelée Extra de Cassis;Cassis*, Sucre de canne*;;9b00ff;Gelée Extra - Cassis.svg;True
confiture;Marmelade d'Orange Amère; Oranges amères, Sucre de canne*, eau;;ff5d00;Marmelade - Orange Amère - Non Bio.svg;False
pesto;Pesto de Livèche;Huile d'olive*, Livèche* (34%), *amandes*, jus de citron*, ail*, sel;;119200;Pesto - Livèche.svg;True
pesto;Pesto d'Ail des Ours;Huile d'olive*, Ail des ours* (34%), *Amandes*, Jus de citron*, Sel;;196b00;Pesto - Ail des Ours.svg;True
sirop;Lavande;Sucre* (55%), Infusion de lavande* (42%), Jus de citron* (3%);;4200ff;Sirop - Lavande.svg;True
sirop;Thym;Sucre* (55%), Infusion de thym* (42%), Jus de citron* (3%);;0048ff;Sirop - Thym.svg;True
sirop;Mélisse;Sucre* (55%), Infusion de mélisse* (42%), Jus de citron* (3%);;d00676;Sirop - Mélisse.svg;True
sirop;Menthe Verte;Sucre* (55%), Infusion de menthe verte* (42%), Jus de citron* (3%);;007e49;Sirop - Menthe Verte.svg;True
sirop;Menthe et Mélisse;Sucre* (55%), Infusion de menthe verte* et mélisse* (42%), Jus de citron* (3%);;fabf12;Sirop - Menthe et Mélisse.svg;True
sirop;Sureau;Sucre* (55%), Infusion de sureau* (42%), Jus de citron* (3%);;8aa700;Sirop - Sureau.svg;True
sirop;Romarin;Sucre* (55%), Infusion de romarin* (42%), Jus de citron* (3%);;007d87;Sirop - Romarin.svg;True
tisane;J'ai Bien Mangé...; Thym*, Sauge*, Romarin*, Soucis*;;0c7c00;Tisane - Digestion - Thym Sauge Romarin Soucis.svg;True
tisane;Nuit Étoilée;Agastache*, Mélisse*, Aubépine*;;5b7aff;Tisane - Nuit Étoilée - Agastache Mélisse Aubépine.svg;True
tisane;Nuit Étoilée;Mélisse*, Lavande*, Aubépine*;;5b7aff;Tisane - Nuit Étoilée - Mélisse Lavande Aubépine.svg;True
tisane;Réconfort de la Gorge; Origan*, Agastache*, Thym*, Bleuet*;;00a07b;Tisane - Réconfort de la Gorge - Origan Agastache Thym Bleuet.svg;True
tisane;Équilibre Féminin;Achillée millefeuille*, Lavande vraie*, Cynorhodon*;;64139f;Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Cynorhodon.svg;True
tisane;Équilibre Féminin;Achillée millefeuille*, Lavande vraie*, Ortie piquante*;;64139f;Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Ortie piquante.svg;True
tisane;Équilibre Féminin;Achillée millefeuille*, Lavande vraie*, Pétales de Cynorhodon*;;64139f;Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Pétales de Cynorhodon.svg;True
tisane;Équilibre Féminin;Achillée millefeuille*, Lavande vraie*, Sauge officinale*, Ortie piquante*;;64139f;Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Sauge officinale Ortie piquante.svg;True
tisane;Joie de Vivre;Basilic sacré*, Sarriette des montagnes*, Lavande vraie*;;ff6d00;Tisane - Joie de Vivre - Basilic Sacré Sarriette des Montagnes Lavande Vraie.svg;True
sel;Grillades - Viande et Légumes;Sel de Camargue, Thym*, Origan*, Sarriette*, Mélisse*, Souci*;;c80003;Sel - Grillades.svg;True
sel;Herbes de Provence;Sel de Camargue, Thym*, Origan*, Sarriette*, Romarin*;;6307a6;Sel - Herbes de Provence.svg;True
sel;Poisson et Viande Blanche;Sel de Camargue, Thym*, Sauge*, Agastache*, Bleuet*;;5b7aff;Sel - Poisson et Viande Blanche.svg;True
chocolat;Menthe Poivrée;"Chocolat de couverture noir* (pâte de cacao*, sucre de canne*, beurre de cacao*; peut contenir : *lait), *crème entière* (crème de lait à 30% de matière grasse*, stabilisants : carraghénanes), menthe poivrée*";;007e49;Chocolat - Menthe Poivrée.svg;True
1 aromate Herbes de Provence Thym*, Sarriette*, Origan* Le classique des herbes aromatiques pour donner une touche méditerranéenne à vos plats. 6307a6 Aromate - Herbes de Provence - Thym Sarriette Origan.svg True
2 aromate Origan Origan* La fameuse herbe à pizza, mais qui peut aussi assaisonner vos soupes, plats mijotés, poissons ou charcuteries. 4800b9 Aromate - Origan.svg True
3 aromate Sarriette Sarriette des montagnes* Le pèbre d'aï s'utilise en cuisine dans les pommes de terre, champignons, ragoûts de légumes, riz... 00d40e Aromate - Sarriette.svg True
4 aromate Sauge Sauge officinale* La plante qui sauve peut être utilisée en cuisine, dans vos viandes en sauce, légumes, vinaigrettes... d00676 Aromate - Sauge.svg True
5 aromate Thym Thym* Le roi des aromates, à utiliser en branche ou effeuillé, pour agrémenter vos grillades, plats en sauce, poêlées... 0048ff Aromate - Thym.svg True
6 confiture Confiture d'Abricot Abricot*, Sucre de canne* ff5d00 Confiture - Abricot.svg True
7 confiture Confiture d'Abricot Abricot, Sucre de canne* ff5d00 Confiture - Abricot - Non Bio.svg False
8 confiture Confiture d'Abricot Lavande Abricot, Lavande*, Sucre de canne* 9683ec Confiture - Abricot Lavande - Non Bio.svg False
9 confiture Gelée Extra de Groseille Groseille*, Sucre de canne* ec001a Gelée Extra - Groseille.svg True
10 confiture Gelée Extra de Cassis Cassis*, Sucre de canne* 9b00ff Gelée Extra - Cassis.svg True
11 confiture Marmelade d'Orange Amère Oranges amères, Sucre de canne*, eau ff5d00 Marmelade - Orange Amère - Non Bio.svg False
12 pesto Pesto de Livèche Huile d'olive*, Livèche* (34%), *amandes*, jus de citron*, ail*, sel 119200 Pesto - Livèche.svg True
13 pesto Pesto d'Ail des Ours Huile d'olive*, Ail des ours* (34%), *Amandes*, Jus de citron*, Sel 196b00 Pesto - Ail des Ours.svg True
14 sirop Lavande Sucre* (55%), Infusion de lavande* (42%), Jus de citron* (3%) 4200ff Sirop - Lavande.svg True
15 sirop Thym Sucre* (55%), Infusion de thym* (42%), Jus de citron* (3%) 0048ff Sirop - Thym.svg True
16 sirop Mélisse Sucre* (55%), Infusion de mélisse* (42%), Jus de citron* (3%) d00676 Sirop - Mélisse.svg True
17 sirop Menthe Verte Sucre* (55%), Infusion de menthe verte* (42%), Jus de citron* (3%) 007e49 Sirop - Menthe Verte.svg True
18 sirop Menthe et Mélisse Sucre* (55%), Infusion de menthe verte* et mélisse* (42%), Jus de citron* (3%) fabf12 Sirop - Menthe et Mélisse.svg True
19 sirop Sureau Sucre* (55%), Infusion de sureau* (42%), Jus de citron* (3%) 8aa700 Sirop - Sureau.svg True
20 sirop Romarin Sucre* (55%), Infusion de romarin* (42%), Jus de citron* (3%) 007d87 Sirop - Romarin.svg True
21 tisane J'ai Bien Mangé... Thym*, Sauge*, Romarin*, Soucis* 0c7c00 Tisane - Digestion - Thym Sauge Romarin Soucis.svg True
22 tisane Nuit Étoilée Agastache*, Mélisse*, Aubépine* 5b7aff Tisane - Nuit Étoilée - Agastache Mélisse Aubépine.svg True
23 tisane Nuit Étoilée Mélisse*, Lavande*, Aubépine* 5b7aff Tisane - Nuit Étoilée - Mélisse Lavande Aubépine.svg True
24 tisane Réconfort de la Gorge Origan*, Agastache*, Thym*, Bleuet* 00a07b Tisane - Réconfort de la Gorge - Origan Agastache Thym Bleuet.svg True
25 tisane Équilibre Féminin Achillée millefeuille*, Lavande vraie*, Cynorhodon* 64139f Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Cynorhodon.svg True
26 tisane Équilibre Féminin Achillée millefeuille*, Lavande vraie*, Ortie piquante* 64139f Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Ortie piquante.svg True
27 tisane Équilibre Féminin Achillée millefeuille*, Lavande vraie*, Pétales de Cynorhodon* 64139f Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Pétales de Cynorhodon.svg True
28 tisane Équilibre Féminin Achillée millefeuille*, Lavande vraie*, Sauge officinale*, Ortie piquante* 64139f Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Sauge officinale Ortie piquante.svg True
29 tisane Joie de Vivre Basilic sacré*, Sarriette des montagnes*, Lavande vraie* ff6d00 Tisane - Joie de Vivre - Basilic Sacré Sarriette des Montagnes Lavande Vraie.svg True
30 sel Grillades - Viande et Légumes Sel de Camargue, Thym*, Origan*, Sarriette*, Mélisse*, Souci* c80003 Sel - Grillades.svg True
31 sel Herbes de Provence Sel de Camargue, Thym*, Origan*, Sarriette*, Romarin* 6307a6 Sel - Herbes de Provence.svg True
32 sel Poisson et Viande Blanche Sel de Camargue, Thym*, Sauge*, Agastache*, Bleuet* 5b7aff Sel - Poisson et Viande Blanche.svg True
33 chocolat Menthe Poivrée Chocolat de couverture noir* (pâte de cacao*, sucre de canne*, beurre de cacao*; peut contenir : *lait), *crème entière* (crème de lait à 30% de matière grasse*, stabilisants : carraghénanes), menthe poivrée* 007e49 Chocolat - Menthe Poivrée.svg True

65
scripts/renew_lineup.py Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
import argparse
from string import Template
import sys
import os
import csv
from subber import Subber
TEMPLATES_DIR = "/home/maxime/Documents/Anne/Etiquettes/Source SVGs/templates"
OUT_DIR = "/home/maxime/Documents/Anne/Etiquettes/Source SVGs/autogenerated_svgs"
JAM_DESIGNATION_FONTSIZE_DEFAULT = 42.6667
JAM_DESIGNATION_FONTSIZE_SMALL = 36
templates = {
'aromate': f"{TEMPLATES_DIR}/Aromate.svg",
'chocolat': f"{TEMPLATES_DIR}/Chocolat.svg",
'confiture': f"{TEMPLATES_DIR}/Confiture.svg",
'pesto': f"{TEMPLATES_DIR}/Pesto.svg",
'sirop': f"{TEMPLATES_DIR}/Sirop.svg",
'tisane': f"{TEMPLATES_DIR}/Tisane.svg",
'sel': f"{TEMPLATES_DIR}/Sel.svg",
}
ALLERGEN_BEGIN_STYLE = '<tspan style="font-weight:bold">'
ALLERGEN_END_STYLE = '</tspan>'
def parse_args():
parser = argparse.ArgumentParser(description='Renew whole lineup from template and list of subs')
parser.add_argument('--list', required=True, help='Lineup file')
return parser.parse_args()
def main():
args = parse_args()
with open(args.list) as csvfile:
reader = csv.reader(csvfile, delimiter=';', quotechar='"')
for row in reader:
template = templates[row[0]]
outfile = f"{OUT_DIR}/{row[5]}"
with open(outfile, 'w') as out:
# TODO Fix bold formatting with allergen and parenthesis
ingredients = [e.strip() for e in row[2].split(',')]
ingredients = [e if not e.startswith('*') else ALLERGEN_BEGIN_STYLE + e[1:] + ALLERGEN_END_STYLE for e in ingredients]
ingredients_sub = ", ".join(ingredients)
AB_logo_visibility = 'inline' if row[6] == 'True' else 'none'
subs = {
'designation': row[1].strip(),
'ingredients': ingredients_sub,
'description': row[3].strip(),
'color': row[4],
'AB': AB_logo_visibility,
'designation_fontsize': JAM_DESIGNATION_FONTSIZE_DEFAULT,
}
s = Subber(subs)
s.sub(template, out)
if __name__ == "__main__":
main()

14
scripts/subber.py Normal file
View File

@@ -0,0 +1,14 @@
from string import Template
class Subber():
def __init__(self, subs):
self.subs = subs
def sub(self, infile, outfile):
with open(infile) as template:
lines = template.readlines()
data = ''.join(lines)
outfile.write(Template(data).safe_substitute(self.subs))

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 MiB

View File

@@ -5,7 +5,7 @@
viewBox="0 0 102 48.000001" viewBox="0 0 102 48.000001"
version="1.1" version="1.1"
id="svg8" id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" inkscape:version="1.2 (dc2aeda, 2022-05-15)"
sodipodi:docname="Pesto.svg" sodipodi:docname="Pesto.svg"
inkscape:export-filename="/home/maxime/Documents/Anne/Etiquettes/Bitmap Images/Pesto - Ail des Ours.png" inkscape:export-filename="/home/maxime/Documents/Anne/Etiquettes/Bitmap Images/Pesto - Ail des Ours.png"
inkscape:export-xdpi="600" inkscape:export-xdpi="600"
@@ -788,8 +788,8 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="4.1140114" inkscape:zoom="4.1140114"
inkscape:cx="163.22269" inkscape:cx="163.46576"
inkscape:cy="91.75959" inkscape:cy="91.638054"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:current-layer="layer11" inkscape:current-layer="layer11"
showgrid="false" showgrid="false"
@@ -1229,7 +1229,7 @@
id="text4193" id="text4193"
y="-5.6109309" y="-5.6109309"
x="15.053011" x="15.053011"
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1" style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
xml:space="preserve" xml:space="preserve"
inkscape:label="Email"><tspan inkscape:label="Email"><tspan
style="font-size:2.11667px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1" style="font-size:2.11667px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
@@ -1247,7 +1247,7 @@
mask="none" /> mask="none" />
<text <text
xml:space="preserve" xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1" style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
x="15.044743" x="15.044743"
y="-7.7611365" y="-7.7611365"
id="text4199" id="text4199"
@@ -1267,7 +1267,7 @@
id="text4205" id="text4205"
y="-10.030697" y="-10.030697"
x="14.999268" x="14.999268"
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1" style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
xml:space="preserve" xml:space="preserve"
inkscape:label="Adresse"><tspan inkscape:label="Adresse"><tspan
style="font-size:2.11667px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1" style="font-size:2.11667px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
@@ -1277,7 +1277,7 @@
sodipodi:role="line">Lieu-dit Lèbre, 04200, Authon</tspan></text> sodipodi:role="line">Lieu-dit Lèbre, 04200, Authon</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:1.76389px;line-height:0;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1" style="font-style:normal;font-weight:normal;font-size:1.76389px;line-height:0;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
x="15.003401" x="15.003401"
y="-12.237692" y="-12.237692"
id="text4211" id="text4211"
@@ -1299,7 +1299,7 @@
<flowRoot <flowRoot
inkscape:label="Culture a la main" inkscape:label="Culture a la main"
transform="matrix(0.26458212,0,0,0.26458212,-107.34655,179.95759)" transform="matrix(0.26458212,0,0,0.26458212,-107.34655,179.95759)"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16.0001px;line-height:0;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;letter-spacing:0px;word-spacing:-1px;text-anchor:middle;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00001" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16.0001px;line-height:0;font-family:Droid Sans Fallback;-inkscape-font-specification:Droid Sans Fallback;text-align:center;letter-spacing:0px;word-spacing:-1px;text-anchor:middle;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00001"
id="flowRoot1405" id="flowRoot1405"
xml:space="preserve"><flowRegion xml:space="preserve"><flowRegion
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:190.101px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;word-spacing:-1px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke-width:1.00001" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:190.101px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;word-spacing:-1px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke-width:1.00001"
@@ -1316,7 +1316,7 @@
id="flowPara31669">dans les Alpes de Haute-Provence</flowPara></flowRoot> id="flowPara31669">dans les Alpes de Haute-Provence</flowPara></flowRoot>
<text <text
xml:space="preserve" xml:space="preserve"
style="font-size:2.46944px;line-height:1.5434px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#000000;fill-opacity:1;stroke-width:0.264583" style="font-size:2.46944px;line-height:1.5434px;font-family:Droid Sans Fallback;-inkscape-font-specification:Droid Sans Fallback;text-align:center;text-anchor:middle;fill:#000000;fill-opacity:1;stroke-width:0.264583"
x="13.274965" x="13.274965"
y="246.99561" y="246.99561"
id="text31972" id="text31972"
@@ -1339,7 +1339,7 @@
<flowRoot <flowRoot
inkscape:label="Texte Quantité et Lot" inkscape:label="Texte Quantité et Lot"
transform="matrix(0.26458212,0,0,0.26458212,-10.572926,-21.404941)" transform="matrix(0.26458212,0,0,0.26458212,-10.572926,-21.404941)"
style="font-style:normal;font-weight:normal;font-size:5.33336px;line-height:0px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00001" style="font-style:normal;font-weight:normal;font-size:5.33336px;line-height:0px;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00001"
id="flowRoot82745" id="flowRoot82745"
xml:space="preserve"><flowRegion xml:space="preserve"><flowRegion
style="line-height:0px;stroke-width:1.00001" style="line-height:0px;stroke-width:1.00001"
@@ -1351,31 +1351,31 @@
width="30.898319" width="30.898319"
id="rect82729" /></flowRegion><flowPara id="rect82729" /></flowRegion><flowPara
id="flowPara82733" id="flowPara82733"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:6px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:6px;font-family:Droid Sans Fallback;-inkscape-font-specification:Droid Sans Fallback;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
id="flowPara82735" id="flowPara82735"
style="line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara style="line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
id="flowPara82739" id="flowPara82739"
style="line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara style="line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
id="flowPara82741" id="flowPara82741"
style="font-weight:bold;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara style="font-weight:bold;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
id="flowPara82743">POIDS NET :</flowPara><flowPara id="flowPara82743">POIDS NET :</flowPara><flowPara
style="font-weight:bold;font-size:8.00004px;line-height:6.88px;text-align:center;text-anchor:middle;stroke-width:1.00001" style="font-weight:bold;font-size:8.00004px;line-height:6.88px;text-align:center;text-anchor:middle;stroke-width:1.00001;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
id="flowPara83405">${q}g</flowPara><flowPara id="flowPara83405">${q}g</flowPara><flowPara
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
id="flowPara83420">LOT $lot</flowPara><flowPara id="flowPara83420">LOT $lot</flowPara><flowPara
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
id="flowPara83411" /></flowRoot> id="flowPara83411" /></flowRoot>
</g> </g>
<text <text
xml:space="preserve" xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,0,1.0617673)" transform="matrix(0.26458333,0,0,0.26458333,0,1.0617673)"
id="text31980" id="text31980"
style="font-size:9.33333px;line-height:10px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:start;white-space:pre;shape-inside:url(#rect31982);display:inline;fill:#000000;fill-opacity:1" style="font-size:9.33333px;line-height:10px;font-family:'Droid Sans Fallback';-inkscape-font-specification:'Droid Sans Fallback';text-align:start;white-space:pre;shape-inside:url(#rect31982);display:inline;fill:#000000;fill-opacity:1"
inkscape:label="Ingrédients"><tspan inkscape:label="Ingrédients"><tspan
x="162.51953" x="162.51953"
y="71.082662" y="71.082662"
id="tspan3150">${ingredients} id="tspan2493">${ingredients}
</tspan></text> </tspan></text>
<flowRoot <flowRoot
xml:space="preserve" xml:space="preserve"
@@ -1397,28 +1397,28 @@
xml:space="preserve" xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,72.572369,-44.503941)" transform="matrix(0.26458333,0,0,0.26458333,72.572369,-44.503941)"
id="text169273-3" id="text169273-3"
style="font-size:5.33333px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:start;white-space:pre;shape-inside:url(#rect169275-8);display:inline;fill:#000000;fill-opacity:1;stroke-width:1.562" style="font-size:5.33333px;font-family:'Droid Sans Fallback';-inkscape-font-specification:'Droid Sans Fallback';text-align:start;white-space:pre;shape-inside:url(#rect169275-8);display:inline;fill:#000000;fill-opacity:1;stroke-width:1.562"
x="-198.57587" x="-198.57587"
y="0" y="0"
inkscape:label="Bloc Texte Conservation"><tspan inkscape:label="Bloc Texte Conservation"><tspan
x="-111.76953" x="-111.76953"
y="270.68079" y="270.68079"
id="tspan3152">À conserver au frais après ouverture. id="tspan2495">À conserver au frais après ouverture.
</tspan><tspan </tspan><tspan
x="-111.76953" x="-111.76953"
y="277.34745" y="277.46601"
id="tspan3154">Couvrir d'huile après chaque utilisation. id="tspan2497">Couvrir d'huile après chaque utilisation.
</tspan><tspan </tspan><tspan
x="-111.76953" x="-111.76953"
y="284.0141" y="284.25122"
id="tspan3158">À consommer de préférence avant : <tspan id="tspan2501">À consommer de préférence avant : <tspan
style="font-weight:bold" style="font-weight:bold"
id="tspan3156">${dluo}</tspan> id="tspan2499">${dluo}</tspan>
</tspan><tspan </tspan><tspan
x="-111.76953" x="-111.76953"
y="290.68076" y="291.03644"
id="tspan3162"><tspan id="tspan2505"><tspan
style="font-weight:bold" style="font-weight:bold"
id="tspan3160">*Produits issus de l'agriculture biologique.</tspan></tspan></text> id="tspan2503">*Produits issus de l'agriculture biologique.</tspan></tspan></text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.3 MiB

After

Width:  |  Height:  |  Size: 5.3 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