diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 8214388..b7fecff 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,15 +1,12 @@ FROM python:3.11 -ENV PYTHONUNBUFFERED=1 \ - POETRY_VIRTUALENVS_CREATE=false +ENV PYTHONUNBUFFERED=1 WORKDIR /app RUN pip install poetry -COPY ../poetry.lock /app -COPY ../pyproject.toml /app +COPY . . -RUN poetry install --no-root +RUN poetry install -COPY . . EXPOSE 8000 -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] +CMD ["poetry", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/food_helper/settings.py b/food_helper/settings.py index bf6682f..cf27df6 100644 --- a/food_helper/settings.py +++ b/food_helper/settings.py @@ -11,6 +11,7 @@ """ import os +from datetime import timedelta from pathlib import Path from .env import env @@ -30,7 +31,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env("DEBUG") -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = [] # if running project from docker add "0.0.0.0" INSTALLED_APPS = [ @@ -91,26 +92,26 @@ # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases -if env("ENVIRONMENT") == "ci": - DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": "cidb", - } - } -else: - DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": env("DB_NAME"), - "USER": env("DB_USER"), - "PASSWORD": env("DB_PASSWORD"), - "HOST": env("DB_HOST"), # localhost db - # "HOST": "db", docker db - "PORT": env("DB_PORT"), - } + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env("DB_NAME"), + "USER": env("DB_USER"), + "PASSWORD": env("DB_PASSWORD"), + "HOST": env("DB_HOST"), # localhost db + # "HOST": "db", # docker db + "PORT": env("DB_PORT"), } +} +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), + "DEFAULT_PARSER_CLASSES": ("rest_framework.parsers.JSONParser",), + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework_simplejwt.authentication.JWTAuthentication" + ], +} # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators @@ -158,3 +159,10 @@ LOGIN_REDIRECT_URL = "home-page" LOGIN_URL = "login-page" + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=120), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, +} diff --git a/food_helper/urls.py b/food_helper/urls.py index f09e35e..2f395aa 100644 --- a/food_helper/urls.py +++ b/food_helper/urls.py @@ -1,19 +1,3 @@ -""" -URL configuration for food_helper project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -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.contrib import admin from django.urls import include, path @@ -24,4 +8,5 @@ path("", include("products.urls")), path("", include("user_ingredients.urls")), path("", include("recipe_ingredients.urls")), + path("", include("ingredients.urls")), ] diff --git a/ingredients/api/__init__.py b/ingredients/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ingredients/api/serializers.py b/ingredients/api/serializers.py new file mode 100644 index 0000000..0196185 --- /dev/null +++ b/ingredients/api/serializers.py @@ -0,0 +1,17 @@ +from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator + +from ..models import Ingredient + + +class IngredientSerializer(serializers.ModelSerializer): + class Meta: + model = Ingredient + fields = "__all__" + validators = [ + UniqueTogetherValidator( + queryset=Ingredient.objects.all(), + fields=("product", "quantity_type"), + message="Ingredient exists - use get method with parameters", + ) + ] diff --git a/ingredients/api/views.py b/ingredients/api/views.py new file mode 100644 index 0000000..ac847a8 --- /dev/null +++ b/ingredients/api/views.py @@ -0,0 +1,11 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from ingredients.api.serializers import IngredientSerializer +from ingredients.models import Ingredient + + +class IngredientViewSet(viewsets.ModelViewSet): + serializer_class = IngredientSerializer + queryset = Ingredient.objects.all() + permission_classes = [IsAuthenticated] diff --git a/ingredients/migrations/0001_initial.py b/ingredients/migrations/0001_initial.py index 4e84aca..a582c55 100644 --- a/ingredients/migrations/0001_initial.py +++ b/ingredients/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-01-11 21:33 +# Generated by Django 4.2.5 on 2024-06-24 16:24 import django.db.models.deletion from django.db import migrations, models @@ -46,5 +46,8 @@ class Migration(migrations.Migration): ), ), ], + options={ + "unique_together": {("product", "quantity_type")}, + }, ), ] diff --git a/ingredients/models.py b/ingredients/models.py index 683766a..93851fa 100644 --- a/ingredients/models.py +++ b/ingredients/models.py @@ -18,5 +18,8 @@ class Ingredient(models.Model): choices=AMOUNT_TYPE_CHOICES, ) + class Meta: + unique_together = ["product", "quantity_type"] + def __str__(self): return f"{self.product.name}, {self.quantity_type}" diff --git a/ingredients/urls.py b/ingredients/urls.py new file mode 100644 index 0000000..67ab027 --- /dev/null +++ b/ingredients/urls.py @@ -0,0 +1,9 @@ +from django.urls import include, path +from rest_framework.routers import SimpleRouter + +from .api.views import IngredientViewSet + +router = SimpleRouter() +router.register("ingredients", IngredientViewSet) + +urlpatterns = [path("api/", include(router.urls))] diff --git a/poetry.lock b/poetry.lock index 567244c..5404206 100644 --- a/poetry.lock +++ b/poetry.lock @@ -384,6 +384,37 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coreapi" +version = "2.3.3" +description = "Python client library for Core API." +optional = false +python-versions = "*" +files = [ + {file = "coreapi-2.3.3-py2.py3-none-any.whl", hash = "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3"}, + {file = "coreapi-2.3.3.tar.gz", hash = "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb"}, +] + +[package.dependencies] +coreschema = "*" +itypes = "*" +requests = "*" +uritemplate = "*" + +[[package]] +name = "coreschema" +version = "0.0.4" +description = "Core Schema." +optional = false +python-versions = "*" +files = [ + {file = "coreschema-0.0.4-py2-none-any.whl", hash = "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f"}, + {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"}, +] + +[package.dependencies] +jinja2 = "*" + [[package]] name = "coverage" version = "7.3.2" @@ -618,6 +649,20 @@ files = [ [package.dependencies] Django = ">=3.2" +[[package]] +name = "django-filter" +version = "24.2" +description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, + {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, +] + +[package.dependencies] +Django = ">=4.2" + [[package]] name = "django-rest-framework" version = "0.1.0" @@ -646,6 +691,30 @@ files = [ django = ">=3.0" pytz = "*" +[[package]] +name = "djangorestframework-simplejwt" +version = "5.3.1" +description = "A minimal JSON Web Token authentication plugin for Django REST Framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "djangorestframework_simplejwt-5.3.1-py3-none-any.whl", hash = "sha256:381bc966aa46913905629d472cd72ad45faa265509764e20ffd440164c88d220"}, + {file = "djangorestframework_simplejwt-5.3.1.tar.gz", hash = "sha256:6c4bd37537440bc439564ebf7d6085e74c5411485197073f508ebdfa34bc9fae"}, +] + +[package.dependencies] +django = ">=3.2" +djangorestframework = ">=3.12" +pyjwt = ">=1.7.1,<3" + +[package.extras] +crypto = ["cryptography (>=3.3.1)"] +dev = ["Sphinx (>=1.6.5,<2)", "cryptography", "flake8", "freezegun", "ipython", "isort", "pep8", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel"] +doc = ["Sphinx (>=1.6.5,<2)", "sphinx_rtd_theme (>=0.1.9)"] +lint = ["flake8", "isort", "pep8"] +python-jose = ["python-jose (==3.3.0)"] +test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] + [[package]] name = "dparse" version = "0.6.4b0" @@ -945,6 +1014,17 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib" plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] +[[package]] +name = "itypes" +version = "1.2.0" +description = "Simple immutable types for python." +optional = false +python-versions = "*" +files = [ + {file = "itypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6"}, + {file = "itypes-1.2.0.tar.gz", hash = "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1"}, +] + [[package]] name = "jaraco-classes" version = "3.3.1" @@ -1242,6 +1322,19 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "openapi-codec" +version = "1.3.2" +description = "An OpenAPI codec for Core API." +optional = false +python-versions = "*" +files = [ + {file = "openapi-codec-1.3.2.tar.gz", hash = "sha256:1bce63289edf53c601ea3683120641407ff6b708803b8954c8a876fe778d2145"}, +] + +[package.dependencies] +coreapi = ">=2.2.0" + [[package]] name = "packaging" version = "23.2" @@ -1519,6 +1612,23 @@ files = [ [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pyparsing" version = "3.1.1" @@ -2015,6 +2125,113 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "simplejson" +version = "3.19.2" +description = "Simple, fast, extensible JSON encoder/decoder for Python" +optional = false +python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "simplejson-3.19.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3471e95110dcaf901db16063b2e40fb394f8a9e99b3fe9ee3acc6f6ef72183a2"}, + {file = "simplejson-3.19.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3194cd0d2c959062b94094c0a9f8780ffd38417a5322450a0db0ca1a23e7fbd2"}, + {file = "simplejson-3.19.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:8a390e56a7963e3946ff2049ee1eb218380e87c8a0e7608f7f8790ba19390867"}, + {file = "simplejson-3.19.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1537b3dd62d8aae644f3518c407aa8469e3fd0f179cdf86c5992792713ed717a"}, + {file = "simplejson-3.19.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a8617625369d2d03766413bff9e64310feafc9fc4f0ad2b902136f1a5cd8c6b0"}, + {file = "simplejson-3.19.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:2c433a412e96afb9a3ce36fa96c8e61a757af53e9c9192c97392f72871e18e69"}, + {file = "simplejson-3.19.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f1c70249b15e4ce1a7d5340c97670a95f305ca79f376887759b43bb33288c973"}, + {file = "simplejson-3.19.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:287e39ba24e141b046812c880f4619d0ca9e617235d74abc27267194fc0c7835"}, + {file = "simplejson-3.19.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6f0a0b41dd05eefab547576bed0cf066595f3b20b083956b1405a6f17d1be6ad"}, + {file = "simplejson-3.19.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f98d918f7f3aaf4b91f2b08c0c92b1774aea113334f7cde4fe40e777114dbe6"}, + {file = "simplejson-3.19.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d74beca677623481810c7052926365d5f07393c72cbf62d6cce29991b676402"}, + {file = "simplejson-3.19.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7f2398361508c560d0bf1773af19e9fe644e218f2a814a02210ac2c97ad70db0"}, + {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ad331349b0b9ca6da86064a3599c425c7a21cd41616e175ddba0866da32df48"}, + {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:332c848f02d71a649272b3f1feccacb7e4f7e6de4a2e6dc70a32645326f3d428"}, + {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25785d038281cd106c0d91a68b9930049b6464288cea59ba95b35ee37c2d23a5"}, + {file = "simplejson-3.19.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18955c1da6fc39d957adfa346f75226246b6569e096ac9e40f67d102278c3bcb"}, + {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:11cc3afd8160d44582543838b7e4f9aa5e97865322844b75d51bf4e0e413bb3e"}, + {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b01fda3e95d07a6148702a641e5e293b6da7863f8bc9b967f62db9461330562c"}, + {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:778331444917108fa8441f59af45886270d33ce8a23bfc4f9b192c0b2ecef1b3"}, + {file = "simplejson-3.19.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9eb117db8d7ed733a7317c4215c35993b815bf6aeab67523f1f11e108c040672"}, + {file = "simplejson-3.19.2-cp310-cp310-win32.whl", hash = "sha256:39b6d79f5cbfa3eb63a869639cfacf7c41d753c64f7801efc72692c1b2637ac7"}, + {file = "simplejson-3.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:5675e9d8eeef0aa06093c1ff898413ade042d73dc920a03e8cea2fb68f62445a"}, + {file = "simplejson-3.19.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed628c1431100b0b65387419551e822987396bee3c088a15d68446d92f554e0c"}, + {file = "simplejson-3.19.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:adcb3332979cbc941b8fff07181f06d2b608625edc0a4d8bc3ffc0be414ad0c4"}, + {file = "simplejson-3.19.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:08889f2f597ae965284d7b52a5c3928653a9406d88c93e3161180f0abc2433ba"}, + {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7938a78447174e2616be223f496ddccdbf7854f7bf2ce716dbccd958cc7d13"}, + {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a970a2e6d5281d56cacf3dc82081c95c1f4da5a559e52469287457811db6a79b"}, + {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554313db34d63eac3b3f42986aa9efddd1a481169c12b7be1e7512edebff8eaf"}, + {file = "simplejson-3.19.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d36081c0b1c12ea0ed62c202046dca11438bee48dd5240b7c8de8da62c620e9"}, + {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a3cd18e03b0ee54ea4319cdcce48357719ea487b53f92a469ba8ca8e39df285e"}, + {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66e5dc13bfb17cd6ee764fc96ccafd6e405daa846a42baab81f4c60e15650414"}, + {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:972a7833d4a1fcf7a711c939e315721a88b988553fc770a5b6a5a64bd6ebeba3"}, + {file = "simplejson-3.19.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3e74355cb47e0cd399ead3477e29e2f50e1540952c22fb3504dda0184fc9819f"}, + {file = "simplejson-3.19.2-cp311-cp311-win32.whl", hash = "sha256:1dd4f692304854352c3e396e9b5f0a9c9e666868dd0bdc784e2ac4c93092d87b"}, + {file = "simplejson-3.19.2-cp311-cp311-win_amd64.whl", hash = "sha256:9300aee2a8b5992d0f4293d88deb59c218989833e3396c824b69ba330d04a589"}, + {file = "simplejson-3.19.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b8d940fd28eb34a7084877747a60873956893e377f15a32ad445fe66c972c3b8"}, + {file = "simplejson-3.19.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4969d974d9db826a2c07671273e6b27bc48e940738d768fa8f33b577f0978378"}, + {file = "simplejson-3.19.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c594642d6b13d225e10df5c16ee15b3398e21a35ecd6aee824f107a625690374"}, + {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f5a398b5e77bb01b23d92872255e1bcb3c0c719a3be40b8df146570fe7781a"}, + {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176a1b524a3bd3314ed47029a86d02d5a95cc0bee15bd3063a1e1ec62b947de6"}, + {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3c7363a8cb8c5238878ec96c5eb0fc5ca2cb11fc0c7d2379863d342c6ee367a"}, + {file = "simplejson-3.19.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:346820ae96aa90c7d52653539a57766f10f33dd4be609206c001432b59ddf89f"}, + {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de9a2792612ec6def556d1dc621fd6b2073aff015d64fba9f3e53349ad292734"}, + {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1c768e7584c45094dca4b334af361e43b0aaa4844c04945ac7d43379eeda9bc2"}, + {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:9652e59c022e62a5b58a6f9948b104e5bb96d3b06940c6482588176f40f4914b"}, + {file = "simplejson-3.19.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9c1a4393242e321e344213a90a1e3bf35d2f624aa8b8f6174d43e3c6b0e8f6eb"}, + {file = "simplejson-3.19.2-cp312-cp312-win32.whl", hash = "sha256:7cb98be113911cb0ad09e5523d0e2a926c09a465c9abb0784c9269efe4f95917"}, + {file = "simplejson-3.19.2-cp312-cp312-win_amd64.whl", hash = "sha256:6779105d2fcb7fcf794a6a2a233787f6bbd4731227333a072d8513b252ed374f"}, + {file = "simplejson-3.19.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:061e81ea2d62671fa9dea2c2bfbc1eec2617ae7651e366c7b4a2baf0a8c72cae"}, + {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4280e460e51f86ad76dc456acdbfa9513bdf329556ffc8c49e0200878ca57816"}, + {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11c39fbc4280d7420684494373b7c5904fa72a2b48ef543a56c2d412999c9e5d"}, + {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bccb3e88ec26ffa90f72229f983d3a5d1155e41a1171190fa723d4135523585b"}, + {file = "simplejson-3.19.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bb5b50dc6dd671eb46a605a3e2eb98deb4a9af787a08fcdddabe5d824bb9664"}, + {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d94245caa3c61f760c4ce4953cfa76e7739b6f2cbfc94cc46fff6c050c2390c5"}, + {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d0e5ffc763678d48ecc8da836f2ae2dd1b6eb2d27a48671066f91694e575173c"}, + {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d222a9ed082cd9f38b58923775152003765016342a12f08f8c123bf893461f28"}, + {file = "simplejson-3.19.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8434dcdd347459f9fd9c526117c01fe7ca7b016b6008dddc3c13471098f4f0dc"}, + {file = "simplejson-3.19.2-cp36-cp36m-win32.whl", hash = "sha256:c9ac1c2678abf9270e7228133e5b77c6c3c930ad33a3c1dfbdd76ff2c33b7b50"}, + {file = "simplejson-3.19.2-cp36-cp36m-win_amd64.whl", hash = "sha256:92c4a4a2b1f4846cd4364855cbac83efc48ff5a7d7c06ba014c792dd96483f6f"}, + {file = "simplejson-3.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0d551dc931638e2102b8549836a1632e6e7cf620af3d093a7456aa642bff601d"}, + {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73a8a4653f2e809049999d63530180d7b5a344b23a793502413ad1ecea9a0290"}, + {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40847f617287a38623507d08cbcb75d51cf9d4f9551dd6321df40215128325a3"}, + {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be893258d5b68dd3a8cba8deb35dc6411db844a9d35268a8d3793b9d9a256f80"}, + {file = "simplejson-3.19.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9eb3cff1b7d71aa50c89a0536f469cb8d6dcdd585d8f14fb8500d822f3bdee4"}, + {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d0f402e787e6e7ee7876c8b05e2fe6464820d9f35ba3f172e95b5f8b699f6c7f"}, + {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbbcc6b0639aa09b9649f36f1bcb347b19403fe44109948392fbb5ea69e48c3e"}, + {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:2fc697be37585eded0c8581c4788fcfac0e3f84ca635b73a5bf360e28c8ea1a2"}, + {file = "simplejson-3.19.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b0a3eb6dd39cce23801a50c01a0976971498da49bc8a0590ce311492b82c44b"}, + {file = "simplejson-3.19.2-cp37-cp37m-win32.whl", hash = "sha256:49f9da0d6cd17b600a178439d7d2d57c5ef01f816b1e0e875e8e8b3b42db2693"}, + {file = "simplejson-3.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c87c22bd6a987aca976e3d3e23806d17f65426191db36d40da4ae16a6a494cbc"}, + {file = "simplejson-3.19.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e4c166f743bb42c5fcc60760fb1c3623e8fda94f6619534217b083e08644b46"}, + {file = "simplejson-3.19.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a48679310e1dd5c9f03481799311a65d343748fe86850b7fb41df4e2c00c087"}, + {file = "simplejson-3.19.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0521e0f07cb56415fdb3aae0bbd8701eb31a9dfef47bb57206075a0584ab2a2"}, + {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d2d5119b1d7a1ed286b8af37357116072fc96700bce3bec5bb81b2e7057ab41"}, + {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c1467d939932901a97ba4f979e8f2642415fcf02ea12f53a4e3206c9c03bc17"}, + {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49aaf4546f6023c44d7e7136be84a03a4237f0b2b5fb2b17c3e3770a758fc1a0"}, + {file = "simplejson-3.19.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60848ab779195b72382841fc3fa4f71698a98d9589b0a081a9399904487b5832"}, + {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0436a70d8eb42bea4fe1a1c32d371d9bb3b62c637969cb33970ad624d5a3336a"}, + {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:49e0e3faf3070abdf71a5c80a97c1afc059b4f45a5aa62de0c2ca0444b51669b"}, + {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ff836cd4041e16003549449cc0a5e372f6b6f871eb89007ab0ee18fb2800fded"}, + {file = "simplejson-3.19.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3848427b65e31bea2c11f521b6fc7a3145d6e501a1038529da2391aff5970f2f"}, + {file = "simplejson-3.19.2-cp38-cp38-win32.whl", hash = "sha256:3f39bb1f6e620f3e158c8b2eaf1b3e3e54408baca96a02fe891794705e788637"}, + {file = "simplejson-3.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:0405984f3ec1d3f8777c4adc33eac7ab7a3e629f3b1c05fdded63acc7cf01137"}, + {file = "simplejson-3.19.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:445a96543948c011a3a47c8e0f9d61e9785df2544ea5be5ab3bc2be4bd8a2565"}, + {file = "simplejson-3.19.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a8c3cc4f9dfc33220246760358c8265dad6e1104f25f0077bbca692d616d358"}, + {file = "simplejson-3.19.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af9c7e6669c4d0ad7362f79cb2ab6784d71147503e62b57e3d95c4a0f222c01c"}, + {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:064300a4ea17d1cd9ea1706aa0590dcb3be81112aac30233823ee494f02cb78a"}, + {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9453419ea2ab9b21d925d0fd7e3a132a178a191881fab4169b6f96e118cc25bb"}, + {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e038c615b3906df4c3be8db16b3e24821d26c55177638ea47b3f8f73615111c"}, + {file = "simplejson-3.19.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16ca9c90da4b1f50f089e14485db8c20cbfff2d55424062791a7392b5a9b3ff9"}, + {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1018bd0d70ce85f165185d2227c71e3b1e446186f9fa9f971b69eee223e1e3cd"}, + {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e8dd53a8706b15bc0e34f00e6150fbefb35d2fd9235d095b4f83b3c5ed4fa11d"}, + {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2d022b14d7758bfb98405672953fe5c202ea8a9ccf9f6713c5bd0718eba286fd"}, + {file = "simplejson-3.19.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:febffa5b1eda6622d44b245b0685aff6fb555ce0ed734e2d7b1c3acd018a2cff"}, + {file = "simplejson-3.19.2-cp39-cp39-win32.whl", hash = "sha256:4edcd0bf70087b244ba77038db23cd98a1ace2f91b4a3ecef22036314d77ac23"}, + {file = "simplejson-3.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:aad7405c033d32c751d98d3a65801e2797ae77fac284a539f6c3a3e13005edc4"}, + {file = "simplejson-3.19.2-py3-none-any.whl", hash = "sha256:bcedf4cae0d47839fee7de344f96b5694ca53c786f28b5f773d4f0b265a159eb"}, + {file = "simplejson-3.19.2.tar.gz", hash = "sha256:9eb442a2442ce417801c912df68e1f6ccfcd41577ae7274953ab3ad24ef7d82c"}, +] + [[package]] name = "six" version = "1.16.0" @@ -2143,6 +2360,17 @@ files = [ {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, ] +[[package]] +name = "uritemplate" +version = "4.1.1" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.6" +files = [ + {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, + {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, +] + [[package]] name = "urllib3" version = "2.0.4" @@ -2299,4 +2527,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "c66aab30695848eb5d9eb084f93bb40b578e4f719b73d37568b56c1ada62b589" +content-hash = "6d54f10e2b51f7326000280f22add6049096aa7f4ed7355b57583053dadd3073" diff --git a/products/api/__init__.py b/products/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/api/serializers.py b/products/api/serializers.py new file mode 100644 index 0000000..2c54276 --- /dev/null +++ b/products/api/serializers.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + +from ..models import Product + + +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = "__all__" + + # def validate(self, attrs): + # product, create = Product.objects.get_or_create(name=attrs['name']) + # if not create: + # attrs['msg'] = "Product exist" + # else: + # attrs['msg'] = "Product has been added" + # return attrs diff --git a/products/api/views.py b/products/api/views.py new file mode 100644 index 0000000..72569dd --- /dev/null +++ b/products/api/views.py @@ -0,0 +1,11 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from ..models import Product +from .serializers import ProductSerializer + + +class ProductsViewSet(viewsets.ModelViewSet): + serializer_class = ProductSerializer + queryset = Product.objects.all() + permission_classes = [IsAuthenticated] diff --git a/products/urls.py b/products/urls.py index 32393ba..0b01ff4 100644 --- a/products/urls.py +++ b/products/urls.py @@ -1,5 +1,7 @@ -from django.urls import path +from django.urls import include, path +from rest_framework.routers import SimpleRouter +from .api.views import ProductsViewSet from .views import ( ProductAddPageView, ProductDeleteView, @@ -9,6 +11,9 @@ app_name = "products" +router = SimpleRouter() +router.register("products", ProductsViewSet) + urlpatterns = [ path("product/", ProductHomePageView.as_view(), name="products-home-page"), path("product/add/", ProductAddPageView.as_view(), name="product-add"), @@ -22,4 +27,5 @@ ProductDeleteView.as_view(), name="product-delete", ), + path("api/", include(router.urls)), ] diff --git a/pyproject.toml b/pyproject.toml index 1d8c098..ea460b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,6 +109,15 @@ virtualenv = "20.24.4" wcwidth = "0.2.6" werkzeug = "3.0.0" zipp = "3.17.0" +coreapi = "2.3.3" +coreschema = "0.0.4" +django-filter = "24.2" +djangorestframework-simplejwt = "5.3.1" +itypes = "1.2.0" +openapi-codec = "1.3.2" +pyjwt = "2.8.0" +simplejson = "3.19.2" +uritemplate = "4.1.1" [build-system] diff --git a/recipe_ingredients/api/__init__.py b/recipe_ingredients/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/recipe_ingredients/api/serializers.py b/recipe_ingredients/api/serializers.py new file mode 100644 index 0000000..ff509b8 --- /dev/null +++ b/recipe_ingredients/api/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator + +from ..models import RecipeIngredient + + +class RecipeIngredientSerializer(serializers.ModelSerializer): + class Meta: + model = RecipeIngredient + fields = "__all__" + validators = [ + UniqueTogetherValidator( + queryset=RecipeIngredient.objects.all(), + fields=("amount", "ingredient"), + message="Recipe ingredient has been inserted earlier", + ) + ] + + # def validate(self, attrs): + # if self.instance: + # self.instance.amount = attrs['amount'] + # self.instance.ingredient = attrs['ingredient'] + # attrs['msg'] = "Recipe ingredient has been updated" + # else: + # recipe_ingredient, create = RecipeIngredient.objects.get_or_create(ingredient=attrs['ingredient'], amount=attrs['amount']) + # if create: + # attrs['id'] = recipe_ingredient.id + # attrs['msg'] = "Recipe ingredient has been added" + # return attrs diff --git a/recipe_ingredients/api/views.py b/recipe_ingredients/api/views.py new file mode 100644 index 0000000..3688293 --- /dev/null +++ b/recipe_ingredients/api/views.py @@ -0,0 +1,11 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from ..models import RecipeIngredient +from .serializers import RecipeIngredientSerializer + + +class RecipeIngredientViewSet(viewsets.ModelViewSet): + serializer_class = RecipeIngredientSerializer + queryset = RecipeIngredient.objects.all() + permission_classes = [IsAuthenticated] diff --git a/recipe_ingredients/migrations/0001_initial.py b/recipe_ingredients/migrations/0001_initial.py index 6dc5c7b..b73b2b6 100644 --- a/recipe_ingredients/migrations/0001_initial.py +++ b/recipe_ingredients/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-01-11 21:33 +# Generated by Django 4.2.5 on 2024-06-24 16:24 import django.core.validators import django.db.models.deletion @@ -39,5 +39,8 @@ class Migration(migrations.Migration): ), ), ], + options={ + "unique_together": {("amount", "ingredient")}, + }, ), ] diff --git a/recipe_ingredients/models.py b/recipe_ingredients/models.py index 716cabc..8c07793 100644 --- a/recipe_ingredients/models.py +++ b/recipe_ingredients/models.py @@ -10,3 +10,6 @@ class RecipeIngredient(models.Model): def __str__(self): return f"{self.ingredient}, {self.amount}" + + class Meta: + unique_together = ["amount", "ingredient"] diff --git a/recipe_ingredients/urls.py b/recipe_ingredients/urls.py index 7c4af3b..5c9cdbd 100644 --- a/recipe_ingredients/urls.py +++ b/recipe_ingredients/urls.py @@ -1,5 +1,7 @@ -from django.urls import path +from django.urls import include, path +from rest_framework.routers import SimpleRouter +from .api.views import RecipeIngredientViewSet from .views import ( RecipeIngredientAddView, RecipeIngredientDeleteView, @@ -8,6 +10,9 @@ app_name = "recipe_ingredients" +router = SimpleRouter() +router.register("recipe-ingredients", RecipeIngredientViewSet) + urlpatterns = [ path( "recipe-ingredient//add-ingredient/", @@ -34,4 +39,5 @@ RecipeIngredientDeleteView.as_view(), name="ingredient-delete", ), + path("api/", include(router.urls)), ] diff --git a/recipes/api/__init__.py b/recipes/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/recipes/api/serializers.py b/recipes/api/serializers.py new file mode 100644 index 0000000..f01a006 --- /dev/null +++ b/recipes/api/serializers.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + +from ..models import Recipe + + +class RecipeSerializer(serializers.ModelSerializer): + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + model = Recipe + fields = ["id", "recipe_name", "preparation", "user"] + + +class RecipeEditSerializer(serializers.ModelSerializer): + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + model = Recipe + fields = ["recipe_name", "recipe_ingredient", "preparation", "user"] diff --git a/recipes/api/views.py b/recipes/api/views.py new file mode 100644 index 0000000..985511b --- /dev/null +++ b/recipes/api/views.py @@ -0,0 +1,26 @@ +from rest_framework import mixins, viewsets +from rest_framework.permissions import IsAuthenticated + +from ..models import Recipe +from .serializers import RecipeEditSerializer, RecipeSerializer + + +class RecipeViewSet( + viewsets.GenericViewSet, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, +): + serializer_class = RecipeSerializer + queryset = Recipe.objects.all() + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return super().get_queryset().filter(user=self.request.user) + + +class RecipeEditViewSet(viewsets.GenericViewSet, mixins.UpdateModelMixin): + serializer_class = RecipeEditSerializer + queryset = Recipe.objects.all() + permission_classes = [IsAuthenticated] diff --git a/recipes/forms.py b/recipes/forms.py index 7e4c60c..e8323e3 100644 --- a/recipes/forms.py +++ b/recipes/forms.py @@ -11,11 +11,17 @@ class Meta: def __init__(self, *args, **kwargs): super(CreateNewRecipe, self).__init__(*args, **kwargs) for field_name, field in self.fields.items(): - field.required = False + if field_name != "recipe_name": + field.required = False - def is_valid(self): - filled_recipe_name = self.data.get("recipe_name") - if filled_recipe_name: - return super().is_valid() - else: - return False + +class EditRecipe(forms.ModelForm): + class Meta: + model = Recipe + fields = ["recipe_name", "preparation"] + + def __init__(self, *args, **kwargs): + super(EditRecipe, self).__init__(*args, **kwargs) + for field_name, field in self.fields.items(): + if field_name != "recipe_name": + field.required = False diff --git a/recipes/migrations/0001_initial.py b/recipes/migrations/0001_initial.py index 550e20a..6ac67a4 100644 --- a/recipes/migrations/0001_initial.py +++ b/recipes/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 4.2.5 on 2024-01-17 20:56 +# Generated by Django 4.2.5 on 2024-06-24 16:24 import django.db.models.deletion +from django.conf import settings from django.db import migrations, models @@ -8,7 +9,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("users", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("recipe_ingredients", "0001_initial"), ] @@ -39,7 +40,8 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, - to="users.profile", + related_name="recipes", + to=settings.AUTH_USER_MODEL, ), ), ], diff --git a/recipes/templates/recipes/recipe_form_edit.html b/recipes/templates/recipes/recipe_form_edit.html new file mode 100644 index 0000000..c118500 --- /dev/null +++ b/recipes/templates/recipes/recipe_form_edit.html @@ -0,0 +1,57 @@ +{% extends 'users/dashboard_base.html' %} +{% load crispy_forms_tags %} +{% block content %} +
+
+ {% csrf_token %} +
+ {{ form.recipe_name|as_crispy_field }} + Ingredients +
+ +
+ {% if ingredients.count > 0 %} + + + + + + + + + + + + + + + + + + + {% for ingredient in ingredients %} + + + + + + + + {% endfor %} + +
AmountQuantity typeProduct
{{ ingredient.amount }}{{ ingredient.ingredient.quantity_type }}{{ ingredient.ingredient.product.name }}Edit +
+ {% csrf_token %} + +
+
+ {% endif %} + {{ form.preparation|as_crispy_field }} +{# {{ form.tags|as_crispy_field }}#} +
+ +
+
+ +
+{% endblock %} diff --git a/recipes/templates/recipes/recipe_form.html b/recipes/templates/recipes/recipe_form_new.html similarity index 93% rename from recipes/templates/recipes/recipe_form.html rename to recipes/templates/recipes/recipe_form_new.html index 336e0e7..ca8f4c7 100644 --- a/recipes/templates/recipes/recipe_form.html +++ b/recipes/templates/recipes/recipe_form_new.html @@ -49,11 +49,7 @@ {{ form.preparation|as_crispy_field }} {# {{ form.tags|as_crispy_field }}#}
- {% if "Add" in title %} - {% else %} - - {% endif %}
diff --git a/recipes/tests/test_views.py b/recipes/tests/test_views.py index f6ebb37..9225831 100644 --- a/recipes/tests/test_views.py +++ b/recipes/tests/test_views.py @@ -67,7 +67,7 @@ def test_recipe_home_page_view_GET(self): def test_recipe_add_page_view_GET(self): response = self.client.get(self.recipe_add_page) self.assertEquals(response.status_code, HTTPStatus.OK) - self.assertTemplateUsed(response, "recipes/recipe_form.html") + self.assertTemplateUsed(response, "recipes/recipe_form_new.html") self.assertContains(response, "Add Recipe") self.assertContains(response, "Recipe name") self.assertContains(response, '/edit/", + "recipes//edit/", RecipeEditPageView.as_view(), name="recipe-edit", ), @@ -20,4 +25,10 @@ RecipeDeleteView.as_view(), name="recipe-delete", ), + path("api/", include(router.urls)), + path( + "api/recipes//edit/", + RecipeEditViewSet.as_view({"put": "update"}), + name="recipe-edit", + ), ] diff --git a/recipes/views.py b/recipes/views.py index 5faef14..a43d860 100644 --- a/recipes/views.py +++ b/recipes/views.py @@ -4,7 +4,7 @@ from django.urls import reverse_lazy from django.views.generic import CreateView, DeleteView, ListView, UpdateView -from .forms import CreateNewRecipe +from .forms import CreateNewRecipe, EditRecipe from .models import Recipe @@ -43,7 +43,7 @@ def get(self, request, *args, **kwargs): class RecipeAddPageView(LoginRequiredMixin, CreateView): model = Recipe form_class = CreateNewRecipe - template_name = "recipes/recipe_form.html" + template_name = "recipes/recipe_form_new.html" extra_context = {"title": "Add Recipe"} def get(self, request, *args, **kwargs): @@ -68,13 +68,13 @@ def post(self, request, *args, **kwargs): class RecipeEditPageView(LoginRequiredMixin, UpdateView): model = Recipe - form_class = CreateNewRecipe - template_name = "recipes/recipe_form.html" + form_class = EditRecipe + template_name = "recipes/recipe_form_edit.html" extra_context = {"title": "Edit Recipe"} def get(self, request, *args, **kwargs): - recipe_id = kwargs.get("recipe_id") - recipe = get_object_or_404(self.model, pk=recipe_id) + recipe_id = self.get_object() + recipe = get_object_or_404(self.model, pk=recipe_id.pk) context = self.extra_context context["form"] = self.form_class( initial={ @@ -88,21 +88,21 @@ def get(self, request, *args, **kwargs): return render(request, self.template_name, context) def post(self, request, *args, **kwargs): - recipe_id = kwargs.get("recipe_id") - form = self.form_class(request.POST) + recipe_id = self.get_object() + form = self.form_class(request.POST, instance=recipe_id) if form.is_valid(): - self.model.objects.filter(id=recipe_id).update( + self.model.objects.filter(id=recipe_id.pk).update( recipe_name=form.cleaned_data.get("recipe_name"), preparation=form.cleaned_data.get("preparation"), # tags=form.cleaned_data.get("tags"), ) if "add_ingredient" in request.POST: - return redirect("recipe_ingredients:ingredient-add", recipe_id) + return redirect("recipe_ingredients:ingredient-add", recipe_id.pk) messages.success(request, "Recipe has been updated") return redirect("recipes-home-page") else: messages.warning(request, "Invalid data in recipe") - return redirect("recipe-edit", recipe_id) + return redirect("recipe-edit", recipe_id.pk) class RecipeDeleteView(LoginRequiredMixin, DeleteView): diff --git a/user_ingredients/api/__init__.py b/user_ingredients/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user_ingredients/api/serializers.py b/user_ingredients/api/serializers.py new file mode 100644 index 0000000..5f087e3 --- /dev/null +++ b/user_ingredients/api/serializers.py @@ -0,0 +1,19 @@ +from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator + +from ..models import UserIngredient + + +class UserIngredientSerializer(serializers.ModelSerializer): + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + model = UserIngredient + fields = "__all__" + validators = [ + UniqueTogetherValidator( + queryset=UserIngredient.objects.all(), + fields=("user", "ingredient"), + message="Your ingredient has been inserted earlier", + ) + ] diff --git a/user_ingredients/api/views.py b/user_ingredients/api/views.py new file mode 100644 index 0000000..e84a8a1 --- /dev/null +++ b/user_ingredients/api/views.py @@ -0,0 +1,14 @@ +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from ..models import UserIngredient +from .serializers import UserIngredientSerializer + + +class UserIngredientViewSet(viewsets.ModelViewSet): + serializer_class = UserIngredientSerializer + queryset = UserIngredient.objects.all() + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return super().get_queryset().filter(user=self.request.user) diff --git a/user_ingredients/migrations/0001_initial.py b/user_ingredients/migrations/0001_initial.py index 9a1851c..9f10400 100644 --- a/user_ingredients/migrations/0001_initial.py +++ b/user_ingredients/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2024-01-11 21:33 +# Generated by Django 4.2.5 on 2024-06-24 16:24 import django.core.validators import django.db.models.deletion @@ -47,5 +47,8 @@ class Migration(migrations.Migration): ), ), ], + options={ + "unique_together": {("user", "ingredient")}, + }, ), ] diff --git a/user_ingredients/models.py b/user_ingredients/models.py index 6b2bf99..ad2bac7 100644 --- a/user_ingredients/models.py +++ b/user_ingredients/models.py @@ -13,3 +13,6 @@ class UserIngredient(models.Model): Ingredient, on_delete=models.DO_NOTHING, null=False, blank=False ) amount = models.PositiveIntegerField(validators=[MinValueValidator(1)]) + + class Meta: + unique_together = ["user", "ingredient"] diff --git a/user_ingredients/urls.py b/user_ingredients/urls.py index 24b6fa2..b4929ed 100644 --- a/user_ingredients/urls.py +++ b/user_ingredients/urls.py @@ -1,5 +1,7 @@ -from django.urls import path +from django.urls import include, path +from rest_framework.routers import SimpleRouter +from .api.views import UserIngredientViewSet from .views import ( UserIngredientsAddPageView, UserIngredientsDeletePageView, @@ -9,6 +11,9 @@ app_name = "my_ingredients" +router = SimpleRouter() +router.register("user-ingredients", UserIngredientViewSet) + urlpatterns = [ path( "my-ingredients/", @@ -40,4 +45,5 @@ UserIngredientsDeletePageView.as_view(), name="useringredient-delete", ), + path("api/", include(router.urls)), ] diff --git a/user_ingredients/views.py b/user_ingredients/views.py index c2c1a42..bcbe4fc 100644 --- a/user_ingredients/views.py +++ b/user_ingredients/views.py @@ -109,11 +109,24 @@ def post(self, request, *args, **kwargs): ingredient_form = IngredientForm(request.POST, prefix="ingredient") if form.is_valid() and ingredient_form.is_valid(): ingredient, created = find_ingredient(ingredient_form.cleaned_data) - self.model.objects.filter(id=my_ingredient_id).update( + did_user_have_ingredient = self.model.objects.filter( + user=request.user.profile, ingredient=ingredient, - amount=form.cleaned_data.get("amount"), - ) - messages.success(request, "Successfully edited my ingredient") + ).first() + if did_user_have_ingredient: + did_user_have_ingredient.amount += form.cleaned_data.get("amount") + did_user_have_ingredient.save() + messages.success( + request, + f"{did_user_have_ingredient.ingredient.product.name} has been added previously. Amount was updated", + ) + UserIngredient.objects.get(id=my_ingredient_id).delete() + else: + self.model.objects.filter(id=my_ingredient_id).update( + ingredient=ingredient, + amount=form.cleaned_data.get("amount"), + ) + messages.success(request, "Successfully edited my ingredient") return redirect("my_ingredients:useringredients-home-page") else: messages.warning(request, "Invalid data in editing my ingredient") diff --git a/users/api/__init__.py b/users/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/api/serializers.py b/users/api/serializers.py new file mode 100644 index 0000000..14b8e45 --- /dev/null +++ b/users/api/serializers.py @@ -0,0 +1,39 @@ +from django.contrib.auth.models import User +from django.contrib.auth.password_validation import validate_password +from rest_framework import serializers +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + + +class MyTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user: User): + token = super(MyTokenObtainPairSerializer, cls).get_token(user) + + token["username"] = user.username + return token + + +class RegisterSerializer(serializers.ModelSerializer): + password = serializers.CharField( + write_only=True, required=True, validators=[validate_password] + ) + password2 = serializers.CharField(write_only=True, required=True) + + class Meta: + model = User + fields = ("username", "password", "password2") + + def validate(self, attrs): + if attrs["password"] != attrs["password2"]: + raise serializers.ValidationError( + {"password": "Password fields didn't match"} + ) + return attrs + + def create(self, validated_data): + user = User.objects.create( + username=validated_data["username"], + ) + user.set_password(validated_data["password"]) + user.save() + return user diff --git a/users/api/views.py b/users/api/views.py new file mode 100644 index 0000000..d24a943 --- /dev/null +++ b/users/api/views.py @@ -0,0 +1,17 @@ +from django.contrib.auth.models import User +from rest_framework import generics +from rest_framework.permissions import AllowAny +from rest_framework_simplejwt.views import TokenObtainPairView + +from .serializers import MyTokenObtainPairSerializer, RegisterSerializer + + +class MyObtainTokenPairView(TokenObtainPairView): + permission_classes = (AllowAny,) + serializer_class = MyTokenObtainPairSerializer + + +class APIRegisterView(generics.CreateAPIView): + queryset = User.objects.all() + permission_classes = (AllowAny,) + serializer_class = RegisterSerializer diff --git a/users/urls.py b/users/urls.py index 157ba97..a85f737 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,6 +1,8 @@ from django.contrib.auth import views as auth_views from django.urls import path +from rest_framework_simplejwt.views import TokenRefreshView +from .api.views import APIRegisterView, MyObtainTokenPairView from .views import ( HomePageView, MyLoginView, @@ -28,4 +30,7 @@ MyResetPasswordView.as_view(template_name="users/reset_password.html"), name="reset-password-page", ), + path("api/auth/login/", MyObtainTokenPairView.as_view(), name="token-obtain-view"), + path("api/auth/login/refresh/", TokenRefreshView.as_view(), name="token-refresh"), + path("api/auth/register/", APIRegisterView.as_view(), name="auth-register"), ]