diff --git a/.gitignore b/.gitignore index ef6c23e..9300d48 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ node_modules/ .env .idea/ services/backend/db.sqlite3 -__pycache__/ \ No newline at end of file +__pycache__/ +htmlcov/ +media/ +.coverage \ No newline at end of file diff --git a/README.md b/README.md index 61f65e1..6d30a71 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,5 @@ ## Adding new backend packages -`docker compose run --rm backend uv add ` \ No newline at end of file +`docker compose run --rm backend uv add ` +`docker compose build` :) diff --git a/TODO b/TODO index 1e58ac0..19c5179 100644 --- a/TODO +++ b/TODO @@ -1,12 +1,12 @@ Nastylovat: - lazyload načítat o něco dříve (neli zrušit alespoň mimo galerii) +- mobilní verze -Naprogramovat: +Frontend: - přidat v galerii zvětsovač na obrázky Naplánovat: - kurzy + cena: vyskakovací okno na rezervaci s jménem lekce? jak to vyřešit s kalendářem? (stránka /rezervace) - -DAVID: +Backend: - přidat posílání mailu panu vrchnímu \ No newline at end of file diff --git a/services/backend/backend/settings.py b/services/backend/backend/settings.py index bfd011f..05b3445 100644 --- a/services/backend/backend/settings.py +++ b/services/backend/backend/settings.py @@ -28,7 +28,11 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = env.str('SECRET_KEY', 'django-insecure-x&7qy$na7*@u_4(izkfaz2yiea9+t(nf&#p9cnlq6x)_)jkacf') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False +DEBUG_TOOLBAR_CONFIG = { + "SHOW_TOOLBAR_CALLBACK": lambda request: False, + "IS_RUNNING_TESTS": False, +} DEBUG_TOOLBAR_PANELS = [ 'debug_toolbar.panels.history.HistoryPanel', @@ -72,6 +76,7 @@ INSTALLED_APPS = [ 'tko.apps.TkoConfig', 'debug_toolbar', 'rest_framework', + 'post_office', ] MIDDLEWARE = [ @@ -115,6 +120,7 @@ DATABASES = { 'default': env.db('DATABASE_URL') } +EMAIL_BACKEND = 'post_office.EmailBackend' # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators @@ -152,9 +158,9 @@ USE_TZ = True STATIC_URL = '/static/' -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static') -] +# STATICFILES_DIRS = [ +# os.path.join(BASE_DIR, 'static') +# ] MEDIA_URL = '/media/' diff --git a/services/backend/pyproject.toml b/services/backend/pyproject.toml index b50f327..0bdf4b3 100644 --- a/services/backend/pyproject.toml +++ b/services/backend/pyproject.toml @@ -4,7 +4,8 @@ version = "0.1.0" description = "Add your description here" requires-python = ">=3.12" authors = [ - {name = "Jakub Kropáček", email = "kropikuba@gmail.com"} + {name = "Jakub Kropáček", email = "kropikuba@gmail.com"}, + {name = "Nikola Kubeczková", email = "kubeczkova.n@gmail.com"} ] dependencies = [ "django>=5.1.5", @@ -15,6 +16,10 @@ dependencies = [ "django-environ>=0.12.0", "wait-for-it>=2.3.0", "django-cors-headers>=4.7.0", + "django-post-office>=3.9.1", + "pytest-django>=4.10.0", + "factory-boy>=3.3.3", + "pytest-cov>=6.0.0", ] [tool.uv] @@ -23,3 +28,6 @@ package = false [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "backend.settings" diff --git a/services/backend/tko/tests.py b/services/backend/tko/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/services/backend/tko/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/services/backend/tko/tests/__init__.py b/services/backend/tko/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/tko/tests/factories.py b/services/backend/tko/tests/factories.py new file mode 100644 index 0000000..9f43912 --- /dev/null +++ b/services/backend/tko/tests/factories.py @@ -0,0 +1,62 @@ +from random import randint + +import factory +import datetime + +from django.contrib.auth.models import User +from factory.fuzzy import FuzzyDate +from datetime import date + +from tko.models import Article, Event, ArticleImage, Contact + + +class ContactFactory(factory.django.DjangoModelFactory): + class Meta: + model = Contact + + name = factory.Faker("name") + email = factory.Faker("email") + phone_number = factory.Faker("phone_number") + content = factory.Faker("text") + + +class ArticleFactory(factory.django.DjangoModelFactory): + class Meta: + model = Article + + title = factory.Faker("name") + content = factory.Faker("text") + date = FuzzyDate(datetime.date(2021, 1, 1), date.today()) + author = factory.Faker("name") + active_to = FuzzyDate(datetime.date(2021, 1, 1), datetime.date(2025, 1, 1)) + + +class GalleryFactory(factory.django.DjangoModelFactory): + class Meta: + model = ArticleImage + + article = factory.SubFactory(ArticleFactory) + image = factory.django.FileField(filename='image.png') + main = factory.Faker("boolean") + + +class EventFactory(factory.django.DjangoModelFactory): + class Meta: + model = Event + + title = factory.Faker("name") + start_date = FuzzyDate(datetime.date(2021, 1, 1), datetime.date(2025, 1, 1)) + end_date = factory.LazyAttribute( + lambda obj: obj.start_date + datetime.timedelta(minutes=randint(5, 5 * 24 * 60)) + ) + color = factory.Faker("color") + + +class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = User + + username = factory.Faker("name") + email = factory.Faker("email") + password = "password" + is_superuser = True diff --git a/services/backend/tko/tests/test_admin.py b/services/backend/tko/tests/test_admin.py new file mode 100644 index 0000000..ec7254b --- /dev/null +++ b/services/backend/tko/tests/test_admin.py @@ -0,0 +1,37 @@ +import pytest +from rest_framework.test import APIClient +from django.urls import reverse + +from tko.admin import ContactAdmin +from tko.models import Contact +from tko.tests.factories import UserFactory + +from django.contrib.admin.sites import site + + +# Create your tests here. +@pytest.mark.django_db(transaction=True) +class TestAdmin: + url = reverse("admin:index") + client = APIClient() + + def test_has_add_permission(self): + user = UserFactory() + admin = ContactAdmin(Contact, site) + request = self.client.get(self.url) + request.user = user + assert admin.has_add_permission(request) is False + + def test_has_delete_permission(self): + user = UserFactory() + admin = ContactAdmin(Contact, site) + request = self.client.get(self.url) + request.user = user + assert admin.has_delete_permission(request) is False + + def test_has_change_permission(self): + user = UserFactory() + admin = ContactAdmin(Contact, site) + request = self.client.get(self.url) + request.user = user + assert admin.has_change_permission(request) is False \ No newline at end of file diff --git a/services/backend/tko/tests/test_article.py b/services/backend/tko/tests/test_article.py new file mode 100644 index 0000000..e42378a --- /dev/null +++ b/services/backend/tko/tests/test_article.py @@ -0,0 +1,53 @@ +import pytest +from rest_framework.test import APIClient +from django.urls import reverse + +from tko.tests.factories import ArticleFactory + + +@pytest.mark.django_db(transaction=True) +class TestLoadArticle: + client = APIClient() + + @staticmethod + def create_article(old, new): + for _ in range(new): ArticleFactory(active_to=None) + for _ in range(old): ArticleFactory() + + @pytest.mark.parametrize( + ("articles", "length", "result"), + [ + ((2, 5), 2, 200), + ((2, 1), 1, 200), + ((5, 0), 0, 200), + ((3, 1), 1, 200), + ] + ) + def test_load_articles(self, articles, length, result): + self.create_article(*articles) + response = self.client.get(reverse("load-articles")) + assert response.status_code == result + assert len(response.data) == length + + + @pytest.mark.parametrize( + ("articles", "length", "result"), + [ + ((2, 5), 5, 200), + ((2, 1), 1, 200), + ((5, 0), 0, 200), + ((3, 1), 1, 200), + ] + ) + def test_load_all_articles(self, articles, length, result): + self.create_article(*articles) + response = self.client.get( + path=reverse("load-all-articles") + ) + assert response.status_code == result + assert len(response.data) == length + + + def test_str_article(self): + title = "Test_article" + assert str(ArticleFactory(title=title)) == title \ No newline at end of file diff --git a/services/backend/tko/tests/test_contact.py b/services/backend/tko/tests/test_contact.py new file mode 100644 index 0000000..8e29e35 --- /dev/null +++ b/services/backend/tko/tests/test_contact.py @@ -0,0 +1,40 @@ +import pytest +from rest_framework.test import APIClient +from django.urls import reverse + +from tko.tests.factories import ContactFactory + + +# Create your tests here. +@pytest.mark.django_db(transaction=True) +class TestContact: + url = reverse("create-contact") + client = APIClient() + + @pytest.mark.parametrize( + ("name", "email", "phone", "message", "result"), + [ + ("name", "email@tko.cz", "770707505", "message", 200), + ("name", "email@tko.cz", "", "message", 400), + ("name", "", "770707505", "message", 400), + ("name", "", "", "message", 400), + ("name ahoj", "email@tko.cz", "+420770707505", "message skdo nsdkl skd sakdksd", 200), + ("", "email", "770707505", "message", 400), + ]) + def test_create_contact(self, name, email, phone, message, result): + response = self.client.post( + path=self.url, + data={ + "name": name, + "email": email, + "phone_number": phone, + "content": message, + } + ) + assert response.status_code == result + if result == 200: + assert str(ContactFactory( + name=name, + email=email, + phone_number=phone, + )) == f"{name}, {email}, {phone}" \ No newline at end of file diff --git a/services/backend/tko/tests/test_event.py b/services/backend/tko/tests/test_event.py new file mode 100644 index 0000000..158a444 --- /dev/null +++ b/services/backend/tko/tests/test_event.py @@ -0,0 +1,36 @@ +import pytest +from rest_framework.test import APIClient +from django.urls import reverse + +from tko.tests.factories import EventFactory + + +# Create your tests here. +@pytest.mark.django_db(transaction=True) +class TestEvent: + url = reverse("load-events") + client = APIClient() + + @staticmethod + def create_events(length): + EventFactory.create_batch(size=length) + + @pytest.mark.parametrize( + ("length", "result"), + [ + (2, 200), + (6, 200), + ] + ) + def test_load_events(self, length, result): + self.create_events(length) + response = self.client.get(self.url) + assert response.status_code == result + assert len(response.data) == length + + + def test_str_article_image(self): + title = "Test_event" + assert str( + EventFactory(title=title) + ) == title diff --git a/services/backend/tko/tests/test_gallery.py b/services/backend/tko/tests/test_gallery.py new file mode 100644 index 0000000..7a13395 --- /dev/null +++ b/services/backend/tko/tests/test_gallery.py @@ -0,0 +1,65 @@ +import datetime + +import pytest +from rest_framework.test import APIClient +from django.urls import reverse + +from tko.tests.factories import GalleryFactory, ArticleFactory + + +# Create your tests here. +@pytest.mark.django_db(transaction=True) +class TestGallery: + url = reverse("load-gallery") + client = APIClient() + + + @staticmethod + def create_images(old, new): + for _ in range(new): + article = ArticleFactory(active_to=None) + for _ in range(new): GalleryFactory(article=article) + for _ in range(old): + article = ArticleFactory() + for _ in range(old): GalleryFactory(article=article) + + + @pytest.mark.parametrize( + ("gallery", "length", "result"), + [ + ((2, 5), 29, 200), + ((2, 1), 5, 200), + ((4, 2), 20, 200), + ((3, 1), 10, 200), + ] + ) + def test_load_gallery(self, gallery, length, result): + self.create_images(*gallery) + response = self.client.get(self.url) + assert response.status_code == result + assert len(response.data) == length + + + @pytest.mark.parametrize( + ("gallery", "length", "result"), + [ + ((2, 5), 5, 200), + ((2, 1), 1, 200), + ((4, 0), 0, 200), + ((3, 1), 1, 200), + ] + ) + def test_load_article_images(self, gallery, length, result): + old, new = gallery + self.create_images(*gallery) + response = self.client.get(reverse("load-articles")) + for article in response.data: + assert len(article["images"]) == new + assert response.status_code == result + + + def test_str_article_image(self): + title = "Test_article" + assert str( + GalleryFactory(article=ArticleFactory(title=title)) + ) == f"Image for {title}, {datetime.date.today()}" diff --git a/services/backend/tko/views.py b/services/backend/tko/views.py index 4c9eb1f..0c1e33e 100644 --- a/services/backend/tko/views.py +++ b/services/backend/tko/views.py @@ -1,8 +1,10 @@ from django.utils import timezone from django.db.models import Q +from post_office import mail + from rest_framework.generics import ListAPIView, CreateAPIView -from rest_framework import permissions +from rest_framework.response import Response from tko.models import Article, Event, ArticleImage from tko.serializers import ArticleListSerializer, EventListSerializer, ContactSerializer, ArticleImageSerializer @@ -10,12 +12,26 @@ from tko.serializers import ArticleListSerializer, EventListSerializer, ContactS class ContactView(CreateAPIView): serializer_class = ContactSerializer - permission_classes = [permissions.AllowAny] + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + mail.send( + recipients="kubeczkova.n@gmail.com", + subject="Zpráva z webu TKO", + message=f"Jméno: {serializer.data['name']}\n" + f"Email: {serializer.data['email']}\n" + f"Telefón: {serializer.data['phone_number']}\n\n" + f"Zpráva: {serializer.data['content']}", + ) + + return Response(serializer.data) class NewArticleListView(ListAPIView): serializer_class = ArticleListSerializer - permission_classes = [permissions.AllowAny] def get_queryset(self): return Article.objects.filter( @@ -25,7 +41,6 @@ class NewArticleListView(ListAPIView): class AllArticleListView(ListAPIView): serializer_class = ArticleListSerializer - permission_classes = [permissions.AllowAny] def get_queryset(self): return Article.objects.filter( @@ -36,10 +51,8 @@ class AllArticleListView(ListAPIView): class GalleryView(ListAPIView): queryset = ArticleImage.objects.all().order_by("-article_id", "main") serializer_class = ArticleImageSerializer - permission_classes = [permissions.AllowAny] class EventListView(ListAPIView): queryset = Event.objects.all() serializer_class = EventListSerializer - permission_classes = [permissions.AllowAny] diff --git a/services/backend/uv.lock b/services/backend/uv.lock index 2f1ccc1..4aca3ff 100644 --- a/services/backend/uv.lock +++ b/services/backend/uv.lock @@ -34,8 +34,12 @@ dependencies = [ { name = "django-cors-headers" }, { name = "django-debug-toolbar" }, { name = "django-environ" }, + { name = "django-post-office" }, { name = "djangorestframework" }, + { name = "factory-boy" }, { name = "psycopg2-binary" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, { name = "uvicorn", extra = ["standard"] }, { name = "wait-for-it" }, ] @@ -46,12 +50,33 @@ requires-dist = [ { name = "django-cors-headers", specifier = ">=4.7.0" }, { name = "django-debug-toolbar", specifier = ">=5.0.1" }, { name = "django-environ", specifier = ">=0.12.0" }, + { name = "django-post-office", specifier = ">=3.9.1" }, { name = "djangorestframework", specifier = ">=3.15.2" }, + { name = "factory-boy", specifier = ">=3.3.3" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-django", specifier = ">=4.10.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" }, { name = "wait-for-it", specifier = ">=2.3.0" }, ] +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + [[package]] name = "click" version = "8.1.8" @@ -73,6 +98,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "coverage" +version = "7.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/bf/3effb7453498de9c14a81ca21e1f92e6723ce7ebdc5402ae30e4dcc490ac/coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393", size = 810332 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/b0/4eaba302a86ec3528231d7cfc954ae1929ec5d42b032eb6f5b5f5a9155d2/coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d", size = 211253 }, + { url = "https://files.pythonhosted.org/packages/fd/68/21b973e6780a3f2457e31ede1aca6c2f84bda4359457b40da3ae805dcf30/coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f", size = 211504 }, + { url = "https://files.pythonhosted.org/packages/d1/b4/c19e9c565407664390254252496292f1e3076c31c5c01701ffacc060e745/coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1", size = 245566 }, + { url = "https://files.pythonhosted.org/packages/7b/0e/f9829cdd25e5083638559c8c267ff0577c6bab19dacb1a4fcfc1e70e41c0/coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0", size = 242455 }, + { url = "https://files.pythonhosted.org/packages/29/57/a3ada2e50a665bf6d9851b5eb3a9a07d7e38f970bdd4d39895f311331d56/coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2", size = 244713 }, + { url = "https://files.pythonhosted.org/packages/0f/d3/f15c7d45682a73eca0611427896016bad4c8f635b0fc13aae13a01f8ed9d/coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af", size = 244476 }, + { url = "https://files.pythonhosted.org/packages/19/3b/64540074e256082b220e8810fd72543eff03286c59dc91976281dc0a559c/coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59", size = 242695 }, + { url = "https://files.pythonhosted.org/packages/8a/c1/9cad25372ead7f9395a91bb42d8ae63e6cefe7408eb79fd38797e2b763eb/coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2", size = 243888 }, + { url = "https://files.pythonhosted.org/packages/66/c6/c3e6c895bc5b95ccfe4cb5838669dbe5226ee4ad10604c46b778c304d6f9/coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8", size = 213744 }, + { url = "https://files.pythonhosted.org/packages/cc/8a/6df2fcb4c3e38ec6cd7e211ca8391405ada4e3b1295695d00aa07c6ee736/coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04", size = 214546 }, + { url = "https://files.pythonhosted.org/packages/ec/2a/1a254eaadb01c163b29d6ce742aa380fc5cfe74a82138ce6eb944c42effa/coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585", size = 211277 }, + { url = "https://files.pythonhosted.org/packages/cf/00/9636028365efd4eb6db71cdd01d99e59f25cf0d47a59943dbee32dd1573b/coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c", size = 211551 }, + { url = "https://files.pythonhosted.org/packages/6f/c8/14aed97f80363f055b6cd91e62986492d9fe3b55e06b4b5c82627ae18744/coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019", size = 245068 }, + { url = "https://files.pythonhosted.org/packages/d6/76/9c5fe3f900e01d7995b0cda08fc8bf9773b4b1be58bdd626f319c7d4ec11/coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf", size = 242109 }, + { url = "https://files.pythonhosted.org/packages/c0/81/760993bb536fb674d3a059f718145dcd409ed6d00ae4e3cbf380019fdfd0/coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b", size = 244129 }, + { url = "https://files.pythonhosted.org/packages/00/be/1114a19f93eae0b6cd955dabb5bee80397bd420d846e63cd0ebffc134e3d/coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f", size = 244201 }, + { url = "https://files.pythonhosted.org/packages/06/8d/9128fd283c660474c7dc2b1ea5c66761bc776b970c1724989ed70e9d6eee/coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777", size = 242282 }, + { url = "https://files.pythonhosted.org/packages/d4/2a/6d7dbfe9c1f82e2cdc28d48f4a0c93190cf58f057fa91ba2391b92437fe6/coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a", size = 243570 }, + { url = "https://files.pythonhosted.org/packages/cf/3e/29f1e4ce3bb951bcf74b2037a82d94c5064b3334304a3809a95805628838/coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a", size = 213772 }, + { url = "https://files.pythonhosted.org/packages/bc/3a/cf029bf34aefd22ad34f0e808eba8d5830f297a1acb483a2124f097ff769/coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1", size = 214575 }, + { url = "https://files.pythonhosted.org/packages/92/4c/fb8b35f186a2519126209dce91ab8644c9a901cf04f8dfa65576ca2dd9e8/coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511", size = 212113 }, + { url = "https://files.pythonhosted.org/packages/59/90/e834ffc86fd811c5b570a64ee1895b20404a247ec18a896b9ba543b12097/coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24", size = 212333 }, + { url = "https://files.pythonhosted.org/packages/a5/a1/27f0ad39569b3b02410b881c42e58ab403df13fcd465b475db514b83d3d3/coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950", size = 256566 }, + { url = "https://files.pythonhosted.org/packages/9f/3b/21fa66a1db1b90a0633e771a32754f7c02d60236a251afb1b86d7e15d83a/coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d", size = 252276 }, + { url = "https://files.pythonhosted.org/packages/d6/e5/4ab83a59b0f8ac4f0029018559fc4c7d042e1b4552a722e2bfb04f652296/coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e", size = 254616 }, + { url = "https://files.pythonhosted.org/packages/db/7a/4224417c0ccdb16a5ba4d8d1fcfaa18439be1624c29435bb9bc88ccabdfb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862", size = 255707 }, + { url = "https://files.pythonhosted.org/packages/51/20/ff18a329ccaa3d035e2134ecf3a2e92a52d3be6704c76e74ca5589ece260/coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271", size = 253876 }, + { url = "https://files.pythonhosted.org/packages/e4/e8/1d6f1a6651672c64f45ffad05306dad9c4c189bec694270822508049b2cb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de", size = 254687 }, + { url = "https://files.pythonhosted.org/packages/6b/ea/1b9a14cf3e2bc3fd9de23a336a8082091711c5f480b500782d59e84a8fe5/coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c", size = 214486 }, + { url = "https://files.pythonhosted.org/packages/cc/bb/faa6bcf769cb7b3b660532a30d77c440289b40636c7f80e498b961295d07/coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c", size = 215647 }, + { url = "https://files.pythonhosted.org/packages/52/26/9f53293ff4cc1d47d98367ce045ca2e62746d6be74a5c6851a474eabf59b/coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31", size = 203006 }, +] + [[package]] name = "django" version = "5.1.5" @@ -122,6 +186,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957 }, ] +[[package]] +name = "django-post-office" +version = "3.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bleach", extra = ["css"] }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/e9/7369fb453269ded8d2fff58299fe653033702bd15a427bf35353d4606df3/django-post_office-3.9.1.tar.gz", hash = "sha256:3d8f502f7829e4cf83e830d9efd6909bb44690af6bc41c7e4fc5a85d7b04df10", size = 76624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/12/19fdf17fb1a0d5e4418798cf9ed66b8aee39da564e875ec025627e08abdf/django_post_office-3.9.1-py3-none-any.whl", hash = "sha256:0082c4dd17854f66077ef457cf868c9bc5b6247de99d5755c45ddea1c518ed6f", size = 85671 }, +] + [[package]] name = "djangorestframework" version = "3.15.2" @@ -134,6 +211,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/b6/fa99d8f05eff3a9310286ae84c4059b08c301ae4ab33ae32e46e8ef76491/djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", size = 1071235 }, ] +[[package]] +name = "factory-boy" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/98/75cacae9945f67cfe323829fc2ac451f64517a8a330b572a06a323997065/factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03", size = 164146 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/2bc5f5546ff2ccb3f7de06742853483ab75bf74f36a92254702f8baecc79/factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc", size = 37036 }, +] + +[[package]] +name = "faker" +version = "37.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/a6/b77f42021308ec8b134502343da882c0905d725a4d661c7adeaf7acaf515/faker-37.1.0.tar.gz", hash = "sha256:ad9dc66a3b84888b837ca729e85299a96b58fdaef0323ed0baace93c9614af06", size = 1875707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a1/8936bc8e79af80ca38288dd93ed44ed1f9d63beb25447a4c59e746e01f8d/faker-37.1.0-py3-none-any.whl", hash = "sha256:dc2f730be71cb770e9c715b13374d80dbcee879675121ab51f9683d262ae9a1c", size = 1918783 }, +] + [[package]] name = "h11" version = "0.14.0" @@ -174,6 +275,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + [[package]] name = "psycopg2-binary" version = "2.9.10" @@ -205,6 +333,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, ] +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-django" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/10/a096573b4b896f18a8390d9dafaffc054c1f613c60bf838300732e538890/pytest_django-4.10.0.tar.gz", hash = "sha256:1091b20ea1491fd04a310fc9aaff4c01b4e8450e3b157687625e16a6b5f3a366", size = 84710 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/4c/a4fe18205926216e1aebe1f125cba5bce444f91b6e4de4f49fa87e322775/pytest_django-4.10.0-py3-none-any.whl", hash = "sha256:57c74ef3aa9d89cae5a5d73fbb69a720a62673ade7ff13b9491872409a3f5918", size = 23975 }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -258,6 +426,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, ] +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -368,6 +548,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 }, ] +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, +] + [[package]] name = "websockets" version = "14.2" diff --git a/services/frontend/components/menu/Header.vue b/services/frontend/components/menu/Header.vue index de90477..b48a3fa 100644 --- a/services/frontend/components/menu/Header.vue +++ b/services/frontend/components/menu/Header.vue @@ -54,18 +54,30 @@