-
+
diff --git a/Dockerfile b/Dockerfile index 069885a..0c13b1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,13 +3,13 @@ FROM alpine RUN apk --no-cache add python3 inkscape bash imagemagick ghostscript ttf-opensans RUN apk --no-cache add py3-pip && pip3 install --break-system-packages django tzdata gunicorn -ADD backend /root/zetikettes +ADD backend /zetikettes RUN mkdir -p /usr/share/fonts/TTF \ - && cp /root/zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \ + && cp /zetikettes/fonts/*.ttf /usr/share/fonts/TTF/ \ && fc-cache -fv # the script will look for templates in /data -WORKDIR /root/zetikettes/zetikettes +WORKDIR /zetikettes/zetikettes CMD /usr/bin/gunicorn zetikettes.wsgi -b 0.0.0.0:8000 --timeout 600 --forwarded-allow-ips="*" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3ac6b12 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +LIBPREFIX ?= /var/lib/zetikettes +image-name = pol/zetikettes +uuid = $(shell id -u):$(shell id -g) + +.PHONY: image +image: ## build the docker images + docker compose build + +.PHONY: initial-db +initial-db: image ## create and populate a database + mkdir -p $(LIBPREFIX)/data + docker run --rm \ + --user $(uuid) \ + -v $(LIBPREFIX)/data:/data \ + $(image-name) \ + /zetikettes/zetikettes/manage.py migrate + docker run --rm \ + --user $(uuid) \ + -v $(LIBPREFIX)/data:/data \ + $(image-name) \ + /zetikettes/zetikettes/manage.py loaddata initial_db + cp templates/*.svg $(LIBPREFIX)/data/ + +.PHONY: superuser +superuser: image ## create a superuser in the django admin + docker run --rm \ + --user $(uuid) \ + -v $(LIBPREFIX)/data:/data \ + -it \ + $(image-name) \ + /zetikettes/zetikettes/manage.py createsuperuser + +.PHONY: staticfiles +staticfiles: image ## install all static files + cp -r frontend $(LIBPREFIX)/ + docker run --rm \ + --user $(uuid) \ + -v $(LIBPREFIX):/libdir \ + -w /libdir \ + $(image-name) \ + /zetikettes/zetikettes/manage.py collectstatic + + +.PHONY: help +help: ## Show this help + @echo Noteworthy targets: + @egrep '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index f5e78a3..b6f5bcf 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,53 @@ -zetikettes -========== +zetikettes 2.0 +============== -ouaich. +ouaich. tavu. + +**NOTE**: release 2.0 broke compatibility with previous "releases". Maxime has all the recent data to repopulate a new database. Initial setup ------------- +### Database setup and population + +Populate an empty database (if none already exists): + +``` +sudo make initial-db +``` + +This will setup an initial database under `/var/lib/zetikettes/data` + +Optionally, you may need credentials to access the admin page: + +``` +make superuser +``` + +### System service + ``` -docker build -t zetikettes . sudo mkdir -p /etc/docker/compose/zetikettes -sudo cp docker-compose.yml /etc/docker/compose/zetikettes/ +sudo cp compose.yml /etc/docker/compose/zetikettes/ sudo systemctl enable --now docker-compose@zetikettes ``` +If not already present in `/etc/systemd/system/`, the `docker-compose@` service file is provided. + +### www static files + +``` +make staticfiles +``` + +This will install frontend and django admin static files under `/var/lib/zetikettes` + +### Nginx + +Example configuration is provided in `nginx_locations`. + Nginx is configured to: -- redirect /zetikettes/srv/ to localhost:8000 -- redirect /zetikettes/ to /var/lib/zetikettes/static - -Conf is in /etc/nginx/sites-available/default - -Test ----- - -``` -docker run --rm -it -v $PWD/templates:/data zetikettes /bin/bash /root/zetikettes/old/mkjam.sh -``` - -This should produce a .pdf in `templates/`. Open it to check that -layout & fonts are correct. - -Run ---- - -``` -docker-compose up -``` - -Notes for deploying -------------------- - -.h3 Initialize empty database -``` -python manage.py migrate -``` - -.h3 Prepare static files -``` -python manage.py collectstatic -``` -The files will be in `www_static/` and need to be moved to `/var/lib/zetikettes/www_static` - -.h3 Change host settings -If not deploying on `aerith.ponteilla.net`, you'll need to edit `backend/zetikettes/zetikettes/settings.py` to change a couple things in there. - - -Change available templates --------------------------- - -1. go to /zetikettes/admin -1. add the newtikette -1. still no need to restart the container (magic!) -2. profit. +- redirect `/zetikettes/srv/` to `localhost:8000` +- redirect `/zetikettes/` to `/var/lib/zetikettes/frontend` +- redirect `/zetikettes/srv/static` to `/var/lib/zetikettes/www_static` diff --git a/backend/zetikettes/initial_db.json b/backend/zetikettes/initial_db.json new file mode 100644 index 0000000..197ca3d --- /dev/null +++ b/backend/zetikettes/initial_db.json @@ -0,0 +1,152 @@ +[ +{ + "model": "tikette.tisub", + "pk": 2, + "fields": { + "name": "dluo", + "descritpion": "DLUO", + "default": "décembre 1992", + "type": "YM" + } +}, +{ + "model": "tikette.tisub", + "pk": 3, + "fields": { + "name": "lot", + "descritpion": "№ de lot", + "default": "1234-5", + "type": "ST" + } +}, +{ + "model": "tikette.tisub", + "pk": 4, + "fields": { + "name": "qty", + "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": "fruit", + "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 + ] + } +} +] diff --git a/backend/zetikettes/tikette/admin.py b/backend/zetikettes/tikette/admin.py index 149dd87..a52c284 100644 --- a/backend/zetikettes/tikette/admin.py +++ b/backend/zetikettes/tikette/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin -from .models import Tikette, Tikategory, Tisub +from .models import Tikette, Tizer, Tikategory, Tisub admin.site.register(Tikette) +admin.site.register(Tizer) admin.site.register(Tikategory) admin.site.register(Tisub) diff --git a/backend/zetikettes/tikette/migrations/0001_initial.py b/backend/zetikettes/tikette/migrations/0001_initial.py index 3ce01fe..e3c7914 100644 --- a/backend/zetikettes/tikette/migrations/0001_initial.py +++ b/backend/zetikettes/tikette/migrations/0001_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 4.2.2 on 2023-07-03 14:37 +# Generated by Django 5.2.4 on 2025-08-05 12:57 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): @@ -18,6 +18,7 @@ class Migration(migrations.Migration): ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=50)), ('landscape', models.BooleanField()), + ('prototempalte', models.FileField(upload_to='')), ], options={ 'verbose_name_plural': 'tikategoriez', @@ -30,22 +31,41 @@ class Migration(migrations.Migration): ('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', }, ), + 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( name='Tikette', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=100)), - ('svg', models.FileField(upload_to='')), + ('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')), - ('subs', models.ManyToManyField(to='tikette.tisub')), ], options={ 'verbose_name_plural': 'tikettz', }, ), + migrations.AddField( + model_name='tikategory', + name='subs', + field=models.ManyToManyField(to='tikette.tisub'), + ), ] diff --git a/backend/zetikettes/tikette/migrations/0002_remove_tikette_subs_tikategory_subs.py b/backend/zetikettes/tikette/migrations/0002_remove_tikette_subs_tikategory_subs.py deleted file mode 100644 index 105a2f8..0000000 --- a/backend/zetikettes/tikette/migrations/0002_remove_tikette_subs_tikategory_subs.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-04 14:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tikette', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='tikette', - name='subs', - ), - migrations.AddField( - model_name='tikategory', - name='subs', - field=models.ManyToManyField(to='tikette.tisub'), - ), - ] diff --git a/backend/zetikettes/tikette/models.py b/backend/zetikettes/tikette/models.py index f2b4701..9e8f1a0 100644 --- a/backend/zetikettes/tikette/models.py +++ b/backend/zetikettes/tikette/models.py @@ -1,9 +1,18 @@ from django.db import models class Tisub(models.Model): + class Type(models.TextChoices): + SHORT_TEXT = "ST" + LONG_TEXT = "LT" + COLOR = "C" + YEAR_MONTH = "YM" + BOOLEAN = "B" + name = models.CharField(max_length=50) descritpion = models.TextField() default = models.TextField() + # everything is text. type is for UX (e.g. color picker) + type = models.CharField(max_length=2, choices=Type, default=Type.SHORT_TEXT) def __str__(self): return self.name @@ -16,6 +25,10 @@ class Tikategory(models.Model): name = models.CharField(max_length=50) landscape = models.BooleanField() subs = models.ManyToManyField(Tisub) + # For now we'll hardcode the following:xi + # [designation, ingredients, description, color, AB, designation_fontsize] + #protosubs = models.ManyToManyField(Tisub, related_name="protosubs") + prototempalte = models.FileField() def __str__(self): return self.name @@ -25,9 +38,19 @@ class Tikategory(models.Model): class Tikette(models.Model): + class AbVisibility(models.TextChoices): + VISIBLE = "inline" + INVISIBLE = "none" + title = models.CharField(max_length=100) category = models.ForeignKey(Tikategory, on_delete=models.CASCADE) - svg = models.FileField() + + designation = models.CharField(max_length=100) + ingredients = models.TextField() + description = models.TextField() + color = models.CharField(max_length=6) + ab = models.CharField(max_length=7, choices=AbVisibility) + # designation_fontsize is hardcoded to 42.6667 def __str__(self): return self.title @@ -35,4 +58,12 @@ class Tikette(models.Model): class Meta: verbose_name_plural = "tikettz" -# Create your models here. + +class Tizer(models.Model): + email = models.CharField() + + def __str__(self): + return self.email + + class Meta: + verbose_name_plural = "tizerz" diff --git a/backend/zetikettes/tikette/views.py b/backend/zetikettes/tikette/views.py index 9f974c3..1dd56bb 100644 --- a/backend/zetikettes/tikette/views.py +++ b/backend/zetikettes/tikette/views.py @@ -10,16 +10,26 @@ from .models import Tikette, Tikategory CORS={'access-control-allow-origin': '*'} -def index(request): + +def get_list(request): tikettes = [{ + 'id': x.id, 'title': x.title, 'category': x.category.name, - 'sticker': x.svg.name, 'landscape': x.category.landscape, 'subs': {x.name: x.default for x in x.category.subs.all()}, } for x in Tikette.objects.all()] return JsonResponse({'status': 'ok', 'tikettes': tikettes}, headers=CORS) + +def 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 def generate(request): if request.method != "POST": @@ -29,3 +39,26 @@ def generate(request): pdfpath = stickersheet.generate(payload, out_dir=settings.TIKETTE_OUT_DIR) 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) diff --git a/backend/zetikettes/zetikettes/urls.py b/backend/zetikettes/zetikettes/urls.py index ef57f02..4059fad 100644 --- a/backend/zetikettes/zetikettes/urls.py +++ b/backend/zetikettes/zetikettes/urls.py @@ -24,6 +24,9 @@ import tikette.views urlpatterns = [ path('admin/', admin.site.urls), - path('list', tikette.views.index), - path('', tikette.views.generate), + 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), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/docker-compose.yml b/compose.yml similarity index 79% rename from docker-compose.yml rename to compose.yml index 2805735..c2aa60e 100644 --- a/docker-compose.yml +++ b/compose.yml @@ -3,7 +3,8 @@ version: '3.1' services: zetikettes: - image: zetikettes + build: . + image: pol/zetikettes restart: always ports: - 127.0.0.1:8000:8000 diff --git a/docker-compose@.service b/docker-compose@.service new file mode 100644 index 0000000..85a90be --- /dev/null +++ b/docker-compose@.service @@ -0,0 +1,14 @@ +[Unit] +Description=%i service with docker compose +PartOf=docker.service +After=docker.service + +[Service] +Type=oneshot +RemainAfterExit=true +WorkingDirectory=/etc/docker/compose/%i +ExecStart=/usr/bin/docker-compose up -d --remove-orphans +ExecStop=/usr/bin/docker-compose down + +[Install] +WantedBy=multi-user.target diff --git a/frontend/config.js b/frontend/config.js index e17daec..3f5561e 100644 --- a/frontend/config.js +++ b/frontend/config.js @@ -1 +1 @@ -const backend_api = 'http://jenova.ponteilla.net:8001/' +const backend_api = 'http://localhost:8000/' diff --git a/frontend/index.html b/frontend/index.html index 1530b38..e6d8810 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,15 +3,19 @@ -