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
60 changed files with 2054 additions and 1293 deletions

View File

@ -1,15 +1,15 @@
FROM alpine:3.18
FROM alpine
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript font-noto py3-pip
RUN pip3 install --break-system-packages django tzdata gunicorn google-api-python-client
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
ADD backend /zetikettes
ADD backend /root/zetikettes
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
# 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="*"

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 --noinput
.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.
**NOTE**: release 2.0 broke compatibility with previous "releases". Maxime has all the recent data to repopulate a new database.
ouaich.
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 cp compose.yml /etc/docker/compose/zetikettes/
sudo cp docker-compose.yml /etc/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:
- redirect `/zetikettes/srv/` to `localhost:8000`
- redirect `/zetikettes/` to `/var/lib/zetikettes/frontend`
- redirect `/zetikettes/srv/static` to `/var/lib/zetikettes/www_static`
- redirect /zetikettes/srv/ to localhost:8000
- redirect /zetikettes/ to /var/lib/zetikettes/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 @@
[]

60
backend/old/makeplanche.py Executable file
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()))

58
backend/old/makesticker.py Executable file
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)

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

@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zetikettes.settings")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zetikettes.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@ -18,5 +18,5 @@ def main():
execute_from_command_line(sys.argv)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -1,9 +1,8 @@
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(Tizer)
admin.site.register(Tikategory)
admin.site.register(Tisub)

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class TiketteConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "tikette"
default_auto_field = 'django.db.models.BigAutoField'
name = 'tikette'

View File

@ -0,0 +1,61 @@
import os
import subprocess
import tempfile
from .makesticker import makesticker
from .makeplanche import makeplanche
OUT_DIR = tempfile.gettempdir()
DEFAULT_DPI = 300
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 generate(request, out_dir):
""" Generate a sticker sheet.
request: dict-like with the following fields:
sticker: mandatory. filename for the sticker
subs: mandatory. dict-like with key-value template subtitution fields
landscape: optional. defaults to False
out_dir: you get it
"""
# fill in sticker details
sticker_out = tempfile.mktemp(suffix='.svg')
try:
with open(sticker_out, 'w') as stickout:
landscape = request.get('landscape', False)
makesticker(os.path.join(out_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)
return os.path.basename(pdf_out)
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)

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,16 +0,0 @@
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
def cors_everywhere(get_response):
def middleware(request):
if request.method == "OPTIONS":
response = HttpResponse()
else:
response = get_response(request)
response["Access-Control-Allow-Origin"] = request.headers.get("Origin", "*")
response["Access-Control-Allow-Credentials"] = "true"
response["Access-Control-Allow-Headers"] = "x-csrftoken"
return response
return middleware

View File

@ -1,127 +1,51 @@
# 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
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = []
dependencies = [
]
operations = [
migrations.CreateModel(
name="Tikategory",
name='Tikategory',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=50)),
("landscape", models.BooleanField()),
("prototempalte", models.FileField(upload_to="")),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('landscape', models.BooleanField()),
],
options={
"verbose_name_plural": "tikategoriez",
'verbose_name_plural': 'tikategoriez',
},
),
migrations.CreateModel(
name="Tisub",
name='Tisub',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=50)),
("descritpion", 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,
),
),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('descritpion', models.TextField()),
('default', models.TextField()),
],
options={
"verbose_name_plural": "tisubz",
'verbose_name_plural': 'tisubz',
},
),
migrations.CreateModel(
name="Tizer",
name='Tikette',
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("email", models.CharField()),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('svg', models.FileField(upload_to='')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tikette.tikategory')),
('subs', models.ManyToManyField(to='tikette.tisub')),
],
options={
"verbose_name_plural": "tizerz",
'verbose_name_plural': 'tikettz',
},
),
migrations.CreateModel(
name="Tikette",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=100)),
("designation", models.CharField(max_length=100)),
("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",
),
),
],
options={
"verbose_name_plural": "tikettz",
},
),
migrations.AddField(
model_name="tikategory",
name="subs",
field=models.ManyToManyField(to="tikette.tisub"),
),
]

View File

