Compare commits

..

16 Commits

Author SHA1 Message Date
db7de847f2 remove old stuff 2025-08-07 14:27:54 +02:00
ba34b2d647 black backend 2025-08-07 12:09:27 +02:00
684987dfd5 403 and 404 handlers 2025-08-07 12:04:50 +02:00
d1923f640c   2025-08-07 11:43:10 +02:00
d32caa2c38 Store ingredients with stars in the DB 2025-08-06 22:22:46 +02:00
fbb0312c7c Restore sticky footer 2025-08-06 22:16:02 +02:00
5903c6bd36 Django won't serve static files in DEBUG = False mode 2025-08-06 20:00:16 +00:00
767b722025 Fix another CSRF issue
Also turn that DEBUG to False, just for shits & giggles
2025-08-06 21:42:31 +02:00
6b5a43b7a9 Fix autoresizing textareas 2025-08-06 19:22:08 +02:00
0cf8784259 Wider view on desktop 2025-08-06 19:17:40 +02:00
3474239727 Now with edits, delete confirmations
- and also a better alignment on actions
- and konami code that works before login
2025-08-06 19:12:44 +02:00
3d35d47254 Fix small issue with make staticfiles 2025-08-06 15:42:27 +02:00
c219091c2c Add google sign-in
Also, try to be a bit more correct with CSRF and CORS.
It works on my machine.
2025-08-06 15:28:41 +02:00
69327d9edf Add utility scripts 2025-08-05 22:24:57 +02:00
3c6579e7a3 Point frontend to production backend 2025-08-05 22:20:22 +02:00
50df9778e2 This time we have the correct font 2025-08-05 21:42:23 +02:00
25 changed files with 726 additions and 418 deletions

View File

@@ -1,7 +1,7 @@
FROM alpine:3.18
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript font-droid
RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn
RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript font-noto py3-pip
RUN pip3 install --break-system-packages django tzdata gunicorn google-api-python-client
ADD backend /zetikettes

View File

@@ -38,7 +38,7 @@ staticfiles: image ## install all static files
-v $(LIBPREFIX):/libdir \
-w /libdir \
$(image-name) \
/zetikettes/zetikettes/manage.py collectstatic
/zetikettes/zetikettes/manage.py collectstatic --noinput
.PHONY: help

View File

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

View File

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

View File

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

View File

@@ -8,64 +8,120 @@ 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()),
("prototempalte", models.FileField(upload_to="")),
],
options={
'verbose_name_plural': 'tikategoriez',
"verbose_name_plural": "tikategoriez",
},
),
migrations.CreateModel(
name='Tisub',
name="Tisub",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('descritpion', models.TextField()),
('default', models.TextField()),
('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()),
(
"type",
models.CharField(
choices=[
("ST", "Short Text"),
("LT", "Long Text"),
("C", "Color"),
("YM", "Year Month"),
("B", "Boolean"),
],
default="ST",
max_length=2,
),
),
],
options={
'verbose_name_plural': 'tisubz',
"verbose_name_plural": "tisubz",
},
),
migrations.CreateModel(
name='Tizer',
name="Tizer",
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",
),
),
("email", models.CharField()),
],
options={
'verbose_name_plural': 'tizerz',
"verbose_name_plural": "tizerz",
},
),
migrations.CreateModel(
name='Tikette',
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')),
(
"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',
"verbose_name_plural": "tikettz",
},
),
migrations.AddField(
model_name='tikategory',
name='subs',
field=models.ManyToManyField(to='tikette.tisub'),
model_name="tikategory",
name="subs",
field=models.ManyToManyField(to="tikette.tisub"),
),
]

View File

@@ -1,5 +1,6 @@
from django.db import models
class Tisub(models.Model):
class Type(models.TextChoices):
SHORT_TEXT = "ST"

View File

