Compare commits
20 Commits
topic/blěk
...
main
Author | SHA1 | Date | |
---|---|---|---|
db7de847f2 | |||
ba34b2d647 | |||
684987dfd5 | |||
d1923f640c | |||
d32caa2c38 | |||
fbb0312c7c | |||
5903c6bd36 | |||
767b722025 | |||
6b5a43b7a9 | |||
0cf8784259 | |||
3474239727 | |||
3d35d47254 | |||
c219091c2c | |||
69327d9edf | |||
3c6579e7a3 | |||
50df9778e2 | |||
d706ae1645 | |||
6a05e46946 | |||
f6ad73ef85 | |||
e222962eee |
12
Dockerfile
12
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 py3-pip && pip3 install --break-system-packages django tzdata gunicorn
|
||||
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
|
||||
|
||||
ADD backend /root/zetikettes
|
||||
ADD backend /zetikettes
|
||||
|
||||
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
|
||||
|
||||
|
||||
# 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="*"
|
||||
|
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 --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
|
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
|
||||
-------------
|
||||
|
||||
### 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 docker-compose.yml /etc/docker/compose/zetikettes/
|
||||
sudo cp 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/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.
|
||||
- redirect `/zetikettes/srv/` to `localhost:8000`
|
||||
- redirect `/zetikettes/` to `/var/lib/zetikettes/frontend`
|
||||
- redirect `/zetikettes/srv/static` to `/var/lib/zetikettes/www_static`
|
||||
|
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,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,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
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
@ -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()
|
||||
|
@ -1,8 +1,9 @@
|
||||
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(Tizer)
|
||||
admin.site.register(Tikategory)
|
||||
admin.site.register(Tisub)
|
||||
|
||||
|
@ -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"
|
||||
|
@ -1,61 +0,0 @@
|
||||
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)
|
@ -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)
|
16
backend/zetikettes/tikette/middleware.py
Normal file
16
backend/zetikettes/tikette/middleware.py
Normal file
@ -0,0 +1,16 @@
|
||||
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
|
@ -1,51 +1,127 @@
|
||||
# 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
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
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()),
|
||||
(
|
||||
"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="")),
|
||||
],
|
||||
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()),
|
||||
(
|
||||
"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,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'tisubz',
|
||||
"verbose_name_plural": "tisubz",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tikette',
|
||||
name="Tizer",
|
||||
fields=[
|
||||
('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')),
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("email", models.CharField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'tikettz',
|
||||
"verbose_name_plural": "tizerz",
|
||||
},
|
||||
),
|
||||
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"),
|
||||
),
|
||||
]
|
||||
|
@ -1,9 +1,19 @@
|
||||
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
|
||||
@ -16,6 +26,10 @@ 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
|
||||
@ -25,9 +39,19 @@ 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)
|
||||
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):
|
||||
return self.title
|
||||
@ -35,4 +59,12 @@ class Tikette(models.Model):
|
||||
class Meta:
|
||||
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
69
backend/zetikettes/tikette/planche/generate.py
Normal file
69
backend/zetikettes/tikette/planche/generate.py
Normal file
@ -0,0 +1,69 @@
|
||||
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)
|
62
backend/zetikettes/tikette/planche/makeplanche.py
Executable file
62
backend/zetikettes/tikette/planche/makeplanche.py
Executable file
@ -0,0 +1,62 @@
|
||||
#!/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()))
|
54
backend/zetikettes/tikette/planche/makesticker.py
Executable file
54
backend/zetikettes/tikette/planche/makesticker.py
Executable file
@ -0,0 +1,54 @@
|
||||
#!/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)
|
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 |
@ -1,31 +1,164 @@
|
||||
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
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from google.auth.transport import requests
|
||||
from google.oauth2 import id_token
|
||||
|
||||
from . import generate as stickersheet
|
||||
from .models import Tikette, Tikategory
|
||||
from .planche import generate as stickersheet
|
||||
from .models import Tikette, Tikategory, Tizer
|
||||
|
||||
CORS={'access-control-allow-origin': '*'}
|
||||
|
||||
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)
|
||||
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()
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def generate(request):
|
||||
if request.method != "POST":
|
||||
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
|
||||
|
||||
@post_please
|
||||
def signin(request):
|
||||
payload = json.loads(request.body)
|
||||
pdfpath = stickersheet.generate(payload, out_dir=settings.TIKETTE_OUT_DIR)
|
||||
token = payload["token"]
|
||||
|
||||
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)
|
||||
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()
|
||||
|
@ -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()
|
||||
|
@ -15,74 +15,75 @@ 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 = True
|
||||
DEBUG = False
|
||||
|
||||
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',
|
||||
"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",
|
||||
]
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,16 +93,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",
|
||||
},
|
||||
]
|
||||
|
||||
@ -109,9 +110,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
|
||||
|
||||
@ -121,11 +122,16 @@ 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'
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# not a secret
|
||||
GOOGLE_OAUTH_CLIENT_ID = (
|
||||
"634510965520-c5l7f15fn4koraqhpqfe01ssn8v0q2qk.apps.googleusercontent.com"
|
||||
)
|
||||
|
@ -14,6 +14,7 @@ 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
|
||||
@ -23,7 +24,16 @@ from django.views.generic.base import TemplateView
|
||||
import tikette.views
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('list', tikette.views.index),
|
||||
path('', tikette.views.generate),
|
||||
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),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
handler403 = "tikette.views.handler403"
|
||||
handler404 = "tikette.views.handler404"
|
||||
|
@ -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()
|
||||
|
@ -3,7 +3,8 @@ version: '3.1'
|
||||
services:
|
||||
|
||||
zetikettes:
|
||||
image: zetikettes
|
||||
build: .
|
||||
image: pol/zetikettes
|
||||
restart: always
|
||||
ports:
|
||||
- 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,2 @@
|
||||
const backend_api = 'http://jenova.ponteilla.net:8001/'
|
||||
const backend_api = "/zetikettes/srv/"
|
||||
const google_oauth_client_id = '634510965520-c5l7f15fn4koraqhpqfe01ssn8v0q2qk.apps.googleusercontent.com';
|
||||
|
@ -3,40 +3,121 @@
|
||||
<meta charset="utf-8" />
|
||||
<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>
|
||||
<!--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>
|
||||
<header class="container center orange-text">
|
||||
<h1>Zétikwett's</h1>
|
||||
</header>
|
||||
<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>
|
||||
<main class="container row">
|
||||
<div class="col m6 offset-m3 s12">
|
||||
<ul class="collapsible" id="appbody"></ul>
|
||||
<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> ?
|
||||
</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>
|
||||
</main>
|
||||
<footer class="page-footer">
|
||||
<footer class="page-footer orange">
|
||||
<div class="container row">
|
||||
<span class="col right">displayed with recycled electrons.</span>
|
||||
</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
@ -1,45 +0,0 @@
|
||||
<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>
|
@ -1,125 +0,0 @@
|
||||
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();
|
||||
});
|
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,203 @@
|
||||
const params = {
|
||||
'dluo': 'DLUO',
|
||||
'lot': 'Nº de lot',
|
||||
'qty': 'Poids net (g)',
|
||||
'vol': 'Volume net (cL)',
|
||||
'q': 'Poids net (g)',
|
||||
'teneur': 'Teneur en fruits (%)',
|
||||
'fruit': 'Quantité de fruits pour 100g (g)',
|
||||
'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,
|
||||
};
|
||||
}
|
||||
|
||||
function loadAll(zetikettes) {
|
||||
const appbody = $("#appbody");
|
||||
appbody.empty();
|
||||
for (let zett of zetikettes) {
|
||||
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)));
|
||||
addProduct(zett);
|
||||
}
|
||||
|
||||
$('.collapsible').collapsible();
|
||||
|
||||
konami();
|
||||
setCategories();
|
||||
}
|
||||
|
||||
function konami() {
|
||||
@ -76,16 +215,53 @@ function konami() {
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(async () => {
|
||||
async function googleCred(creds) {
|
||||
const token = creds.credential;
|
||||
await post(backend_api + 'signin', {token});
|
||||
$('#signin-prompt').hide();
|
||||
reload();
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
disableButtons();
|
||||
|
||||
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();
|
||||
});
|
||||
|
@ -6,6 +6,10 @@ 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;
|
||||
|
47
scripts/provision.py
Executable file
47
scripts/provision.py
Executable file
@ -0,0 +1,47 @@
|
||||
#!/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()
|
@ -7,8 +7,9 @@ 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"
|
||||
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "..", "templates")
|
||||
OUT_DIR = "."
|
||||
DEFAULT_CSV = 'product_definitions.csv'
|
||||
|
||||
JAM_DESIGNATION_FONTSIZE_DEFAULT = 42.6667
|
||||
JAM_DESIGNATION_FONTSIZE_SMALL = 36
|
||||
@ -30,7 +31,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', required=True, help='Lineup file')
|
||||
parser.add_argument('--list', default=DEFAULT_CSV, help='Lineup file')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
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"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.2 (dc2aeda, 2022-05-15)"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
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.46576"
|
||||
inkscape:cy="91.638054"
|
||||
inkscape:cx="163.22269"
|
||||
inkscape:cy="91.75959"
|
||||
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: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"
|
||||
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: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"
|
||||
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: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"
|
||||
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: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"
|
||||
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: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"
|
||||
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: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"
|
||||
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: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"
|
||||
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: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"
|
||||
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;-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
|
||||
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
|
||||
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
|
||||
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>
|
||||
</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:'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
|
||||
x="162.51953"
|
||||
y="71.082662"
|
||||
id="tspan2493">${ingredients}
|
||||
id="tspan3150">${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:'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"
|
||||
y="0"
|
||||
inkscape:label="Bloc Texte Conservation"><tspan
|
||||
x="-111.76953"
|
||||
y="270.68079"
|
||||
id="tspan2495">À conserver au frais après ouverture.
|
||||
id="tspan3152">À conserver au frais après ouverture.
|
||||
</tspan><tspan
|
||||
x="-111.76953"
|
||||
y="277.46601"
|
||||
id="tspan2497">Couvrir d'huile après chaque utilisation.
|
||||
y="277.34745"
|
||||
id="tspan3154">Couvrir d'huile après chaque utilisation.
|
||||
</tspan><tspan
|
||||
x="-111.76953"
|
||||
y="284.25122"
|
||||
id="tspan2501">À consommer de préférence avant : <tspan
|
||||
y="284.0141"
|
||||
id="tspan3158">À consommer de préférence avant : <tspan
|
||||
style="font-weight:bold"
|
||||
id="tspan2499">${dluo}</tspan>
|
||||
id="tspan3156">${dluo}</tspan>
|
||||
</tspan><tspan
|
||||
x="-111.76953"
|
||||
y="291.03644"
|
||||
id="tspan2505"><tspan
|
||||
y="290.68076"
|
||||
id="tspan3162"><tspan
|
||||
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>
|
||||
</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
|
Loading…
Reference in New Issue
Block a user