Compare commits
4 Commits
topic/blěk
...
d706ae1645
| Author | SHA1 | Date | |
|---|---|---|---|
| d706ae1645 | |||
| 6a05e46946 | |||
| f6ad73ef85 | |||
| e222962eee |
10
Dockerfile
10
Dockerfile
@@ -1,15 +1,15 @@
|
|||||||
FROM alpine
|
FROM alpine:3.18
|
||||||
|
|
||||||
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript ttf-opensans
|
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript font-droid
|
||||||
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 /root/zetikettes
|
ADD backend /zetikettes
|
||||||
|
|
||||||
RUN mkdir -p /usr/share/fonts/TTF \
|
RUN mkdir -p /usr/share/fonts/TTF \
|
||||||
&& cp /root/zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \
|
&& cp /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 /root/zetikettes/zetikettes
|
WORKDIR /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="*"
|
||||||
|
|||||||
48
Makefile
Normal file
48
Makefile
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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
|
||||||
93
README.md
93
README.md
@@ -1,64 +1,53 @@
|
|||||||
zetikettes
|
zetikettes 2.0
|
||||||
==========
|
==============
|
||||||
|
|
||||||
ouaich.
|
ouaich. tavu.
|
||||||
|
|
||||||
|
**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 docker-compose.yml /etc/docker/compose/zetikettes/
|
sudo cp 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/static
|
- redirect `/zetikettes/` to `/var/lib/zetikettes/frontend`
|
||||||
|
- 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.
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -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(-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>
|
|
||||||
|
Before Width: | Height: | Size: 896 B |
@@ -1,269 +0,0 @@
|
|||||||
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.")
|
|
||||||
152
backend/zetikettes/initial_db.json
Normal file
152
backend/zetikettes/initial_db.json
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Tikette, Tikategory, Tisub
|
from .models import Tikette, Tizer, 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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +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('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()))
|
|
||||||
@@ -1,58 +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('--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)
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 4.2.2 on 2023-07-03 14:37
|
# Generated by Django 5.2.4 on 2025-08-05 12:57
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -18,6 +18,7 @@ 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',
|
||||||
@@ -30,22 +31,41 @@ 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)),
|
||||||
('svg', models.FileField(upload_to='')),
|
('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')),
|
('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'),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
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
|
||||||
@@ -16,6 +25,10 @@ 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
|
||||||
@@ -25,9 +38,19 @@ 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
|
||||||
@@ -35,4 +58,12 @@ 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"
|
||||||
|
|||||||
@@ -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(-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>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
0
backend/zetikettes/tikette/planche/__init__.py
Normal file
0
backend/zetikettes/tikette/planche/__init__.py
Normal file
@@ -7,6 +7,8 @@ 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')
|
||||||
@@ -22,31 +24,31 @@ def inkscapize(svg_in, pdf_out):
|
|||||||
os.unlink(png_out)
|
os.unlink(png_out)
|
||||||
|
|
||||||
|
|
||||||
def generate(request, out_dir):
|
def generate(template, subs, out_dir, landscape=False):
|
||||||
""" Generate a sticker sheet.
|
""" Generate a sticker sheet.
|
||||||
|
|
||||||
request: dict-like with the following fields:
|
template: file name for the sticker template
|
||||||
sticker: mandatory. filename for the sticker
|
subs: dict-like with key-value template subtitution fields
|
||||||
subs: mandatory. dict-like with key-value template subtitution fields
|
|
||||||
landscape: optional. defaults to False
|
|
||||||
|
|
||||||
out_dir: you get it
|
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
|
# 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:
|
||||||
landscape = request.get('landscape', False)
|
makesticker(os.path.join(out_dir, template), stickout, subs)
|
||||||
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)
|
makeplanche(stickin, planchout, landscape=landscape)
|
||||||
|
|
||||||
# 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')
|
||||||
@@ -20,6 +20,9 @@ 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,
|
||||||
@@ -29,7 +32,7 @@ def parse_args():
|
|||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
|
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE ,landscape=False):
|
||||||
with open(template) as tpl:
|
with open(template) as tpl:
|
||||||
tpl_data = tpl.read()
|
tpl_data = tpl.read()
|
||||||
|
|
||||||
@@ -38,19 +41,11 @@ def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
|
|||||||
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 = {
|
||||||
'left0': sticker_data,
|
'sticker': sticker_data,
|
||||||
'left1': sticker_data,
|
'rotate': rotate,
|
||||||
'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))
|
||||||
@@ -22,25 +22,18 @@ 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, landscape=False):
|
def makesticker(sticker: str, out: typing.IO, subs: dict):
|
||||||
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))
|
||||||
|
|
||||||
|
|
||||||
@@ -50,8 +43,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'),
|
||||||
'fruit': args.pop('fruit'),
|
'f': args.pop('fruit'),
|
||||||
'qty': args.pop('quantite'),
|
'q': args.pop('quantite'),
|
||||||
'size': args.pop('size'),
|
'size': args.pop('size'),
|
||||||
}
|
}
|
||||||
args['subs'] = subs
|
args['subs'] = subs
|
||||||
24
backend/zetikettes/tikette/planche/planche.svg.in
Normal file
24
backend/zetikettes/tikette/planche/planche.svg.in
Normal 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(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>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -5,27 +5,83 @@ 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 . import generate as stickersheet
|
from .planche 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,
|
||||||
'sticker': x.svg.name,
|
'prototempalte': x.category.prototempalte.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)
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import tikette.views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('list', tikette.views.index),
|
path('list', tikette.views.get_list),
|
||||||
path('', tikette.views.generate),
|
path('categories', tikette.views.get_categories),
|
||||||
|
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)
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ version: '3.1'
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
zetikettes:
|
zetikettes:
|
||||||
image: zetikettes
|
build: .
|
||||||
|
image: pol/zetikettes
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8000:8000
|
- 127.0.0.1:8000:8000
|
||||||
14
docker-compose@.service
Normal file
14
docker-compose@.service
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[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
|
||||||
@@ -1 +1 @@
|
|||||||
const backend_api = 'http://jenova.ponteilla.net:8001/'
|
const backend_api = 'http://localhost:8000/'
|
||||||
|
|||||||
@@ -3,15 +3,19 @@
|
|||||||
<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>zetikettes 0.2</title>
|
<title>Zětikwett's</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>
|
||||||
|
|
||||||
@@ -28,15 +32,70 @@ main {
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="container center orange-text">
|
<nav class="light-blue">
|
||||||
<h1>Zétikwett's</h1>
|
<div class="nav-wrapper container">
|
||||||
</header>
|
<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" 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">
|
||||||
<ul class="collapsible" id="appbody"></ul>
|
<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">
|
||||||
|
<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">
|
<footer class="page-footer orange">
|
||||||
<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>
|
||||||
|
|||||||
1
frontend/jscolor.min.js
vendored
Normal file
1
frontend/jscolor.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
47
frontend/placeholder.css
Normal file
47
frontend/placeholder.css
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,64 +1,122 @@
|
|||||||
const params = {
|
const params = {
|
||||||
'dluo': 'DLUO',
|
'dluo': 'DLUO',
|
||||||
'lot': 'Nº de lot',
|
'lot': 'Nº de lot',
|
||||||
'qty': 'Poids net (g)',
|
'q': 'Poids net (g)',
|
||||||
'vol': 'Volume net (cL)',
|
|
||||||
'teneur': 'Teneur en fruits (%)',
|
'teneur': 'Teneur en fruits (%)',
|
||||||
'fruit': 'Quantité de fruits pour 100g (g)',
|
'f': '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) {
|
||||||
const block = $('<div class="section">');
|
addProduct(zett);
|
||||||
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() {
|
||||||
@@ -76,16 +134,22 @@ function konami() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(async () => {
|
async function reload() {
|
||||||
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);
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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,65 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
Before Width: | Height: | Size: 5.3 MiB |
@@ -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 (dc2aeda, 2022-05-15)"
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
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.46576"
|
inkscape:cx="163.22269"
|
||||||
inkscape:cy="91.638054"
|
inkscape:cy="91.75959"
|
||||||
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:Droid Sans Fallback;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:sans-serif;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:Droid Sans Fallback;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:sans-serif;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:Droid Sans Fallback;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:sans-serif;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:Droid Sans Fallback;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:sans-serif;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: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"
|
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"
|
||||||
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:Droid Sans Fallback;-inkscape-font-specification:Droid Sans Fallback;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:sans-serif;-inkscape-font-specification:sans-serif;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:Droid Sans Fallback;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:sans-serif;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:Droid Sans Fallback;-inkscape-font-specification:Droid Sans Fallback;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:sans-serif;-inkscape-font-specification:'sans-serif Bold';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;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
|
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001"
|
||||||
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;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
|
style="font-weight:bold;font-size:8.00004px;line-height:6.88px;text-align:center;text-anchor:middle;stroke-width:1.00001"
|
||||||
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;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
|
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001"
|
||||||
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;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
|
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001"
|
||||||
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:'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"
|
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"
|
||||||
inkscape:label="Ingrédients"><tspan
|
inkscape:label="Ingrédients"><tspan
|
||||||
x="162.51953"
|
x="162.51953"
|
||||||
y="71.082662"
|
y="71.082662"
|
||||||
id="tspan2493">${ingredients}
|
id="tspan3150">${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:'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"
|
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"
|
||||||
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="tspan2495">À conserver au frais après ouverture.
|
id="tspan3152">À conserver au frais après ouverture.
|
||||||
</tspan><tspan
|
</tspan><tspan
|
||||||
x="-111.76953"
|
x="-111.76953"
|
||||||
y="277.46601"
|
y="277.34745"
|
||||||
id="tspan2497">Couvrir d'huile après chaque utilisation.
|
id="tspan3154">Couvrir d'huile après chaque utilisation.
|
||||||
</tspan><tspan
|
</tspan><tspan
|
||||||
x="-111.76953"
|
x="-111.76953"
|
||||||
y="284.25122"
|
y="284.0141"
|
||||||
id="tspan2501">À consommer de préférence avant : <tspan
|
id="tspan3158">À consommer de préférence avant : <tspan
|
||||||
style="font-weight:bold"
|
style="font-weight:bold"
|
||||||
id="tspan2499">${dluo}</tspan>
|
id="tspan3156">${dluo}</tspan>
|
||||||
</tspan><tspan
|
</tspan><tspan
|
||||||
x="-111.76953"
|
x="-111.76953"
|
||||||
y="291.03644"
|
y="290.68076"
|
||||||
id="tspan2505"><tspan
|
id="tspan3162"><tspan
|
||||||
style="font-weight:bold"
|
style="font-weight:bold"
|
||||||
id="tspan2503">*Produits issus de l'agriculture biologique.</tspan></tspan></text>
|
id="tspan3160">*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 |
@@ -1,16 +0,0 @@
|
|||||||
[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
|
|
||||||
Reference in New Issue
Block a user