Compare commits
3 Commits
main
...
topic/blěk
Author | SHA1 | Date | |
---|---|---|---|
3de3586fe0 | |||
9b6811c410 | |||
ebe9516557 |
12
Dockerfile
12
Dockerfile
@ -1,15 +1,15 @@
|
||||
FROM alpine:3.18
|
||||
FROM alpine
|
||||
|
||||
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript font-noto py3-pip
|
||||
RUN pip3 install --break-system-packages django tzdata gunicorn google-api-python-client
|
||||
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript ttf-opensans
|
||||
RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn
|
||||
|
||||
ADD backend /zetikettes
|
||||
ADD backend /root/zetikettes
|
||||
|
||||
RUN mkdir -p /usr/share/fonts/TTF \
|
||||
&& cp /zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \
|
||||
&& cp /root/zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \
|
||||
&& fc-cache -fv
|
||||
|
||||
|
||||
# the script will look for templates in /data
|
||||
WORKDIR /zetikettes/zetikettes
|
||||
WORKDIR /root/zetikettes/zetikettes
|
||||
CMD /usr/bin/gunicorn zetikettes.wsgi -b 0.0.0.0:8000 --timeout 600 --forwarded-allow-ips="*"
|
||||
|
48
Makefile
48
Makefile
@ -1,48 +0,0 @@
|
||||
LIBPREFIX ?= /var/lib/zetikettes
|
||||
image-name = pol/zetikettes
|
||||
uuid = $(shell id -u):$(shell id -g)
|
||||
|
||||
.PHONY: image
|
||||
image: ## build the docker images
|
||||
docker compose build
|
||||
|
||||
.PHONY: initial-db
|
||||
initial-db: image ## create and populate a database
|
||||
mkdir -p $(LIBPREFIX)/data
|
||||
docker run --rm \
|
||||
--user $(uuid) \
|
||||
-v $(LIBPREFIX)/data:/data \
|
||||
$(image-name) \
|
||||
/zetikettes/zetikettes/manage.py migrate
|
||||
docker run --rm \
|
||||
--user $(uuid) \
|
||||
-v $(LIBPREFIX)/data:/data \
|
||||
$(image-name) \
|
||||
/zetikettes/zetikettes/manage.py loaddata initial_db
|
||||
cp templates/*.svg $(LIBPREFIX)/data/
|
||||
|
||||
.PHONY: superuser
|
||||
superuser: image ## create a superuser in the django admin
|
||||
docker run --rm \
|
||||
--user $(uuid) \
|
||||
-v $(LIBPREFIX)/data:/data \
|
||||
-it \
|
||||
$(image-name) \
|
||||
/zetikettes/zetikettes/manage.py createsuperuser
|
||||
|
||||
.PHONY: staticfiles
|
||||
staticfiles: image ## install all static files
|
||||
cp -r frontend $(LIBPREFIX)/
|
||||
docker run --rm \
|
||||
--user $(uuid) \
|
||||
-v $(LIBPREFIX):/libdir \
|
||||
-w /libdir \
|
||||
$(image-name) \
|
||||
/zetikettes/zetikettes/manage.py collectstatic --noinput
|
||||
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show this help
|
||||
@echo Noteworthy targets:
|
||||
@egrep '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
.DEFAULT_GOAL := help
|
93
README.md
93
README.md
@ -1,53 +1,64 @@
|
||||
zetikettes 2.0
|
||||
==============
|
||||
zetikettes
|
||||
==========
|
||||
|
||||
ouaich. tavu.
|
||||
|
||||
**NOTE**: release 2.0 broke compatibility with previous "releases". Maxime has all the recent data to repopulate a new database.
|
||||
ouaich.
|
||||
|
||||
|
||||
Initial setup
|
||||
-------------
|
||||
|
||||
### Database setup and population
|
||||
|
||||
Populate an empty database (if none already exists):
|
||||
|
||||
```
|
||||
sudo make initial-db
|
||||
```
|
||||
|
||||
This will setup an initial database under `/var/lib/zetikettes/data`
|
||||
|
||||
Optionally, you may need credentials to access the admin page:
|
||||
|
||||
```
|
||||
make superuser
|
||||
```
|
||||
|
||||
### System service
|
||||
|
||||
```
|
||||
docker build -t zetikettes .
|
||||
sudo mkdir -p /etc/docker/compose/zetikettes
|
||||
sudo cp compose.yml /etc/docker/compose/zetikettes/
|
||||
sudo cp docker-compose.yml /etc/docker/compose/zetikettes/
|
||||
sudo systemctl enable --now docker-compose@zetikettes
|
||||
```
|
||||
|
||||
If not already present in `/etc/systemd/system/`, the `docker-compose@` service file is provided.
|
||||
|
||||
### www static files
|
||||
|
||||
```
|
||||
make staticfiles
|
||||
```
|
||||
|
||||
This will install frontend and django admin static files under `/var/lib/zetikettes`
|
||||
|
||||
### Nginx
|
||||
|
||||
Example configuration is provided in `nginx_locations`.
|
||||
|
||||
Nginx is configured to:
|
||||
- redirect `/zetikettes/srv/` to `localhost:8000`
|
||||
- redirect `/zetikettes/` to `/var/lib/zetikettes/frontend`
|
||||
- redirect `/zetikettes/srv/static` to `/var/lib/zetikettes/www_static`
|
||||
- redirect /zetikettes/srv/ to localhost:8000
|
||||
- redirect /zetikettes/ to /var/lib/zetikettes/static
|
||||
|
||||
Conf is in /etc/nginx/sites-available/default
|
||||
|
||||
Test
|
||||
----
|
||||
|
||||
```
|
||||
docker run --rm -it -v $PWD/templates:/data zetikettes /bin/bash /root/zetikettes/old/mkjam.sh
|
||||
```
|
||||
|
||||
This should produce a .pdf in `templates/`. Open it to check that
|
||||
layout & fonts are correct.
|
||||
|
||||
Run
|
||||
---
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Notes for deploying
|
||||
-------------------
|
||||
|
||||
.h3 Initialize empty database
|
||||
```
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
.h3 Prepare static files
|
||||
```
|
||||
python manage.py collectstatic
|
||||
```
|
||||
The files will be in `www_static/` and need to be moved to `/var/lib/zetikettes/www_static`
|
||||
|
||||
.h3 Change host settings
|
||||
If not deploying on `aerith.ponteilla.net`, you'll need to edit `backend/zetikettes/zetikettes/settings.py` to change a couple things in there.
|
||||
|
||||
|
||||
Change available templates
|
||||
--------------------------
|
||||
|
||||
1. go to /zetikettes/admin
|
||||
1. add the newtikette
|
||||
1. still no need to restart the container (magic!)
|
||||
2. profit.
|
||||
|
BIN
backend/fonts/DejaVuMathTeXGyre.ttf
Normal file
BIN
backend/fonts/DejaVuMathTeXGyre.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSans-Bold.ttf
Normal file
BIN
backend/fonts/DejaVuSans-Bold.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSans-BoldOblique.ttf
Normal file
BIN
backend/fonts/DejaVuSans-BoldOblique.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSans-ExtraLight.ttf
Normal file
BIN
backend/fonts/DejaVuSans-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSans-Oblique.ttf
Normal file
BIN
backend/fonts/DejaVuSans-Oblique.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSans.ttf
Normal file
BIN
backend/fonts/DejaVuSans.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSansCondensed-Bold.ttf
Normal file
BIN
backend/fonts/DejaVuSansCondensed-Bold.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSansCondensed-BoldOblique.ttf
Normal file
BIN
backend/fonts/DejaVuSansCondensed-BoldOblique.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSansCondensed-Oblique.ttf
Normal file
BIN
backend/fonts/DejaVuSansCondensed-Oblique.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSansCondensed.ttf
Normal file
BIN
backend/fonts/DejaVuSansCondensed.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSansMono-Bold.ttf
Normal file
BIN
backend/fonts/DejaVuSansMono-Bold.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSansMono-BoldOblique.ttf
Normal file
BIN
backend/fonts/DejaVuSansMono-BoldOblique.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSansMono-Oblique.ttf
Normal file
BIN
backend/fonts/DejaVuSansMono-Oblique.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/DejaVuSansMono.ttf
Normal file
BIN
backend/fonts/DejaVuSansMono.ttf
Normal file
Binary file not shown.
1
backend/old/data/tikettes.json
Normal file
1
backend/old/data/tikettes.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
60
backend/old/makeplanche.py
Executable file
60
backend/old/makeplanche.py
Executable file
@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
import sys
|
||||
|
||||
DEFAULT_SHEET_TEMPLATE = Path(__file__).parent / 'planche.svg.in'
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Make a sheet with 12 stickers')
|
||||
parser.add_argument('--template',
|
||||
'-t',
|
||||
default=DEFAULT_SHEET_TEMPLATE,
|
||||
help='path to the sheet template')
|
||||
parser.add_argument('--out',
|
||||
'-o',
|
||||
default=sys.stdout,
|
||||
type=argparse.FileType('w'),
|
||||
help='output path (default: stdout)')
|
||||
parser.add_argument('sticker',
|
||||
type=argparse.FileType('r'),
|
||||
default=sys.stdin,
|
||||
nargs='?',
|
||||
help='path to the sticker SVG (default: stdin)')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
|
||||
with open(template) as tpl:
|
||||
tpl_data = tpl.read()
|
||||
|
||||
lines = sticker.readlines()
|
||||
if lines[0].startswith('<?xml'):
|
||||
lines = lines[1:]
|
||||
sticker_data = ''.join(lines)
|
||||
|
||||
subs = {
|
||||
'left0': sticker_data,
|
||||
'left1': sticker_data,
|
||||
'left2': sticker_data,
|
||||
'left3': sticker_data,
|
||||
'left4': sticker_data,
|
||||
'left5': sticker_data,
|
||||
'right0': sticker_data,
|
||||
'right1': sticker_data,
|
||||
'right2': sticker_data,
|
||||
'right3': sticker_data,
|
||||
'right4': sticker_data,
|
||||
'right5': sticker_data,
|
||||
}
|
||||
|
||||
out.write(Template(tpl_data).substitute(subs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
makeplanche(**vars(parse_args()))
|
58
backend/old/makesticker.py
Executable file
58
backend/old/makesticker.py
Executable file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
from string import Template
|
||||
import sys
|
||||
import typing
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Fill in sticker details')
|
||||
parser.add_argument('--out',
|
||||
'-o',
|
||||
default=sys.stdout,
|
||||
type=argparse.FileType('w'),
|
||||
help='output path (default: stdout)')
|
||||
parser.add_argument('--dluo',
|
||||
'-d',
|
||||
required=True,
|
||||
help='Date Limite d\'Utilisation Optimale')
|
||||
parser.add_argument('--lot', '-l', required=True, help='Numéro de lot')
|
||||
parser.add_argument('--quantite', '-q', required=False, help='Quantité (volume ou masse)')
|
||||
parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre')
|
||||
parser.add_argument('--fruit', '-f', required=False, help='Quantité de fruits')
|
||||
parser.add_argument('--size', '-s', required=False, help='Masse de produit')
|
||||
parser.add_argument('--landscape',
|
||||
action='store_true',
|
||||
help='input sticker is in landscape orientation')
|
||||
parser.add_argument('sticker', help='path to the sticker template')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def makesticker(sticker: str, out: typing.IO, subs: dict, landscape=False):
|
||||
with open(sticker) as fin:
|
||||
lines = fin.readlines()
|
||||
if lines[0].startswith('<?xml'):
|
||||
lines = lines[1:]
|
||||
sticker_data = ''.join(lines)
|
||||
|
||||
if landscape:
|
||||
# Rotate the sticker 90 degrees
|
||||
sticker_data = "<g transform=\"rotate(-90,0,0) translate(-102,0)\">{}</g>".format(sticker_data)
|
||||
|
||||
out.write(Template(sticker_data).substitute(subs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = vars(parse_args())
|
||||
subs = {
|
||||
'dluo': args.pop('dluo'),
|
||||
'lot': args.pop('lot'),
|
||||
'teneur': args.pop('teneur'),
|
||||
'fruit': args.pop('fruit'),
|
||||
'qty': args.pop('quantite'),
|
||||
'size': args.pop('size'),
|
||||
}
|
||||
args['subs'] = subs
|
||||
makesticker(**args)
|
25
backend/old/mkjam.sh
Executable file
25
backend/old/mkjam.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
DEFAULT_DPI=300
|
||||
LARGE='370' # Large container (370g)
|
||||
SMALL='220' # Small container (220g)
|
||||
|
||||
SIZE=$LARGE
|
||||
DLUO='décembre 2023'
|
||||
LOT='0722-2'
|
||||
TENEUR='50' # %
|
||||
FRUIT='80' # g
|
||||
STICKER='Gelée Extra - Cassis.svg'
|
||||
DATADIR=/data
|
||||
|
||||
PDF="`basename \"${STICKER}\" .svg` - ${LOT} (${SIZE}g).pdf"
|
||||
|
||||
here=$(dirname $(readlink -f $0))
|
||||
cd $DATADIR
|
||||
|
||||
$here/makesticker.py --landscape --dluo "$DLUO" --lot "$LOT" --teneur "$TENEUR" --fruit "$FRUIT" --quantite "$SIZE" -o out.svg "$STICKER" && \
|
||||
$here/makeplanche.py -o pout.svg -t $here/planche.svg.in out.svg && \
|
||||
rm out.svg && \
|
||||
inkscape --export-type="png" --export-dpi=$DEFAULT_DPI pout.svg && \
|
||||
rm pout.svg && \
|
||||
convert pout.png "$PDF"
|
24
backend/old/planche.svg.in
Normal file
24
backend/old/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(-42,144) rotate(-90,144,0)">
|
||||
<g transform="translate( 0,0)">$right0</g>
|
||||
<g transform="translate( 48,0)">$right1</g>
|
||||
<g transform="translate( 96,0)">$right2</g>
|
||||
<g transform="translate(144,0)">$right3</g>
|
||||
<g transform="translate(192,0)">$right4</g>
|
||||
<g transform="translate(240,0)">$right5</g>
|
||||
</g>
|
||||
<g transform="translate(-42,144) rotate(90,144,0)">
|
||||
<g transform="translate( 0,0)">$left0</g>
|
||||
<g transform="translate( 48,0)">$left1</g>
|
||||
<g transform="translate( 96,0)">$left2</g>
|
||||
<g transform="translate(144,0)">$left3</g>
|
||||
<g transform="translate(192,0)">$left4</g>
|
||||
<g transform="translate(240,0)">$left5</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 896 B |
269
backend/old/web.py
Normal file
269
backend/old/web.py
Normal file
@ -0,0 +1,269 @@
|
||||
import argparse
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import string
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from makesticker import makesticker
|
||||
from makeplanche import makeplanche
|
||||
|
||||
# defaults
|
||||
PORT = 8000
|
||||
OUT_DIR = tempfile.gettempdir()
|
||||
DATA_DIR = '/data'
|
||||
TEMPLATE_DIR = DATA_DIR
|
||||
DEFAULT_DPI = 300
|
||||
LEMOTDEPASSE = 'je veux ajouter une étiquette'
|
||||
TIKETTES = os.path.join(DATA_DIR, 'tikettes.json')
|
||||
|
||||
config = {}
|
||||
|
||||
def tikettes_path():
|
||||
return config.get('tikettes', TIKETTES)
|
||||
|
||||
def template_dir():
|
||||
return config.get('template_dir', TEMPLATE_DIR)
|
||||
|
||||
|
||||
def inkscapize(svg_in, pdf_out):
|
||||
png_out = tempfile.mktemp(suffix='.png')
|
||||
try:
|
||||
cmd = ['inkscape', '--export-type=png', f'--export-filename={png_out}',
|
||||
f'--export-dpi={DEFAULT_DPI}', svg_in]
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
cmd = ['convert', png_out, pdf_out]
|
||||
subprocess.check_call(cmd)
|
||||
finally:
|
||||
if os.path.exists(png_out):
|
||||
os.unlink(png_out)
|
||||
|
||||
|
||||
def handle_newtikette(formdata):
|
||||
with open(tikettes_path()) as f:
|
||||
tikettes = json.load(f)
|
||||
|
||||
sticker = formdata['title'] + '.svg'
|
||||
with open(os.path.join(template_dir(), sticker), 'w') as f:
|
||||
f.write(formdata['sticker'])
|
||||
|
||||
known_subs = {
|
||||
"dluo": "germinal 9999",
|
||||
"fruit": "80",
|
||||
"teneur": "50",
|
||||
"lot": "0000-0",
|
||||
"qty": "370",
|
||||
"vol": "50",
|
||||
}
|
||||
|
||||
newtikette = {
|
||||
'title': formdata['title'],
|
||||
'sticker': sticker,
|
||||
'landscape': 'landscape' in formdata,
|
||||
'subs': {k: v for k, v in known_subs.items() if k in formdata},
|
||||
}
|
||||
|
||||
logging.info(f'adding newtikette: {newtikette}')
|
||||
|
||||
tikettes.append(newtikette)
|
||||
|
||||
with open(tikettes_path(), 'w') as f:
|
||||
json.dump(tikettes, f, indent=2)
|
||||
|
||||
return json.dumps({'status': 'ok', 'message': 'newtikette added'})
|
||||
|
||||
|
||||
def handle_generate(request):
|
||||
request = json.loads(request)
|
||||
|
||||
# fill in sticker details
|
||||
sticker_out = tempfile.mktemp(suffix='.svg')
|
||||
|
||||
try:
|
||||
with open(sticker_out, 'w') as stickout:
|
||||
landscape = request.get('landscape', False)
|
||||
print(f'landscape: {landscape}')
|
||||
makesticker(os.path.join(template_dir(), request['sticker']), stickout,
|
||||
request['subs'], landscape=landscape)
|
||||
|
||||
# make sticker sheet
|
||||
planche_out = tempfile.mktemp(suffix='.svg')
|
||||
with open(sticker_out, 'r') as stickin:
|
||||
with open(planche_out, 'w') as planchout:
|
||||
makeplanche(stickin, planchout)
|
||||
|
||||
# process to printable pdf
|
||||
pdf_out = tempfile.mktemp(dir=OUT_DIR, suffix='.pdf')
|
||||
inkscapize(planche_out, pdf_out)
|
||||
|
||||
response = {'status': 'ok', 'file': os.path.basename(pdf_out),
|
||||
'message': 'this is the way'}
|
||||
return json.dumps(response)
|
||||
|
||||
finally:
|
||||
if os.path.exists(sticker_out):
|
||||
os.unlink(sticker_out)
|
||||
if 'planche_out' in locals() and os.path.exists(planche_out):
|
||||
os.unlink(planche_out)
|
||||
|
||||
|
||||
def parse_multipart(data, boundary):
|
||||
logging.debug(f'parsing data with boundary: {boundary}')
|
||||
f = io.StringIO(data)
|
||||
out = []
|
||||
|
||||
if boundary not in f.readline():
|
||||
return out
|
||||
|
||||
while True:
|
||||
headers = []
|
||||
while True:
|
||||
h = f.readline().strip()
|
||||
try:
|
||||
k, v = re.findall(r'^([a-zA-Z-]+): (.*)$', h)[0]
|
||||
except IndexError:
|
||||
break
|
||||
headers.append((k, v))
|
||||
metadata = None
|
||||
otherheaders = []
|
||||
logging.debug(headers)
|
||||
for k, v in headers:
|
||||
if k == 'Content-Disposition':
|
||||
parts = v.split('; ')
|
||||
if parts[0] != 'form-data':
|
||||
continue
|
||||
d = dict(re.findall(r'([^=]+)="(.*)"$', p)[0] for p in parts[1:])
|
||||
metadata = d
|
||||
else:
|
||||
otherheaders.append((k, v))
|
||||
|
||||
value = io.StringIO()
|
||||
while True:
|
||||
l = f.readline()
|
||||
if not l:
|
||||
break
|
||||
if boundary in l:
|
||||
# value.write(l.split(boundary)[0])
|
||||
break
|
||||
value.write(l)
|
||||
if metadata is None:
|
||||
break
|
||||
logging.debug(f'({metadata}, {value.getvalue()[:-2]}, {otherheaders})')
|
||||
out.append((metadata, value.getvalue()[:-2], otherheaders))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class MyServer(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
logging.info(f'GET {self.path}')
|
||||
if self.path == '/list':
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "application/json")
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
with open(tikettes_path()) as f:
|
||||
tikettes = json.load(f)
|
||||
|
||||
resp = {'status': 'ok', 'message': 'this is the way',
|
||||
'tikettes': tikettes}
|
||||
self.wfile.write(json.dumps(resp).encode())
|
||||
return
|
||||
match = re.match(r'/data/(\w+\.pdf)', self.path)
|
||||
if match is not None:
|
||||
pdf_path = os.path.join(OUT_DIR, match.groups()[0])
|
||||
if not os.path.exists(pdf_path):
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "application/pdf")
|
||||
self.end_headers()
|
||||
with open(pdf_path, 'rb') as f:
|
||||
self.wfile.write(f.read())
|
||||
return
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html; charset=UTF-8")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"<html><head><title>Zetikettes</title></head>")
|
||||
self.wfile.write(b"<body>")
|
||||
self.wfile.write("<p>This is not the way.</p>".encode())
|
||||
self.wfile.write(b"</body></html>")
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
logging.info(f'POST {self.path}')
|
||||
try:
|
||||
length = int(self.headers['content-length'])
|
||||
req = self.rfile.read(length).decode()
|
||||
if self.path == '/newtikette':
|
||||
boundary = re.findall(r'boundary=(.*)$', self.headers['content-type'])[0]
|
||||
formdata = {p['name']: v for p, v, _ in parse_multipart(req, boundary)}
|
||||
logging.info({k: v[:100] for k, v in formdata.items()})
|
||||
|
||||
if formdata.get('lemotdepasse', None) != LEMOTDEPASSE:
|
||||
logging.warning(f'wrong lemotdepasse')
|
||||
self.send_response(403)
|
||||
self.send_header("Content-type", "application/json")
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({'status': 'notok', 'message': 'bad lemotdepasse'}).encode())
|
||||
return
|
||||
|
||||
resp = handle_newtikette(formdata).encode()
|
||||
else:
|
||||
resp = handle_generate(req).encode()
|
||||
except:
|
||||
self.send_response(500)
|
||||
self.send_header("Content-type", "text/plain")
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(traceback.format_exc().encode())
|
||||
raise
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "application/json")
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(resp)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Zetikette backend')
|
||||
parser.add_argument('--port', type=int, default=PORT, help=f'default: {PORT}')
|
||||
parser.add_argument('--data-dir', default=DATA_DIR, help=f'default: {DATA_DIR}')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
args = parse_args()
|
||||
|
||||
config.update({
|
||||
'tikettes': os.path.join(args.data_dir, 'tikettes.json'),
|
||||
'template_dir': args.data_dir,
|
||||
})
|
||||
|
||||
webServer = HTTPServer(('', args.port), MyServer)
|
||||
print(f"Server started on port {args.port}")
|
||||
|
||||
try:
|
||||
webServer.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
webServer.server_close()
|
||||
print("Server stopped.")
|
@ -1,152 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "dluo",
|
||||
"descritpion": "DLUO",
|
||||
"default": "décembre 1992",
|
||||
"type": "YM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "lot",
|
||||
"descritpion": "№ de lot",
|
||||
"default": "1234-5",
|
||||
"type": "ST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "q",
|
||||
"descritpion": "Poids net (g)",
|
||||
"default": "370",
|
||||
"type": "ST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "teneur",
|
||||
"descritpion": "Teneur en fruits (%)",
|
||||
"default": "50",
|
||||
"type": "ST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tisub",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "f",
|
||||
"descritpion": "Quantité de fruits pour 100g (g)",
|
||||
"default": "60",
|
||||
"type": "ST"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Pesto",
|
||||
"landscape": true,
|
||||
"prototempalte": "Pesto.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Confiture",
|
||||
"landscape": true,
|
||||
"prototempalte": "Confiture.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "Chocolat",
|
||||
"landscape": false,
|
||||
"prototempalte": "Chocolat.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Aromate",
|
||||
"landscape": false,
|
||||
"prototempalte": "Aromate.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Sel",
|
||||
"landscape": true,
|
||||
"prototempalte": "Sel.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "Sirop",
|
||||
"landscape": false,
|
||||
"prototempalte": "Sirop.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "tikette.tikategory",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Tisane",
|
||||
"landscape": false,
|
||||
"prototempalte": "Tisane.svg",
|
||||
"subs": [
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
@ -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,9 +1,8 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Tikette, Tizer, Tikategory, Tisub
|
||||
from .models import Tikette, Tikategory, Tisub
|
||||
|
||||
admin.site.register(Tikette)
|
||||
admin.site.register(Tizer)
|
||||
admin.site.register(Tikategory)
|
||||
admin.site.register(Tisub)
|
||||
|
||||
|
@ -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'
|
||||
|
61
backend/zetikettes/tikette/generate.py
Normal file
61
backend/zetikettes/tikette/generate.py
Normal file
@ -0,0 +1,61 @@
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from .makesticker import makesticker
|
||||
from .makeplanche import makeplanche
|
||||
|
||||
OUT_DIR = tempfile.gettempdir()
|
||||
DEFAULT_DPI = 300
|
||||
|
||||
def inkscapize(svg_in, pdf_out):
|
||||
png_out = tempfile.mktemp(suffix='.png')
|
||||
try:
|
||||
cmd = ['inkscape', '--export-type=png', f'--export-filename={png_out}',
|
||||
f'--export-dpi={DEFAULT_DPI}', svg_in]
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
cmd = ['convert', png_out, pdf_out]
|
||||
subprocess.check_call(cmd)
|
||||
finally:
|
||||
if os.path.exists(png_out):
|
||||
os.unlink(png_out)
|
||||
|
||||
|
||||
def generate(request, out_dir):
|
||||
""" Generate a sticker sheet.
|
||||
|
||||
request: dict-like with the following fields:
|
||||
sticker: mandatory. filename for the sticker
|
||||
subs: mandatory. dict-like with key-value template subtitution fields
|
||||
landscape: optional. defaults to False
|
||||
|
||||
out_dir: you get it
|
||||
"""
|
||||
|
||||
# fill in sticker details
|
||||
sticker_out = tempfile.mktemp(suffix='.svg')
|
||||
|
||||
try:
|
||||
with open(sticker_out, 'w') as stickout:
|
||||
landscape = request.get('landscape', False)
|
||||
makesticker(os.path.join(out_dir, request['sticker']), stickout,
|
||||
request['subs'], landscape=landscape)
|
||||
|
||||
# make sticker sheet
|
||||
planche_out = tempfile.mktemp(suffix='.svg')
|
||||
with open(sticker_out, 'r') as stickin:
|
||||
with open(planche_out, 'w') as planchout:
|
||||
makeplanche(stickin, planchout)
|
||||
|
||||
# process to printable pdf
|
||||
pdf_out = tempfile.mktemp(dir=out_dir, suffix='.pdf')
|
||||
inkscapize(planche_out, pdf_out)
|
||||
|
||||
return os.path.basename(pdf_out)
|
||||
|
||||
finally:
|
||||
if os.path.exists(sticker_out):
|
||||
os.unlink(sticker_out)
|
||||
if 'planche_out' in locals() and os.path.exists(planche_out):
|
||||
os.unlink(planche_out)
|
60
backend/zetikettes/tikette/makeplanche.py
Executable file
60
backend/zetikettes/tikette/makeplanche.py
Executable file
@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
import sys
|
||||
|
||||
DEFAULT_SHEET_TEMPLATE = Path(__file__).parent / 'planche.svg.in'
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Make a sheet with 12 stickers')
|
||||
parser.add_argument('--template',
|
||||
'-t',
|
||||
default=DEFAULT_SHEET_TEMPLATE,
|
||||
help='path to the sheet template')
|
||||
parser.add_argument('--out',
|
||||
'-o',
|
||||
default=sys.stdout,
|
||||
type=argparse.FileType('w'),
|
||||
help='output path (default: stdout)')
|
||||
parser.add_argument('sticker',
|
||||
type=argparse.FileType('r'),
|
||||
default=sys.stdin,
|
||||
nargs='?',
|
||||
help='path to the sticker SVG (default: stdin)')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
|
||||
with open(template) as tpl:
|
||||
tpl_data = tpl.read()
|
||||
|
||||
lines = sticker.readlines()
|
||||
if lines[0].startswith('<?xml'):
|
||||
lines = lines[1:]
|
||||
sticker_data = ''.join(lines)
|
||||
|
||||
subs = {
|
||||
'left0': sticker_data,
|
||||
'left1': sticker_data,
|
||||
'left2': sticker_data,
|
||||
'left3': sticker_data,
|
||||
'left4': sticker_data,
|
||||
'left5': sticker_data,
|
||||
'right0': sticker_data,
|
||||
'right1': sticker_data,
|
||||
'right2': sticker_data,
|
||||
'right3': sticker_data,
|
||||
'right4': sticker_data,
|
||||
'right5': sticker_data,
|
||||
}
|
||||
|
||||
out.write(Template(tpl_data).substitute(subs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
makeplanche(**vars(parse_args()))
|
58
backend/zetikettes/tikette/makesticker.py
Executable file
58
backend/zetikettes/tikette/makesticker.py
Executable file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
from string import Template
|
||||
import sys
|
||||
import typing
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Fill in sticker details')
|
||||
parser.add_argument('--out',
|
||||
'-o',
|
||||
default=sys.stdout,
|
||||
type=argparse.FileType('w'),
|
||||
help='output path (default: stdout)')
|
||||
parser.add_argument('--dluo',
|
||||
'-d',
|
||||
required=True,
|
||||
help='Date Limite d\'Utilisation Optimale')
|
||||
parser.add_argument('--lot', '-l', required=True, help='Numéro de lot')
|
||||
parser.add_argument('--quantite', '-q', required=False, help='Quantité (volume ou masse)')
|
||||
parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre')
|
||||
parser.add_argument('--fruit', '-f', required=False, help='Quantité de fruits')
|
||||
parser.add_argument('--size', '-s', required=False, help='Masse de produit')
|
||||
parser.add_argument('--landscape',
|
||||
action='store_true',
|
||||
help='input sticker is in landscape orientation')
|
||||
parser.add_argument('sticker', help='path to the sticker template')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def makesticker(sticker: str, out: typing.IO, subs: dict, landscape=False):
|
||||
with open(sticker) as fin:
|
||||
lines = fin.readlines()
|
||||
if lines[0].startswith('<?xml'):
|
||||
lines = lines[1:]
|
||||
sticker_data = ''.join(lines)
|
||||
|
||||
if landscape:
|
||||
# Rotate the sticker 90 degrees
|
||||
sticker_data = "<g transform=\"rotate(-90,0,0) translate(-102,0)\">{}</g>".format(sticker_data)
|
||||
|
||||
out.write(Template(sticker_data).substitute(subs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = vars(parse_args())
|
||||
subs = {
|
||||
'dluo': args.pop('dluo'),
|
||||
'lot': args.pop('lot'),
|
||||
'teneur': args.pop('teneur'),
|
||||
'fruit': args.pop('fruit'),
|
||||
'qty': args.pop('quantite'),
|
||||
'size': args.pop('size'),
|
||||
}
|
||||
args['subs'] = subs
|
||||
makesticker(**args)
|
@ -1,16 +0,0 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def cors_everywhere(get_response):
|
||||
def middleware(request):
|
||||
if request.method == "OPTIONS":
|
||||
response = HttpResponse()
|
||||
else:
|
||||
response = get_response(request)
|
||||
response["Access-Control-Allow-Origin"] = request.headers.get("Origin", "*")
|
||||
response["Access-Control-Allow-Credentials"] = "true"
|
||||
response["Access-Control-Allow-Headers"] = "x-csrftoken"
|
||||
return response
|
||||
|
||||
return middleware
|
@ -1,127 +1,51 @@
|
||||
# Generated by Django 5.2.4 on 2025-08-05 12:57
|
||||
# Generated by Django 4.2.2 on 2023-07-03 14:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Tikategory",
|
||||
name='Tikategory',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=50)),
|
||||
("landscape", models.BooleanField()),
|
||||
("prototempalte", models.FileField(upload_to="")),
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('landscape', models.BooleanField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "tikategoriez",
|
||||
'verbose_name_plural': 'tikategoriez',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Tisub",
|
||||
name='Tisub',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=50)),
|
||||
("descritpion", models.TextField()),
|
||||
("default", models.TextField()),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("ST", "Short Text"),
|
||||
("LT", "Long Text"),
|
||||
("C", "Color"),
|
||||
("YM", "Year Month"),
|
||||
("B", "Boolean"),
|
||||
],
|
||||
default="ST",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('descritpion', models.TextField()),
|
||||
('default', models.TextField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "tisubz",
|
||||
'verbose_name_plural': 'tisubz',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Tizer",
|
||||
name='Tikette',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("email", models.CharField()),
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('svg', models.FileField(upload_to='')),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tikette.tikategory')),
|
||||
('subs', models.ManyToManyField(to='tikette.tisub')),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "tizerz",
|
||||
'verbose_name_plural': 'tikettz',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Tikette",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=100)),
|
||||
("designation", models.CharField(max_length=100)),
|
||||
("ingredients", models.TextField()),
|
||||
("description", models.TextField()),
|
||||
("color", models.CharField(max_length=6)),
|
||||
(
|
||||
"ab",
|
||||
models.CharField(
|
||||
choices=[("inline", "Visible"), ("none", "Invisible")],
|
||||
max_length=7,
|
||||
),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="tikette.tikategory",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "tikettz",
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="tikategory",
|
||||
name="subs",
|
||||
field=models.ManyToManyField(to="tikette.tisub"),
|
||||
),
|
||||
]
|
||||
|
@ -1,19 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Tisub(models.Model):
|
||||
class Type(models.TextChoices):
|
||||
SHORT_TEXT = "ST"
|
||||
LONG_TEXT = "LT"
|
||||
COLOR = "C"
|
||||
YEAR_MONTH = "YM"
|
||||
BOOLEAN = "B"
|
||||
|
||||
name = models.CharField(max_length=50)
|
||||
descritpion = models.TextField()
|
||||
default = models.TextField()
|
||||
# everything is text. type is for UX (e.g. color picker)
|
||||
type = models.CharField(max_length=2, choices=Type, default=Type.SHORT_TEXT)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -26,10 +16,6 @@ class Tikategory(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
landscape = models.BooleanField()
|
||||
subs = models.ManyToManyField(Tisub)
|
||||
# For now we'll hardcode the following:xi
|
||||
# [designation, ingredients, description, color, AB, designation_fontsize]
|
||||
# protosubs = models.ManyToManyField(Tisub, related_name="protosubs")
|
||||
prototempalte = models.FileField()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@ -39,19 +25,9 @@ class Tikategory(models.Model):
|
||||
|
||||
|
||||
class Tikette(models.Model):
|
||||
class AbVisibility(models.TextChoices):
|
||||
VISIBLE = "inline"
|
||||
INVISIBLE = "none"
|
||||
|
||||
title = models.CharField(max_length=100)
|
||||
category = models.ForeignKey(Tikategory, on_delete=models.CASCADE)
|
||||
|
||||
designation = models.CharField(max_length=100)
|
||||
ingredients = models.TextField()
|
||||
description = models.TextField()
|
||||
color = models.CharField(max_length=6)
|
||||
ab = models.CharField(max_length=7, choices=AbVisibility)
|
||||
# designation_fontsize is hardcoded to 42.6667 in planche/generate.py
|
||||
svg = models.FileField()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@ -59,12 +35,4 @@ class Tikette(models.Model):
|
||||
class Meta:
|
||||
verbose_name_plural = "tikettz"
|
||||
|
||||
|
||||
class Tizer(models.Model):
|
||||
email = models.CharField()
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "tizerz"
|
||||
# Create your models here.
|
||||
|
24
backend/zetikettes/tikette/planche.svg.in
Normal file
24
backend/zetikettes/tikette/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(-42,144) rotate(-90,144,0)">
|
||||
<g transform="translate( 0,0) scale(0.26458)">$right0</g>
|
||||
<g transform="translate( 48,0) scale(0.26458)">$right1</g>
|
||||
<g transform="translate( 96,0) scale(0.26458)">$right2</g>
|
||||
<g transform="translate(144,0) scale(0.26458)">$right3</g>
|
||||
<g transform="translate(192,0) scale(0.26458)">$right4</g>
|
||||
<g transform="translate(240,0) scale(0.26458)">$right5</g>
|
||||
</g>
|
||||
<g transform="translate(-42,144) rotate(90,144,0)">
|
||||
<g transform="translate( 0,0) scale(0.26458)">$left0</g>
|
||||
<g transform="translate( 48,0) scale(0.26458)">$left1</g>
|
||||
<g transform="translate( 96,0) scale(0.26458)">$left2</g>
|
||||
<g transform="translate(144,0) scale(0.26458)">$left3</g>
|
||||
<g transform="translate(192,0) scale(0.26458)">$left4</g>
|
||||
<g transform="translate(240,0) scale(0.26458)">$left5</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -1,69 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from .makesticker import makesticker
|
||||
from .makeplanche import makeplanche
|
||||
|
||||
OUT_DIR = tempfile.gettempdir()
|
||||
DEFAULT_DPI = 300
|
||||
DESIGNATION_FONTSIZE = 42.6667
|
||||
|
||||
|
||||
def inkscapize(svg_in, pdf_out):
|
||||
png_out = tempfile.mktemp(suffix=".png")
|
||||
try:
|
||||
cmd = [
|
||||
"inkscape",
|
||||
"--export-type=png",
|
||||
f"--export-filename={png_out}",
|
||||
f"--export-dpi={DEFAULT_DPI}",
|
||||
svg_in,
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
cmd = ["convert", png_out, pdf_out]
|
||||
subprocess.check_call(cmd)
|
||||
finally:
|
||||
if os.path.exists(png_out):
|
||||
os.unlink(png_out)
|
||||
|
||||
|
||||
def generate(template, subs, out_dir, landscape=False):
|
||||
"""Generate a sticker sheet.
|
||||
|
||||
template: file name for the sticker template
|
||||
subs: dict-like with key-value template subtitution fields
|
||||
out_dir: you get it
|
||||
landscape: optional. defaults to False
|
||||
"""
|
||||
|
||||
# default designation font size
|
||||
subs["designation_fontsize"] = subs.get(
|
||||
"designation_fontsize", DESIGNATION_FONTSIZE
|
||||
)
|
||||
|
||||
# fill in sticker details
|
||||
sticker_out = tempfile.mktemp(suffix=".svg")
|
||||
|
||||
try:
|
||||
with open(sticker_out, "w") as stickout:
|
||||
makesticker(os.path.join(out_dir, template), stickout, subs)
|
||||
|
||||
# make sticker sheet
|
||||
planche_out = tempfile.mktemp(suffix=".svg")
|
||||
with open(sticker_out, "r") as stickin:
|
||||
with open(planche_out, "w") as planchout:
|
||||
makeplanche(stickin, planchout, landscape=landscape)
|
||||
|
||||
# process to printable pdf
|
||||
pdf_out = tempfile.mktemp(dir=out_dir, suffix=".pdf")
|
||||
inkscapize(planche_out, pdf_out)
|
||||
|
||||
return os.path.basename(pdf_out)
|
||||
|
||||
finally:
|
||||
if os.path.exists(sticker_out):
|
||||
os.unlink(sticker_out)
|
||||
if "planche_out" in locals() and os.path.exists(planche_out):
|
||||
os.unlink(planche_out)
|
@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
import sys
|
||||
|
||||
DEFAULT_SHEET_TEMPLATE = Path(__file__).parent / "planche.svg.in"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Make a sheet with 12 stickers")
|
||||
parser.add_argument(
|
||||
"--template",
|
||||
"-t",
|
||||
default=DEFAULT_SHEET_TEMPLATE,
|
||||
help="path to the sheet template",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
"-o",
|
||||
default=sys.stdout,
|
||||
type=argparse.FileType("w"),
|
||||
help="output path (default: stdout)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--landscape",
|
||||
action="store_true",
|
||||
help="input sticker is in landscape orientation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"sticker",
|
||||
type=argparse.FileType("r"),
|
||||
default=sys.stdin,
|
||||
nargs="?",
|
||||
help="path to the sticker SVG (default: stdin)",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE, landscape=False):
|
||||
with open(template) as tpl:
|
||||
tpl_data = tpl.read()
|
||||
|
||||
lines = sticker.readlines()
|
||||
if lines[0].startswith("<?xml"):
|
||||
lines = lines[1:]
|
||||
sticker_data = "".join(lines)
|
||||
|
||||
rotate = "translate(102, 0) rotate(90)" if not landscape else ""
|
||||
|
||||
subs = {
|
||||
"sticker": sticker_data,
|
||||
"rotate": rotate,
|
||||
}
|
||||
|
||||
out.write(Template(tpl_data).substitute(subs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
makeplanche(**vars(parse_args()))
|
@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
from string import Template
|
||||
import sys
|
||||
import typing
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Fill in sticker details")
|
||||
parser.add_argument(
|
||||
"--out",
|
||||
"-o",
|
||||
default=sys.stdout,
|
||||
type=argparse.FileType("w"),
|
||||
help="output path (default: stdout)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dluo", "-d", required=True, help="Date Limite d'Utilisation Optimale"
|
||||
)
|
||||
parser.add_argument("--lot", "-l", required=True, help="Numéro de lot")
|
||||
parser.add_argument(
|
||||
"--quantite", "-q", required=False, help="Quantité (volume ou masse)"
|
||||
)
|
||||
parser.add_argument("--teneur", "-t", required=False, help="Teneur en sucre")
|
||||
parser.add_argument("--fruit", "-f", required=False, help="Quantité de fruits")
|
||||
parser.add_argument("--size", "-s", required=False, help="Masse de produit")
|
||||
parser.add_argument("sticker", help="path to the sticker template")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def makesticker(sticker: str, out: typing.IO, subs: dict):
|
||||
with open(sticker) as fin:
|
||||
lines = fin.readlines()
|
||||
if lines[0].startswith("<?xml"):
|
||||
lines = lines[1:]
|
||||
sticker_data = "".join(lines)
|
||||
|
||||
out.write(Template(sticker_data).substitute(subs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = vars(parse_args())
|
||||
subs = {
|
||||
"dluo": args.pop("dluo"),
|
||||
"lot": args.pop("lot"),
|
||||
"teneur": args.pop("teneur"),
|
||||
"f": args.pop("fruit"),
|
||||
"q": args.pop("quantite"),
|
||||
"size": args.pop("size"),
|
||||
}
|
||||
args["subs"] = subs
|
||||
makesticker(**args)
|
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 210 297"
|
||||
height="297mm"
|
||||
width="210mm">
|
||||
<g transform="translate(3, 4.5)">
|
||||
|
||||
<g transform="translate(0, 0) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 48) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 96) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 144) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 192) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 240) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
|
||||
<g transform="rotate(180, 102, 144)">
|
||||
<g transform="translate(0, 0) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 48) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 96) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 144) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 192) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
<g transform="translate(0, 240) ${rotate} scale(0.26458)">${sticker}</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,164 +1,31 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from google.auth.transport import requests
|
||||
from google.oauth2 import id_token
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from .planche import generate as stickersheet
|
||||
from .models import Tikette, Tikategory, Tizer
|
||||
from . import generate as stickersheet
|
||||
from .models import Tikette, Tikategory
|
||||
|
||||
CORS={'access-control-allow-origin': '*'}
|
||||
|
||||
def ok(payload=None):
|
||||
return JsonResponse({"status": "ok", **(payload or {})})
|
||||
|
||||
|
||||
def post_please(f):
|
||||
def __f(request):
|
||||
if request.method != "POST":
|
||||
return JsonResponse(
|
||||
{"status": "notok", "message": "this isn't the way"},
|
||||
status=405,
|
||||
headers={"Allow": "POST"},
|
||||
)
|
||||
return f(request)
|
||||
|
||||
return __f
|
||||
|
||||
|
||||
def auth_only(f):
|
||||
def __f(request):
|
||||
# check that email is valid
|
||||
# exp?
|
||||
if "user_data" not in request.session:
|
||||
raise PermissionDenied("Not logged in")
|
||||
email = request.session["user_data"]["email"]
|
||||
resp = Tizer.objects.filter(email=email)
|
||||
if not resp:
|
||||
raise PermissionDenied("User not found")
|
||||
return f(request)
|
||||
|
||||
return __f
|
||||
|
||||
|
||||
def quirk_bold_allergens(ingredients):
|
||||
out = []
|
||||
for ing in (x.strip() for x in ingredients.split(",")):
|
||||
if ing.startswith("*"):
|
||||
out.append(f'<tspan style="font-weight:bold">{ing[1:]}</tspan>')
|
||||
else:
|
||||
out.append(ing)
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
def handler403(request, exception):
|
||||
return JsonResponse({"status": "notok", "message": "permission denied"}, status=403)
|
||||
|
||||
|
||||
def handler404(request, exception):
|
||||
return JsonResponse(
|
||||
{"status": "notok", "message": "endpoint not found"}, status=404
|
||||
)
|
||||
|
||||
|
||||
@auth_only
|
||||
@ensure_csrf_cookie
|
||||
def get_list(request):
|
||||
tikettes = [
|
||||
{
|
||||
"id": x.id,
|
||||
"title": x.title,
|
||||
"category": x.category.name,
|
||||
"category_id": x.category.id,
|
||||
"prototempalte": x.category.prototempalte.name,
|
||||
"landscape": x.category.landscape,
|
||||
"designation": x.designation,
|
||||
"ingredients": x.ingredients,
|
||||
"description": x.description,
|
||||
"ab": x.ab,
|
||||
"color": x.color,
|
||||
"subs": {x.name: x.default for x in x.category.subs.all()},
|
||||
}
|
||||
for x in Tikette.objects.all()
|
||||
]
|
||||
return ok({"tikettes": tikettes})
|
||||
|
||||
|
||||
@auth_only
|
||||
def get_categories(request):
|
||||
tikats = [
|
||||
{
|
||||
"id": x.id,
|
||||
"name": x.name,
|
||||
}
|
||||
for x in Tikategory.objects.all()
|
||||
]
|
||||
return ok({"tikats": tikats})
|
||||
|
||||
|
||||
@auth_only
|
||||
@post_please
|
||||
def generate(request):
|
||||
payload = json.loads(request.body)
|
||||
|
||||
subs = dict(payload["subs"])
|
||||
for key in ("designation", "ingredients", "description", "color", "AB"):
|
||||
subs[key] = payload[key]
|
||||
|
||||
subs["ingredients"] = quirk_bold_allergens(subs["ingredients"])
|
||||
|
||||
pdfpath = stickersheet.generate(
|
||||
template=payload["template"],
|
||||
subs=subs,
|
||||
out_dir=settings.TIKETTE_OUT_DIR,
|
||||
landscape=payload["landscape"],
|
||||
)
|
||||
|
||||
return ok({"file": pdfpath})
|
||||
|
||||
|
||||
@auth_only
|
||||
@post_please
|
||||
def newtikette(request):
|
||||
payload = json.loads(request.body)
|
||||
tikette = Tikette(**payload)
|
||||
tikette.save()
|
||||
|
||||
return ok()
|
||||
|
||||
|
||||
@auth_only
|
||||
@post_please
|
||||
def deletetikette(request):
|
||||
payload = json.loads(request.body)
|
||||
Tikette.objects.get(id=payload["id"]).delete()
|
||||
|
||||
return ok()
|
||||
|
||||
def index(request):
|
||||
tikettes = [{
|
||||
'title': x.title,
|
||||
'category': x.category.name,
|
||||
'sticker': x.svg.name,
|
||||
'landscape': x.category.landscape,
|
||||
'subs': {x.name: x.default for x in x.category.subs.all()},
|
||||
} for x in Tikette.objects.all()]
|
||||
return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS)
|
||||
|
||||
@csrf_exempt
|
||||
@post_please
|
||||
def signin(request):
|
||||
def generate(request):
|
||||
if request.method != "POST":
|
||||
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
|
||||
|
||||
payload = json.loads(request.body)
|
||||
token = payload["token"]
|
||||
pdfpath = stickersheet.generate(payload, out_dir=settings.TIKETTE_OUT_DIR)
|
||||
|
||||
try:
|
||||
user_data = id_token.verify_oauth2_token(
|
||||
token, requests.Request(), settings.GOOGLE_OAUTH_CLIENT_ID
|
||||
)
|
||||
except ValueError:
|
||||
raise
|
||||
return JsonResponse({"status": "notok"}, status=403)
|
||||
|
||||
request.session["user_data"] = user_data
|
||||
|
||||
return ok(user_data)
|
||||
|
||||
|
||||
def signout(request):
|
||||
del request.session["user_data"]
|
||||
return ok()
|
||||
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)
|
||||
|
@ -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,75 +15,74 @@ from pathlib import Path
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# TIKETTE_OUT_DIR = BASE_DIR / 'data'
|
||||
TIKETTE_OUT_DIR = Path("/data")
|
||||
#TIKETTE_OUT_DIR = BASE_DIR / 'data'
|
||||
TIKETTE_OUT_DIR = Path('/data')
|
||||
MEDIA_ROOT = TIKETTE_OUT_DIR
|
||||
MEDIA_URL = "/data/"
|
||||
MEDIA_URL = '/data/'
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-64qxpe55#9wy=5@#dl0)3w7ywxh48m!f&!slp9e7v4lh@hjdct"
|
||||
SECRET_KEY = 'django-insecure-64qxpe55#9wy=5@#dl0)3w7ywxh48m!f&!slp9e7v4lh@hjdct'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
CSRF_TRUSTED_ORIGINS = ["https://*.ponteilla.net"]
|
||||
ALLOWED_HOSTS = ['*']
|
||||
CSRF_TRUSTED_ORIGINS = ['https://*.ponteilla.net']
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"tikette",
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'tikette',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"tikette.middleware.cors_everywhere",
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "zetikettes.urls"
|
||||
ROOT_URLCONF = 'zetikettes.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "zetikettes.wsgi.application"
|
||||
WSGI_APPLICATION = 'zetikettes.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": TIKETTE_OUT_DIR / "db.sqlite3",
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': TIKETTE_OUT_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,16 +92,16 @@ DATABASES = {
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
@ -110,9 +109,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -122,16 +121,11 @@ USE_TZ = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "/zetikettes/srv/static/"
|
||||
STATIC_URL = '/zetikettes/srv/static/'
|
||||
|
||||
STATIC_ROOT = "www_static"
|
||||
STATIC_ROOT = 'www_static'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# not a secret
|
||||
GOOGLE_OAUTH_CLIENT_ID = (
|
||||
"634510965520-c5l7f15fn4koraqhpqfe01ssn8v0q2qk.apps.googleusercontent.com"
|
||||
)
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
@ -14,7 +14,6 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
@ -24,16 +23,7 @@ from django.views.generic.base import TemplateView
|
||||
import tikette.views
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("list", tikette.views.get_list),
|
||||
path("categories", tikette.views.get_categories),
|
||||
path("generate", tikette.views.generate),
|
||||
path("newtikette", tikette.views.newtikette),
|
||||
path("deletetikette", tikette.views.deletetikette),
|
||||
path("updatetikette", tikette.views.newtikette), # yes, we use newtikette
|
||||
path("signin", tikette.views.signin),
|
||||
path("signout", tikette.views.signout),
|
||||
path('admin/', admin.site.urls),
|
||||
path('list', tikette.views.index),
|
||||
path('', tikette.views.generate),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
handler403 = "tikette.views.handler403"
|
||||
handler404 = "tikette.views.handler404"
|
||||
|
@ -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,8 +3,7 @@ version: '3.1'
|
||||
services:
|
||||
|
||||
zetikettes:
|
||||
build: .
|
||||
image: pol/zetikettes
|
||||
image: zetikettes
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:8000:8000
|
@ -1,14 +0,0 @@
|
||||
[Unit]
|
||||
Description=%i service with docker compose
|
||||
PartOf=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=true
|
||||
WorkingDirectory=/etc/docker/compose/%i
|
||||
ExecStart=/usr/bin/docker-compose up -d --remove-orphans
|
||||
ExecStop=/usr/bin/docker-compose down
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,2 +1 @@
|
||||
const backend_api = "/zetikettes/srv/"
|
||||
const google_oauth_client_id = '634510965520-c5l7f15fn4koraqhpqfe01ssn8v0q2qk.apps.googleusercontent.com';
|
||||
const backend_api = 'http://jenova.ponteilla.net:8001/'
|
||||
|
@ -3,121 +3,40 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Zětikwett's</title>
|
||||
<title>zetikettes 0.2</title>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!--Import Google Icon Font-->
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<!-- Compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||
<link rel="stylesheet" href="placeholder.css">
|
||||
|
||||
<!-- Compiled and minified JavaScript -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
<script src="https://accounts.google.com/gsi/client"></script>
|
||||
|
||||
<script src="jscolor.min.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="zetikettes.js"></script>
|
||||
|
||||
<style>
|
||||
/* for the footer */
|
||||
body {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.confirm {
|
||||
width: 35%;
|
||||
min-width: 20em;
|
||||
max-width: 85%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="light-blue">
|
||||
<div class="nav-wrapper container">
|
||||
<a href="#" class="brand-logo">Zětikwett's</a>
|
||||
<ul id="nav-mobile" class="right hide-on-med-and-down">
|
||||
<li>
|
||||
<a class="modal-trigger btn orange disabled" href="#newproduct">
|
||||
<i class="material-icons left">add</i>Nouveau produit
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<header class="container center orange-text">
|
||||
<h1>Zétikwett's</h1>
|
||||
</header>
|
||||
<main class="container row">
|
||||
<p></p>
|
||||
<div class="col s4 offset-s4" id="signin-prompt" style="display: none"></div>
|
||||
<div class="col m10 offset-m1 s12">
|
||||
<a class="modal-trigger btn orange hide-on-large-only disabled" href="#newproduct"><i class="material-icons left">add</i>Nouveau produit</a>
|
||||
<ul class="collapsible collection" id="appbody">
|
||||
<div class="shimmer">
|
||||
<li><div class="collapsible-header"><h6 class="faux-text short"></h6></div></li>
|
||||
<li><div class="collapsible-header"><h6 class="faux-text"></h6></div></li>
|
||||
<li><div class="collapsible-header"><h6 class="faux-text shorter"></h6></div></li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="confirmdelete" class="modal confirm">
|
||||
<div class="modal-content">
|
||||
Supprimer le produit <b id="delete-title">blařg titre long de test sa mère</b> ?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-close waves-effect btn-flat" id="delete-cancel">annuler</button>
|
||||
<button type="submit" class="modal-close waves-effect waves-green btn" id="delete-confirm">supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Structure -->
|
||||
<div id="newproduct" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="input-field">
|
||||
<input id="new-name" type="text">
|
||||
<label>Nom interne du produit</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<select id="new-type">
|
||||
</select>
|
||||
<label>Type de produit</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<input id="new-designation" type="text">
|
||||
<label>Désignation</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<textarea id="new-ingredients" class="materialize-textarea"></textarea>
|
||||
<label>Ingrédients</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<textarea id="new-description" class="materialize-textarea"></textarea>
|
||||
<label>Description</label>
|
||||
</div>
|
||||
<div class="input-field">
|
||||
<input value="#97A1CC" data-jscolor="{previewSize: 0}" id="new-color" type="text">
|
||||
<label>Couleur</label>
|
||||
</div>
|
||||
<label>
|
||||
<input type="checkbox" id="new-organic">
|
||||
<span>Bio</span>
|
||||
</label>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="modal-close waves-effect waves-green btn" id="new-add">Ajouter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col m6 offset-m3 s12">
|
||||
<ul class="collapsible" id="appbody"></ul>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer orange">
|
||||
<footer class="page-footer">
|
||||
<div class="container row">
|
||||
<span class="col right">displayed with recycled electrons.</span>
|
||||
</div>
|
||||
|
1
frontend/jscolor.min.js
vendored
1
frontend/jscolor.min.js
vendored
File diff suppressed because one or more lines are too long
45
frontend/newtikette.html
Normal file
45
frontend/newtikette.html
Normal file
@ -0,0 +1,45 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>zetikettes 0.2</title>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!-- Compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||
|
||||
<!-- Compiled and minified JavaScript -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||
|
||||
<script src="config.js"></script>
|
||||
<script src="newtikette.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="container center orange-text">
|
||||
<h1>Zétikwett's</h1>
|
||||
</header>
|
||||
<main class="container row">
|
||||
<div class="col m6 offset-m3 s12">
|
||||
<div id="appbody" class="container"></div>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="page-footer">
|
||||
<div class="container row">
|
||||
<span class="col right">displayed with recycled electrons.</span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
125
frontend/newtikette.js
Normal file
125
frontend/newtikette.js
Normal file
@ -0,0 +1,125 @@
|
||||
const params = [
|
||||
['dluo', 'DLUO', true],
|
||||
['lot', 'Nº de lot', true],
|
||||
['qty', 'Poids net (g)', true],
|
||||
['teneur', 'Teneur en fruits (%)', false],
|
||||
['fruit', 'Quantité de fruits pour 100g (g)', false],
|
||||
]
|
||||
|
||||
function post() {
|
||||
const lemot = $('input[type=password]').val();
|
||||
const formdata = new FormData($('form')[0]);
|
||||
formdata.append('lemotdepasse', lemot);
|
||||
$.ajax({
|
||||
url: backend_api + `newtikette`,
|
||||
type: 'POST',
|
||||
data: formdata,
|
||||
|
||||
contentType: false,
|
||||
processData: false,
|
||||
|
||||
xhr: function () {
|
||||
var myXhr = $.ajaxSettings.xhr();
|
||||
if (myXhr.upload) {
|
||||
// For handling the progress of the upload
|
||||
myXhr.upload.addEventListener('progress', function (e) {
|
||||
if (e.lengthComputable) {
|
||||
$('progress').attr({
|
||||
value: e.loaded,
|
||||
max: e.total,
|
||||
});
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
return myXhr;
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
if (err.status == 403) {
|
||||
M.toast({html: 'access denied'});
|
||||
} else if (err.status == 500) {
|
||||
M.toast({html: 'server error'});
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
$('.progress').hide();
|
||||
$('.btn').removeClass('disabled');
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(async () => {
|
||||
const appbody = $("#appbody");
|
||||
const block = $('<form class="section">');
|
||||
block.append($(`<div class="input-field"><label class="active">Title</label>
|
||||
<input type="text" name="title" value="Gelée de brandade aux cêpes biou">`));
|
||||
block.append($(`<div class="input-field"><label class="active">
|
||||
<input type="checkbox" name="landscape"><span>landscape</span></label>`));
|
||||
const subst = $('<div class="section"><label class="active">Substitutions</label><p></p>');
|
||||
for (let param of params) {
|
||||
subst.append($(`<div><label class="active"><input type="checkbox" name="${param[0]}" ${param[2] ? 'checked': ''}>
|
||||
<span>\${${param[0]}}: ${param[1]}</span></label>`));
|
||||
}
|
||||
block.append(subst);
|
||||
|
||||
block.append(`<div class="file-field input-field">
|
||||
<div class="btn">
|
||||
<span>File</span>
|
||||
<input type="file" name="sticker" accept=".svg">
|
||||
</div>
|
||||
<div class="file-path-wrapper">
|
||||
<input class="file-path validate" type="text" placeholder="Template .svg file">
|
||||
</div>
|
||||
`)
|
||||
|
||||
const loader = $('<div class="progress"><div class="determinate"></div></div>')
|
||||
.hide();
|
||||
|
||||
let setup = false;
|
||||
|
||||
const action = $('<div class="section">')
|
||||
.append($('<a class="btn">add newtikette<a>')
|
||||
.click(() => {
|
||||
if (!setup) {
|
||||
setup = true;
|
||||
$('input[type=password]').on('keydown', e => {
|
||||
if (e.keyCode == 13 && !e.repeat) {
|
||||
$('.modal').modal('close');
|
||||
post();
|
||||
}
|
||||
});
|
||||
$("#pushgo").click(() => { post(); });
|
||||
}
|
||||
|
||||
$('.modal').modal('open');
|
||||
$('input[type=password]').focus();
|
||||
|
||||
loader.show();
|
||||
$('.btn').addClass("disabled");
|
||||
$('.modal-close').removeClass("disabled");
|
||||
|
||||
})
|
||||
.append(loader));
|
||||
|
||||
const lemotdepasse = $(`
|
||||
<div id="modal1" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>A lemotdepasse is needed</h4>
|
||||
<div class="input-field"><label class="active">lemotdepasse</label>
|
||||
<input type="password">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn modal-close" id="pushgo">go</a>
|
||||
</div>
|
||||
`);
|
||||
|
||||
appbody
|
||||
.append(lemotdepasse)
|
||||
.append(block)
|
||||
.append(action);
|
||||
|
||||
$(".modal").modal();
|
||||
});
|
@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Read the blog post here:
|
||||
* https://letsbuildui.dev/articles/how-to-build-a-skeleton-loading-placeholder
|
||||
*/
|
||||
.faux-text {
|
||||
background: #dddddd;
|
||||
border-radius: 4px;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.faux-text.short {
|
||||
width: 75%;
|
||||
}
|
||||
.faux-text.shorter {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shimmer::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.4) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
animation: shimmer 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
@ -1,203 +1,64 @@
|
||||
const params = {
|
||||
'dluo': 'DLUO',
|
||||
'lot': 'Nº de lot',
|
||||
'q': 'Poids net (g)',
|
||||
'qty': 'Poids net (g)',
|
||||
'vol': 'Volume net (cL)',
|
||||
'teneur': 'Teneur en fruits (%)',
|
||||
'f': 'Quantité de fruits pour 100g (g)',
|
||||
}
|
||||
|
||||
var tikats;
|
||||
|
||||
function getCookie(name) {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.startsWith(name + '=')) {
|
||||
return cookie.substring(name.length + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function post(url, data) {
|
||||
const csrf_token = getCookie('csrftoken');
|
||||
return $.ajax({
|
||||
url,
|
||||
data: JSON.stringify(data),
|
||||
method: 'POST',
|
||||
xhrFields: { withCredentials: true },
|
||||
headers: { 'X-CSRFToken': csrf_token },
|
||||
});
|
||||
}
|
||||
|
||||
function disableButtons() {
|
||||
$('.btn').addClass("disabled");
|
||||
}
|
||||
|
||||
function enableButtons() {
|
||||
$('.btn').removeClass('disabled');
|
||||
}
|
||||
|
||||
function resetEditModal(force = false) {
|
||||
if ($("#new-add").text() === "Ajouter" && !force) {
|
||||
return;
|
||||
}
|
||||
$("#new-name").val("");
|
||||
$("#new-type").val("").formSelect();
|
||||
$("#new-designation").val("");
|
||||
$("#new-ingredients").val("");
|
||||
$("#new-description").val("");
|
||||
$("#new-color")[0].jscolor.fromString("#97a1cc");
|
||||
$("#new-organic").prop("checked", false);
|
||||
M.updateTextFields();
|
||||
M.textareaAutoResize($('#new-ingredients'));
|
||||
M.textareaAutoResize($('#new-description'));
|
||||
|
||||
$("#new-add").text("Ajouter").off('click').click(() => {
|
||||
const req = getTiketteData();
|
||||
post(backend_api + 'newtikette', req).then(() => {
|
||||
resetEditModal(true);
|
||||
reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openEditModal(zett) {
|
||||
M.Modal.getInstance($("#newproduct")).open();
|
||||
|
||||
$("#new-name").val(zett.title);
|
||||
$("#new-type").val(zett.category_id).formSelect();
|
||||
$("#new-designation").val(zett.designation);
|
||||
$("#new-ingredients").val(zett.ingredients);
|
||||
$("#new-description").val(zett.description);
|
||||
$("#new-color")[0].jscolor.fromString(zett.color);
|
||||
$("#new-organic").prop("checked", zett.ab === "inline");
|
||||
M.updateTextFields();
|
||||
M.textareaAutoResize($('#new-ingredients'));
|
||||
M.textareaAutoResize($('#new-description'));
|
||||
|
||||
$("#new-add").text("Modifier").off('click').click(() => {
|
||||
const req = getTiketteData();
|
||||
req.id = zett.id;
|
||||
resetEditModal();
|
||||
post(backend_api + 'updatetikette', req).then(reload);
|
||||
});
|
||||
}
|
||||
|
||||
function addProduct(tikette) {
|
||||
const zett = tikette;
|
||||
const appbody = $("#appbody");
|
||||
const block = $('<div class="section">');
|
||||
for (let sub in zett.subs) {
|
||||
block.append($(`<div class="input-field"><label class="active">${params[sub]}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`));
|
||||
}
|
||||
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
|
||||
.hide();
|
||||
|
||||
const action = $('<div class="section">')
|
||||
.append($('<a class="btn">générer<a>')
|
||||
.click(() => {
|
||||
const subs = block.find(':text')
|
||||
.toArray()
|
||||
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
|
||||
const req = {
|
||||
template: zett.prototempalte,
|
||||
designation: zett.designation,
|
||||
description: zett.description,
|
||||
ingredients: zett.ingredients,
|
||||
color: zett.color,
|
||||
AB: zett.ab, // mind the capitalization here
|
||||
subs,
|
||||
landscape: zett.landscape,
|
||||
};
|
||||
|
||||
loader.show();
|
||||
disableButtons();
|
||||
|
||||
post(backend_api + 'generate', req)
|
||||
.then(data => {
|
||||
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">ouvrir le pdf</a>`);
|
||||
action.append(pdfbtn);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
.always(() => {
|
||||
loader.hide();
|
||||
enableButtons();
|
||||
});
|
||||
})
|
||||
.append(loader));
|
||||
|
||||
const deleteAction = $('<b class="material-icons">delete</b>').click(() => {
|
||||
$("#delete-title").text(zett.title);
|
||||
$("#delete-confirm").off('click').click(() => {
|
||||
const req = {
|
||||
id: zett.id,
|
||||
};
|
||||
post(backend_api + 'deletetikette', req).then(reload);
|
||||
});
|
||||
M.Modal.getInstance($("#confirmdelete")).open();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const editAction = $('<b class="material-icons">edit</b>').click(() => {
|
||||
openEditModal(zett);
|
||||
return false;
|
||||
});
|
||||
|
||||
appbody
|
||||
.append($('<li>')
|
||||
.append($('<div class="collapsible-header valign-wrapper">')
|
||||
.append(`<h6 class="blue-text">${zett.title}</h6>`)
|
||||
.append($('<span class="actions grey-text">')
|
||||
.append(editAction)
|
||||
.append(deleteAction)
|
||||
))
|
||||
.append($('<div class="collapsible-body">')
|
||||
.append(block)
|
||||
.append(action)));
|
||||
}
|
||||
|
||||
function setCategories() {
|
||||
const katsel = $('#new-type');
|
||||
katsel.empty();
|
||||
|
||||
for (let kat of tikats) {
|
||||
katsel.append($(`<option value="${kat.id}">${kat.name}</option>`));
|
||||
}
|
||||
|
||||
$('select').formSelect();
|
||||
}
|
||||
|
||||
function getTiketteData() {
|
||||
const title = $("#new-name").val();
|
||||
const category_id = $("#new-type").val();
|
||||
const designation = $("#new-designation").val();
|
||||
const ingredients = $("#new-ingredients").val();
|
||||
const description = $("#new-description").val();
|
||||
const color = $("#new-color").val().substring(1);
|
||||
const ab = $("#new-organic").is(":checked") ? 'inline' : 'none';
|
||||
|
||||
return {
|
||||
title,
|
||||
category_id,
|
||||
designation,
|
||||
ingredients,
|
||||
description,
|
||||
color,
|
||||
ab,
|
||||
};
|
||||
'fruit': 'Quantité de fruits pour 100g (g)',
|
||||
}
|
||||
|
||||
function loadAll(zetikettes) {
|
||||
const appbody = $("#appbody");
|
||||
appbody.empty();
|
||||
for (let zett of zetikettes) {
|
||||
addProduct(zett);
|
||||
const block = $('<div class="section">');
|
||||
for (let sub in zett.subs) {
|
||||
block.append($(`<div class="input-field"><label class="active">${params[sub]}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`));
|
||||
}
|
||||
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
|
||||
.hide();
|
||||
|
||||
const action = $('<div class="section">')
|
||||
.append($('<a class="btn">generate<a>')
|
||||
.click(() => {
|
||||
const subs = block.find(':text')
|
||||
.toArray()
|
||||
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
|
||||
const req = {
|
||||
sticker: zett.sticker,
|
||||
subs,
|
||||
landscape: zett.landscape,
|
||||
};
|
||||
|
||||
loader.show();
|
||||
$('.btn').addClass("disabled");
|
||||
|
||||
$.post(backend_api, JSON.stringify(req))
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
|
||||
action.append(pdfbtn);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
.always(() => {
|
||||
loader.hide();
|
||||
$('.btn').removeClass('disabled');
|
||||
});
|
||||
})
|
||||
.append(loader));
|
||||
|
||||
appbody
|
||||
.append($('<li>')
|
||||
.append($(`<div class="collapsible-header"><h6 class="blue-text">${zett.title}</h6></div>`))
|
||||
.append($('<div class="collapsible-body">')
|
||||
.append(block)
|
||||
.append(action)));
|
||||
}
|
||||
setCategories();
|
||||
|
||||
$('.collapsible').collapsible();
|
||||
|
||||
konami();
|
||||
}
|
||||
|
||||
function konami() {
|
||||
@ -215,53 +76,16 @@ function konami() {
|
||||
});
|
||||
}
|
||||
|
||||
async function googleCred(creds) {
|
||||
const token = creds.credential;
|
||||
await post(backend_api + 'signin', {token});
|
||||
$('#signin-prompt').hide();
|
||||
reload();
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
disableButtons();
|
||||
|
||||
$(document).ready(async () => {
|
||||
try {
|
||||
const resp = await $.ajax({
|
||||
url: backend_api + 'list',
|
||||
timeout: 1000,
|
||||
xhrFields: { withCredentials: true },
|
||||
});
|
||||
tikats = (await $.ajax({
|
||||
url: backend_api + 'categories',
|
||||
timeout: 1000,
|
||||
xhrFields: { withCredentials: true },
|
||||
})).tikats.sort((a, b) => a.name > b.name ? 1 : -1);
|
||||
loadAll(resp.tikettes.sort((a, b) => (a.title < b.title) ? -1 : 1));
|
||||
enableButtons();
|
||||
} catch(e) {
|
||||
if (e.status === 403) {
|
||||
$("#signin-prompt").show();
|
||||
google.accounts.id.prompt(); // also display the One Tap dialog
|
||||
return;
|
||||
}
|
||||
const appbody = $("#appbody");
|
||||
appbody.append(`<li>Could not reach backend server`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
google.accounts.id.initialize({
|
||||
client_id: google_oauth_client_id,
|
||||
callback: googleCred,
|
||||
});
|
||||
google.accounts.id.renderButton(
|
||||
document.getElementById("signin-prompt"),
|
||||
{ theme: "outline", size: "large" } // customization attributes
|
||||
);
|
||||
konami();
|
||||
|
||||
$('.collapsible').collapsible();
|
||||
$('.modal').modal({onOpenStart: resetEditModal});
|
||||
|
||||
reload();
|
||||
});
|
||||
|
@ -6,10 +6,6 @@ location /zetikettes/srv/static {
|
||||
alias /var/lib/zetikettes/www_static;
|
||||
}
|
||||
|
||||
location /zetikettes/srv/data {
|
||||
alias /var/lib/zetikettes/data;
|
||||
}
|
||||
|
||||
location ^~ /zetikettes/srv {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header SCRIPT_NAME /zetikettes/srv;
|
||||
|
@ -1,47 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import csv
|
||||
import dataclasses
|
||||
import requests
|
||||
|
||||
backend = 'http://localhost:8000'
|
||||
products_file = './product_definitions.csv'
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Tikette:
|
||||
category: str
|
||||
designation: str
|
||||
ingredients: str
|
||||
description: str
|
||||
color: str
|
||||
svg: str
|
||||
ab: str
|
||||
|
||||
def newtikette_payload(self, katzids):
|
||||
return {
|
||||
'title': self.svg.split('.')[0],
|
||||
'category_id': katzids[self.category],
|
||||
'designation': self.designation,
|
||||
'ingredients': self.ingredients,
|
||||
'description': self.description,
|
||||
'color': self.color,
|
||||
'ab': 'inline' if self.ab is 'True' else 'none',
|
||||
}
|
||||
|
||||
|
||||
def get_tikettes():
|
||||
with open('./product_definitions.csv') as f:
|
||||
return [Tikette(*x) for x in csv.reader(f, delimiter=';')]
|
||||
|
||||
|
||||
def populate_database():
|
||||
katz = requests.get(backend + '/categories', json=True).json()['tikats']
|
||||
katzids = {x['name'].lower(): x['id'] for x in katz}
|
||||
|
||||
for tk in get_tikettes():
|
||||
requests.post(backend + '/newtikette', json=tk.newtikette_payload(katzids))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
populate_database()
|
@ -7,9 +7,8 @@ import os
|
||||
import csv
|
||||
from subber import Subber
|
||||
|
||||
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "..", "templates")
|
||||
OUT_DIR = "."
|
||||
DEFAULT_CSV = 'product_definitions.csv'
|
||||
TEMPLATES_DIR = "/home/maxime/Documents/Anne/Etiquettes/Source SVGs/templates"
|
||||
OUT_DIR = "/home/maxime/Documents/Anne/Etiquettes/Source SVGs/autogenerated_svgs"
|
||||
|
||||
JAM_DESIGNATION_FONTSIZE_DEFAULT = 42.6667
|
||||
JAM_DESIGNATION_FONTSIZE_SMALL = 36
|
||||
@ -31,7 +30,7 @@ ALLERGEN_END_STYLE = '</tspan>'
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Renew whole lineup from template and list of subs')
|
||||
parser.add_argument('--list', default=DEFAULT_CSV, help='Lineup file')
|
||||
parser.add_argument('--list', required=True, help='Lineup file')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
980
templates/Gelée Extra - Cassis.svg
Normal file
980
templates/Gelée Extra - Cassis.svg
Normal file
File diff suppressed because one or more lines are too long
After 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.2 (b0a8486541, 2022-12-01)"
|
||||
inkscape:version="1.2 (dc2aeda, 2022-05-15)"
|
||||
sodipodi:docname="Pesto.svg"
|
||||
inkscape:export-filename="/home/maxime/Documents/Anne/Etiquettes/Bitmap Images/Pesto - Ail des Ours.png"
|
||||
inkscape:export-xdpi="600"
|
||||
@ -788,8 +788,8 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4.1140114"
|
||||
inkscape:cx="163.22269"
|
||||
inkscape:cy="91.75959"
|
||||
inkscape:cx="163.46576"
|
||||
inkscape:cy="91.638054"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer11"
|
||||
showgrid="false"
|
||||
@ -1229,7 +1229,7 @@
|
||||
id="text4193"
|
||||
y="-5.6109309"
|
||||
x="15.053011"
|
||||
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
xml:space="preserve"
|
||||
inkscape:label="Email"><tspan
|
||||
style="font-size:2.11667px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
@ -1247,7 +1247,7 @@
|
||||
mask="none" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
x="15.044743"
|
||||
y="-7.7611365"
|
||||
id="text4199"
|
||||
@ -1267,7 +1267,7 @@
|
||||
id="text4205"
|
||||
y="-10.030697"
|
||||
x="14.999268"
|
||||
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:0;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
xml:space="preserve"
|
||||
inkscape:label="Adresse"><tspan
|
||||
style="font-size:2.11667px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
@ -1277,7 +1277,7 @@
|
||||
sodipodi:role="line">Lieu-dit Lèbre, 04200, Authon</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:1.76389px;line-height:0;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
style="font-style:normal;font-weight:normal;font-size:1.76389px;line-height:0;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0700042;stroke-opacity:1"
|
||||
x="15.003401"
|
||||
y="-12.237692"
|
||||
id="text4211"
|
||||
@ -1299,7 +1299,7 @@
|
||||
<flowRoot
|
||||
inkscape:label="Culture a la main"
|
||||
transform="matrix(0.26458212,0,0,0.26458212,-107.34655,179.95759)"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16.0001px;line-height:0;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;letter-spacing:0px;word-spacing:-1px;text-anchor:middle;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00001"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:16.0001px;line-height:0;font-family:Droid Sans Fallback;-inkscape-font-specification:Droid Sans Fallback;text-align:center;letter-spacing:0px;word-spacing:-1px;text-anchor:middle;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00001"
|
||||
id="flowRoot1405"
|
||||
xml:space="preserve"><flowRegion
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:190.101px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;word-spacing:-1px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke-width:1.00001"
|
||||
@ -1316,7 +1316,7 @@
|
||||
id="flowPara31669">dans les Alpes de Haute-Provence</flowPara></flowRoot>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-size:2.46944px;line-height:1.5434px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#000000;fill-opacity:1;stroke-width:0.264583"
|
||||
style="font-size:2.46944px;line-height:1.5434px;font-family:Droid Sans Fallback;-inkscape-font-specification:Droid Sans Fallback;text-align:center;text-anchor:middle;fill:#000000;fill-opacity:1;stroke-width:0.264583"
|
||||
x="13.274965"
|
||||
y="246.99561"
|
||||
id="text31972"
|
||||
@ -1339,7 +1339,7 @@
|
||||
<flowRoot
|
||||
inkscape:label="Texte Quantité et Lot"
|
||||
transform="matrix(0.26458212,0,0,0.26458212,-10.572926,-21.404941)"
|
||||
style="font-style:normal;font-weight:normal;font-size:5.33336px;line-height:0px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00001"
|
||||
style="font-style:normal;font-weight:normal;font-size:5.33336px;line-height:0px;font-family:Droid Sans Fallback;letter-spacing:0px;word-spacing:0px;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00001"
|
||||
id="flowRoot82745"
|
||||
xml:space="preserve"><flowRegion
|
||||
style="line-height:0px;stroke-width:1.00001"
|
||||
@ -1351,31 +1351,31 @@
|
||||
width="30.898319"
|
||||
id="rect82729" /></flowRegion><flowPara
|
||||
id="flowPara82733"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:6px;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:6px;font-family:Droid Sans Fallback;-inkscape-font-specification:Droid Sans Fallback;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
|
||||
id="flowPara82735"
|
||||
style="line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
|
||||
id="flowPara82739"
|
||||
style="line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
|
||||
id="flowPara82741"
|
||||
style="font-weight:bold;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001" /><flowPara
|
||||
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001"
|
||||
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
|
||||
id="flowPara82743">POIDS NET :</flowPara><flowPara
|
||||
style="font-weight:bold;font-size:8.00004px;line-height:6.88px;text-align:center;text-anchor:middle;stroke-width:1.00001"
|
||||
style="font-weight:bold;font-size:8.00004px;line-height:6.88px;text-align:center;text-anchor:middle;stroke-width:1.00001;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
|
||||
id="flowPara83405">${q}g</flowPara><flowPara
|
||||
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001"
|
||||
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
|
||||
id="flowPara83420">LOT $lot</flowPara><flowPara
|
||||
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001"
|
||||
style="font-weight:normal;font-size:5.33336px;line-height:6.88px;text-align:start;text-anchor:start;stroke-width:1.00001;-inkscape-font-specification:'Droid Sans Fallback';font-family:'Droid Sans Fallback';font-style:normal;font-stretch:normal;font-variant:normal"
|
||||
id="flowPara83411" /></flowRoot>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,0,1.0617673)"
|
||||
id="text31980"
|
||||
style="font-size:9.33333px;line-height:10px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:start;white-space:pre;shape-inside:url(#rect31982);display:inline;fill:#000000;fill-opacity:1"
|
||||
style="font-size:9.33333px;line-height:10px;font-family:'Droid Sans Fallback';-inkscape-font-specification:'Droid Sans Fallback';text-align:start;white-space:pre;shape-inside:url(#rect31982);display:inline;fill:#000000;fill-opacity:1"
|
||||
inkscape:label="Ingrédients"><tspan
|
||||
x="162.51953"
|
||||
y="71.082662"
|
||||
id="tspan3150">${ingredients}
|
||||
id="tspan2493">${ingredients}
|
||||
</tspan></text>
|
||||
<flowRoot
|
||||
xml:space="preserve"
|
||||
@ -1397,28 +1397,28 @@
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,72.572369,-44.503941)"
|
||||
id="text169273-3"
|
||||
style="font-size:5.33333px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:start;white-space:pre;shape-inside:url(#rect169275-8);display:inline;fill:#000000;fill-opacity:1;stroke-width:1.562"
|
||||
style="font-size:5.33333px;font-family:'Droid Sans Fallback';-inkscape-font-specification:'Droid Sans Fallback';text-align:start;white-space:pre;shape-inside:url(#rect169275-8);display:inline;fill:#000000;fill-opacity:1;stroke-width:1.562"
|
||||
x="-198.57587"
|
||||
y="0"
|
||||
inkscape:label="Bloc Texte Conservation"><tspan
|
||||
x="-111.76953"
|
||||
y="270.68079"
|
||||
id="tspan3152">À conserver au frais après ouverture.
|
||||
id="tspan2495">À conserver au frais après ouverture.
|
||||
</tspan><tspan
|
||||
x="-111.76953"
|
||||
y="277.34745"
|
||||
id="tspan3154">Couvrir d'huile après chaque utilisation.
|
||||
y="277.46601"
|
||||
id="tspan2497">Couvrir d'huile après chaque utilisation.
|
||||
</tspan><tspan
|
||||
x="-111.76953"
|
||||
y="284.0141"
|
||||
id="tspan3158">À consommer de préférence avant : <tspan
|
||||
y="284.25122"
|
||||
id="tspan2501">À consommer de préférence avant : <tspan
|
||||
style="font-weight:bold"
|
||||
id="tspan3156">${dluo}</tspan>
|
||||
id="tspan2499">${dluo}</tspan>
|
||||
</tspan><tspan
|
||||
x="-111.76953"
|
||||
y="290.68076"
|
||||
id="tspan3162"><tspan
|
||||
y="291.03644"
|
||||
id="tspan2505"><tspan
|
||||
style="font-weight:bold"
|
||||
id="tspan3160">*Produits issus de l'agriculture biologique.</tspan></tspan></text>
|
||||
id="tspan2503">*Produits issus de l'agriculture biologique.</tspan></tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.3 MiB After Width: | Height: | Size: 5.3 MiB |
16
zetikettes.service
Normal file
16
zetikettes.service
Normal file
@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Zetikettes backend service
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/docker run --rm --name %n \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-v /var/lib/zetikettes/data:/data \
|
||||
zetikettes
|
||||
Restart=on-failure
|
||||
ExecStartPre=-/usr/bin/docker exec %n stop
|
||||
ExecStartPre=-/usr/bin/docker rm %n
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
Loading…
Reference in New Issue
Block a user