Compare commits
No commits in common. "d706ae164547f70f2b8f3cc19ff20963de98fa48" and "1426ca5fa6a6f91b25157db40faaf1bf49d80973" have entirely different histories.
d706ae1645
...
1426ca5fa6
10
Dockerfile
@ -1,15 +1,15 @@
|
|||||||
FROM alpine:3.18
|
FROM alpine
|
||||||
|
|
||||||
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript font-droid
|
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript ttf-opensans
|
||||||
RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn
|
RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn
|
||||||
|
|
||||||
ADD backend /zetikettes
|
ADD backend /root/zetikettes
|
||||||
|
|
||||||
RUN mkdir -p /usr/share/fonts/TTF \
|
RUN mkdir -p /usr/share/fonts/TTF \
|
||||||
&& cp /zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \
|
&& cp /root/zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \
|
||||||
&& fc-cache -fv
|
&& fc-cache -fv
|
||||||
|
|
||||||
|
|
||||||
# the script will look for templates in /data
|
# the script will look for templates in /data
|
||||||
WORKDIR /zetikettes/zetikettes
|
WORKDIR /root/zetikettes/zetikettes
|
||||||
CMD /usr/bin/gunicorn zetikettes.wsgi -b 0.0.0.0:8000 --timeout 600 --forwarded-allow-ips="*"
|
CMD /usr/bin/gunicorn zetikettes.wsgi -b 0.0.0.0:8000 --timeout 600 --forwarded-allow-ips="*"
|
||||||
|
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
|
|
||||||
|
|
||||||
|
|
||||||
.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
@ -1,53 +1,64 @@
|
|||||||
zetikettes 2.0
|
zetikettes
|
||||||
==============
|
==========
|
||||||
|
|
||||||
ouaich. tavu.
|
ouaich.
|
||||||
|
|
||||||
**NOTE**: release 2.0 broke compatibility with previous "releases". Maxime has all the recent data to repopulate a new database.
|
|
||||||
|
|
||||||
|
|
||||||
Initial setup
|
Initial setup
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
### Database setup and population
|
|
||||||
|
|
||||||
Populate an empty database (if none already exists):
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo make initial-db
|
|
||||||
```
|
|
||||||
|
|
||||||
This will setup an initial database under `/var/lib/zetikettes/data`
|
|
||||||
|
|
||||||
Optionally, you may need credentials to access the admin page:
|
|
||||||
|
|
||||||
```
|
|
||||||
make superuser
|
|
||||||
```
|
|
||||||
|
|
||||||
### System service
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
docker build -t zetikettes .
|
||||||
sudo mkdir -p /etc/docker/compose/zetikettes
|
sudo mkdir -p /etc/docker/compose/zetikettes
|
||||||
sudo cp compose.yml /etc/docker/compose/zetikettes/
|
sudo cp docker-compose.yml /etc/docker/compose/zetikettes/
|
||||||
sudo systemctl enable --now docker-compose@zetikettes
|
sudo systemctl enable --now docker-compose@zetikettes
|
||||||
```
|
```
|
||||||
|
|
||||||
If not already present in `/etc/systemd/system/`, the `docker-compose@` service file is provided.
|
|
||||||
|
|
||||||
### www static files
|
|
||||||
|
|
||||||
```
|
|
||||||
make staticfiles
|
|
||||||
```
|
|
||||||
|
|
||||||
This will install frontend and django admin static files under `/var/lib/zetikettes`
|
|
||||||
|
|
||||||
### Nginx
|
|
||||||
|
|
||||||
Example configuration is provided in `nginx_locations`.
|
|
||||||
|
|
||||||
Nginx is configured to:
|
Nginx is configured to:
|
||||||
- redirect `/zetikettes/srv/` to `localhost:8000`
|
- redirect /zetikettes/srv/ to localhost:8000
|
||||||
- redirect `/zetikettes/` to `/var/lib/zetikettes/frontend`
|
- redirect /zetikettes/ to /var/lib/zetikettes/static
|
||||||
- redirect `/zetikettes/srv/static` to `/var/lib/zetikettes/www_static`
|
|
||||||
|
Conf is in /etc/nginx/sites-available/default
|
||||||
|
|
||||||
|
Test
|
||||||
|
----
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --rm -it -v $PWD/templates:/data zetikettes /bin/bash /root/zetikettes/old/mkjam.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This should produce a .pdf in `templates/`. Open it to check that
|
||||||
|
layout & fonts are correct.
|
||||||
|
|
||||||
|
Run
|
||||||
|
---
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes for deploying
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.h3 Initialize empty database
|
||||||
|
```
|
||||||
|
python manage.py migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
.h3 Prepare static files
|
||||||
|
```
|
||||||
|
python manage.py collectstatic
|
||||||
|
```
|
||||||
|
The files will be in `www_static/` and need to be moved to `/var/lib/zetikettes/www_static`
|
||||||
|
|
||||||
|
.h3 Change host settings
|
||||||
|
If not deploying on `aerith.ponteilla.net`, you'll need to edit `backend/zetikettes/zetikettes/settings.py` to change a couple things in there.
|
||||||
|
|
||||||
|
|
||||||
|
Change available templates
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
1. go to /zetikettes/admin
|
||||||
|
1. add the newtikette
|
||||||
|
1. still no need to restart the container (magic!)
|
||||||
|
2. profit.
|
||||||
|
BIN
backend/fonts/DejaVuMathTeXGyre.ttf
Normal file
BIN
backend/fonts/DejaVuSans-Bold.ttf
Normal file
BIN
backend/fonts/DejaVuSans-BoldOblique.ttf
Normal file
BIN
backend/fonts/DejaVuSans-ExtraLight.ttf
Normal file
BIN
backend/fonts/DejaVuSans-Oblique.ttf
Normal file
BIN
backend/fonts/DejaVuSans.ttf
Normal file
BIN
backend/fonts/DejaVuSansCondensed-Bold.ttf
Normal file
BIN
backend/fonts/DejaVuSansCondensed-BoldOblique.ttf
Normal file
BIN
backend/fonts/DejaVuSansCondensed-Oblique.ttf
Normal file
BIN
backend/fonts/DejaVuSansCondensed.ttf
Normal file
BIN
backend/fonts/DejaVuSansMono-Bold.ttf
Normal file
BIN
backend/fonts/DejaVuSansMono-BoldOblique.ttf
Normal file
BIN
backend/fonts/DejaVuSansMono-Oblique.ttf
Normal file
BIN
backend/fonts/DejaVuSansMono.ttf
Normal file
1
backend/old/data/tikettes.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
[]
|
@ -20,9 +20,6 @@ def parse_args():
|
|||||||
default=sys.stdout,
|
default=sys.stdout,
|
||||||
type=argparse.FileType('w'),
|
type=argparse.FileType('w'),
|
||||||
help='output path (default: stdout)')
|
help='output path (default: stdout)')
|
||||||
parser.add_argument('--landscape',
|
|
||||||
action='store_true',
|
|
||||||
help='input sticker is in landscape orientation')
|
|
||||||
parser.add_argument('sticker',
|
parser.add_argument('sticker',
|
||||||
type=argparse.FileType('r'),
|
type=argparse.FileType('r'),
|
||||||
default=sys.stdin,
|
default=sys.stdin,
|
||||||
@ -32,7 +29,7 @@ def parse_args():
|
|||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE ,landscape=False):
|
def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE):
|
||||||
with open(template) as tpl:
|
with open(template) as tpl:
|
||||||
tpl_data = tpl.read()
|
tpl_data = tpl.read()
|
||||||
|
|
||||||
@ -41,11 +38,19 @@ def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE ,landscape=False):
|
|||||||
lines = lines[1:]
|
lines = lines[1:]
|
||||||
sticker_data = ''.join(lines)
|
sticker_data = ''.join(lines)
|
||||||
|
|
||||||
rotate = "translate(102, 0) rotate(90)" if not landscape else ""
|
|
||||||
|
|
||||||
subs = {
|
subs = {
|
||||||
'sticker': sticker_data,
|
'left0': sticker_data,
|
||||||
'rotate': rotate,
|
'left1': sticker_data,
|
||||||
|
'left2': sticker_data,
|
||||||
|
'left3': sticker_data,
|
||||||
|
'left4': sticker_data,
|
||||||
|
'left5': sticker_data,
|
||||||
|
'right0': sticker_data,
|
||||||
|
'right1': sticker_data,
|
||||||
|
'right2': sticker_data,
|
||||||
|
'right3': sticker_data,
|
||||||
|
'right4': sticker_data,
|
||||||
|
'right5': sticker_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
out.write(Template(tpl_data).substitute(subs))
|
out.write(Template(tpl_data).substitute(subs))
|
@ -22,18 +22,25 @@ def parse_args():
|
|||||||
parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre')
|
parser.add_argument('--teneur', '-t', required=False, help='Teneur en sucre')
|
||||||
parser.add_argument('--fruit', '-f', required=False, help='Quantité de fruits')
|
parser.add_argument('--fruit', '-f', required=False, help='Quantité de fruits')
|
||||||
parser.add_argument('--size', '-s', required=False, help='Masse de produit')
|
parser.add_argument('--size', '-s', required=False, help='Masse de produit')
|
||||||
|
parser.add_argument('--landscape',
|
||||||
|
action='store_true',
|
||||||
|
help='input sticker is in landscape orientation')
|
||||||
parser.add_argument('sticker', help='path to the sticker template')
|
parser.add_argument('sticker', help='path to the sticker template')
|
||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def makesticker(sticker: str, out: typing.IO, subs: dict):
|
def makesticker(sticker: str, out: typing.IO, subs: dict, landscape=False):
|
||||||
with open(sticker) as fin:
|
with open(sticker) as fin:
|
||||||
lines = fin.readlines()
|
lines = fin.readlines()
|
||||||
if lines[0].startswith('<?xml'):
|
if lines[0].startswith('<?xml'):
|
||||||
lines = lines[1:]
|
lines = lines[1:]
|
||||||
sticker_data = ''.join(lines)
|
sticker_data = ''.join(lines)
|
||||||
|
|
||||||
|
if landscape:
|
||||||
|
# Rotate the sticker 90 degrees
|
||||||
|
sticker_data = "<g transform=\"rotate(-90,0,0) translate(-102,0)\">{}</g>".format(sticker_data)
|
||||||
|
|
||||||
out.write(Template(sticker_data).substitute(subs))
|
out.write(Template(sticker_data).substitute(subs))
|
||||||
|
|
||||||
|
|
||||||
@ -43,8 +50,8 @@ if __name__ == "__main__":
|
|||||||
'dluo': args.pop('dluo'),
|
'dluo': args.pop('dluo'),
|
||||||
'lot': args.pop('lot'),
|
'lot': args.pop('lot'),
|
||||||
'teneur': args.pop('teneur'),
|
'teneur': args.pop('teneur'),
|
||||||
'f': args.pop('fruit'),
|
'fruit': args.pop('fruit'),
|
||||||
'q': args.pop('quantite'),
|
'qty': args.pop('quantite'),
|
||||||
'size': args.pop('size'),
|
'size': args.pop('size'),
|
||||||
}
|
}
|
||||||
args['subs'] = subs
|
args['subs'] = subs
|
25
backend/old/mkjam.sh
Executable file
@ -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
@ -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
@ -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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,9 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Tikette, Tizer, Tikategory, Tisub
|
from .models import Tikette, Tikategory, Tisub
|
||||||
|
|
||||||
admin.site.register(Tikette)
|
admin.site.register(Tikette)
|
||||||
admin.site.register(Tizer)
|
|
||||||
admin.site.register(Tikategory)
|
admin.site.register(Tikategory)
|
||||||
admin.site.register(Tisub)
|
admin.site.register(Tisub)
|
||||||
|
|
||||||
|
@ -7,8 +7,6 @@ from .makeplanche import makeplanche
|
|||||||
|
|
||||||
OUT_DIR = tempfile.gettempdir()
|
OUT_DIR = tempfile.gettempdir()
|
||||||
DEFAULT_DPI = 300
|
DEFAULT_DPI = 300
|
||||||
DESIGNATION_FONTSIZE = 42.6667
|
|
||||||
|
|
||||||
|
|
||||||
def inkscapize(svg_in, pdf_out):
|
def inkscapize(svg_in, pdf_out):
|
||||||
png_out = tempfile.mktemp(suffix='.png')
|
png_out = tempfile.mktemp(suffix='.png')
|
||||||
@ -24,31 +22,31 @@ def inkscapize(svg_in, pdf_out):
|
|||||||
os.unlink(png_out)
|
os.unlink(png_out)
|
||||||
|
|
||||||
|
|
||||||
def generate(template, subs, out_dir, landscape=False):
|
def generate(request, out_dir):
|
||||||
""" Generate a sticker sheet.
|
""" Generate a sticker sheet.
|
||||||
|
|
||||||
template: file name for the sticker template
|
request: dict-like with the following fields:
|
||||||
subs: dict-like with key-value template subtitution fields
|
sticker: mandatory. filename for the sticker
|
||||||
out_dir: you get it
|
subs: mandatory. dict-like with key-value template subtitution fields
|
||||||
landscape: optional. defaults to False
|
landscape: optional. defaults to False
|
||||||
"""
|
|
||||||
|
|
||||||
# default designation font size
|
out_dir: you get it
|
||||||
subs['designation_fontsize'] = subs.get('designation_fontsize',
|
"""
|
||||||
DESIGNATION_FONTSIZE)
|
|
||||||
|
|
||||||
# fill in sticker details
|
# fill in sticker details
|
||||||
sticker_out = tempfile.mktemp(suffix='.svg')
|
sticker_out = tempfile.mktemp(suffix='.svg')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(sticker_out, 'w') as stickout:
|
with open(sticker_out, 'w') as stickout:
|
||||||
makesticker(os.path.join(out_dir, template), stickout, subs)
|
landscape = request.get('landscape', False)
|
||||||
|
makesticker(os.path.join(out_dir, request['sticker']), stickout,
|
||||||
|
request['subs'], landscape=landscape)
|
||||||
|
|
||||||
# make sticker sheet
|
# make sticker sheet
|
||||||
planche_out = tempfile.mktemp(suffix='.svg')
|
planche_out = tempfile.mktemp(suffix='.svg')
|
||||||
with open(sticker_out, 'r') as stickin:
|
with open(sticker_out, 'r') as stickin:
|
||||||
with open(planche_out, 'w') as planchout:
|
with open(planche_out, 'w') as planchout:
|
||||||
makeplanche(stickin, planchout, landscape=landscape)
|
makeplanche(stickin, planchout)
|
||||||
|
|
||||||
# process to printable pdf
|
# process to printable pdf
|
||||||
pdf_out = tempfile.mktemp(dir=out_dir, suffix='.pdf')
|
pdf_out = tempfile.mktemp(dir=out_dir, suffix='.pdf')
|
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
@ -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,7 +1,7 @@
|
|||||||
# Generated by Django 5.2.4 on 2025-08-05 12:57
|
# Generated by Django 4.2.2 on 2023-07-03 14:37
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -18,7 +18,6 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=50)),
|
('name', models.CharField(max_length=50)),
|
||||||
('landscape', models.BooleanField()),
|
('landscape', models.BooleanField()),
|
||||||
('prototempalte', models.FileField(upload_to='')),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'tikategoriez',
|
'verbose_name_plural': 'tikategoriez',
|
||||||
@ -31,41 +30,22 @@ class Migration(migrations.Migration):
|
|||||||
('name', models.CharField(max_length=50)),
|
('name', models.CharField(max_length=50)),
|
||||||
('descritpion', models.TextField()),
|
('descritpion', models.TextField()),
|
||||||
('default', models.TextField()),
|
('default', models.TextField()),
|
||||||
('type', models.CharField(choices=[('ST', 'Short Text'), ('LT', 'Long Text'), ('C', 'Color'), ('YM', 'Year Month'), ('B', 'Boolean')], default='ST', max_length=2)),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'tisubz',
|
'verbose_name_plural': 'tisubz',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name='Tizer',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('email', models.CharField()),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name_plural': 'tizerz',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Tikette',
|
name='Tikette',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('title', models.CharField(max_length=100)),
|
('title', models.CharField(max_length=100)),
|
||||||
('designation', models.CharField(max_length=100)),
|
('svg', models.FileField(upload_to='')),
|
||||||
('ingredients', models.TextField()),
|
|
||||||
('description', models.TextField()),
|
|
||||||
('color', models.CharField(max_length=6)),
|
|
||||||
('ab', models.CharField(choices=[('inline', 'Visible'), ('none', 'Invisible')], max_length=7)),
|
|
||||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tikette.tikategory')),
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tikette.tikategory')),
|
||||||
|
('subs', models.ManyToManyField(to='tikette.tisub')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name_plural': 'tikettz',
|
'verbose_name_plural': 'tikettz',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
|
||||||
model_name='tikategory',
|
|
||||||
name='subs',
|
|
||||||
field=models.ManyToManyField(to='tikette.tisub'),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class Tisub(models.Model):
|
class Tisub(models.Model):
|
||||||
class Type(models.TextChoices):
|
|
||||||
SHORT_TEXT = "ST"
|
|
||||||
LONG_TEXT = "LT"
|
|
||||||
COLOR = "C"
|
|
||||||
YEAR_MONTH = "YM"
|
|
||||||
BOOLEAN = "B"
|
|
||||||
|
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
descritpion = models.TextField()
|
descritpion = models.TextField()
|
||||||
default = models.TextField()
|
default = models.TextField()
|
||||||
# everything is text. type is for UX (e.g. color picker)
|
|
||||||
type = models.CharField(max_length=2, choices=Type, default=Type.SHORT_TEXT)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -25,10 +16,6 @@ class Tikategory(models.Model):
|
|||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
landscape = models.BooleanField()
|
landscape = models.BooleanField()
|
||||||
subs = models.ManyToManyField(Tisub)
|
subs = models.ManyToManyField(Tisub)
|
||||||
# For now we'll hardcode the following:xi
|
|
||||||
# [designation, ingredients, description, color, AB, designation_fontsize]
|
|
||||||
#protosubs = models.ManyToManyField(Tisub, related_name="protosubs")
|
|
||||||
prototempalte = models.FileField()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -38,19 +25,9 @@ class Tikategory(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Tikette(models.Model):
|
class Tikette(models.Model):
|
||||||
class AbVisibility(models.TextChoices):
|
|
||||||
VISIBLE = "inline"
|
|
||||||
INVISIBLE = "none"
|
|
||||||
|
|
||||||
title = models.CharField(max_length=100)
|
title = models.CharField(max_length=100)
|
||||||
category = models.ForeignKey(Tikategory, on_delete=models.CASCADE)
|
category = models.ForeignKey(Tikategory, on_delete=models.CASCADE)
|
||||||
|
svg = models.FileField()
|
||||||
designation = models.CharField(max_length=100)
|
|
||||||
ingredients = models.TextField()
|
|
||||||
description = models.TextField()
|
|
||||||
color = models.CharField(max_length=6)
|
|
||||||
ab = models.CharField(max_length=7, choices=AbVisibility)
|
|
||||||
# designation_fontsize is hardcoded to 42.6667 in planche/generate.py
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
@ -58,12 +35,4 @@ class Tikette(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "tikettz"
|
verbose_name_plural = "tikettz"
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
class Tizer(models.Model):
|
|
||||||
email = models.CharField()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.email
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "tizerz"
|
|
||||||
|
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)">$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 |
@ -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 |
@ -5,83 +5,27 @@ from django.http import JsonResponse
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from .planche import generate as stickersheet
|
from . import generate as stickersheet
|
||||||
from .models import Tikette, Tikategory
|
from .models import Tikette, Tikategory
|
||||||
|
|
||||||
CORS={'access-control-allow-origin': '*'}
|
CORS={'access-control-allow-origin': '*'}
|
||||||
|
|
||||||
|
def index(request):
|
||||||
def quirk_bold_allergens(ingredients):
|
|
||||||
out = []
|
|
||||||
for ing in (x.strip() for x in ingredients.split(',')):
|
|
||||||
if ing.startswith('*'):
|
|
||||||
out.append(f'<tspan style="font-weight:bold">{ing[1:]}</tspan>')
|
|
||||||
else:
|
|
||||||
out.append(ing)
|
|
||||||
return ", ".join(out)
|
|
||||||
|
|
||||||
|
|
||||||
def get_list(request):
|
|
||||||
tikettes = [{
|
tikettes = [{
|
||||||
'id': x.id,
|
|
||||||
'title': x.title,
|
'title': x.title,
|
||||||
'category': x.category.name,
|
'category': x.category.name,
|
||||||
'prototempalte': x.category.prototempalte.name,
|
'sticker': x.svg.name,
|
||||||
'landscape': x.category.landscape,
|
'landscape': x.category.landscape,
|
||||||
'designation': x.designation,
|
|
||||||
'ingredients': quirk_bold_allergens(x.ingredients),
|
|
||||||
'description': x.description,
|
|
||||||
'ab': x.ab,
|
|
||||||
'color': x.color,
|
|
||||||
'subs': {x.name: x.default for x in x.category.subs.all()},
|
'subs': {x.name: x.default for x in x.category.subs.all()},
|
||||||
} for x in Tikette.objects.all()]
|
} for x in Tikette.objects.all()]
|
||||||
return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS)
|
return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS)
|
||||||
|
|
||||||
|
|
||||||
def get_categories(request):
|
|
||||||
tikats = [{
|
|
||||||
'id': x.id,
|
|
||||||
'name': x.name,
|
|
||||||
} for x in Tikategory.objects.all()]
|
|
||||||
return JsonResponse({'status': 'ok', 'tikats': tikats}, headers=CORS)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def generate(request):
|
def generate(request):
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
|
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
|
||||||
|
|
||||||
payload = json.loads(request.body)
|
payload = json.loads(request.body)
|
||||||
|
pdfpath = stickersheet.generate(payload, out_dir=settings.TIKETTE_OUT_DIR)
|
||||||
subs = dict(payload['subs'])
|
|
||||||
for key in ('designation', 'ingredients', 'description', 'color', 'AB'):
|
|
||||||
subs[key] = payload[key]
|
|
||||||
|
|
||||||
pdfpath = stickersheet.generate(template=payload['template'], subs=subs,
|
|
||||||
out_dir=settings.TIKETTE_OUT_DIR,
|
|
||||||
landscape=payload['landscape'])
|
|
||||||
|
|
||||||
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)
|
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def newtikette(request):
|
|
||||||
if request.method != "POST":
|
|
||||||
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
|
|
||||||
|
|
||||||
payload = json.loads(request.body)
|
|
||||||
tikette = Tikette(**payload)
|
|
||||||
tikette.save()
|
|
||||||
|
|
||||||
return JsonResponse({'status': 'ok'}, headers=CORS)
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def deletetikette(request):
|
|
||||||
if request.method != "POST":
|
|
||||||
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
|
|
||||||
|
|
||||||
payload = json.loads(request.body)
|
|
||||||
Tikette.objects.get(id=payload["id"]).delete()
|
|
||||||
|
|
||||||
return JsonResponse({'status': 'ok'}, headers=CORS)
|
|
||||||
|
@ -24,9 +24,6 @@ import tikette.views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('list', tikette.views.get_list),
|
path('list', tikette.views.index),
|
||||||
path('categories', tikette.views.get_categories),
|
path('', tikette.views.generate),
|
||||||
path('generate', tikette.views.generate),
|
|
||||||
path('newtikette', tikette.views.newtikette),
|
|
||||||
path('deletetikette', tikette.views.deletetikette),
|
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
@ -3,8 +3,7 @@ version: '3.1'
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
zetikettes:
|
zetikettes:
|
||||||
build: .
|
image: zetikettes
|
||||||
image: pol/zetikettes
|
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8000:8000
|
- 127.0.0.1:8000:8000
|
@ -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 +1 @@
|
|||||||
const backend_api = 'http://localhost:8000/'
|
const backend_api = 'http://jenova.ponteilla.net:8001/'
|
||||||
|
@ -3,19 +3,15 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<title>Zětikwett's</title>
|
<title>zetikettes 0.2</title>
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<!--Import Google Icon Font-->
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
||||||
<!-- Compiled and minified CSS -->
|
<!-- Compiled and minified CSS -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
|
||||||
<link rel="stylesheet" href="placeholder.css">
|
|
||||||
|
|
||||||
<!-- Compiled and minified JavaScript -->
|
<!-- Compiled and minified JavaScript -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
|
||||||
|
|
||||||
<script src="jscolor.min.js"></script>
|
|
||||||
<script src="config.js"></script>
|
<script src="config.js"></script>
|
||||||
<script src="zetikettes.js"></script>
|
<script src="zetikettes.js"></script>
|
||||||
|
|
||||||
@ -32,70 +28,15 @@ main {
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="light-blue">
|
<header class="container center orange-text">
|
||||||
<div class="nav-wrapper container">
|
<h1>Zétikwett's</h1>
|
||||||
<a href="#" class="brand-logo">Zětikwett's</a>
|
</header>
|
||||||
<ul id="nav-mobile" class="right hide-on-med-and-down">
|
|
||||||
<li>
|
|
||||||
<a class="modal-trigger btn orange" href="#newproduct">
|
|
||||||
<i class="material-icons left">add</i>Nouveau produit
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<main class="container row">
|
<main class="container row">
|
||||||
<p></p>
|
|
||||||
<div class="col m6 offset-m3 s12">
|
<div class="col m6 offset-m3 s12">
|
||||||
<a class="modal-trigger btn orange hide-on-large-only" href="#newproduct"><i class="material-icons left">add</i>Nouveau produit</a>
|
<ul class="collapsible" id="appbody"></ul>
|
||||||
<ul class="collapsible" id="appbody">
|
|
||||||
<div class="shimmer">
|
|
||||||
<li><div class="collapsible-header"><h6 class="faux-text short"></h6></div></li>
|
|
||||||
<li><div class="collapsible-header"><h6 class="faux-text"></h6></div></li>
|
|
||||||
<li><div class="collapsible-header"><h6 class="faux-text shorter"></h6></div></li>
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Structure -->
|
|
||||||
<div id="newproduct" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="input-field">
|
|
||||||
<input id="new-name" type="text">
|
|
||||||
<label>Nom interne du produit</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-field">
|
|
||||||
<select id="new-type">
|
|
||||||
</select>
|
|
||||||
<label>Type de produit</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-field">
|
|
||||||
<input id="new-designation" type="text">
|
|
||||||
<label>Désignation</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-field">
|
|
||||||
<textarea id="new-ingredients" class="materialize-textarea"></textarea>
|
|
||||||
<label>Ingrédients</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-field">
|
|
||||||
<textarea id="new-description" class="materialize-textarea"></textarea>
|
|
||||||
<label>Description</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-field">
|
|
||||||
<input value="#97A1CC" data-jscolor="{previewSize: 0}" id="new-color" type="text">
|
|
||||||
<label>Couleur</label>
|
|
||||||
</div>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="new-organic">
|
|
||||||
<span>Bio</span>
|
|
||||||
</label>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" class="modal-close waves-effect waves-green btn" id="new-add">Ajouter</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer class="page-footer orange">
|
<footer class="page-footer">
|
||||||
<div class="container row">
|
<div class="container row">
|
||||||
<span class="col right">displayed with recycled electrons.</span>
|
<span class="col right">displayed with recycled electrons.</span>
|
||||||
</div>
|
</div>
|
||||||
|
1
frontend/jscolor.min.js
vendored
@ -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,122 +1,64 @@
|
|||||||
const params = {
|
const params = {
|
||||||
'dluo': 'DLUO',
|
'dluo': 'DLUO',
|
||||||
'lot': 'Nº de lot',
|
'lot': 'Nº de lot',
|
||||||
'q': 'Poids net (g)',
|
'qty': 'Poids net (g)',
|
||||||
|
'vol': 'Volume net (cL)',
|
||||||
'teneur': 'Teneur en fruits (%)',
|
'teneur': 'Teneur en fruits (%)',
|
||||||
'f': 'Quantité de fruits pour 100g (g)',
|
'fruit': 'Quantité de fruits pour 100g (g)',
|
||||||
}
|
|
||||||
|
|
||||||
var tikats;
|
|
||||||
|
|
||||||
function addProduct(tikette) {
|
|
||||||
const zett = tikette;
|
|
||||||
const appbody = $("#appbody");
|
|
||||||
const block = $('<div class="section">');
|
|
||||||
for (let sub in zett.subs) {
|
|
||||||
block.append($(`<div class="input-field"><label class="active">${params[sub]}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`));
|
|
||||||
}
|
|
||||||
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
const action = $('<div class="section">')
|
|
||||||
.append($('<a class="btn">generate<a>')
|
|
||||||
.click(() => {
|
|
||||||
const subs = block.find(':text')
|
|
||||||
.toArray()
|
|
||||||
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
|
|
||||||
const req = {
|
|
||||||
template: zett.prototempalte,
|
|
||||||
designation: zett.designation,
|
|
||||||
description: zett.description,
|
|
||||||
ingredients: zett.ingredients,
|
|
||||||
color: zett.color,
|
|
||||||
AB: zett.ab, // mind the capitalization here
|
|
||||||
subs,
|
|
||||||
landscape: zett.landscape,
|
|
||||||
};
|
|
||||||
|
|
||||||
loader.show();
|
|
||||||
$('.btn').addClass("disabled");
|
|
||||||
|
|
||||||
$.post(backend_api + 'generate', JSON.stringify(req))
|
|
||||||
.then(data => {
|
|
||||||
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
|
|
||||||
action.append(pdfbtn);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
})
|
|
||||||
.always(() => {
|
|
||||||
loader.hide();
|
|
||||||
$('.btn').removeClass('disabled');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.append(loader));
|
|
||||||
|
|
||||||
const deleteAction = $('<a class="btn-flat grey-text"><b class="material-icons">delete</b>');
|
|
||||||
deleteAction.click(() => {
|
|
||||||
const req = {
|
|
||||||
id: zett.id,
|
|
||||||
};
|
|
||||||
$.post(backend_api + 'deletetikette', JSON.stringify(req)).then(reload);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
appbody
|
|
||||||
.append($('<li>')
|
|
||||||
.append($('<div class="collapsible-header valign-wrapper">')
|
|
||||||
.append(`<h6 class="blue-text">${zett.title}</h6>`)
|
|
||||||
.append($('<span class="badge">')
|
|
||||||
.append(deleteAction)))
|
|
||||||
.append($('<div class="collapsible-body">')
|
|
||||||
.append(block)
|
|
||||||
.append(action)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCategories() {
|
|
||||||
const katsel = $('#new-type');
|
|
||||||
katsel.empty();
|
|
||||||
|
|
||||||
for (let kat of tikats) {
|
|
||||||
katsel.append($(`<option value="${kat.id}">${kat.name}</option>`));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAll(zetikettes) {
|
function loadAll(zetikettes) {
|
||||||
const appbody = $("#appbody");
|
const appbody = $("#appbody");
|
||||||
appbody.empty();
|
|
||||||
for (let zett of zetikettes) {
|
for (let zett of zetikettes) {
|
||||||
addProduct(zett);
|
const block = $('<div class="section">');
|
||||||
|
for (let sub in zett.subs) {
|
||||||
|
block.append($(`<div class="input-field"><label class="active">${params[sub]}</label><input type="text" name="${sub}" value="${zett.subs[sub]}">`));
|
||||||
|
}
|
||||||
|
const loader = $('<div class="progress"><div class="indeterminate"></div></div>')
|
||||||
|
.hide();
|
||||||
|
|
||||||
|
const action = $('<div class="section">')
|
||||||
|
.append($('<a class="btn">generate<a>')
|
||||||
|
.click(() => {
|
||||||
|
const subs = block.find(':text')
|
||||||
|
.toArray()
|
||||||
|
.reduce((obj, el) => ({...obj, [el.name]: el.value}), {});
|
||||||
|
const req = {
|
||||||
|
sticker: zett.sticker,
|
||||||
|
subs,
|
||||||
|
landscape: zett.landscape,
|
||||||
|
};
|
||||||
|
|
||||||
|
loader.show();
|
||||||
|
$('.btn').addClass("disabled");
|
||||||
|
|
||||||
|
$.post(backend_api, JSON.stringify(req))
|
||||||
|
.then(data => {
|
||||||
|
console.log(data);
|
||||||
|
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
|
||||||
|
action.append(pdfbtn);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
loader.hide();
|
||||||
|
$('.btn').removeClass('disabled');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.append(loader));
|
||||||
|
|
||||||
|
appbody
|
||||||
|
.append($('<li>')
|
||||||
|
.append($(`<div class="collapsible-header"><h6 class="blue-text">${zett.title}</h6></div>`))
|
||||||
|
.append($('<div class="collapsible-body">')
|
||||||
|
.append(block)
|
||||||
|
.append(action)));
|
||||||
}
|
}
|
||||||
setCategories();
|
|
||||||
|
|
||||||
$('.collapsible').collapsible();
|
$('.collapsible').collapsible();
|
||||||
$('.modal').modal();
|
|
||||||
$('select').formSelect();
|
|
||||||
|
|
||||||
konami();
|
konami();
|
||||||
|
|
||||||
$('#new-add').off('click').click(() => {
|
|
||||||
const title = $("#new-name").val();
|
|
||||||
const category_id = $("#new-type").val();
|
|
||||||
const designation = $("#new-designation").val();
|
|
||||||
const ingredients = $("#new-ingredients").val();
|
|
||||||
const description = $("#new-description").val();
|
|
||||||
const color = $("#new-color").val().substring(1);
|
|
||||||
const ab = $("#new-organic").is(":checked") ? 'visible' : 'none';
|
|
||||||
|
|
||||||
const req = {
|
|
||||||
title,
|
|
||||||
category_id,
|
|
||||||
designation,
|
|
||||||
ingredients,
|
|
||||||
description,
|
|
||||||
color,
|
|
||||||
ab,
|
|
||||||
};
|
|
||||||
$.post(backend_api + 'newtikette', JSON.stringify(req)).then(reload);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function konami() {
|
function konami() {
|
||||||
@ -134,22 +76,16 @@ function konami() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reload() {
|
$(document).ready(async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await $.ajax({
|
const resp = await $.ajax({
|
||||||
url: backend_api + 'list',
|
url: backend_api + 'list',
|
||||||
timeout: 1000,
|
timeout: 1000,
|
||||||
});
|
});
|
||||||
tikats = (await $.ajax({
|
|
||||||
url: backend_api + 'categories',
|
|
||||||
timeout: 1000,
|
|
||||||
})).tikats.sort((a, b) => a.name > b.name ? 1 : -1);
|
|
||||||
loadAll(resp.tikettes.sort((a, b) => (a.title < b.title) ? -1 : 1));
|
loadAll(resp.tikettes.sort((a, b) => (a.title < b.title) ? -1 : 1));
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
const appbody = $("#appbody");
|
const appbody = $("#appbody");
|
||||||
appbody.append(`<li>Could not reach backend server`);
|
appbody.append(`<li>Could not reach backend server`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
$(document).ready(reload);
|
|
||||||
|
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 5.3 MiB |
Before Width: | Height: | Size: 5.3 MiB After Width: | Height: | Size: 5.3 MiB |
1431
templates/Sel.svg
Before Width: | Height: | Size: 5.2 MiB |
2249
templates/Sirop.svg
Before Width: | Height: | Size: 1.2 MiB |
1392
templates/Tisane.svg
Before Width: | Height: | Size: 1.1 MiB |
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
|