@ -1,19 +1,9 @@
from django.db import models
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)
descritpion = 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):
return self.name
@ -26,10 +16,6 @@ class Tikategory(models.Model):
name = models.CharField(max_length=50)
landscape = models.BooleanField()
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):
return self.name
@ -39,19 +25,9 @@ class Tikategory(models.Model):
class Tikette(models.Model):
class AbVisibility(models.TextChoices):
VISIBLE = "inline"
INVISIBLE = "none"
title = models.CharField(max_length=100)
category = models.ForeignKey(Tikategory, on_delete=models.CASCADE)
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
svg = models.FileField()
def __str__(self):
return self.title
@ -59,12 +35,4 @@ class Tikette(models.Model):
class Meta:
verbose_name_plural = "tikettz"
class Tizer(models.Model):
email = models.CharField()
def __str__(self):
return self.email
class Meta:
verbose_name_plural = "tizerz"
# Create your models here.

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,69 +0,0 @@
import os
import subprocess
import tempfile
from .makesticker import makesticker
from .makeplanche import makeplanche
OUT_DIR = tempfile.gettempdir()
DEFAULT_DPI = 300
DESIGNATION_FONTSIZE = 42.6667
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 generate(template, subs, out_dir, landscape=False):
"""Generate a sticker sheet.
template: file name for the sticker template
subs: dict-like with key-value template subtitution fields
out_dir: you get it
landscape: optional. defaults to False
"""
# default designation font size
subs["designation_fontsize"] = subs.get(
"designation_fontsize", DESIGNATION_FONTSIZE
)
# fill in sticker details
sticker_out = tempfile.mktemp(suffix=".svg")
try:
with open(sticker_out, "w") as stickout:
makesticker(os.path.join(out_dir, template), stickout, subs)
# 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, landscape=landscape)
# process to printable pdf
pdf_out = tempfile.mktemp(dir=out_dir, suffix=".pdf")
inkscapize(planche_out, pdf_out)
return os.path.basename(pdf_out)
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)

View File

@ -1,62 +0,0 @@
#!/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(
"--landscape",
action="store_true",
help="input sticker is in landscape orientation",
)
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, landscape=False):
with open(template) as tpl:
tpl_data = tpl.read()
lines = sticker.readlines()
if lines[0].startswith("<?xml"):
lines = lines[1:]
sticker_data = "".join(lines)
rotate = "translate(102, 0) rotate(90)" if not landscape else ""
subs = {
"sticker": sticker_data,
"rotate": rotate,
}
out.write(Template(tpl_data).substitute(subs))
if __name__ == "__main__":
makeplanche(**vars(parse_args()))

View File

@ -1,54 +0,0 @@
#!/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("sticker", help="path to the sticker template")
return parser.parse_args()
def makesticker(sticker: str, out: typing.IO, subs: dict):
with open(sticker) as fin:
lines = fin.readlines()
if lines[0].startswith("<?xml"):
lines = lines[1:]
sticker_data = "".join(lines)
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"),
"f": args.pop("fruit"),
"q": args.pop("quantite"),
"size": args.pop("size"),
}
args["subs"] = subs
makesticker(**args)

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