@@ -11,13 +11,18 @@ DESIGNATION_FONTSIZE = 42.6667
def inkscapize(svg_in, pdf_out):
png_out = tempfile.mktemp(suffix='.png')
png_out = tempfile.mktemp(suffix=".png")
try:
cmd = ['inkscape', '--export-type=png', f'--export-filename={png_out}',
f'--export-dpi={DEFAULT_DPI}', svg_in]
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]
cmd = ["convert", png_out, pdf_out]
subprocess.check_call(cmd)
finally:
if os.path.exists(png_out):
@@ -34,24 +39,25 @@ def generate(template, subs, out_dir, landscape=False):
"""
# default designation font size
subs['designation_fontsize'] = subs.get('designation_fontsize',
DESIGNATION_FONTSIZE)
subs["designation_fontsize"] = subs.get(
"designation_fontsize", DESIGNATION_FONTSIZE
)
# fill in sticker details
sticker_out = tempfile.mktemp(suffix='.svg')
sticker_out = tempfile.mktemp(suffix=".svg")
try:
with open(sticker_out, 'w') as stickout:
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:
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')
pdf_out = tempfile.mktemp(dir=out_dir, suffix=".pdf")
inkscapize(planche_out, pdf_out)
return os.path.basename(pdf_out)
@@ -59,5 +65,5 @@ def generate(template, subs, out_dir, landscape=False):
finally:
if os.path.exists(sticker_out):
os.unlink(sticker_out)
if 'planche_out' in locals() and os.path.exists(planche_out):
if "planche_out" in locals() and os.path.exists(planche_out):
os.unlink(planche_out)

View File

@@ -5,29 +5,36 @@ from pathlib import Path
from string import Template
import sys
DEFAULT_SHEET_TEMPLATE = Path(__file__).parent / 'planche.svg.in'
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',
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',
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'),
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)')
nargs="?",
help="path to the sticker SVG (default: stdin)",
)
return parser.parse_args()
@@ -37,15 +44,15 @@ def makeplanche(sticker, out, template=DEFAULT_SHEET_TEMPLATE ,landscape=False):
tpl_data = tpl.read()
lines = sticker.readlines()
if lines[0].startswith('<?xml'):
if lines[0].startswith("<?xml"):
lines = lines[1:]
sticker_data = ''.join(lines)
sticker_data = "".join(lines)
rotate = "translate(102, 0) rotate(90)" if not landscape else ""
subs = {
'sticker': sticker_data,
'rotate': rotate,
"sticker": sticker_data,
"rotate": rotate,
}
out.write(Template(tpl_data).substitute(subs))

View File

@@ -7,22 +7,25 @@ import typing
def parse_args():
parser = argparse.ArgumentParser(description='Fill in sticker details')
parser.add_argument('--out',
'-o',
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')
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()
@@ -30,9 +33,9 @@ def parse_args():
def makesticker(sticker: str, out: typing.IO, subs: dict):
with open(sticker) as fin:
lines = fin.readlines()
if lines[0].startswith('<?xml'):
if lines[0].startswith("<?xml"):
lines = lines[1:]
sticker_data = ''.join(lines)
sticker_data = "".join(lines)
out.write(Template(sticker_data).substitute(subs))
@@ -40,12 +43,12 @@ def makesticker(sticker: str, out: typing.IO, subs: dict):
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'),
"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
args["subs"] = subs
makesticker(**args)

View File

@@ -1,87 +1,164 @@
import json
import sys
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from google.auth.transport import requests
from google.oauth2 import id_token
from .planche import generate as stickersheet
from .models import Tikette, Tikategory
from .models import Tikette, Tikategory, Tizer
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('*'):
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,
'prototempalte': x.category.prototempalte.name,
'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()},
} for x in Tikette.objects.all()]
return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS)
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 JsonResponse({'status': 'ok', 'tikats': tikats}, headers=CORS)
tikats = [
{
"id": x.id,
"name": x.name,
}
for x in Tikategory.objects.all()
]
return ok({"tikats": tikats})
@csrf_exempt
@auth_only
@post_please
def generate(request):
if request.method != "POST":
return JsonResponse({'status': 'notok', 'message': 'this isn\'t the way'})
payload = json.loads(request.body)
subs = dict(payload['subs'])
for key in ('designation', 'ingredients', 'description', 'color', 'AB'):
subs = dict(payload["subs"])
for key in ("designation", "ingredients", "description", "color", "AB"):
subs[key] = payload[key]
pdfpath = stickersheet.generate(template=payload['template'], subs=subs,
subs["ingredients"] = quirk_bold_allergens(subs["ingredients"])
pdfpath = stickersheet.generate(
template=payload["template"],
subs=subs,
out_dir=settings.TIKETTE_OUT_DIR,
landscape=payload['landscape'])
landscape=payload["landscape"],
)
return JsonResponse({'status': 'ok', 'file': pdfpath}, headers=CORS)
return ok({"file": pdfpath})
@csrf_exempt
@auth_only
@post_please
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)
return ok()
@csrf_exempt
@auth_only
@post_please
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)
return ok()
@csrf_exempt
@post_please
def signin(request):
payload = json.loads(request.body)
token = payload["token"]
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()

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1,2 @@
const backend_api = 'http://localhost:8000/'
const backend_api = "/zetikettes/srv/"
const google_oauth_client_id = '634510965520-c5l7f15fn4koraqhpqfe01ssn8v0q2qk.apps.googleusercontent.com';

View File

@@ -14,21 +14,33 @@
<!-- 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>
@@ -37,7 +49,7 @@ main {
<a href="#" class="brand-logo">Zětikwett's</a>
<ul id="nav-mobile" class="right hide-on-med-and-down">
<li>
<a class="modal-trigger btn orange" href="#newproduct">
<a class="modal-trigger btn orange disabled" href="#newproduct">
<i class="material-icons left">add</i>Nouveau produit
</a>
</li>
@@ -46,9 +58,10 @@ main {
</nav>
<main class="container row">
<p></p>
<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">
<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>
@@ -56,6 +69,15 @@ main {
</div>
</ul>
</div>
<div id="confirmdelete" class="modal confirm">
<div class="modal-content">
Supprimer le produit <b id="delete-title">blařg titre long de test sa mère</b>&nbsp;?
</div>
<div class="modal-footer">
<button class="modal-close waves-effect btn-flat" id="delete-cancel">annuler</button>
<button type="submit" class="modal-close waves-effect waves-green btn" id="delete-confirm">supprimer</button>
</div>
</div>
<!-- Modal Structure -->
<div id="newproduct" class="modal">

View File

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

View File

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

View File

@@ -8,6 +8,82 @@ const params = {
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");
@@ -19,7 +95,7 @@ function addProduct(tikette) {
.hide();
const action = $('<div class="section">')
.append($('<a class="btn">generate<a>')
.append($('<a class="btn">générer<a>')
.click(() => {
const subs = block.find(':text')
.toArray()
@@ -36,11 +112,11 @@ function addProduct(tikette) {
};
loader.show();
$('.btn').addClass("disabled");
disableButtons();
$.post(backend_api + 'generate', JSON.stringify(req))
post(backend_api + 'generate', req)
.then(data => {
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">open pdf</a>`);
const pdfbtn = $(`<a class="btn" href="${backend_api}data/${data.file}" target="_blank">ouvrir le pdf</a>`);
action.append(pdfbtn);
})
.catch(err => {
@@ -48,27 +124,37 @@ function addProduct(tikette) {
})
.always(() => {
loader.hide();
$('.btn').removeClass('disabled');
enableButtons();
});
})
.append(loader));
const deleteAction = $('<a class="btn-flat grey-text"><b class="material-icons">delete</b>');
deleteAction.click(() => {
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', JSON.stringify(req)).then(reload);
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="badge">')
.append(deleteAction)))
.append($('<span class="actions grey-text">')
.append(editAction)
.append(deleteAction)
))
.append($('<div class="collapsible-body">')
.append(block)
.append(action)));
@@ -81,6 +167,28 @@ function setCategories() {
for (let kat of tikats) {
katsel.append($(`<option value="${kat.id}">${kat.name}</option>`));
}
$('select').formSelect();
}
function getTiketteData() {
const title = $("#new-name").val();
const category_id = $("#new-type").val();
const designation = $("#new-designation").val();
const ingredients = $("#new-ingredients").val();
const description = $("#new-description").val();
const color = $("#new-color").val().substring(1);
const ab = $("#new-organic").is(":checked") ? 'inline' : 'none';
return {
title,
category_id,
designation,
ingredients,
description,
color,
ab,
};
}
function loadAll(zetikettes) {
@@ -90,33 +198,6 @@ function loadAll(zetikettes) {
addProduct(zett);
}
setCategories();
$('.collapsible').collapsible();
$('.modal').modal();
$('select').formSelect();
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() {
@@ -134,22 +215,53 @@ function konami() {
});
}
async function googleCred(creds) {
const token = creds.credential;
await post(backend_api + 'signin', {token});
$('#signin-prompt').hide();
reload();
}
async function reload() {
disableButtons();
try {
const resp = await $.ajax({
url: backend_api + 'list',
timeout: 1000,
xhrFields: { withCredentials: true },
});
tikats = (await $.ajax({
url: backend_api + 'categories',
timeout: 1000,
xhrFields: { withCredentials: true },
})).tikats.sort((a, b) => a.name > b.name ? 1 : -1);
loadAll(resp.tikettes.sort((a, b) => (a.title < b.title) ? -1 : 1));
enableButtons();
} catch(e) {
if (e.status === 403) {
$("#signin-prompt").show();
google.accounts.id.prompt(); // also display the One Tap dialog
return;
}
const appbody = $("#appbody");
appbody.append(`<li>Could not reach backend server`);
throw e;
}
}
$(document).ready(reload);
$(document).ready(() => {
google.accounts.id.initialize({
client_id: google_oauth_client_id,
callback: googleCred,
});
google.accounts.id.renderButton(
document.getElementById("signin-prompt"),
{ theme: "outline", size: "large" } // customization attributes
);
konami();
$('.collapsible').collapsible();
$('.modal').modal({onOpenStart: resetEditModal});
reload();
});

View File

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

View File

@@ -0,0 +1,33 @@
aromate;Herbes de Provence;Thym*, Sarriette*, Origan*;Le classique des herbes aromatiques pour donner une touche méditerranéenne à vos plats.;6307a6;Aromate - Herbes de Provence - Thym Sarriette Origan.svg;True
aromate;Origan;Origan*;La fameuse herbe à pizza, mais qui peut aussi assaisonner vos soupes, plats mijotés, poissons ou charcuteries.;4800b9;Aromate - Origan.svg;True
aromate;Sarriette;Sarriette des montagnes*;Le pèbre d'aï s'utilise en cuisine dans les pommes de terre, champignons, ragoûts de légumes, riz...;00d40e;Aromate - Sarriette.svg;True
aromate;Sauge;Sauge officinale*;La plante qui sauve peut être utilisée en cuisine, dans vos viandes en sauce, légumes, vinaigrettes...;d00676;Aromate - Sauge.svg;True
aromate;Thym;Thym*;Le roi des aromates, à utiliser en branche ou effeuillé, pour agrémenter vos grillades, plats en sauce, poêlées...;0048ff;Aromate - Thym.svg;True
confiture;Confiture d'Abricot;Abricot*, Sucre de canne*;;ff5d00;Confiture - Abricot.svg;True
confiture;Confiture d'Abricot;Abricot, Sucre de canne*;;ff5d00;Confiture - Abricot - Non Bio.svg;False
confiture;Confiture d'Abricot Lavande;Abricot, Lavande*, Sucre de canne*;;9683ec;Confiture - Abricot Lavande - Non Bio.svg;False
confiture;Gelée Extra de Groseille;Groseille*, Sucre de canne*;;ec001a;Gelée Extra - Groseille.svg;True
confiture;Gelée Extra de Cassis;Cassis*, Sucre de canne*;;9b00ff;Gelée Extra - Cassis.svg;True
confiture;Marmelade d'Orange Amère; Oranges amères, Sucre de canne*, eau;;ff5d00;Marmelade - Orange Amère - Non Bio.svg;False
pesto;Pesto de Livèche;Huile d'olive*, Livèche* (34%), *amandes*, jus de citron*, ail*, sel;;119200;Pesto - Livèche.svg;True
pesto;Pesto d'Ail des Ours;Huile d'olive*, Ail des ours* (34%), *Amandes*, Jus de citron*, Sel;;196b00;Pesto - Ail des Ours.svg;True
sirop;Lavande;Sucre* (55%), Infusion de lavande* (42%), Jus de citron* (3%);;4200ff;Sirop - Lavande.svg;True
sirop;Thym;Sucre* (55%), Infusion de thym* (42%), Jus de citron* (3%);;0048ff;Sirop - Thym.svg;True
sirop;Mélisse;Sucre* (55%), Infusion de mélisse* (42%), Jus de citron* (3%);;d00676;Sirop - Mélisse.svg;True
sirop;Menthe Verte;Sucre* (55%), Infusion de menthe verte* (42%), Jus de citron* (3%);;007e49;Sirop - Menthe Verte.svg;True
sirop;Menthe et Mélisse;Sucre* (55%), Infusion de menthe verte* et mélisse* (42%), Jus de citron* (3%);;fabf12;Sirop - Menthe et Mélisse.svg;True
sirop;Sureau;Sucre* (55%), Infusion de sureau* (42%), Jus de citron* (3%);;8aa700;Sirop - Sureau.svg;True
sirop;Romarin;Sucre* (55%), Infusion de romarin* (42%), Jus de citron* (3%);;007d87;Sirop - Romarin.svg;True
tisane;J'ai Bien Mangé...; Thym*, Sauge*, Romarin*, Soucis*;;0c7c00;Tisane - Digestion - Thym Sauge Romarin Soucis.svg;True
tisane;Nuit Étoilée;Agastache*, Mélisse*, Aubépine*;;5b7aff;Tisane - Nuit Étoilée - Agastache Mélisse Aubépine.svg;True
tisane;Nuit Étoilée;Mélisse*, Lavande*, Aubépine*;;5b7aff;Tisane - Nuit Étoilée - Mélisse Lavande Aubépine.svg;True
tisane;Réconfort de la Gorge; Origan*, Agastache*, Thym*, Bleuet*;;00a07b;Tisane - Réconfort de la Gorge - Origan Agastache Thym Bleuet.svg;True
tisane;Équilibre Féminin;Achillée millefeuille*, Lavande vraie*, Cynorhodon*;;64139f;Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Cynorhodon.svg;True
tisane;Équilibre Féminin;Achillée millefeuille*, Lavande vraie*, Ortie piquante*;;64139f;Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Ortie piquante.svg;True
tisane;Équilibre Féminin;Achillée millefeuille*, Lavande vraie*, Pétales de Cynorhodon*;;64139f;Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Pétales de Cynorhodon.svg;True
tisane;Équilibre Féminin;Achillée millefeuille*, Lavande vraie*, Sauge officinale*, Ortie piquante*;;64139f;Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Sauge officinale Ortie piquante.svg;True
tisane;Joie de Vivre;Basilic sacré*, Sarriette des montagnes*, Lavande vraie*;;ff6d00;Tisane - Joie de Vivre - Basilic Sacré Sarriette des Montagnes Lavande Vraie.svg;True
sel;Grillades - Viande et Légumes;Sel de Camargue, Thym*, Origan*, Sarriette*, Mélisse*, Souci*;;c80003;Sel - Grillades.svg;True
sel;Herbes de Provence;Sel de Camargue, Thym*, Origan*, Sarriette*, Romarin*;;6307a6;Sel - Herbes de Provence.svg;True
sel;Poisson et Viande Blanche;Sel de Camargue, Thym*, Sauge*, Agastache*, Bleuet*;;5b7aff;Sel - Poisson et Viande Blanche.svg;True
chocolat;Menthe Poivrée;"Chocolat de couverture noir* (pâte de cacao*, sucre de canne*, beurre de cacao*; peut contenir : *lait), *crème entière* (crème de lait à 30% de matière grasse*, stabilisants : carraghénanes), menthe poivrée*";;007e49;Chocolat - Menthe Poivrée.svg;True
1 aromate Herbes de Provence Thym*, Sarriette*, Origan* Le classique des herbes aromatiques pour donner une touche méditerranéenne à vos plats. 6307a6 Aromate - Herbes de Provence - Thym Sarriette Origan.svg True
2 aromate Origan Origan* La fameuse herbe à pizza, mais qui peut aussi assaisonner vos soupes, plats mijotés, poissons ou charcuteries. 4800b9 Aromate - Origan.svg True
3 aromate Sarriette Sarriette des montagnes* Le pèbre d'aï s'utilise en cuisine dans les pommes de terre, champignons, ragoûts de légumes, riz... 00d40e Aromate - Sarriette.svg True
4 aromate Sauge Sauge officinale* La plante qui sauve peut être utilisée en cuisine, dans vos viandes en sauce, légumes, vinaigrettes... d00676 Aromate - Sauge.svg True
5 aromate Thym Thym* Le roi des aromates, à utiliser en branche ou effeuillé, pour agrémenter vos grillades, plats en sauce, poêlées... 0048ff Aromate - Thym.svg True
6 confiture Confiture d'Abricot Abricot*, Sucre de canne* ff5d00 Confiture - Abricot.svg True
7 confiture Confiture d'Abricot Abricot, Sucre de canne* ff5d00 Confiture - Abricot - Non Bio.svg False
8 confiture Confiture d'Abricot Lavande Abricot, Lavande*, Sucre de canne* 9683ec Confiture - Abricot Lavande - Non Bio.svg False
9 confiture Gelée Extra de Groseille Groseille*, Sucre de canne* ec001a Gelée Extra - Groseille.svg True
10 confiture Gelée Extra de Cassis Cassis*, Sucre de canne* 9b00ff Gelée Extra - Cassis.svg True
11 confiture Marmelade d'Orange Amère Oranges amères, Sucre de canne*, eau ff5d00 Marmelade - Orange Amère - Non Bio.svg False
12 pesto Pesto de Livèche Huile d'olive*, Livèche* (34%), *amandes*, jus de citron*, ail*, sel 119200 Pesto - Livèche.svg True
13 pesto Pesto d'Ail des Ours Huile d'olive*, Ail des ours* (34%), *Amandes*, Jus de citron*, Sel 196b00 Pesto - Ail des Ours.svg True
14 sirop Lavande Sucre* (55%), Infusion de lavande* (42%), Jus de citron* (3%) 4200ff Sirop - Lavande.svg True
15 sirop Thym Sucre* (55%), Infusion de thym* (42%), Jus de citron* (3%) 0048ff Sirop - Thym.svg True
16 sirop Mélisse Sucre* (55%), Infusion de mélisse* (42%), Jus de citron* (3%) d00676 Sirop - Mélisse.svg True
17 sirop Menthe Verte Sucre* (55%), Infusion de menthe verte* (42%), Jus de citron* (3%) 007e49 Sirop - Menthe Verte.svg True
18 sirop Menthe et Mélisse Sucre* (55%), Infusion de menthe verte* et mélisse* (42%), Jus de citron* (3%) fabf12 Sirop - Menthe et Mélisse.svg True
19 sirop Sureau Sucre* (55%), Infusion de sureau* (42%), Jus de citron* (3%) 8aa700 Sirop - Sureau.svg True
20 sirop Romarin Sucre* (55%), Infusion de romarin* (42%), Jus de citron* (3%) 007d87 Sirop - Romarin.svg True
21 tisane J'ai Bien Mangé... Thym*, Sauge*, Romarin*, Soucis* 0c7c00 Tisane - Digestion - Thym Sauge Romarin Soucis.svg True
22 tisane Nuit Étoilée Agastache*, Mélisse*, Aubépine* 5b7aff Tisane - Nuit Étoilée - Agastache Mélisse Aubépine.svg True
23 tisane Nuit Étoilée Mélisse*, Lavande*, Aubépine* 5b7aff Tisane - Nuit Étoilée - Mélisse Lavande Aubépine.svg True
24 tisane Réconfort de la Gorge Origan*, Agastache*, Thym*, Bleuet* 00a07b Tisane - Réconfort de la Gorge - Origan Agastache Thym Bleuet.svg True
25 tisane Équilibre Féminin Achillée millefeuille*, Lavande vraie*, Cynorhodon* 64139f Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Cynorhodon.svg True
26 tisane Équilibre Féminin Achillée millefeuille*, Lavande vraie*, Ortie piquante* 64139f Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Ortie piquante.svg True
27 tisane Équilibre Féminin Achillée millefeuille*, Lavande vraie*, Pétales de Cynorhodon* 64139f Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Pétales de Cynorhodon.svg True
28 tisane Équilibre Féminin Achillée millefeuille*, Lavande vraie*, Sauge officinale*, Ortie piquante* 64139f Tisane - Équilibre Féminin - Achillée millefeuille Lavande vraie Sauge officinale Ortie piquante.svg True
29 tisane Joie de Vivre Basilic sacré*, Sarriette des montagnes*, Lavande vraie* ff6d00 Tisane - Joie de Vivre - Basilic Sacré Sarriette des Montagnes Lavande Vraie.svg True
30 sel Grillades - Viande et Légumes Sel de Camargue, Thym*, Origan*, Sarriette*, Mélisse*, Souci* c80003 Sel - Grillades.svg True
31 sel Herbes de Provence Sel de Camargue, Thym*, Origan*, Sarriette*, Romarin* 6307a6 Sel - Herbes de Provence.svg True
32 sel Poisson et Viande Blanche Sel de Camargue, Thym*, Sauge*, Agastache*, Bleuet* 5b7aff Sel - Poisson et Viande Blanche.svg True
33 chocolat Menthe Poivrée Chocolat de couverture noir* (pâte de cacao*, sucre de canne*, beurre de cacao*; peut contenir : *lait), *crème entière* (crème de lait à 30% de matière grasse*, stabilisants : carraghénanes), menthe poivrée* 007e49 Chocolat - Menthe Poivrée.svg True

47
scripts/provision.py Executable file
View File

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

66
scripts/renew_lineup.py Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
import argparse
from string import Template
import sys
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'
JAM_DESIGNATION_FONTSIZE_DEFAULT = 42.6667
JAM_DESIGNATION_FONTSIZE_SMALL = 36
templates = {
'aromate': f"{TEMPLATES_DIR}/Aromate.svg",
'chocolat': f"{TEMPLATES_DIR}/Chocolat.svg",
'confiture': f"{TEMPLATES_DIR}/Confiture.svg",
'pesto': f"{TEMPLATES_DIR}/Pesto.svg",
'sirop': f"{TEMPLATES_DIR}/Sirop.svg",
'tisane': f"{TEMPLATES_DIR}/Tisane.svg",
'sel': f"{TEMPLATES_DIR}/Sel.svg",
}
ALLERGEN_BEGIN_STYLE = '<tspan style="font-weight:bold">'
ALLERGEN_END_STYLE = '</tspan>'
def parse_args():
parser = argparse.ArgumentParser(description='Renew whole lineup from template and list of subs')
parser.add_argument('--list', default=DEFAULT_CSV, help='Lineup file')
return parser.parse_args()
def main():
args = parse_args()
with open(args.list) as csvfile:
reader = csv.reader(csvfile, delimiter=';', quotechar='"')
for row in reader:
template = templates[row[0]]
outfile = f"{OUT_DIR}/{row[5]}"
with open(outfile, 'w') as out:
# TODO Fix bold formatting with allergen and parenthesis
ingredients = [e.strip() for e in row[2].split(',')]
ingredients = [e if not e.startswith('*') else ALLERGEN_BEGIN_STYLE + e[1:] + ALLERGEN_END_STYLE for e in ingredients]
ingredients_sub = ", ".join(ingredients)
AB_logo_visibility = 'inline' if row[6] == 'True' else 'none'
subs = {
'designation': row[1].strip(),
'ingredients': ingredients_sub,
'description': row[3].strip(),
'color': row[4],
'AB': AB_logo_visibility,
'designation_fontsize': JAM_DESIGNATION_FONTSIZE_DEFAULT,
}
s = Subber(subs)
s.sub(template, out)
if __name__ == "__main__":
main()

14
scripts/subber.py Normal file
View File

@@ -0,0 +1,14 @@
from string import Template
class Subber():
def __init__(self, subs):
self.subs = subs
def sub(self, infile, outfile):
with open(infile) as template:
lines = template.readlines()
data = ''.join(lines)
outfile.write(Template(data).safe_substitute(self.subs))