@ -1,164 +1,31 @@
import json
import sys
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from google.auth.transport import requests
from google.oauth2 import id_token
from django.views.decorators.csrf import csrf_exempt
from .planche import generate as stickersheet
from .models import Tikette, Tikategory, Tizer
from . import generate as stickersheet
from .models import Tikette, Tikategory
CORS={'access-control-allow-origin': '*'}
def ok(payload=None):
return JsonResponse({"status": "ok", **(payload or {})})
def post_please(f):
def __f(request):
if request.method != "POST":
return JsonResponse(
{"status": "notok", "message": "this isn't the way"},
status=405,
headers={"Allow": "POST"},
)
return f(request)
return __f
def auth_only(f):
def __f(request):
# check that email is valid
# exp?
if "user_data" not in request.session:
raise PermissionDenied("Not logged in")
email = request.session["user_data"]["email"]
resp = Tizer.objects.filter(email=email)
if not resp:
raise PermissionDenied("User not found")
return f(request)
return __f
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 handler403(request, exception):
return JsonResponse({"status": "notok", "message": "permission denied"}, status=403)
def handler404(request, exception):
return JsonResponse(
{"status": "notok", "message": "endpoint not found"}, status=404
)
@auth_only
@ensure_csrf_cookie
def get_list(request):
tikettes = [
{
"id": x.id,
"title": x.title,
"category": x.category.name,
"category_id": x.category.id,
"prototempalte": x.category.prototempalte.name,
"landscape": x.category.landscape,
"designation": x.designation,
"ingredients": x.ingredients,
"description": x.description,
"ab": x.ab,
"color": x.color,
"subs": {x.name: x.default for x in x.category.subs.all()},
}
for x in Tikette.objects.all()
]
return ok({"tikettes": tikettes})
@auth_only
def get_categories(request):
tikats = [
{
"id": x.id,
"name": x.name,
}
for x in Tikategory.objects.all()
]
return ok({"tikats": tikats})
@auth_only
@post_please
def generate(request):
payload = json.loads(request.body)
subs = dict(payload["subs"])
for key in ("designation", "ingredients", "description", "color", "AB"):
subs[key] = payload[key]
subs["ingredients"] = quirk_bold_allergens(subs["ingredients"])
pdfpath = stickersheet.generate(
template=payload["template"],
subs=subs,
out_dir=settings.TIKETTE_OUT_DIR,
landscape=payload["landscape"],
)
return ok({"file": pdfpath})
@auth_only
@post_please
def newtikette(request):
payload = json.loads(request.body)
tikette = Tikette(**payload)
tikette.save()
return ok()
@auth_only
@post_please
def deletetikette(request):
payload = json.loads(request.body)
Tikette.objects.get(id=payload["id"]).delete()
return ok()
def index(request):
tikettes = [{
'title': x.title,
'category': x.category.name,
'sticker': x.svg.name,
'landscape': x.category.landscape,
'subs': {x.name: x.default for x in x.category.subs.all()},
} for x in Tikette.objects.all()]
return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS)
@csrf_exempt
@post_please
def signin(request):
def generate(request):
if request.method != "POST":
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
payload = json.loads(request.body)
token = payload["token"]
pdfpath = stickersheet.generate(payload, out_dir=settings.TIKETTE_OUT_DIR)
try:
user_data = id_token.verify_oauth2_token(
token, requests.Request(), settings.GOOGLE_OAUTH_CLIENT_ID
)
except ValueError:
raise
return JsonResponse({"status": "notok"}, status=403)
request.session["user_data"] = user_data
return ok(user_data)
def signout(request):
del request.session["user_data"]
return ok()
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)

View File

@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zetikettes.settings")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zetikettes.settings')
application = get_asgi_application()

View File

@ -15,75 +15,74 @@ from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# TIKETTE_OUT_DIR = BASE_DIR / 'data'
TIKETTE_OUT_DIR = Path("/data")
#TIKETTE_OUT_DIR = BASE_DIR / 'data'
TIKETTE_OUT_DIR = Path('/data')
MEDIA_ROOT = TIKETTE_OUT_DIR
MEDIA_URL = "/data/"
MEDIA_URL = '/data/'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-64qxpe55#9wy=5@#dl0)3w7ywxh48m!f&!slp9e7v4lh@hjdct"
SECRET_KEY = 'django-insecure-64qxpe55#9wy=5@#dl0)3w7ywxh48m!f&!slp9e7v4lh@hjdct'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
DEBUG = True
ALLOWED_HOSTS = ["*"]
CSRF_TRUSTED_ORIGINS = ["https://*.ponteilla.net"]
ALLOWED_HOSTS = ['*']
CSRF_TRUSTED_ORIGINS = ['https://*.ponteilla.net']
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"tikette",
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'tikette',
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"tikette.middleware.cors_everywhere",
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = "zetikettes.urls"
ROOT_URLCONF = 'zetikettes.urls'
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = "zetikettes.wsgi.application"
WSGI_APPLICATION = 'zetikettes.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": TIKETTE_OUT_DIR / "db.sqlite3",
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': TIKETTE_OUT_DIR / 'db.sqlite3',
}
}
@ -93,16 +92,16 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
@ -110,9 +109,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "en-us"
LANGUAGE_CODE = 'en-us'
TIME_ZONE = "UTC"
TIME_ZONE = 'UTC'
USE_I18N = True
@ -122,16 +121,11 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = "/zetikettes/srv/static/"
STATIC_URL = '/zetikettes/srv/static/'
STATIC_ROOT = "www_static"
STATIC_ROOT = 'www_static'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# not a secret
GOOGLE_OAUTH_CLIENT_ID = (
"634510965520-c5l7f15fn4koraqhpqfe01ssn8v0q2qk.apps.googleusercontent.com"
)
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@ -14,7 +14,6 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
@ -24,16 +23,7 @@ from django.views.generic.base import TemplateView
import tikette.views
urlpatterns = [
path("admin/", admin.site.urls),
path("list", tikette.views.get_list),
path("categories", tikette.views.get_categories),
path("generate", tikette.views.generate),
path("newtikette", tikette.views.newtikette),
path("deletetikette", tikette.views.deletetikette),
path("updatetikette", tikette.views.newtikette), # yes, we use newtikette
path("signin", tikette.views.signin),
path("signout", tikette.views.signout),
path('admin/', admin.site.urls),
path('list', tikette.views.index),
path('', tikette.views.generate),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
handler403 = "tikette.views.handler403"
handler404 = "tikette.views.handler404"

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zetikettes.settings")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'zetikettes.settings')
application = get_wsgi_application()

View File

@ -3,8 +3,7 @@ version: '3.1'
services:
zetikettes:
build: .
image: pol/zetikettes
image: zetikettes
restart: always
ports:
- 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,2 +1 @@
const backend_api = "/zetikettes/srv/"
const google_oauth_client_id = '634510965520-c5l7f15fn4koraqhpqfe01ssn8v0q2qk.apps.googleusercontent.com';
const backend_api = 'http://jenova.ponteilla.net:8001/'

View File

@ -3,121 +3,40 @@
<meta charset="utf-8" />
<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>
<!--Import Google Icon Font-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- 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="placeholder.css">
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script src="https://accounts.google.com/gsi/client"></script>
<script src="jscolor.min.js"></script>
<script src="config.js"></script>
<script src="zetikettes.js"></script>
<style>
/* for the footer */
body {
display: flex;
min-height: 100vh;
flex-direction: column;
}
main {
flex: 1 0 auto;
}
.actions {
margin-left: auto;
white-space: nowrap;
}
.confirm {
width: 35%;
min-width: 20em;
max-width: 85%;
}
</style>
</head>
<body>
<nav class="light-blue">
<div class="nav-wrapper container">
<a href="#" class="brand-logo">Zětikwett's</a>
<ul id="nav-mobile" class="right hide-on-med-and-down">
<li>
<a class="modal-trigger btn orange disabled" href="#newproduct">
<i class="material-icons left">add</i>Nouveau produit
</a>
</li>
</ul>
</div>
</nav>
<header class="container center orange-text">
<h1>Zétikwett's</h1>
</header>
<main class="container row">
<p></p>
<div class="col s4 offset-s4" id="signin-prompt" style="display: none"></div>
<div class="col m10 offset-m1 s12">
<a class="modal-trigger btn orange hide-on-large-only disabled" href="#newproduct"><i class="material-icons left">add</i>Nouveau produit</a>
<ul class="collapsible collection" 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>
<div id="confirmdelete" class="modal confirm">
<div class="modal-content">
Supprimer le produit <b id="delete-title">blařg titre long de test sa mère</b>&nbsp;?
</div>
<div class="modal-footer">
<button class="modal-close waves-effect btn-flat" id="delete-cancel">annuler</button>
<button type="submit" class="modal-close waves-effect waves-green btn" id="delete-confirm">supprimer</button>
</div>
</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 class="col m6 offset-m3 s12">
<ul class="collapsible" id="appbody"></ul>
</div>
</main>
<footer class="page-footer orange">
<footer class="page-footer">
<div class="container row">
<span class="col right">displayed with recycled electrons.</span>
</div>

File diff suppressed because one or more lines are too long

45
frontend/newtikette.html Normal file
View File

@ -0,0 +1,45 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>zetikettes 0.2</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<!-- Compiled and minified JavaScript -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script src="config.js"></script>
<script src="newtikette.js"></script>
<style>
body {
display: flex;
min-height: 100vh;
flex-direction: column;
}
main {
flex: 1 0 auto;
}
</style>
</head>
<body>
<header class="container center orange-text">
<h1>Zétikwett's</h1>
</header>
<main class="container row">
<div class="col m6 offset-m3 s12">
<div id="appbody" class="container"></div>
</div>
</main>
<footer class="page-footer">
<div class="container row">
<span class="col right">displayed with recycled electrons.</span>
</div>
</footer>
</body>
</html>

125
frontend/newtikette.js Normal file
View File

@ -0,0 +1,125 @@
const params = [
['dluo', 'DLUO', true],
['lot', 'Nº de lot', true],
['qty', 'Poids net (g)', true],
['teneur', 'Teneur en fruits (%)', false],
['fruit', 'Quantité de fruits pour 100g (g)', false],
]
function post() {
const lemot = $('input[type=password]').val();
const formdata = new FormData($('form')[0]);
formdata.append('lemotdepasse', lemot);
$.ajax({
url: backend_api + `newtikette`,
type: 'POST',
data: formdata,
contentType: false,
processData: false,
xhr: function () {
var myXhr = $.ajaxSettings.xhr();
if (myXhr.upload) {
// For handling the progress of the upload
myXhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) {
$('progress').attr({
value: e.loaded,
max: e.total,
});
}
}, false);
}
return myXhr;
}
})
.then(data => {
console.log(data);
})
.catch(err => {
console.log(err);
if (err.status == 403) {
M.toast({html: 'access denied'});
} else if (err.status == 500) {
M.toast({html: 'server error'});
}
})
.always(() => {
$('.progress').hide();
$('.btn').removeClass('disabled');
});
}
$(document).ready(async () => {
const appbody = $("#appbody");
const block = $('<form class="section">');
block.append($(`<div class="input-field"><label class="active">Title</label>
<input type="text" name="title" value="Gelée de brandade aux cêpes biou">`));
block.append($(`<div class="input-field"><label class="active">
<input type="checkbox" name="landscape"><span>landscape</span></label>`));
const subst = $('<div class="section"><label class="active">Substitutions</label><p></p>');
for (let param of params) {
subst.append($(`<div><label class="active"><input type="checkbox" name="${param[0]}" ${param[2] ? 'checked': ''}>
<span>\${${param[0]}}: ${param[1]}</span></label>`));
}
block.append(subst);
block.append(`<div class="file-field input-field">
<div class="btn">
<span>File</span>
<input type="file" name="sticker" accept=".svg">
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text" placeholder="Template .svg file">
</div>
`)
const loader = $('<div class="progress"><div class="determinate"></div></div>')
.hide();
let setup = false;
const action = $('<div class="section">')
.append($('<a class="btn">add newtikette<a>')
.click(() => {
if (!setup) {
setup = true;
$('input[type=password]').on('keydown', e => {
if (e.keyCode == 13 && !e.repeat) {
$('.modal').modal('close');
post();
}
});
$("#pushgo").click(() => { post(); });
}
$('.modal').modal('open');
$('input[type=password]').focus();
loader.show();
$('.btn').addClass("disabled");
$('.modal-close').removeClass("disabled");
})
.append(loader));
const lemotdepasse = $(`
<div id="modal1" class="modal">
<div class="modal-content">
<h4>A lemotdepasse is needed</h4>
<div class="input-field"><label class="active">lemotdepasse</label>
<input type="password">
</div>
<div class="modal-footer">
<a class="btn modal-close" id="pushgo">go</a>
</div>
`);
appbody
.append(lemotdepasse)
.append(block)
.append(action);
$(".modal").modal();
});

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,203 +1,64 @@
const params = {
'dluo': 'DLUO',
'lot': 'Nº de lot',
'q': 'Poids net (g)',
'qty': 'Poids net (g)',
'vol': 'Volume net (cL)',
'teneur': 'Teneur en fruits (%)',
'f': 'Quantité de fruits pour 100g (g)',
}
var tikats;
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(name + '=')) {
return cookie.substring(name.length + 1);
}
}
return null;
}
function post(url, data) {
const csrf_token = getCookie('csrftoken');
return $.ajax({
url,
data: JSON.stringify(data),
method: 'POST',
xhrFields: { withCredentials: true },
headers: { 'X-CSRFToken': csrf_token },
});
}
function disableButtons() {
$('.btn').addClass("disabled");
}
function enableButtons() {
$('.btn').removeClass('disabled');
}
function resetEditModal(force = false) {
if ($("#new-add").text() === "Ajouter" && !force) {
return;
}
$("#new-name").val("");
$("#new-type").val("").formSelect();
$("#new-designation").val("");
$("#new-ingredients").val("");
$("#new-description").val("");
$("#new-color")[0].jscolor.fromString("#97a1cc");
$("#new-organic").prop("checked", false);
M.updateTextFields();
M.textareaAutoResize($('#new-ingredients'));
M.textareaAutoResize($('#new-description'));
$("#new-add").text("Ajouter").off('click').click(() => {
const req = getTiketteData();
post(backend_api + 'newtikette', req).then(() => {
resetEditModal(true);
reload();
});
});
}
function openEditModal(zett) {
M.Modal.getInstance($("#newproduct")).open();
$("#new-name").val(zett.title);
$("#new-type").val(zett.category_id).formSelect();
$("#new-designation").val(zett.designation);
$("#new-ingredients").val(zett.ingredients);
$("#new-description").val(zett.description);
$("#new-color")[0].jscolor.fromString(zett.color);
$("#new-organic").prop("checked", zett.ab === "inline");
M.updateTextFields();
M.textareaAutoResize($('#new-ingredients'));
M.textareaAutoResize($('#new-description'));
$("#new-add").text("Modifier").off('click').click(() => {
const req = getTiketteData();
req.id = zett.id;
resetEditModal();
post(backend_api + 'updatetikette', req).then(reload);
});
}
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">générer<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();
disableButtons();
post(backend_api + 'generate', req)
.then(data => {
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">ouvrir le pdf</a>`);
action.append(pdfbtn);
})
.catch(err => {
console.error(err);
})
.always(() => {
loader.hide();
enableButtons();
});
})
.append(loader));
const deleteAction = $('<b class="material-icons">delete</b>').click(() => {
$("#delete-title").text(zett.title);
$("#delete-confirm").off('click').click(() => {
const req = {
id: zett.id,
};
post(backend_api + 'deletetikette', req).then(reload);
});
M.Modal.getInstance($("#confirmdelete")).open();
return false;
});
const editAction = $('<b class="material-icons">edit</b>').click(() => {
openEditModal(zett);
return false;
});
appbody
.append($('<li>')
.append($('<div class="collapsible-header valign-wrapper">')
.append(`<h6 class="blue-text">${zett.title}</h6>`)
.append($('<span class="actions grey-text">')
.append(editAction)
.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>`));
}
$('select').formSelect();
}
function getTiketteData() {
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") ? 'inline' : 'none';
return {
title,
category_id,
designation,
ingredients,
description,
color,
ab,
};
'fruit': 'Quantité de fruits pour 100g (g)',
}
function loadAll(zetikettes) {
const appbody = $("#appbody");
appbody.empty();
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();
konami();
}
function konami() {
@ -215,53 +76,16 @@ function konami() {
});
}
async function googleCred(creds) {
const token = creds.credential;
await post(backend_api + 'signin', {token});
$('#signin-prompt').hide();
reload();
}
async function reload() {
disableButtons();
$(document).ready(async () => {
try {
const resp = await $.ajax({
url: backend_api + 'list',
timeout: 1000,
xhrFields: { withCredentials: true },
});
tikats = (await $.ajax({
url: backend_api + 'categories',
timeout: 1000,
xhrFields: { withCredentials: true },
})).tikats.sort((a, b) => a.name > b.name ? 1 : -1);
loadAll(resp.tikettes.sort((a, b) => (a.title < b.title) ? -1 : 1));
enableButtons();
} catch(e) {
if (e.status === 403) {
$("#signin-prompt").show();
google.accounts.id.prompt(); // also display the One Tap dialog
return;
}
const appbody = $("#appbody");
appbody.append(`<li>Could not reach backend server`);
throw e;
}
}
$(document).ready(() => {
google.accounts.id.initialize({
client_id: google_oauth_client_id,
callback: googleCred,
});
google.accounts.id.renderButton(
document.getElementById("signin-prompt"),
{ theme: "outline", size: "large" } // customization attributes
);
konami();
$('.collapsible').collapsible();
$('.modal').modal({onOpenStart: resetEditModal});
reload();
});

View File

@ -6,10 +6,6 @@ location /zetikettes/srv/static {
alias /var/lib/zetikettes/www_static;
}
location /zetikettes/srv/data {
alias /var/lib/zetikettes/data;
}
location ^~ /zetikettes/srv {
proxy_pass http://127.0.0.1:8000;
proxy_set_header SCRIPT_NAME /zetikettes/srv;

View File

@ -1,47 +0,0 @@
#!/usr/bin/env python3
import csv
import dataclasses
import requests
backend = 'http://localhost:8000'
products_file = './product_definitions.csv'
@dataclasses.dataclass
class Tikette:
category: str
designation: str
ingredients: str
description: str
color: str
svg: str
ab: str
def newtikette_payload(self, katzids):
return {
'title': self.svg.split('.')[0],
'category_id': katzids[self.category],
'designation': self.designation,
'ingredients': self.ingredients,
'description': self.description,
'color': self.color,
'ab': 'inline' if self.ab is 'True' else 'none',
}
def get_tikettes():
with open('./product_definitions.csv') as f:
return [Tikette(*x) for x in csv.reader(f, delimiter=';')]
def populate_database():
katz = requests.get(backend + '/categories', json=True).json()['tikats']
katzids = {x['name'].lower(): x['id'] for x in katz}
for tk in get_tikettes():
requests.post(backend + '/newtikette', json=tk.newtikette_payload(katzids))
if __name__ == "__main__":
populate_database()

View File

@ -7,9 +7,8 @@ import os
import csv
from subber import Subber
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "..", "templates")
OUT_DIR = "."
DEFAULT_CSV = 'product_definitions.csv'
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
@ -31,7 +30,7 @@ ALLERGEN_END_STYLE = '</tspan>'
def parse_args():
parser = argparse.ArgumentParser(description='Renew whole lineup from template and list of subs')
parser.add_argument('--list', default=DEFAULT_CSV, help='Lineup file')
parser.add_argument('--list', required=True, help='Lineup file')
return parser.parse_args()

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"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
inkscape:version="1.2 (dc2aeda, 2022-05-15)"
sodipodi:docname="Pesto.svg"
inkscape:export-filename="/home/maxime/Documents/Anne/Etiquettes/Bitmap Images/Pesto - Ail des Ours.png"
inkscape:export-xdpi="600"
@ -788,8 +788,8 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.1140114"
inkscape:cx="163.22269"
inkscape:cy="91.75959"
inkscape:cx="163.46576"
inkscape:cy="91.638054"
inkscape:document-units="mm"
inkscape:current-layer="layer11"
showgrid="false"
@ -1229,7 +1229,7 @@
id="text4193"
y="-5.6109309"
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"
inkscape:label="Email"><tspan
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" />
<text
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"
y="-7.7611365"
id="text4199"
@ -1267,7 +1267,7 @@
id="text4205"
y="-10.030697"
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"
inkscape:label="Adresse"><tspan
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>
<text
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"
y="-12.237692"
id="text4211"
@ -1299,7 +1299,7 @@
<flowRoot
inkscape:label="Culture a la main"
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"
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"
@ -1316,7 +1316,7 @@
id="flowPara31669">dans les Alpes de Haute-Provence</flowPara></flowRoot>
<text
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"
y="246.99561"
id="text31972"
@ -1339,7 +1339,7 @@
<flowRoot
inkscape:label="Texte Quantité et Lot"
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"
xml:space="preserve"><flowRegion
style="line-height:0px;stroke-width:1.00001"
@ -1351,31 +1351,31 @@
width="30.898319"
id="rect82729" /></flowRegion><flowPara
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"
style="line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
id="flowPara82739"
style="line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
id="flowPara82741"
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
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
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
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>
</g>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,0,1.0617673)"
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
x="162.51953"
y="71.082662"
id="tspan3150">${ingredients}
id="tspan2493">${ingredients}
</tspan></text>
<flowRoot
xml:space="preserve"
@ -1397,28 +1397,28 @@
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,72.572369,-44.503941)"
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"
y="0"
inkscape:label="Bloc Texte Conservation"><tspan
x="-111.76953"
y="270.68079"
id="tspan3152">À conserver au frais après ouverture.
id="tspan2495">À conserver au frais après ouverture.
</tspan><tspan
x="-111.76953"
y="277.34745"
id="tspan3154">Couvrir d'huile après chaque utilisation.
y="277.46601"
id="tspan2497">Couvrir d'huile après chaque utilisation.
</tspan><tspan
x="-111.76953"
y="284.0141"
id="tspan3158">À consommer de préférence avant : <tspan
y="284.25122"
id="tspan2501">À consommer de préférence avant : <tspan
style="font-weight:bold"
id="tspan3156">${dluo}</tspan>
id="tspan2499">${dluo}</tspan>
</tspan><tspan
x="-111.76953"
y="290.68076"
id="tspan3162"><tspan
y="291.03644"
id="tspan2505"><tspan
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>
</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