From 193c1ccfda7a9a2381178ebacb47bc0046df945e Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 22 Dec 2024 16:49:56 +0200 Subject: [PATCH 01/21] create empty base drf project --- .gitignore | 5 ++ manage.py | 22 +++++ planetarium/__init__.py | 0 planetarium/admin.py | 3 + planetarium/apps.py | 6 ++ planetarium/migrations/__init__.py | 0 planetarium/models.py | 3 + planetarium/tests.py | 3 + planetarium/urls.py | 5 ++ planetarium/views.py | 3 + planetarium_service/__init__.py | 0 planetarium_service/asgi.py | 16 ++++ planetarium_service/settings.py | 127 +++++++++++++++++++++++++++++ planetarium_service/urls.py | 25 ++++++ planetarium_service/wsgi.py | 16 ++++ requirements.txt | 12 +++ 16 files changed, 246 insertions(+) create mode 100644 .gitignore create mode 100644 manage.py create mode 100644 planetarium/__init__.py create mode 100644 planetarium/admin.py create mode 100644 planetarium/apps.py create mode 100644 planetarium/migrations/__init__.py create mode 100644 planetarium/models.py create mode 100644 planetarium/tests.py create mode 100644 planetarium/urls.py create mode 100644 planetarium/views.py create mode 100644 planetarium_service/__init__.py create mode 100644 planetarium_service/asgi.py create mode 100644 planetarium_service/settings.py create mode 100644 planetarium_service/urls.py create mode 100644 planetarium_service/wsgi.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14daecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +.venv +.vscode +db.sqlite3 +__pycache__ \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..783e3f7 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "planetarium_service.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/planetarium/__init__.py b/planetarium/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/planetarium/admin.py b/planetarium/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/planetarium/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/planetarium/apps.py b/planetarium/apps.py new file mode 100644 index 0000000..9357546 --- /dev/null +++ b/planetarium/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PlanetariumConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "planetarium" diff --git a/planetarium/migrations/__init__.py b/planetarium/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/planetarium/models.py b/planetarium/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/planetarium/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/planetarium/tests.py b/planetarium/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/planetarium/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/planetarium/urls.py b/planetarium/urls.py new file mode 100644 index 0000000..b300e8e --- /dev/null +++ b/planetarium/urls.py @@ -0,0 +1,5 @@ +urlpatterns = [ + +] + +app_name = "planetarium" diff --git a/planetarium/views.py b/planetarium/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/planetarium/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/planetarium_service/__init__.py b/planetarium_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/planetarium_service/asgi.py b/planetarium_service/asgi.py new file mode 100644 index 0000000..21085f0 --- /dev/null +++ b/planetarium_service/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for planetarium_service project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "planetarium_service.settings") + +application = get_asgi_application() diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py new file mode 100644 index 0000000..518c9bb --- /dev/null +++ b/planetarium_service/settings.py @@ -0,0 +1,127 @@ +""" +Django settings for planetarium_service project. + +Generated by 'django-admin startproject' using Django 5.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-02eb(otr$%qq@%ogajlag9ef#l$ie870!4sf+q&znj@pra1(e&" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + + "rest_framework", + + "planetarium", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "planetarium_service.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "planetarium_service.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/planetarium_service/urls.py b/planetarium_service/urls.py new file mode 100644 index 0000000..a13ac99 --- /dev/null +++ b/planetarium_service/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for planetarium_service project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/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 path, include + +urlpatterns = [ + path("admin/", admin.site.urls), + path("api/", include("planetarium.urls"), name="planetarium"), + path("api-auth/", include("rest_framework.urls")), +] diff --git a/planetarium_service/wsgi.py b/planetarium_service/wsgi.py new file mode 100644 index 0000000..ccc9cdd --- /dev/null +++ b/planetarium_service/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for planetarium_service project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "planetarium_service.settings") + +application = get_wsgi_application() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3d0feed --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +asgiref==3.8.1 +black==24.10.0 +click==8.1.8 +colorama==0.4.6 +Django==5.1.4 +djangorestframework==3.15.2 +mypy-extensions==1.0.0 +packaging==24.2 +pathspec==0.12.1 +platformdirs==4.3.6 +sqlparse==0.5.3 +tzdata==2024.2 From aa015697f430b766cdd949f9df7c5cb19fa4a67b Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 22 Dec 2024 17:02:46 +0200 Subject: [PATCH 02/21] add drf-spectacular and accounts app --- accounts/__init__.py | 0 accounts/admin.py | 3 +++ accounts/apps.py | 6 ++++++ accounts/migrations/__init__.py | 0 accounts/models.py | 3 +++ accounts/tests.py | 3 +++ accounts/views.py | 3 +++ planetarium_service/settings.py | 14 ++++++++++++-- requirements.txt | 11 +++++++++++ 9 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 accounts/__init__.py create mode 100644 accounts/admin.py create mode 100644 accounts/apps.py create mode 100644 accounts/migrations/__init__.py create mode 100644 accounts/models.py create mode 100644 accounts/tests.py create mode 100644 accounts/views.py diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..0cb51e6 --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "accounts" diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 518c9bb..a56c910 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -37,9 +37,8 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "rest_framework", - + "drf_spectacular", "planetarium", ] @@ -125,3 +124,14 @@ # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + +SPECTACULAR_SETTINGS = { + "TITLE": "Your Project API", + "DESCRIPTION": "Your project description", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, +} diff --git a/requirements.txt b/requirements.txt index 3d0feed..8bd2d85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,23 @@ asgiref==3.8.1 +attrs==24.3.0 black==24.10.0 click==8.1.8 colorama==0.4.6 Django==5.1.4 +django-filter==24.3 djangorestframework==3.15.2 +drf-spectacular==0.28.0 +inflection==0.5.1 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +Markdown==3.7 mypy-extensions==1.0.0 packaging==24.2 pathspec==0.12.1 platformdirs==4.3.6 +PyYAML==6.0.2 +referencing==0.35.1 +rpds-py==0.22.3 sqlparse==0.5.3 tzdata==2024.2 +uritemplate==4.1.1 From 6c24cbdac6e1cb5f255289c029fa23cb5689a88e Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 22 Dec 2024 17:12:48 +0200 Subject: [PATCH 03/21] add accounts app and jwt authentication --- accounts/admin.py | 5 ++++- accounts/models.py | 5 ++++- accounts/urls.py | 15 +++++++++++++++ planetarium/models.py | 1 - planetarium_service/settings.py | 7 +++++++ planetarium_service/urls.py | 1 + requirements.txt | 2 ++ 7 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 accounts/urls.py diff --git a/accounts/admin.py b/accounts/admin.py index 8c38f3f..6c9a82c 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin -# Register your models here. +from accounts.models import User + + +admin.site.register(User) diff --git a/accounts/models.py b/accounts/models.py index 71a8362..12b5f8e 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,3 +1,6 @@ +from django.contrib.auth.models import AbstractUser from django.db import models -# Create your models here. + +class User(AbstractUser): + pass \ No newline at end of file diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..d6f856d --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenVerifyView, +) + + +app_name = "accounts" + +urlpatterns = [ + path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), +] diff --git a/planetarium/models.py b/planetarium/models.py index 71a8362..beeb308 100644 --- a/planetarium/models.py +++ b/planetarium/models.py @@ -1,3 +1,2 @@ from django.db import models -# Create your models here. diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index a56c910..4ebb321 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -37,8 +37,12 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework", "drf_spectacular", + "rest_framework_simplejwt", + + "accounts", "planetarium", ] @@ -126,6 +130,9 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } diff --git a/planetarium_service/urls.py b/planetarium_service/urls.py index a13ac99..3f6723b 100644 --- a/planetarium_service/urls.py +++ b/planetarium_service/urls.py @@ -21,5 +21,6 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/", include("planetarium.urls"), name="planetarium"), + path("accounts/", include("accounts.urls"), name="accounts"), path("api-auth/", include("rest_framework.urls")), ] diff --git a/requirements.txt b/requirements.txt index 8bd2d85..06b7a8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ colorama==0.4.6 Django==5.1.4 django-filter==24.3 djangorestframework==3.15.2 +djangorestframework-simplejwt==5.3.1 drf-spectacular==0.28.0 inflection==0.5.1 jsonschema==4.23.0 @@ -15,6 +16,7 @@ mypy-extensions==1.0.0 packaging==24.2 pathspec==0.12.1 platformdirs==4.3.6 +PyJWT==2.10.1 PyYAML==6.0.2 referencing==0.35.1 rpds-py==0.22.3 From 28e459fe6f7c9e16c76f22f7096e4be108ef4833 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 22 Dec 2024 17:54:19 +0200 Subject: [PATCH 04/21] add register user and serializer --- accounts/models.py | 3 +-- accounts/serializers.py | 16 ++++++++++++++++ accounts/urls.py | 4 +++- accounts/views.py | 10 ++++++++-- planetarium/urls.py | 3 +++ planetarium_service/settings.py | 2 ++ planetarium_service/urls.py | 4 ++-- 7 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 accounts/serializers.py diff --git a/accounts/models.py b/accounts/models.py index 12b5f8e..3d30525 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,6 +1,5 @@ from django.contrib.auth.models import AbstractUser -from django.db import models class User(AbstractUser): - pass \ No newline at end of file + pass diff --git a/accounts/serializers.py b/accounts/serializers.py new file mode 100644 index 0000000..5aa4aee --- /dev/null +++ b/accounts/serializers.py @@ -0,0 +1,16 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers + + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = get_user_model() + fields = ("id", "username", "email", "first_name", "last_name", "password") + read_only_fields = ("id",) + extra_kwargs = { + "password": {"write_only": True, "min_length": 8}, + } + + def create(self, validated_data): + return get_user_model().objects.create_user(**validated_data) diff --git a/accounts/urls.py b/accounts/urls.py index d6f856d..ed6b161 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -4,12 +4,14 @@ TokenRefreshView, TokenVerifyView, ) +from .views import CreateUserView app_name = "accounts" urlpatterns = [ - path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path("register/", CreateUserView.as_view(), name="create"), path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"), ] diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..628ecd0 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,9 @@ -from django.shortcuts import render +from rest_framework import generics -# Create your views here. +from accounts.models import User +from accounts.serializers import UserSerializer + + +class CreateUserView(generics.CreateAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer diff --git a/planetarium/urls.py b/planetarium/urls.py index b300e8e..282657f 100644 --- a/planetarium/urls.py +++ b/planetarium/urls.py @@ -1,3 +1,6 @@ +from django.urls import path + + urlpatterns = [ ] diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 4ebb321..6130d58 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -129,6 +129,8 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +AUTH_USER_MODEL = "accounts.User" + REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", diff --git a/planetarium_service/urls.py b/planetarium_service/urls.py index 3f6723b..08c6c2c 100644 --- a/planetarium_service/urls.py +++ b/planetarium_service/urls.py @@ -20,7 +20,7 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("api/", include("planetarium.urls"), name="planetarium"), - path("accounts/", include("accounts.urls"), name="accounts"), + path("api/planetarium", include("planetarium.urls"), name="planetarium"), + path("api/accounts/", include("accounts.urls"), name="accounts"), path("api-auth/", include("rest_framework.urls")), ] From 205059142c2eecc95ac327731ffec98e9c438046 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 22 Dec 2024 18:21:15 +0200 Subject: [PATCH 05/21] create all planetarium models --- planetarium/models.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/planetarium/models.py b/planetarium/models.py index beeb308..476f0c1 100644 --- a/planetarium/models.py +++ b/planetarium/models.py @@ -1,2 +1,37 @@ from django.db import models +from django.conf import settings + + +class ShowTheme(models.Model): + name = models.CharField(max_length=127) + + +class AstronomyShow(models.Model): + title = models.CharField(max_length=255) + description = models.TextField() + show_theme = models.ManyToManyField(ShowTheme, related_name="astronomy_shows") + + +class PlanetariumDome(models.Model): + name = models.CharField(max_length=255) + rows = models.IntegerField() + seats_in_row = models.IntegerField() + + +class ShowSession(models.Model): + astronomy_show = models.ForeignKey(AstronomyShow, on_delete=models.CASCADE) + planetarium_dome = models.ForeignKey(PlanetariumDome, on_delete=models.CASCADE) + show_time = models.DateTimeField() + + +class Reservation(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + + +class Ticket(models.Model): + row = models.IntegerField() + seat = models.IntegerField() + show_session = models.ForeignKey(ShowSession, on_delete=models.CASCADE) + reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE) From 8befbda451e0f044983a6beb95585c8cd49f8c02 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 22 Dec 2024 18:29:19 +0200 Subject: [PATCH 06/21] create serializers for all planetarium models --- planetarium/serializers.py | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 planetarium/serializers.py diff --git a/planetarium/serializers.py b/planetarium/serializers.py new file mode 100644 index 0000000..57caf67 --- /dev/null +++ b/planetarium/serializers.py @@ -0,0 +1,48 @@ +from rest_framework import serializers + +from planetarium.models import ShowTheme, AstronomyShow, PlanetariumDome, ShowSession, Reservation, Ticket + + +class ShowThemeSerializer(serializers.ModelSerializer): + class Meta: + model = ShowTheme + fields = ["id", "name"] + + +class AstronomyShowSerializer(serializers.ModelSerializer): + show_theme = ShowThemeSerializer(many=True) + + class Meta: + model = AstronomyShow + fields = ["id", "title", "description", "show_theme"] + + +class PlanetariumDomeSerializer(serializers.ModelSerializer): + class Meta: + model = PlanetariumDome + fields = ["id", "name", "rows", "seats_in_row"] + + +class ShowSessionSerializer(serializers.ModelSerializer): + astronomy_show = AstronomyShowSerializer() + planetarium_dome = PlanetariumDomeSerializer() + + class Meta: + model = ShowSession + fields = ["id", "astronomy_show", "planetarium_dome", "show_time"] + + +class ReservationSerializer(serializers.ModelSerializer): + user = serializers.SlugRelatedField(read_only=True, slug_field="username") + + class Meta: + model = Reservation + fields = ["id", "created_at", "user"] + + +class TicketSerializer(serializers.ModelSerializer): + show_session = ShowSessionSerializer() + reservation = ReservationSerializer() + class Meta: + model = Ticket + fields = ["id", "row", "seat", "show_session", "reservation"] From ecae97e683447c80299372b47b6e781f974698ae Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 22 Dec 2024 19:08:54 +0200 Subject: [PATCH 07/21] create views and urls for all models --- accounts/serializers.py | 1 - planetarium/serializers.py | 10 +++++++- planetarium/urls.py | 21 ++++++++++++++-- planetarium/views.py | 49 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/accounts/serializers.py b/accounts/serializers.py index 5aa4aee..6b7ec17 100644 --- a/accounts/serializers.py +++ b/accounts/serializers.py @@ -2,7 +2,6 @@ from rest_framework import serializers - class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() diff --git a/planetarium/serializers.py b/planetarium/serializers.py index 57caf67..d499413 100644 --- a/planetarium/serializers.py +++ b/planetarium/serializers.py @@ -1,6 +1,13 @@ from rest_framework import serializers -from planetarium.models import ShowTheme, AstronomyShow, PlanetariumDome, ShowSession, Reservation, Ticket +from planetarium.models import ( + ShowTheme, + AstronomyShow, + PlanetariumDome, + ShowSession, + Reservation, + Ticket, +) class ShowThemeSerializer(serializers.ModelSerializer): @@ -43,6 +50,7 @@ class Meta: class TicketSerializer(serializers.ModelSerializer): show_session = ShowSessionSerializer() reservation = ReservationSerializer() + class Meta: model = Ticket fields = ["id", "row", "seat", "show_session", "reservation"] diff --git a/planetarium/urls.py b/planetarium/urls.py index 282657f..f2edb17 100644 --- a/planetarium/urls.py +++ b/planetarium/urls.py @@ -1,8 +1,25 @@ -from django.urls import path +from django.urls import path, include +from rest_framework import routers +from planetarium.views import ( + ShowThemeViewSet, + AstronomyShowViewSet, + PlanetariumDomeViewSet, + ShowSessionViewSet, + ReservationViewSet, + TicketViewSet, +) -urlpatterns = [ +router = routers.DefaultRouter() +router.register("show-theme", ShowThemeViewSet) +router.register("astronomy-show", AstronomyShowViewSet) +router.register("planetarium-dome", PlanetariumDomeViewSet) +router.register("show-session", ShowSessionViewSet) +router.register("reservation", ReservationViewSet) +router.register("ticket", TicketViewSet) +urlpatterns = [ + path("", include(router.urls)), ] app_name = "planetarium" diff --git a/planetarium/views.py b/planetarium/views.py index 91ea44a..98d6f0e 100644 --- a/planetarium/views.py +++ b/planetarium/views.py @@ -1,3 +1,48 @@ -from django.shortcuts import render +from rest_framework import viewsets -# Create your views here. +from planetarium.models import ( + ShowTheme, + AstronomyShow, + PlanetariumDome, + ShowSession, + Reservation, + Ticket, +) +from planetarium.serializers import ( + ShowThemeSerializer, + AstronomyShowSerializer, + PlanetariumDomeSerializer, + ShowSessionSerializer, + ReservationSerializer, + TicketSerializer, +) + + +class ShowThemeViewSet(viewsets.ModelViewSet): + queryset = ShowTheme.objects.all() + serializer_class = ShowThemeSerializer + + +class AstronomyShowViewSet(viewsets.ModelViewSet): + queryset = AstronomyShow.objects.all() + serializer_class = AstronomyShowSerializer + + +class PlanetariumDomeViewSet(viewsets.ModelViewSet): + queryset = PlanetariumDome.objects.all() + serializer_class = PlanetariumDomeSerializer + + +class ShowSessionViewSet(viewsets.ModelViewSet): + queryset = ShowSession.objects.all() + serializer_class = ShowSessionSerializer + + +class ReservationViewSet(viewsets.ModelViewSet): + queryset = Reservation.objects.all() + serializer_class = ReservationSerializer + + +class TicketViewSet(viewsets.ModelViewSet): + queryset = Ticket.objects.all() + serializer_class = TicketSerializer From eca4f84d75588cfded2f18ab1b31635d8b7ae302 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 23 Dec 2024 14:57:41 +0200 Subject: [PATCH 08/21] upgrade serializers, add list, retrieve serializers, add related names for models --- accounts/migrations/0001_initial.py | 132 +++++++++++++++ accounts/urls.py | 6 +- planetarium/migrations/0001_initial.py | 153 ++++++++++++++++++ ...alter_astronomyshow_show_theme_and_more.py | 76 +++++++++ planetarium/models.py | 38 ++++- planetarium/serializers.py | 46 ++++-- planetarium/views.py | 51 +++++- planetarium_service/settings.py | 11 +- planetarium_service/urls.py | 2 +- 9 files changed, 491 insertions(+), 24 deletions(-) create mode 100644 accounts/migrations/0001_initial.py create mode 100644 planetarium/migrations/0001_initial.py create mode 100644 planetarium/migrations/0002_alter_ticket_options_alter_astronomyshow_show_theme_and_more.py diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..4cefd74 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,132 @@ +# Generated by Django 5.1.4 on 2024-12-23 10:03 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/accounts/urls.py b/accounts/urls.py index ed6b161..cd57172 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -11,7 +11,7 @@ urlpatterns = [ path("register/", CreateUserView.as_view(), name="create"), - path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), - path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), - path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"), + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path("token/verify/", TokenVerifyView.as_view(), name="token_verify"), ] diff --git a/planetarium/migrations/0001_initial.py b/planetarium/migrations/0001_initial.py new file mode 100644 index 0000000..05dad7a --- /dev/null +++ b/planetarium/migrations/0001_initial.py @@ -0,0 +1,153 @@ +# Generated by Django 5.1.4 on 2024-12-23 10:03 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="AstronomyShow", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("description", models.TextField()), + ], + ), + migrations.CreateModel( + name="PlanetariumDome", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("rows", models.IntegerField()), + ("seats_in_row", models.IntegerField()), + ], + ), + migrations.CreateModel( + name="ShowTheme", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=127)), + ], + ), + migrations.CreateModel( + name="Reservation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="ShowSession", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("show_time", models.DateTimeField()), + ( + "astronomy_show", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="planetarium.astronomyshow", + ), + ), + ( + "planetarium_dome", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="planetarium.planetariumdome", + ), + ), + ], + ), + migrations.AddField( + model_name="astronomyshow", + name="show_theme", + field=models.ManyToManyField( + related_name="astronomy_shows", to="planetarium.showtheme" + ), + ), + migrations.CreateModel( + name="Ticket", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("row", models.IntegerField()), + ("seat", models.IntegerField()), + ( + "reservation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="planetarium.reservation", + ), + ), + ( + "show_session", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="planetarium.showsession", + ), + ), + ], + ), + ] diff --git a/planetarium/migrations/0002_alter_ticket_options_alter_astronomyshow_show_theme_and_more.py b/planetarium/migrations/0002_alter_ticket_options_alter_astronomyshow_show_theme_and_more.py new file mode 100644 index 0000000..e77621a --- /dev/null +++ b/planetarium/migrations/0002_alter_ticket_options_alter_astronomyshow_show_theme_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 5.1.4 on 2024-12-23 12:44 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("planetarium", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterModelOptions( + name="ticket", + options={"ordering": ["row", "seat"]}, + ), + migrations.AlterField( + model_name="astronomyshow", + name="show_theme", + field=models.ManyToManyField( + blank=True, related_name="astronomy_shows", to="planetarium.showtheme" + ), + ), + migrations.AlterField( + model_name="reservation", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reservations", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="showsession", + name="astronomy_show", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="show_sessions", + to="planetarium.astronomyshow", + ), + ), + migrations.AlterField( + model_name="showsession", + name="planetarium_dome", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="show_sessions", + to="planetarium.planetariumdome", + ), + ), + migrations.AlterField( + model_name="ticket", + name="reservation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="planetarium.reservation", + ), + ), + migrations.AlterField( + model_name="ticket", + name="show_session", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", + to="planetarium.showsession", + ), + ), + migrations.AlterUniqueTogether( + name="ticket", + unique_together={("row", "seat", "show_session")}, + ), + ] diff --git a/planetarium/models.py b/planetarium/models.py index 476f0c1..8936388 100644 --- a/planetarium/models.py +++ b/planetarium/models.py @@ -1,16 +1,21 @@ -from django.db import models - from django.conf import settings +from django.db import models class ShowTheme(models.Model): name = models.CharField(max_length=127) + def __str__(self): + return self.name + class AstronomyShow(models.Model): title = models.CharField(max_length=255) description = models.TextField() - show_theme = models.ManyToManyField(ShowTheme, related_name="astronomy_shows") + show_theme = models.ManyToManyField(ShowTheme, blank=True, related_name="astronomy_shows") + + def __str__(self): + return self.title class PlanetariumDome(models.Model): @@ -18,20 +23,37 @@ class PlanetariumDome(models.Model): rows = models.IntegerField() seats_in_row = models.IntegerField() + @property + def capacity(self): + return self.rows * self.seats_in_row + + def __str__(self): + return f"{self.name} ({self.capacity} seats)" + class ShowSession(models.Model): - astronomy_show = models.ForeignKey(AstronomyShow, on_delete=models.CASCADE) - planetarium_dome = models.ForeignKey(PlanetariumDome, on_delete=models.CASCADE) + astronomy_show = models.ForeignKey(AstronomyShow, on_delete=models.CASCADE, related_name="show_sessions") + planetarium_dome = models.ForeignKey(PlanetariumDome, on_delete=models.CASCADE, related_name="show_sessions") show_time = models.DateTimeField() + def __str__(self): + return f"{self.astronomy_show.title} - {self.planetarium_dome.name}" + class Reservation(models.Model): created_at = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="reservations") + + def __str__(self): + return f"{self.user.username} - (created: {self.created_at})" class Ticket(models.Model): row = models.IntegerField() seat = models.IntegerField() - show_session = models.ForeignKey(ShowSession, on_delete=models.CASCADE) - reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE) + show_session = models.ForeignKey(ShowSession, on_delete=models.CASCADE, related_name="tickets") + reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, related_name="tickets") + + class Meta: + unique_together = ("row", "seat", "show_session") + ordering = ["row", "seat"] diff --git a/planetarium/serializers.py b/planetarium/serializers.py index d499413..c5b4637 100644 --- a/planetarium/serializers.py +++ b/planetarium/serializers.py @@ -17,30 +17,45 @@ class Meta: class AstronomyShowSerializer(serializers.ModelSerializer): - show_theme = ShowThemeSerializer(many=True) - class Meta: model = AstronomyShow fields = ["id", "title", "description", "show_theme"] +class AstronomyShowListSerializer(AstronomyShowSerializer): + show_theme = serializers.SlugRelatedField( + many=True, read_only=True, slug_field="name" + ) + + +class AstronomyShowRetrieveSerializer(AstronomyShowSerializer): + show_theme = ShowThemeSerializer(many=True) + + class PlanetariumDomeSerializer(serializers.ModelSerializer): class Meta: model = PlanetariumDome - fields = ["id", "name", "rows", "seats_in_row"] + fields = ["id", "name", "rows", "seats_in_row", "capacity"] class ShowSessionSerializer(serializers.ModelSerializer): - astronomy_show = AstronomyShowSerializer() - planetarium_dome = PlanetariumDomeSerializer() - class Meta: model = ShowSession fields = ["id", "astronomy_show", "planetarium_dome", "show_time"] +class ShowSessionListSerializer(ShowSessionSerializer): + astronomy_show = serializers.CharField(source="astronomy_show.title") + planetarium_dome = serializers.CharField(source="planetarium_dome.name") + + +class ShowSessionRetrieveSerializer(ShowSessionSerializer): + astronomy_show = AstronomyShowRetrieveSerializer() + planetarium_dome = PlanetariumDomeSerializer() + + class ReservationSerializer(serializers.ModelSerializer): - user = serializers.SlugRelatedField(read_only=True, slug_field="username") + user = serializers.CharField(source="user.username") class Meta: model = Reservation @@ -48,9 +63,20 @@ class Meta: class TicketSerializer(serializers.ModelSerializer): - show_session = ShowSessionSerializer() - reservation = ReservationSerializer() - class Meta: model = Ticket fields = ["id", "row", "seat", "show_session", "reservation"] + + +class TicketListSerializer(serializers.ModelSerializer): + user = serializers.CharField(source="reservation.user.username") + astronomy_show = serializers.CharField(source="show_session.astronomy_show.title") + + class Meta: + model = Ticket + fields = ["id", "row", "seat", "user", "astronomy_show"] + + +class TickerRetrieveSerializer(TicketSerializer): + show_session = ShowSessionRetrieveSerializer() + reservation = ReservationSerializer() diff --git a/planetarium/views.py b/planetarium/views.py index 98d6f0e..ab8aac6 100644 --- a/planetarium/views.py +++ b/planetarium/views.py @@ -14,7 +14,8 @@ PlanetariumDomeSerializer, ShowSessionSerializer, ReservationSerializer, - TicketSerializer, + TicketSerializer, AstronomyShowRetrieveSerializer, AstronomyShowListSerializer, ShowSessionListSerializer, + ShowSessionRetrieveSerializer, TicketListSerializer, TickerRetrieveSerializer, ) @@ -27,6 +28,18 @@ class AstronomyShowViewSet(viewsets.ModelViewSet): queryset = AstronomyShow.objects.all() serializer_class = AstronomyShowSerializer + def get_queryset(self): + queryset = self.queryset + if self.action in ["list", "retrieve"]: + return queryset.prefetch_related("show_theme") + + def get_serializer_class(self): + if self.action == "list": + return AstronomyShowListSerializer + if self.action == "retrieve": + return AstronomyShowRetrieveSerializer + return AstronomyShowSerializer + class PlanetariumDomeViewSet(viewsets.ModelViewSet): queryset = PlanetariumDome.objects.all() @@ -37,12 +50,48 @@ class ShowSessionViewSet(viewsets.ModelViewSet): queryset = ShowSession.objects.all() serializer_class = ShowSessionSerializer + def get_queryset(self): + queryset = self.queryset + if self.action in ["list", "retrieve"]: + return queryset.prefetch_related("astronomy_show", "planetarium_dome") + + def get_serializer_class(self): + if self.action == "list": + return ShowSessionListSerializer + if self.action == "retrieve": + return ShowSessionRetrieveSerializer + return ShowSessionSerializer + class ReservationViewSet(viewsets.ModelViewSet): queryset = Reservation.objects.all() serializer_class = ReservationSerializer + def get_queryset(self): + queryset = self.queryset + if self.request.user.is_authenticated: + queryset = queryset.filter(user=self.request.user) + if self.action == "list": + return queryset.prefetch_related("user") + return queryset + + def perform_create(self, serializer): + if self.request.user: + serializer.save(user=self.request.user) + class TicketViewSet(viewsets.ModelViewSet): queryset = Ticket.objects.all() serializer_class = TicketSerializer + + def get_queryset(self): + queryset = self.queryset + if self.action in ["list", "retrieve"]: + return queryset.prefetch_related("show_session", "reservation") + + def get_serializer_class(self): + if self.action == "list": + return TicketListSerializer + if self.action == "retrieve": + return TickerRetrieveSerializer + return TicketSerializer diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 6130d58..2faa7aa 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -9,7 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ - +from datetime import timedelta from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -138,6 +138,15 @@ "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(days=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=7), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, + "UPDATE_LAST_LOGIN": True, +} + SPECTACULAR_SETTINGS = { "TITLE": "Your Project API", "DESCRIPTION": "Your project description", diff --git a/planetarium_service/urls.py b/planetarium_service/urls.py index 08c6c2c..911a7ef 100644 --- a/planetarium_service/urls.py +++ b/planetarium_service/urls.py @@ -20,7 +20,7 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("api/planetarium", include("planetarium.urls"), name="planetarium"), + path("api/planetarium/", include("planetarium.urls"), name="planetarium"), path("api/accounts/", include("accounts.urls"), name="accounts"), path("api-auth/", include("rest_framework.urls")), ] From daa82eef88a25cc51ac58781b92d3f359221031a Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 23 Dec 2024 17:47:31 +0200 Subject: [PATCH 09/21] add debug toolbar, fix n+1 problem --- accounts/migrations/0001_initial.py | 2 +- planetarium/admin.py | 9 ++- planetarium/migrations/0001_initial.py | 13 +++- ...alter_astronomyshow_show_theme_and_more.py | 76 ------------------- planetarium/serializers.py | 13 ++-- planetarium/views.py | 37 +++++++-- planetarium_service/settings.py | 11 +-- planetarium_service/urls.py | 1 + requirements.txt | 1 + 9 files changed, 64 insertions(+), 99 deletions(-) delete mode 100644 planetarium/migrations/0002_alter_ticket_options_alter_astronomyshow_show_theme_and_more.py diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index 4cefd74..ddc25eb 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-23 10:03 +# Generated by Django 5.1.4 on 2024-12-23 14:52 import django.contrib.auth.models import django.contrib.auth.validators diff --git a/planetarium/admin.py b/planetarium/admin.py index 8c38f3f..da9843e 100644 --- a/planetarium/admin.py +++ b/planetarium/admin.py @@ -1,3 +1,10 @@ from django.contrib import admin -# Register your models here. +from planetarium.models import ShowTheme, AstronomyShow, PlanetariumDome, Reservation, ShowSession, Ticket + +admin.site.register(ShowTheme) +admin.site.register(AstronomyShow) +admin.site.register(PlanetariumDome) +admin.site.register(ShowSession) +admin.site.register(Reservation) +admin.site.register(Ticket) diff --git a/planetarium/migrations/0001_initial.py b/planetarium/migrations/0001_initial.py index 05dad7a..80fe52b 100644 --- a/planetarium/migrations/0001_initial.py +++ b/planetarium/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-23 10:03 +# Generated by Django 5.1.4 on 2024-12-23 14:52 import django.db.models.deletion from django.conf import settings @@ -79,6 +79,7 @@ class Migration(migrations.Migration): "user", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="reservations", to=settings.AUTH_USER_MODEL, ), ), @@ -101,6 +102,7 @@ class Migration(migrations.Migration): "astronomy_show", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="show_sessions", to="planetarium.astronomyshow", ), ), @@ -108,6 +110,7 @@ class Migration(migrations.Migration): "planetarium_dome", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="show_sessions", to="planetarium.planetariumdome", ), ), @@ -117,7 +120,7 @@ class Migration(migrations.Migration): model_name="astronomyshow", name="show_theme", field=models.ManyToManyField( - related_name="astronomy_shows", to="planetarium.showtheme" + blank=True, related_name="astronomy_shows", to="planetarium.showtheme" ), ), migrations.CreateModel( @@ -138,6 +141,7 @@ class Migration(migrations.Migration): "reservation", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", to="planetarium.reservation", ), ), @@ -145,9 +149,14 @@ class Migration(migrations.Migration): "show_session", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, + related_name="tickets", to="planetarium.showsession", ), ), ], + options={ + "ordering": ["row", "seat"], + "unique_together": {("row", "seat", "show_session")}, + }, ), ] diff --git a/planetarium/migrations/0002_alter_ticket_options_alter_astronomyshow_show_theme_and_more.py b/planetarium/migrations/0002_alter_ticket_options_alter_astronomyshow_show_theme_and_more.py deleted file mode 100644 index e77621a..0000000 --- a/planetarium/migrations/0002_alter_ticket_options_alter_astronomyshow_show_theme_and_more.py +++ /dev/null @@ -1,76 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-23 12:44 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("planetarium", "0001_initial"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterModelOptions( - name="ticket", - options={"ordering": ["row", "seat"]}, - ), - migrations.AlterField( - model_name="astronomyshow", - name="show_theme", - field=models.ManyToManyField( - blank=True, related_name="astronomy_shows", to="planetarium.showtheme" - ), - ), - migrations.AlterField( - model_name="reservation", - name="user", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="reservations", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AlterField( - model_name="showsession", - name="astronomy_show", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="show_sessions", - to="planetarium.astronomyshow", - ), - ), - migrations.AlterField( - model_name="showsession", - name="planetarium_dome", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="show_sessions", - to="planetarium.planetariumdome", - ), - ), - migrations.AlterField( - model_name="ticket", - name="reservation", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="tickets", - to="planetarium.reservation", - ), - ), - migrations.AlterField( - model_name="ticket", - name="show_session", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="tickets", - to="planetarium.showsession", - ), - ), - migrations.AlterUniqueTogether( - name="ticket", - unique_together={("row", "seat", "show_session")}, - ), - ] diff --git a/planetarium/serializers.py b/planetarium/serializers.py index c5b4637..d9b1a24 100644 --- a/planetarium/serializers.py +++ b/planetarium/serializers.py @@ -45,8 +45,8 @@ class Meta: class ShowSessionListSerializer(ShowSessionSerializer): - astronomy_show = serializers.CharField(source="astronomy_show.title") - planetarium_dome = serializers.CharField(source="planetarium_dome.name") + astronomy_show = serializers.CharField(source="astronomy_show.title", read_only=True) + planetarium_dome = serializers.CharField(source="planetarium_dome.name", read_only=True) class ShowSessionRetrieveSerializer(ShowSessionSerializer): @@ -55,7 +55,10 @@ class ShowSessionRetrieveSerializer(ShowSessionSerializer): class ReservationSerializer(serializers.ModelSerializer): - user = serializers.CharField(source="user.username") + user = serializers.CharField(source="user.email", read_only=True) + created_at = serializers.DateTimeField( + format="%Y-%m-%d %H:%M:%S" + ) class Meta: model = Reservation @@ -69,8 +72,8 @@ class Meta: class TicketListSerializer(serializers.ModelSerializer): - user = serializers.CharField(source="reservation.user.username") - astronomy_show = serializers.CharField(source="show_session.astronomy_show.title") + user = serializers.CharField(source="reservation.user.email", read_only=True) + astronomy_show = serializers.CharField(source="show_session.astronomy_show.title", read_only=True) class Meta: model = Ticket diff --git a/planetarium/views.py b/planetarium/views.py index ab8aac6..2be399d 100644 --- a/planetarium/views.py +++ b/planetarium/views.py @@ -1,4 +1,4 @@ -from rest_framework import viewsets +from rest_framework import viewsets, mixins from planetarium.models import ( ShowTheme, @@ -15,8 +15,7 @@ ShowSessionSerializer, ReservationSerializer, TicketSerializer, AstronomyShowRetrieveSerializer, AstronomyShowListSerializer, ShowSessionListSerializer, - ShowSessionRetrieveSerializer, TicketListSerializer, TickerRetrieveSerializer, -) + ShowSessionRetrieveSerializer, TicketListSerializer, TickerRetrieveSerializer, ) class ShowThemeViewSet(viewsets.ModelViewSet): @@ -52,8 +51,10 @@ class ShowSessionViewSet(viewsets.ModelViewSet): def get_queryset(self): queryset = self.queryset - if self.action in ["list", "retrieve"]: + if self.action == "list": return queryset.prefetch_related("astronomy_show", "planetarium_dome") + if self.action == "retrieve": + return queryset.prefetch_related("astronomy_show__show_theme", "planetarium_dome") def get_serializer_class(self): if self.action == "list": @@ -80,14 +81,36 @@ def perform_create(self, serializer): serializer.save(user=self.request.user) -class TicketViewSet(viewsets.ModelViewSet): +class TicketViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin): queryset = Ticket.objects.all() serializer_class = TicketSerializer def get_queryset(self): queryset = self.queryset - if self.action in ["list", "retrieve"]: - return queryset.prefetch_related("show_session", "reservation") + if self.action == "list": + return queryset.select_related( + "show_session", + "show_session__astronomy_show", + "reservation", + "reservation__user", + ) + # return queryset.select_related( + # "show_session__astronomy_show", + # "reservation__user" + # ).select_related( + # "show_session__astronomy_show__show_theme", + # ).prefetch_related( + # "show_session__planetarium_dome", + # ) + # return self.queryset.select_related( + # "show_session", # ForeignKey до ShowSession + # "show_session__astronomy_show", # Зв'язок до AstronomyShow через ShowSession + # "reservation", # ForeignKey до Reservation + # "reservation__user" # Зв'язок до User через Reservation + # ).annotate( + # astronomy_show_title=F("show_session__astronomy_show__title"), # Анотоване поле для title + # reservation_username=F("reservation__user__username") # Анотоване поле для username + # ) def get_serializer_class(self): if self.action == "list": diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 2faa7aa..8d113e7 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -15,7 +15,6 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ @@ -27,10 +26,10 @@ ALLOWED_HOSTS = [] - # Application definition INSTALLED_APPS = [ + "debug_toolbar", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -54,6 +53,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "debug_toolbar.middleware.DebugToolbarMiddleware", ] ROOT_URLCONF = "planetarium_service.urls" @@ -76,7 +76,6 @@ WSGI_APPLICATION = "planetarium_service.wsgi.application" - # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases @@ -87,7 +86,6 @@ } } - # Password validation # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators @@ -106,7 +104,6 @@ }, ] - # Internationalization # https://docs.djangoproject.com/en/5.1/topics/i18n/ @@ -118,7 +115,6 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ @@ -131,6 +127,8 @@ AUTH_USER_MODEL = "accounts.User" +INTERNAL_IPS = ["127.0.0.1", ] + REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", @@ -138,7 +136,6 @@ "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } - SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(days=1), "REFRESH_TOKEN_LIFETIME": timedelta(days=7), diff --git a/planetarium_service/urls.py b/planetarium_service/urls.py index 911a7ef..8860ecf 100644 --- a/planetarium_service/urls.py +++ b/planetarium_service/urls.py @@ -23,4 +23,5 @@ path("api/planetarium/", include("planetarium.urls"), name="planetarium"), path("api/accounts/", include("accounts.urls"), name="accounts"), path("api-auth/", include("rest_framework.urls")), + path("__debug__/", include("debug_toolbar.urls")), ] diff --git a/requirements.txt b/requirements.txt index 06b7a8e..1f90954 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ black==24.10.0 click==8.1.8 colorama==0.4.6 Django==5.1.4 +django-debug-toolbar==4.4.6 django-filter==24.3 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 From a05b85a2c7f8b2bf65cd96b94d3e46755e0d52f6 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 23 Dec 2024 17:51:04 +0200 Subject: [PATCH 10/21] refactor user model to use email for authentication --- accounts/admin.py | 21 +++++++++- ..._managers_remove_user_username_and_more.py | 31 ++++++++++++++ accounts/models.py | 41 ++++++++++++++++++- 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 accounts/migrations/0002_alter_user_managers_remove_user_username_and_more.py diff --git a/accounts/admin.py b/accounts/admin.py index 6c9a82c..f9261ca 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,6 +1,25 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin +from django.utils.translation import gettext_lazy as _ from accounts.models import User -admin.site.register(User) +@admin.register(User) +class UserAdmin(DjangoUserAdmin): + fieldsets = ( + (None, {"fields": ("email", "password")}), + (_("Personal info"), {"fields": ("first_name", "last_name")}), + (_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", + "groups", "user_permissions")}), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), + ) + add_fieldsets = ( + (None, { + "classes": ("wide",), + "fields": ("email", "password1", "password2"), + }), + ) + list_display = ("email", "first_name", "last_name", "is_staff") + search_fields = ("email", "first_name", "last_name") + ordering = ("email",) diff --git a/accounts/migrations/0002_alter_user_managers_remove_user_username_and_more.py b/accounts/migrations/0002_alter_user_managers_remove_user_username_and_more.py new file mode 100644 index 0000000..0aac443 --- /dev/null +++ b/accounts/migrations/0002_alter_user_managers_remove_user_username_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.4 on 2024-12-23 15:49 + +import accounts.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0001_initial"), + ] + + operations = [ + migrations.AlterModelManagers( + name="user", + managers=[ + ("objects", accounts.models.UserManager()), + ], + ), + migrations.RemoveField( + model_name="user", + name="username", + ), + migrations.AlterField( + model_name="user", + name="email", + field=models.EmailField( + max_length=254, unique=True, verbose_name="email address" + ), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 3d30525..fbc3621 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,5 +1,44 @@ +from django.contrib.auth.base_user import BaseUserManager from django.contrib.auth.models import AbstractUser +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class UserManager(BaseUserManager): + use_in_migrations = True + + def _create_user(self, email, password, **extra_fields): + if not email: + raise ValueError("The given email must be set") + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_user(self, email, password=None, **extra_fields): + extra_fields.setdefault("is_staff", False) + extra_fields.setdefault("is_superuser", False) + return self._create_user(email, password, **extra_fields) + + def create_superuser(self, email, password, **extra_fields): + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + + if extra_fields.get("is_staff") is not True: + raise ValueError("Superuser must have is_staff=True.") + + if extra_fields.get("is_superuser") is not True: + raise ValueError("Superuser must have is_superuser=True.") + + return self._create_user(email, password, **extra_fields) class User(AbstractUser): - pass + username = None + email = models.EmailField(_("email address"), unique=True) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + objects = UserManager() From aae1e5ce034211064c3b3c4174eb10f4ccc15d20 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 23 Dec 2024 18:46:45 +0200 Subject: [PATCH 11/21] refactor user model to use uuid --- accounts/admin.py | 25 ++++++++++++++----- .../0003_alter_user_options_alter_user_id.py | 25 +++++++++++++++++++ accounts/models.py | 4 ++- base/__init__.py | 0 base/migrations/__init__.py | 0 base/models.py | 10 ++++++++ planetarium/admin.py | 9 ++++++- planetarium/models.py | 24 +++++++++++++----- planetarium/serializers.py | 16 +++++++----- planetarium/views.py | 18 ++++++++++--- planetarium_service/settings.py | 8 +++--- 11 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 accounts/migrations/0003_alter_user_options_alter_user_id.py create mode 100644 base/__init__.py create mode 100644 base/migrations/__init__.py create mode 100644 base/models.py diff --git a/accounts/admin.py b/accounts/admin.py index f9261ca..a02656e 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -10,15 +10,28 @@ class UserAdmin(DjangoUserAdmin): fieldsets = ( (None, {"fields": ("email", "password")}), (_("Personal info"), {"fields": ("first_name", "last_name")}), - (_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", - "groups", "user_permissions")}), + ( + _("Permissions"), + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ) + }, + ), (_("Important dates"), {"fields": ("last_login", "date_joined")}), ) add_fieldsets = ( - (None, { - "classes": ("wide",), - "fields": ("email", "password1", "password2"), - }), + ( + None, + { + "classes": ("wide",), + "fields": ("email", "password1", "password2"), + }, + ), ) list_display = ("email", "first_name", "last_name", "is_staff") search_fields = ("email", "first_name", "last_name") diff --git a/accounts/migrations/0003_alter_user_options_alter_user_id.py b/accounts/migrations/0003_alter_user_options_alter_user_id.py new file mode 100644 index 0000000..47ba128 --- /dev/null +++ b/accounts/migrations/0003_alter_user_options_alter_user_id.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.4 on 2024-12-23 16:44 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0002_alter_user_managers_remove_user_username_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="user", + options={}, + ), + migrations.AlterField( + model_name="user", + name="id", + field=models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index fbc3621..f9d9c78 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -3,6 +3,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from base.models import UUIDBaseModel + class UserManager(BaseUserManager): use_in_migrations = True @@ -34,7 +36,7 @@ def create_superuser(self, email, password, **extra_fields): return self._create_user(email, password, **extra_fields) -class User(AbstractUser): +class User(UUIDBaseModel, AbstractUser): username = None email = models.EmailField(_("email address"), unique=True) diff --git a/base/__init__.py b/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/base/migrations/__init__.py b/base/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/base/models.py b/base/models.py new file mode 100644 index 0000000..3e4c84f --- /dev/null +++ b/base/models.py @@ -0,0 +1,10 @@ +import uuid + +from django.db import models + + +class UUIDBaseModel(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + class Meta: + abstract = True diff --git a/planetarium/admin.py b/planetarium/admin.py index da9843e..2750440 100644 --- a/planetarium/admin.py +++ b/planetarium/admin.py @@ -1,6 +1,13 @@ from django.contrib import admin -from planetarium.models import ShowTheme, AstronomyShow, PlanetariumDome, Reservation, ShowSession, Ticket +from planetarium.models import ( + ShowTheme, + AstronomyShow, + PlanetariumDome, + Reservation, + ShowSession, + Ticket, +) admin.site.register(ShowTheme) admin.site.register(AstronomyShow) diff --git a/planetarium/models.py b/planetarium/models.py index 8936388..17c68cb 100644 --- a/planetarium/models.py +++ b/planetarium/models.py @@ -12,7 +12,9 @@ def __str__(self): class AstronomyShow(models.Model): title = models.CharField(max_length=255) description = models.TextField() - show_theme = models.ManyToManyField(ShowTheme, blank=True, related_name="astronomy_shows") + show_theme = models.ManyToManyField( + ShowTheme, blank=True, related_name="astronomy_shows" + ) def __str__(self): return self.title @@ -32,8 +34,12 @@ def __str__(self): class ShowSession(models.Model): - astronomy_show = models.ForeignKey(AstronomyShow, on_delete=models.CASCADE, related_name="show_sessions") - planetarium_dome = models.ForeignKey(PlanetariumDome, on_delete=models.CASCADE, related_name="show_sessions") + astronomy_show = models.ForeignKey( + AstronomyShow, on_delete=models.CASCADE, related_name="show_sessions" + ) + planetarium_dome = models.ForeignKey( + PlanetariumDome, on_delete=models.CASCADE, related_name="show_sessions" + ) show_time = models.DateTimeField() def __str__(self): @@ -42,7 +48,9 @@ def __str__(self): class Reservation(models.Model): created_at = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="reservations") + user = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="reservations" + ) def __str__(self): return f"{self.user.username} - (created: {self.created_at})" @@ -51,8 +59,12 @@ def __str__(self): class Ticket(models.Model): row = models.IntegerField() seat = models.IntegerField() - show_session = models.ForeignKey(ShowSession, on_delete=models.CASCADE, related_name="tickets") - reservation = models.ForeignKey(Reservation, on_delete=models.CASCADE, related_name="tickets") + show_session = models.ForeignKey( + ShowSession, on_delete=models.CASCADE, related_name="tickets" + ) + reservation = models.ForeignKey( + Reservation, on_delete=models.CASCADE, related_name="tickets" + ) class Meta: unique_together = ("row", "seat", "show_session") diff --git a/planetarium/serializers.py b/planetarium/serializers.py index d9b1a24..34e5ce1 100644 --- a/planetarium/serializers.py +++ b/planetarium/serializers.py @@ -45,8 +45,12 @@ class Meta: class ShowSessionListSerializer(ShowSessionSerializer): - astronomy_show = serializers.CharField(source="astronomy_show.title", read_only=True) - planetarium_dome = serializers.CharField(source="planetarium_dome.name", read_only=True) + astronomy_show = serializers.CharField( + source="astronomy_show.title", read_only=True + ) + planetarium_dome = serializers.CharField( + source="planetarium_dome.name", read_only=True + ) class ShowSessionRetrieveSerializer(ShowSessionSerializer): @@ -56,9 +60,7 @@ class ShowSessionRetrieveSerializer(ShowSessionSerializer): class ReservationSerializer(serializers.ModelSerializer): user = serializers.CharField(source="user.email", read_only=True) - created_at = serializers.DateTimeField( - format="%Y-%m-%d %H:%M:%S" - ) + created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S") class Meta: model = Reservation @@ -73,7 +75,9 @@ class Meta: class TicketListSerializer(serializers.ModelSerializer): user = serializers.CharField(source="reservation.user.email", read_only=True) - astronomy_show = serializers.CharField(source="show_session.astronomy_show.title", read_only=True) + astronomy_show = serializers.CharField( + source="show_session.astronomy_show.title", read_only=True + ) class Meta: model = Ticket diff --git a/planetarium/views.py b/planetarium/views.py index 2be399d..752a7c8 100644 --- a/planetarium/views.py +++ b/planetarium/views.py @@ -14,8 +14,14 @@ PlanetariumDomeSerializer, ShowSessionSerializer, ReservationSerializer, - TicketSerializer, AstronomyShowRetrieveSerializer, AstronomyShowListSerializer, ShowSessionListSerializer, - ShowSessionRetrieveSerializer, TicketListSerializer, TickerRetrieveSerializer, ) + TicketSerializer, + AstronomyShowRetrieveSerializer, + AstronomyShowListSerializer, + ShowSessionListSerializer, + ShowSessionRetrieveSerializer, + TicketListSerializer, + TickerRetrieveSerializer, +) class ShowThemeViewSet(viewsets.ModelViewSet): @@ -54,7 +60,9 @@ def get_queryset(self): if self.action == "list": return queryset.prefetch_related("astronomy_show", "planetarium_dome") if self.action == "retrieve": - return queryset.prefetch_related("astronomy_show__show_theme", "planetarium_dome") + return queryset.prefetch_related( + "astronomy_show__show_theme", "planetarium_dome" + ) def get_serializer_class(self): if self.action == "list": @@ -81,7 +89,9 @@ def perform_create(self, serializer): serializer.save(user=self.request.user) -class TicketViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin): +class TicketViewSet( + viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin +): queryset = Ticket.objects.all() serializer_class = TicketSerializer diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 8d113e7..a2471d7 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -9,6 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ + from datetime import timedelta from pathlib import Path @@ -36,13 +37,12 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "rest_framework", "drf_spectacular", "rest_framework_simplejwt", - "accounts", "planetarium", + "base" ] MIDDLEWARE = [ @@ -127,7 +127,9 @@ AUTH_USER_MODEL = "accounts.User" -INTERNAL_IPS = ["127.0.0.1", ] +INTERNAL_IPS = [ + "127.0.0.1", +] REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( From aae871ac71e0ad8073b382e26ca2ca69bdb8b1cf Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 23 Dec 2024 21:56:53 +0200 Subject: [PATCH 12/21] Refactor search, validation, and API schema. --- planetarium/models.py | 2 +- planetarium/serializers.py | 17 +++++++++++++++++ planetarium/views.py | 12 ++++++++---- planetarium_service/settings.py | 8 +++++--- planetarium_service/urls.py | 5 ++++- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/planetarium/models.py b/planetarium/models.py index 17c68cb..4483fbd 100644 --- a/planetarium/models.py +++ b/planetarium/models.py @@ -53,7 +53,7 @@ class Reservation(models.Model): ) def __str__(self): - return f"{self.user.username} - (created: {self.created_at})" + return f"{self.user.email} - (created: {self.created_at})" class Ticket(models.Model): diff --git a/planetarium/serializers.py b/planetarium/serializers.py index 34e5ce1..ae7a7c3 100644 --- a/planetarium/serializers.py +++ b/planetarium/serializers.py @@ -72,6 +72,23 @@ class Meta: model = Ticket fields = ["id", "row", "seat", "show_session", "reservation"] + def validate(self, data): + row = data.get("row") + seat = data.get("seat") + show_session = data.get("show_session") + dome = show_session.planetarium_dome + if not (0 < row < dome.rows): + raise serializers.ValidationError({"row": f"Row {row} is out of range."}) + if not (0 < seat < dome.seats_in_row): + raise serializers.ValidationError( + {"seat": f"Seat {seat} is out of range."} + ) + if Ticket.objects.filter(row=row, seat=seat).exists(): + raise serializers.ValidationError( + {"seat": f"Seat {seat} is already taken."} + ) + return data + class TicketListSerializer(serializers.ModelSerializer): user = serializers.CharField(source="reservation.user.email", read_only=True) diff --git a/planetarium/views.py b/planetarium/views.py index 752a7c8..b98c92d 100644 --- a/planetarium/views.py +++ b/planetarium/views.py @@ -1,4 +1,4 @@ -from rest_framework import viewsets, mixins +from rest_framework import viewsets, filters from planetarium.models import ( ShowTheme, @@ -32,6 +32,8 @@ class ShowThemeViewSet(viewsets.ModelViewSet): class AstronomyShowViewSet(viewsets.ModelViewSet): queryset = AstronomyShow.objects.all() serializer_class = AstronomyShowSerializer + filter_backends = [filters.SearchFilter] + search_fields = ["title", "description"] def get_queryset(self): queryset = self.queryset @@ -75,6 +77,8 @@ def get_serializer_class(self): class ReservationViewSet(viewsets.ModelViewSet): queryset = Reservation.objects.all() serializer_class = ReservationSerializer + filter_backends = [filters.SearchFilter] + search_fields = ["user__email"] def get_queryset(self): queryset = self.queryset @@ -89,11 +93,11 @@ def perform_create(self, serializer): serializer.save(user=self.request.user) -class TicketViewSet( - viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin -): +class TicketViewSet(viewsets.ModelViewSet): queryset = Ticket.objects.all() serializer_class = TicketSerializer + filter_backends = [filters.SearchFilter] + search_fields = ["row", "seat", "reservation__user__email", "show_session__astronomy_show__title"] def get_queryset(self): queryset = self.queryset diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index a2471d7..bdfd4cb 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -38,11 +38,12 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", + "django_filters", "drf_spectacular", "rest_framework_simplejwt", "accounts", "planetarium", - "base" + "base", ] MIDDLEWARE = [ @@ -136,6 +137,7 @@ "rest_framework_simplejwt.authentication.JWTAuthentication", ), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), } SIMPLE_JWT = { @@ -147,8 +149,8 @@ } SPECTACULAR_SETTINGS = { - "TITLE": "Your Project API", - "DESCRIPTION": "Your project description", + "TITLE": "Planetarium API", + "DESCRIPTION": "Book tickets for the planetarium.", "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, } diff --git a/planetarium_service/urls.py b/planetarium_service/urls.py index 8860ecf..100243b 100644 --- a/planetarium_service/urls.py +++ b/planetarium_service/urls.py @@ -17,11 +17,14 @@ from django.contrib import admin from django.urls import path, include +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView urlpatterns = [ path("admin/", admin.site.urls), path("api/planetarium/", include("planetarium.urls"), name="planetarium"), path("api/accounts/", include("accounts.urls"), name="accounts"), - path("api-auth/", include("rest_framework.urls")), + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), + path('api/schema/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'), + path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), path("__debug__/", include("debug_toolbar.urls")), ] From a9f86305d3b71da6ca562391c15d2f576e1fcc39 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 24 Dec 2024 12:47:12 +0200 Subject: [PATCH 13/21] add custom permissions, paginations and throttling --- planetarium/permissions.py | 11 +++++++++++ planetarium_service/settings.py | 13 +++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 planetarium/permissions.py diff --git a/planetarium/permissions.py b/planetarium/permissions.py new file mode 100644 index 0000000..3a046c2 --- /dev/null +++ b/planetarium/permissions.py @@ -0,0 +1,11 @@ +from rest_framework.permissions import BasePermission, SAFE_METHODS + + +class IsAdminAllORIsAuthenticatedOrReadOnly(BasePermission): + def has_permission(self, request, view): + return bool( + request.method in SAFE_METHODS and + request.user and request.user.is_authenticated + ) or ( + request.user and request.user.is_staff + ) diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index bdfd4cb..b5d3de6 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -138,6 +138,19 @@ ), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), + "DEFAULT_THROTTLE_CLASSES": ( + "rest_framework.throttling.AnonRateThrottle", + "rest_framework.throttling.UserRateThrottle", + ), + "DEFAULT_THROTTLE_RATES": { + "anon": "100/day", + "user": "1000/day" + }, + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "PAGE_SIZE": 10, + "DEFAULT_PERMISSION_CLASSES": ( + "planetarium.permissions.IsAdminAllORIsAuthenticatedOrReadOnly", + ) } SIMPLE_JWT = { From e4a075c845679ef238d7e4f44f9bae6459bf8387 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 24 Dec 2024 16:21:05 +0200 Subject: [PATCH 14/21] add docker integration and caching --- .dockerignore | 34 +++++++++++++++++++++++ .env.sample | 1 + .gitignore | 3 +- Dockerfile | 22 +++++++++++++++ README.Docker.md | 22 +++++++++++++++ docker-compose.yaml | 49 +++++++++++++++++++++++++++++++++ planetarium/signals.py | 10 +++++++ planetarium/views.py | 26 +++++------------ planetarium_service/settings.py | 14 ++++++++-- requirements.txt | 3 ++ 10 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.sample create mode 100644 Dockerfile create mode 100644 README.Docker.md create mode 100644 docker-compose.yaml create mode 100644 planetarium/signals.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03a268b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..211ba5e --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +SECRET_KEY=SECRET_KEY \ No newline at end of file diff --git a/.gitignore b/.gitignore index 14daecd..9b74c87 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .venv .vscode db.sqlite3 -__pycache__ \ No newline at end of file +__pycache__ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4c1068c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:alpine + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install --upgrade pip +RUN pip install -r requirements.txt + +COPY . . + +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + appuser + +USER appuser diff --git a/README.Docker.md b/README.Docker.md new file mode 100644 index 0000000..6dae561 --- /dev/null +++ b/README.Docker.md @@ -0,0 +1,22 @@ +### Building and running your application + +When you're ready, start your application by running: +`docker compose up --build`. + +Your application will be available at http://localhost:8000. + +### Deploying your application to the cloud + +First, build your image, e.g.: `docker build -t myapp .`. +If your cloud uses a different CPU architecture than your development +machine (e.g., you are on a Mac M1 and your cloud provider is amd64), +you'll want to build the image for that platform, e.g.: +`docker build --platform=linux/amd64 -t myapp .`. + +Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. + +Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) +docs for more detail on building and pushing. + +### References +* [Docker's Python guide](https://docs.docker.com/language/python/) \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..5fa88b8 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,49 @@ +services: + app: + build: + context: . + command: + "python manage.py runserver 0.0.0.0:8000" + ports: + - "8000:8000" + volumes: + - ./:/app + env_file: + - .env + depends_on: + - redis + restart: always + + # db: + # image: postgres:16.0-alpine + # restart: always + # user: postgres + # secrets: + # - db-password + # volumes: + # - db-data:/var/lib/postgresql/data + # environment: + # - POSTGRES_DB=example + # - POSTGRES_PASSWORD_FILE=/run/secrets/db-password + # expose: + # - 5432 + # healthcheck: + # test: [ "CMD", "pg_isready" ] + # interval: 10s + # timeout: 5s + # retries: 5 + redis: + image: redis:alpine + + redisinsight: + image: redislabs/redisinsight:latest + ports: + - "5540:5540" + depends_on: + - redis + +# volumes: +# db-data: +# secrets: +# db-password: +# file: db/password.txt diff --git a/planetarium/signals.py b/planetarium/signals.py new file mode 100644 index 0000000..e6b33e9 --- /dev/null +++ b/planetarium/signals.py @@ -0,0 +1,10 @@ +from django.core.cache import cache +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + +from planetarium.models import Ticket + + +@receiver([post_delete, post_save], sender=Ticket) +def invalidate_ticket_cache(sender, instance, **kwargs): + cache.delete_pattern("*ticket_view*") diff --git a/planetarium/views.py b/planetarium/views.py index b98c92d..93bf3e1 100644 --- a/planetarium/views.py +++ b/planetarium/views.py @@ -1,4 +1,7 @@ +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page from rest_framework import viewsets, filters +from django.utils.decorators import method_decorator from planetarium.models import ( ShowTheme, @@ -103,28 +106,9 @@ def get_queryset(self): queryset = self.queryset if self.action == "list": return queryset.select_related( - "show_session", "show_session__astronomy_show", - "reservation", "reservation__user", ) - # return queryset.select_related( - # "show_session__astronomy_show", - # "reservation__user" - # ).select_related( - # "show_session__astronomy_show__show_theme", - # ).prefetch_related( - # "show_session__planetarium_dome", - # ) - # return self.queryset.select_related( - # "show_session", # ForeignKey до ShowSession - # "show_session__astronomy_show", # Зв'язок до AstronomyShow через ShowSession - # "reservation", # ForeignKey до Reservation - # "reservation__user" # Зв'язок до User через Reservation - # ).annotate( - # astronomy_show_title=F("show_session__astronomy_show__title"), # Анотоване поле для title - # reservation_username=F("reservation__user__username") # Анотоване поле для username - # ) def get_serializer_class(self): if self.action == "list": @@ -132,3 +116,7 @@ def get_serializer_class(self): if self.action == "retrieve": return TickerRetrieveSerializer return TicketSerializer + + @method_decorator(cache_page(60 * 5, key_prefix="ticket_view")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index b5d3de6..c189284 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -9,7 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ - +import os from datetime import timedelta from pathlib import Path @@ -20,7 +20,7 @@ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-02eb(otr$%qq@%ogajlag9ef#l$ie870!4sf+q&znj@pra1(e&" +SECRET_KEY = os.getenv("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -167,3 +167,13 @@ "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, } + +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://redis:6379/1", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + } +} diff --git a/requirements.txt b/requirements.txt index 1f90954..929bb6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ colorama==0.4.6 Django==5.1.4 django-debug-toolbar==4.4.6 django-filter==24.3 +django-redis==5.4.0 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 drf-spectacular==0.28.0 @@ -18,7 +19,9 @@ packaging==24.2 pathspec==0.12.1 platformdirs==4.3.6 PyJWT==2.10.1 +python-dotenv==1.0.1 PyYAML==6.0.2 +redis==5.0.1 referencing==0.35.1 rpds-py==0.22.3 sqlparse==0.5.3 From 2efa772af2cf3267d6a49cddc199d4811df22963 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 24 Dec 2024 18:08:09 +0200 Subject: [PATCH 15/21] fix security key and add some first tests --- planetarium/signals.py | 2 +- planetarium/tests.py | 97 ++++++++++++++++++++++++++++++++- planetarium_service/settings.py | 3 + 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/planetarium/signals.py b/planetarium/signals.py index e6b33e9..351a489 100644 --- a/planetarium/signals.py +++ b/planetarium/signals.py @@ -7,4 +7,4 @@ @receiver([post_delete, post_save], sender=Ticket) def invalidate_ticket_cache(sender, instance, **kwargs): - cache.delete_pattern("*ticket_view*") + cache.delete("ticket_view") diff --git a/planetarium/tests.py b/planetarium/tests.py index 7ce503c..c4caa59 100644 --- a/planetarium/tests.py +++ b/planetarium/tests.py @@ -1,3 +1,98 @@ +from django.contrib.auth import get_user_model from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient -# Create your tests here. +from planetarium.models import ShowTheme, AstronomyShow, PlanetariumDome, ShowSession, Reservation, Ticket +from planetarium.serializers import ( + ShowThemeSerializer, + AstronomyShowListSerializer, + PlanetariumDomeSerializer, +) + +class ModelsTests(TestCase): + def setUp(self): + self.user = get_user_model().objects.create_user( + email="test@test.com", + password="testpass123" + ) + self.show_theme = ShowTheme.objects.create(name="Test Theme") + self.astronomy_show = AstronomyShow.objects.create( + title="Test Show", + description="Test Description" + ) + self.astronomy_show.show_theme.add(self.show_theme) + self.planetarium_dome = PlanetariumDome.objects.create( + name="Test Dome", + rows=20, + seats_in_row=30 + ) + self.show_session = ShowSession.objects.create( + astronomy_show=self.astronomy_show, + planetarium_dome=self.planetarium_dome, + show_time="2024-03-20 12:00:00" + ) + self.reservation = Reservation.objects.create(user=self.user) + self.ticket = Ticket.objects.create( + row=1, + seat=1, + show_session=self.show_session, + reservation=self.reservation + ) + + def test_show_theme_str(self): + self.assertEqual(str(self.show_theme), self.show_theme.name) + + def test_astronomy_show_str(self): + self.assertEqual(str(self.astronomy_show), self.astronomy_show.title) + +class APITests(TestCase): + def setUp(self): + self.client = APIClient() + self.user = get_user_model().objects.create_user( + email="test@test.com", + password="testpass123" + ) + self.client.force_authenticate(self.user) + + def test_show_theme_list(self): + ShowTheme.objects.create(name="Test Theme") + response = self.client.get(reverse("planetarium:showtheme-list")) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + def test_create_show_theme(self): + payload = {"name": "New Theme"} + response = self.client.post( + reverse("planetarium:showtheme-list"), + payload + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + exists = ShowTheme.objects.filter( + name=payload["name"] + ).exists() + self.assertTrue(exists) + +class SerializerTests(TestCase): + def test_show_theme_serializer(self): + show_theme = ShowTheme.objects.create(name="Test Theme") + serializer = ShowThemeSerializer(show_theme) + + self.assertEqual(set(serializer.data.keys()), {"id", "name"}) + self.assertEqual(serializer.data["name"], "Test Theme") + + def test_astronomy_show_list_serializer(self): + show_theme = ShowTheme.objects.create(name="Test Theme") + astronomy_show = AstronomyShow.objects.create( + title="Test Show", + description="Test Description" + ) + astronomy_show.show_theme.add(show_theme) + + serializer = AstronomyShowListSerializer(astronomy_show) + self.assertEqual(set(serializer.data.keys()), {"id", "title", "description", "show_theme"}) + self.assertEqual(serializer.data["title"], "Test Show") + self.assertEqual(serializer.data["show_theme"], ["Test Theme"]) diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index c189284..22db116 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -12,6 +12,9 @@ import os from datetime import timedelta from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent From 439137177d6c91744d0849942081446a845af4b6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 26 Dec 2024 13:47:42 +0200 Subject: [PATCH 16/21] refactor project structere, rewrite tests, add filtering for the ticket, add makefile --- Makefile | 17 ++ planetarium/apps.py | 3 + planetarium/migrations/0001_initial.py | 2 +- planetarium/permissions.py | 9 +- planetarium/serializers.py | 4 +- planetarium/signals.py | 2 +- planetarium/tests.py | 98 ----------- planetarium/tests/__init__.py | 0 planetarium/tests/api_ticket_tests.py | 225 +++++++++++++++++++++++++ planetarium/urls.py | 12 +- planetarium/views.py | 29 +++- planetarium_service/settings.py | 12 +- planetarium_service/urls.py | 20 ++- 13 files changed, 306 insertions(+), 127 deletions(-) create mode 100644 Makefile delete mode 100644 planetarium/tests.py create mode 100644 planetarium/tests/__init__.py create mode 100644 planetarium/tests/api_ticket_tests.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3852528 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: test run migrate docker docker-test + +test: + python manage.py test + +run: + python manage.py runserver + +migrate: + python manage.py makemigrations + python manage.py migrate + +docker: + docker-compose up --build + +docker-test: + docker-compose run app sh -c "python manage.py test" diff --git a/planetarium/apps.py b/planetarium/apps.py index 9357546..b2d8c28 100644 --- a/planetarium/apps.py +++ b/planetarium/apps.py @@ -4,3 +4,6 @@ class PlanetariumConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "planetarium" + + def ready(self): + import planetarium.signals diff --git a/planetarium/migrations/0001_initial.py b/planetarium/migrations/0001_initial.py index 80fe52b..867d488 100644 --- a/planetarium/migrations/0001_initial.py +++ b/planetarium/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-23 14:52 +# Generated by Django 5.1.4 on 2024-12-25 15:41 import django.db.models.deletion from django.conf import settings diff --git a/planetarium/permissions.py b/planetarium/permissions.py index 3a046c2..10a1199 100644 --- a/planetarium/permissions.py +++ b/planetarium/permissions.py @@ -4,8 +4,7 @@ class IsAdminAllORIsAuthenticatedOrReadOnly(BasePermission): def has_permission(self, request, view): return bool( - request.method in SAFE_METHODS and - request.user and request.user.is_authenticated - ) or ( - request.user and request.user.is_staff - ) + request.method in SAFE_METHODS + and request.user + and request.user.is_authenticated + ) or (request.user and request.user.is_staff) diff --git a/planetarium/serializers.py b/planetarium/serializers.py index ae7a7c3..4f6eaba 100644 --- a/planetarium/serializers.py +++ b/planetarium/serializers.py @@ -80,9 +80,7 @@ def validate(self, data): if not (0 < row < dome.rows): raise serializers.ValidationError({"row": f"Row {row} is out of range."}) if not (0 < seat < dome.seats_in_row): - raise serializers.ValidationError( - {"seat": f"Seat {seat} is out of range."} - ) + raise serializers.ValidationError({"seat": f"Seat {seat} is out of range."}) if Ticket.objects.filter(row=row, seat=seat).exists(): raise serializers.ValidationError( {"seat": f"Seat {seat} is already taken."} diff --git a/planetarium/signals.py b/planetarium/signals.py index 351a489..d098181 100644 --- a/planetarium/signals.py +++ b/planetarium/signals.py @@ -7,4 +7,4 @@ @receiver([post_delete, post_save], sender=Ticket) def invalidate_ticket_cache(sender, instance, **kwargs): - cache.delete("ticket_view") + cache.delete_pattern("ticket_view:*") diff --git a/planetarium/tests.py b/planetarium/tests.py deleted file mode 100644 index c4caa59..0000000 --- a/planetarium/tests.py +++ /dev/null @@ -1,98 +0,0 @@ -from django.contrib.auth import get_user_model -from django.test import TestCase -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient - -from planetarium.models import ShowTheme, AstronomyShow, PlanetariumDome, ShowSession, Reservation, Ticket -from planetarium.serializers import ( - ShowThemeSerializer, - AstronomyShowListSerializer, - PlanetariumDomeSerializer, -) - -class ModelsTests(TestCase): - def setUp(self): - self.user = get_user_model().objects.create_user( - email="test@test.com", - password="testpass123" - ) - self.show_theme = ShowTheme.objects.create(name="Test Theme") - self.astronomy_show = AstronomyShow.objects.create( - title="Test Show", - description="Test Description" - ) - self.astronomy_show.show_theme.add(self.show_theme) - self.planetarium_dome = PlanetariumDome.objects.create( - name="Test Dome", - rows=20, - seats_in_row=30 - ) - self.show_session = ShowSession.objects.create( - astronomy_show=self.astronomy_show, - planetarium_dome=self.planetarium_dome, - show_time="2024-03-20 12:00:00" - ) - self.reservation = Reservation.objects.create(user=self.user) - self.ticket = Ticket.objects.create( - row=1, - seat=1, - show_session=self.show_session, - reservation=self.reservation - ) - - def test_show_theme_str(self): - self.assertEqual(str(self.show_theme), self.show_theme.name) - - def test_astronomy_show_str(self): - self.assertEqual(str(self.astronomy_show), self.astronomy_show.title) - -class APITests(TestCase): - def setUp(self): - self.client = APIClient() - self.user = get_user_model().objects.create_user( - email="test@test.com", - password="testpass123" - ) - self.client.force_authenticate(self.user) - - def test_show_theme_list(self): - ShowTheme.objects.create(name="Test Theme") - response = self.client.get(reverse("planetarium:showtheme-list")) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - - def test_create_show_theme(self): - payload = {"name": "New Theme"} - response = self.client.post( - reverse("planetarium:showtheme-list"), - payload - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - exists = ShowTheme.objects.filter( - name=payload["name"] - ).exists() - self.assertTrue(exists) - -class SerializerTests(TestCase): - def test_show_theme_serializer(self): - show_theme = ShowTheme.objects.create(name="Test Theme") - serializer = ShowThemeSerializer(show_theme) - - self.assertEqual(set(serializer.data.keys()), {"id", "name"}) - self.assertEqual(serializer.data["name"], "Test Theme") - - def test_astronomy_show_list_serializer(self): - show_theme = ShowTheme.objects.create(name="Test Theme") - astronomy_show = AstronomyShow.objects.create( - title="Test Show", - description="Test Description" - ) - astronomy_show.show_theme.add(show_theme) - - serializer = AstronomyShowListSerializer(astronomy_show) - self.assertEqual(set(serializer.data.keys()), {"id", "title", "description", "show_theme"}) - self.assertEqual(serializer.data["title"], "Test Show") - self.assertEqual(serializer.data["show_theme"], ["Test Theme"]) diff --git a/planetarium/tests/__init__.py b/planetarium/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/planetarium/tests/api_ticket_tests.py b/planetarium/tests/api_ticket_tests.py new file mode 100644 index 0000000..d423882 --- /dev/null +++ b/planetarium/tests/api_ticket_tests.py @@ -0,0 +1,225 @@ +import uuid +from datetime import datetime + +from django.contrib.auth import get_user_model +from django.core.cache import cache +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase + +from planetarium.models import ( + AstronomyShow, + PlanetariumDome, + ShowSession, + Reservation, + Ticket, ShowTheme, +) +from planetarium.serializers import TicketListSerializer, TickerRetrieveSerializer + +TICKET_URL = reverse("planetarium:ticket-list") + + +def detail_url(ticket_id): + return reverse("planetarium:ticket-detail", args=[ticket_id]) + + +def sample_show_theme(): + return ShowTheme.objects.create(name=f"Test theme{uuid.uuid4().hex}") + + +def sample_astronomy_show(**params): + defaults = { + "title": f"Test show {uuid.uuid4().hex}", + "description": "Test description", + } + defaults.update(params) + astronomy_show = AstronomyShow.objects.create(**defaults) + astronomy_show.show_theme.add(sample_show_theme().id) + astronomy_show.save() + return astronomy_show + + +def sample_planetarium_dome(**params): + defaults = { + "name": f"Test dome {uuid.uuid4().hex}", + "rows": 2, + "seats_in_row": 2, + } + defaults.update(params) + return PlanetariumDome.objects.create(**defaults) + + +def sample_show_session(**params): + defaults = { + "astronomy_show": sample_astronomy_show(), + "planetarium_dome": sample_planetarium_dome(), + "show_time": datetime.now(), + } + defaults.update(params) + return ShowSession.objects.create(**defaults) + + +def sample_user(**params): + defaults = { + "email": f"user_{uuid.uuid4().hex}@user_test.com", + "password": "password1", + } + defaults.update(params) + return get_user_model().objects.create_user(**defaults) + + +def sample_reservation(**params): + defaults = { + "user": sample_user(), + } + defaults.update(params) + return Reservation.objects.create(**defaults) + + +def sample_ticket(**params): + defaults = { + "row": 1, + "seat": 1, + "show_session": sample_show_session(), + "reservation": sample_reservation(), + } + defaults.update(params) + return Ticket.objects.create(**defaults) + + +class BaseApiTests(APITestCase): + """This class for cleaning cache after each test""" + + def tearDown(self): + cache.clear() + + +class UnauthenticatedTicketTests(BaseApiTests): + """Test the ticket API for unauthenticated users""" + + def test_auth_required(self): + res = self.client.get(TICKET_URL) + self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) + + +class AuthenticatedBusApiTests(BaseApiTests): + def setUp(self): + self.user = get_user_model().objects.create_user( + email="test@test.com", + password="TestPass123", + ) + self.client.force_authenticate(user=self.user) + + def test_ticket_list(self): + """Test get list of tickets for authenticated user""" + sample_ticket() + sample_ticket() + res = self.client.get(TICKET_URL) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(len(res.data["results"]), 2) + + def test_filter_ticket_by_show_session_title(self): + """Test filter tickets by show session title""" + show_session = sample_show_session() + ticket1 = sample_ticket(show_session=show_session) + ticket2 = sample_ticket() + res = self.client.get( + TICKET_URL, + {"title": show_session.astronomy_show.title}, + ) + serializer1 = TicketListSerializer(ticket1) + serializer2 = TicketListSerializer(ticket2) + + self.assertIn(serializer1.data, res.data["results"]) + self.assertNotIn(serializer2.data, res.data["results"]) + + def test_filter_ticket_by_user_email(self): + """Test filter tickets by user email""" + reservation = sample_reservation() + ticket1 = sample_ticket(reservation=reservation) + ticket2 = sample_ticket() + res = self.client.get( + TICKET_URL, + {"email": reservation.user.email}, + ) + serializer1 = TicketListSerializer(ticket1) + serializer2 = TicketListSerializer(ticket2) + self.assertIn(serializer1.data, res.data["results"]) + self.assertNotIn(serializer2.data, res.data["results"]) + + def test_retrieve_ticket(self): + ticket = sample_ticket() + url = detail_url(ticket.id) + res = self.client.get(url) + + serializer = TickerRetrieveSerializer(ticket) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(serializer.data, res.data) + + def test_retrieve_ticket_with_cache(self): + ticket1 = sample_ticket() + cache.set("ticket_view1", ticket1) + res = self.client.get(TICKET_URL) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + cache_data = cache.get("ticket_view1") + self.assertIsNotNone(cache_data) + + ticket2 = sample_ticket() + cache.set("ticket_view2", ticket2) + cache_data = cache.get("ticket_view2") + self.assertIsNotNone(cache_data) + + def test_create_ticket_forbidden(self): + """Test that creating a ticket is forbidden for authenticated users""" + payload = { + "row": 1, + "seat": 1, + "show_session": sample_show_session().id, + "reservation": sample_reservation().id, + } + res = self.client.post(TICKET_URL, payload) + self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) + + +class AdminUserTicketTests(BaseApiTests): + def setUp(self): + self.user = get_user_model().objects.create_user( + email="admin@admin.com", + password="TestPass123", + is_staff=True, + ) + self.client.force_authenticate(user=self.user) + + def test_create_ticket_successful(self): + """Test that creating a ticket is successful for admin users""" + show_session = sample_show_session() + reservation = sample_reservation() + payload = { + "row": 1, + "seat": 1, + "show_session": show_session.id, + "reservation": reservation.id, + } + res = self.client.post(TICKET_URL, payload) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_create_ticket_with_invalid_data(self): + """Test that creating a ticket is successful for admin users""" + show_session = sample_show_session() + reservation = sample_reservation() + payload = { + "row": 1, + "seat": 1, + "show_session": show_session.id, + } + res = self.client.post(TICKET_URL, payload) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_delete_ticket(self): + """Test that deleting a ticket is successful for admin users""" + ticket = sample_ticket() + url = detail_url(ticket.id) + res = self.client.delete(url) + self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse(Ticket.objects.filter(id=ticket.id).exists()) diff --git a/planetarium/urls.py b/planetarium/urls.py index f2edb17..d1f4634 100644 --- a/planetarium/urls.py +++ b/planetarium/urls.py @@ -11,12 +11,12 @@ ) router = routers.DefaultRouter() -router.register("show-theme", ShowThemeViewSet) -router.register("astronomy-show", AstronomyShowViewSet) -router.register("planetarium-dome", PlanetariumDomeViewSet) -router.register("show-session", ShowSessionViewSet) -router.register("reservation", ReservationViewSet) -router.register("ticket", TicketViewSet) +router.register("show-themes", ShowThemeViewSet) +router.register("astronomy-shows", AstronomyShowViewSet) +router.register("planetarium-domes", PlanetariumDomeViewSet) +router.register("show-sessions", ShowSessionViewSet) +router.register("reservations", ReservationViewSet) +router.register("tickets", TicketViewSet) urlpatterns = [ path("", include(router.urls)), diff --git a/planetarium/views.py b/planetarium/views.py index 93bf3e1..4cceb91 100644 --- a/planetarium/views.py +++ b/planetarium/views.py @@ -1,7 +1,6 @@ from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from rest_framework import viewsets, filters -from django.utils.decorators import method_decorator from planetarium.models import ( ShowTheme, @@ -42,6 +41,7 @@ def get_queryset(self): queryset = self.queryset if self.action in ["list", "retrieve"]: return queryset.prefetch_related("show_theme") + return queryset def get_serializer_class(self): if self.action == "list": @@ -68,6 +68,7 @@ def get_queryset(self): return queryset.prefetch_related( "astronomy_show__show_theme", "planetarium_dome" ) + return queryset def get_serializer_class(self): if self.action == "list": @@ -100,15 +101,39 @@ class TicketViewSet(viewsets.ModelViewSet): queryset = Ticket.objects.all() serializer_class = TicketSerializer filter_backends = [filters.SearchFilter] - search_fields = ["row", "seat", "reservation__user__email", "show_session__astronomy_show__title"] + search_fields = [ + "row", + "seat", + "reservation__user__email", + "show_session__astronomy_show__title", + ] + + @staticmethod + def _params_to_ints(self, query_string): + return [int(str_id) for str_id in query_string.split(",")] def get_queryset(self): queryset = self.queryset + + title = self.request.query_params.get("title") + email = self.request.query_params.get("email") + + if title: + queryset = queryset.filter( + show_session__astronomy_show__title__icontains=title + ) + + if email: + queryset = queryset.filter( + reservation__user__email__icontains=email + ) + if self.action == "list": return queryset.select_related( "show_session__astronomy_show", "reservation__user", ) + return queryset.distinct() def get_serializer_class(self): if self.action == "list": diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 22db116..fe49bb4 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -9,6 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.1/ref/settings/ """ + import os from datetime import timedelta from pathlib import Path @@ -145,15 +146,12 @@ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ), - "DEFAULT_THROTTLE_RATES": { - "anon": "100/day", - "user": "1000/day" - }, + "DEFAULT_THROTTLE_RATES": {"anon": "100/day", "user": "1000/day"}, "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 10, "DEFAULT_PERMISSION_CLASSES": ( "planetarium.permissions.IsAdminAllORIsAuthenticatedOrReadOnly", - ) + ), } SIMPLE_JWT = { @@ -174,9 +172,9 @@ CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://redis:6379/1", + "LOCATION": "redis://127.0.0.1:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", - } + }, } } diff --git a/planetarium_service/urls.py b/planetarium_service/urls.py index 100243b..a0c3a16 100644 --- a/planetarium_service/urls.py +++ b/planetarium_service/urls.py @@ -17,14 +17,26 @@ from django.contrib import admin from django.urls import path, include -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularSwaggerView, + SpectacularRedocView, +) urlpatterns = [ path("admin/", admin.site.urls), path("api/planetarium/", include("planetarium.urls"), name="planetarium"), path("api/accounts/", include("accounts.urls"), name="accounts"), - path('api/schema/', SpectacularAPIView.as_view(), name='schema'), - path('api/schema/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'), - path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path("api/schema/", SpectacularAPIView.as_view(), name="schema"), + path( + "api/schema/swagger/", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger", + ), + path( + "api/schema/redoc/", + SpectacularRedocView.as_view(url_name="schema"), + name="redoc", + ), path("__debug__/", include("debug_toolbar.urls")), ] From d3917876c71250b73342adffecace50213ddc3b0 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 26 Dec 2024 14:42:30 +0200 Subject: [PATCH 17/21] update readme.md and redis host --- Makefile | 5 +- README.md | 108 +++++++++++++++++- ...pi_ticket_tests.py => tests_api_ticket.py} | 0 planetarium_service/settings.py | 3 +- 4 files changed, 113 insertions(+), 3 deletions(-) rename planetarium/tests/{api_ticket_tests.py => tests_api_ticket.py} (100%) diff --git a/Makefile b/Makefile index 3852528..aec0227 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: test run migrate docker docker-test +.PHONY: test run migrate docker docker-superuser docker-test test: python manage.py test @@ -13,5 +13,8 @@ migrate: docker: docker-compose up --build +docker-superuser: + docker-compose run app sh -c "python manage.py createsuperuser" + docker-test: docker-compose run app sh -c "python manage.py test" diff --git a/README.md b/README.md index 1501cd0..b8f9b27 100644 --- a/README.md +++ b/README.md @@ -1 +1,107 @@ -# planetarium-api-service \ No newline at end of file +# Planetarium API Service + +The Planetarium API Service is a Django REST Framework-based application that allows managing planetarium show bookings, +ticket reservations, and viewing information about astronomy shows, available domes, and show themes. + +## Key Features + +- Manage show themes. +- View, create, update, and delete astronomy shows. +- Manage planetarium domes (seats, halls, etc.). +- Schedule astronomy show sessions. +- Ticket reservation system for shows. +- Authentication using JWT tokens. +- API response caching for improved performance. + +## Prerequisites + +Make sure you have the following tools installed on your system: + +- Docker and Docker Compose +- Python 3.12+ +- Redis for caching + +## Running the Project with Docker-Compose + +1. Clone the repository: + ```bash + git clone + cd planetarium-api-service + ``` + +2. Create a `.env` file in the root directory of the project: + ```env + SECRET_KEY='' + ``` + +3. Start the application using Docker-Compose: + ```bash + make docker + ``` + or + ```bash + docker-compose up --build + ``` + +4. The API will be available at: [http://localhost:8000](http://localhost:8000). + +### Docker-Compose Configuration + +- **app**: The Django application. +- **redis**: Caching service for API responses. +- **redisinsight**: A tool for monitoring Redis. + +### Database Management + +The project uses SQLite as the default database. You can modify the `settings.py` file to switch to PostgreSQL or +another database of your choice. + +## User Management + +### Creating a Superuser + +To access the admin panel: + +```bash +make docker-superuser +``` + +You can then access the admin panel at: [http://localhost:8000/admin](http://localhost:8000/admin). + +## API Documentation + +The project provides interactive API documentation using `drf-spectacular`: + +- Swagger: [http://localhost:8000/api/schema/swagger/](http://localhost:8000/api/schema/swagger/) +- Redoc: [http://localhost:8000/api/schema/redoc/](http://localhost:8000/api/schema/redoc/) + +## Testing + +Unit tests cover the core API logic. To run tests, execute: + +```bash +make docker-test +``` + +## Project Structure + +- **planetarium/** + - API logic for managing planetarium shows, domes, and tickets. + - Serializers, models, and views. +- **accounts/** + - Custom user model implementation based on email authentication. + +## Key Dependencies + +- Django==5.1.4 +- Django REST Framework +- Django Filters +- Redis +- drf-spectacular +- JWT (JSON Web Token) + +## Future Enhancements + +- Add support for other databases (e.g., PostgreSQL). +- Implement background tasks for booking management (e.g., Celery). +- Introduce monitoring and logging for API requests (e.g., via Sentry). diff --git a/planetarium/tests/api_ticket_tests.py b/planetarium/tests/tests_api_ticket.py similarity index 100% rename from planetarium/tests/api_ticket_tests.py rename to planetarium/tests/tests_api_ticket.py diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index fe49bb4..94825d5 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -13,6 +13,7 @@ import os from datetime import timedelta from pathlib import Path + from dotenv import load_dotenv load_dotenv() @@ -172,7 +173,7 @@ CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/1", + "LOCATION": "redis://redis:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, From b4e8bacd8a6ee7cdacf19421980c66695fd20674 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 26 Dec 2024 15:50:46 +0200 Subject: [PATCH 18/21] add tests for accounts app, delete comments, add __str__ to ticket model, move redis path to .env --- .env.sample | 3 +- Dockerfile | 2 +- accounts/tests.py | 3 -- accounts/tests/__init__.py | 0 accounts/tests/tests_api_user.py | 60 ++++++++++++++++++++++++++++++++ docker-compose.yaml | 24 ------------- planetarium/models.py | 3 ++ planetarium_service/settings.py | 2 +- 8 files changed, 67 insertions(+), 30 deletions(-) delete mode 100644 accounts/tests.py create mode 100644 accounts/tests/__init__.py create mode 100644 accounts/tests/tests_api_user.py diff --git a/.env.sample b/.env.sample index 211ba5e..22a02c9 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,2 @@ -SECRET_KEY=SECRET_KEY \ No newline at end of file +SECRET_KEY=SECRET_KEY +REDIS_URL=REDIS_URL diff --git a/Dockerfile b/Dockerfile index 4c1068c..3b18e14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:alpine +FROM python:3.12-alpine ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 diff --git a/accounts/tests.py b/accounts/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/accounts/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/accounts/tests/__init__.py b/accounts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/tests/tests_api_user.py b/accounts/tests/tests_api_user.py new file mode 100644 index 0000000..334458c --- /dev/null +++ b/accounts/tests/tests_api_user.py @@ -0,0 +1,60 @@ +from django.contrib.auth import get_user_model +from rest_framework.test import APITestCase + +from accounts.serializers import UserSerializer + + +class AccountsUserTest(APITestCase): + def setUp(self): + self.email = "email@email.com" + self.password = "password123" + self.user = get_user_model().objects.create_user( + email=self.email, + password=self.password, + ) + + def test_create_user(self): + """Test creating a user with an email is successful""" + self.assertEqual(self.user.email, self.email) + self.assertTrue(self.user.check_password(self.password)) + self.assertFalse(self.user.is_superuser) + self.assertFalse(self.user.is_staff) + + def test_email_unique(self): + """Test that email is unique""" + with self.assertRaises(Exception): + get_user_model().objects.create_user( + email=self.email, + password="password123", + ) + + def test_create_user_without_email(self): + """Test creating a user without an email raises an error""" + with self.assertRaises(ValueError): + get_user_model().objects.create_user(email=None, password="password123") + + +class AccountsUserSerializerTest(APITestCase): + def test_serializer_user_create(self): + payload = { + "email": "email@email.com", + "password": "password123", + "first_name": "first_name", + "last_name": "last_name", + } + serializer = UserSerializer(data=payload) + self.assertTrue(serializer.is_valid()) + user = serializer.save() + self.assertEqual(user.email, payload["email"]) + self.assertTrue(user.check_password(payload["password"])) + self.assertEqual(user.first_name, payload["first_name"]) + self.assertEqual(user.last_name, payload["last_name"]) + + def test_serializer_user_password_too_short(self): + payload = { + "email": "email@email.com", + "password": "1234567", + } + serializer = UserSerializer(data=payload) + self.assertFalse(serializer.is_valid()) + self.assertEqual(len(serializer.errors["password"]), 1) diff --git a/docker-compose.yaml b/docker-compose.yaml index 5fa88b8..f529da4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,24 +14,6 @@ services: - redis restart: always - # db: - # image: postgres:16.0-alpine - # restart: always - # user: postgres - # secrets: - # - db-password - # volumes: - # - db-data:/var/lib/postgresql/data - # environment: - # - POSTGRES_DB=example - # - POSTGRES_PASSWORD_FILE=/run/secrets/db-password - # expose: - # - 5432 - # healthcheck: - # test: [ "CMD", "pg_isready" ] - # interval: 10s - # timeout: 5s - # retries: 5 redis: image: redis:alpine @@ -41,9 +23,3 @@ services: - "5540:5540" depends_on: - redis - -# volumes: -# db-data: -# secrets: -# db-password: -# file: db/password.txt diff --git a/planetarium/models.py b/planetarium/models.py index 4483fbd..1436a69 100644 --- a/planetarium/models.py +++ b/planetarium/models.py @@ -69,3 +69,6 @@ class Ticket(models.Model): class Meta: unique_together = ("row", "seat", "show_session") ordering = ["row", "seat"] + + def __str__(self): + return f"{self.show_session.astronomy_show.title} - Row {self.row} Seat {self.seat}" diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 94825d5..9ccd379 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -173,7 +173,7 @@ CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://redis:6379/1", + "LOCATION": os.getenv("REDIS_URL"), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, From c0ff341bf5c236b8d1fb6934be53af56d2d358c1 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 27 Dec 2024 17:07:43 +0200 Subject: [PATCH 19/21] implement jazmin, implement postgresql insted of sqlite3, update migrations for uuid and postgresql --- .env.sample | 8 +++ accounts/migrations/0001_initial.py | 55 +++++++------------ ..._managers_remove_user_username_and_more.py | 31 ----------- .../0003_alter_user_options_alter_user_id.py | 25 --------- .../{tests_api_user.py => tests_user.py} | 0 docker-compose.yaml | 24 +++++++- .../management/commands/wait_for_db.py | 19 +++++++ planetarium/migrations/0001_initial.py | 2 +- planetarium_service/settings.py | 14 ++++- requirements.txt | 5 ++ 10 files changed, 85 insertions(+), 98 deletions(-) delete mode 100644 accounts/migrations/0002_alter_user_managers_remove_user_username_and_more.py delete mode 100644 accounts/migrations/0003_alter_user_options_alter_user_id.py rename accounts/tests/{tests_api_user.py => tests_user.py} (100%) create mode 100644 planetarium/management/commands/wait_for_db.py diff --git a/.env.sample b/.env.sample index 22a02c9..caa1227 100644 --- a/.env.sample +++ b/.env.sample @@ -1,2 +1,10 @@ SECRET_KEY=SECRET_KEY REDIS_URL=REDIS_URL +PGDATA=PGDATA + +# DB +POSTGRES_DB=POSTGRES_DB +POSTGRES_USER=POSTGRES_USER +POSTGRES_PASSWORD=POSTGRES_PASSWORD +POSTGRES_HOST=POSTGRES_HOST +POSTGRES_PORT=POSTGRES_PORT diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index ddc25eb..65d459c 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 5.1.4 on 2024-12-23 14:52 +# Generated by Django 5.1.4 on 2024-12-27 14:47 -import django.contrib.auth.models -import django.contrib.auth.validators +import accounts.models import django.utils.timezone +import uuid from django.db import migrations, models @@ -18,15 +18,6 @@ class Migration(migrations.Migration): migrations.CreateModel( name="User", fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), ("password", models.CharField(max_length=128, verbose_name="password")), ( "last_login", @@ -42,21 +33,6 @@ class Migration(migrations.Migration): verbose_name="superuser status", ), ), - ( - "username", - models.CharField( - error_messages={ - "unique": "A user with that username already exists." - }, - help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", - max_length=150, - unique=True, - validators=[ - django.contrib.auth.validators.UnicodeUsernameValidator() - ], - verbose_name="username", - ), - ), ( "first_name", models.CharField( @@ -69,12 +45,6 @@ class Migration(migrations.Migration): blank=True, max_length=150, verbose_name="last name" ), ), - ( - "email", - models.EmailField( - blank=True, max_length=254, verbose_name="email address" - ), - ), ( "is_staff", models.BooleanField( @@ -97,6 +67,21 @@ class Migration(migrations.Migration): default=django.utils.timezone.now, verbose_name="date joined" ), ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "email", + models.EmailField( + max_length=254, unique=True, verbose_name="email address" + ), + ), ( "groups", models.ManyToManyField( @@ -121,12 +106,10 @@ class Migration(migrations.Migration): ), ], options={ - "verbose_name": "user", - "verbose_name_plural": "users", "abstract": False, }, managers=[ - ("objects", django.contrib.auth.models.UserManager()), + ("objects", accounts.models.UserManager()), ], ), ] diff --git a/accounts/migrations/0002_alter_user_managers_remove_user_username_and_more.py b/accounts/migrations/0002_alter_user_managers_remove_user_username_and_more.py deleted file mode 100644 index 0aac443..0000000 --- a/accounts/migrations/0002_alter_user_managers_remove_user_username_and_more.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-23 15:49 - -import accounts.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("accounts", "0001_initial"), - ] - - operations = [ - migrations.AlterModelManagers( - name="user", - managers=[ - ("objects", accounts.models.UserManager()), - ], - ), - migrations.RemoveField( - model_name="user", - name="username", - ), - migrations.AlterField( - model_name="user", - name="email", - field=models.EmailField( - max_length=254, unique=True, verbose_name="email address" - ), - ), - ] diff --git a/accounts/migrations/0003_alter_user_options_alter_user_id.py b/accounts/migrations/0003_alter_user_options_alter_user_id.py deleted file mode 100644 index 47ba128..0000000 --- a/accounts/migrations/0003_alter_user_options_alter_user_id.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.1.4 on 2024-12-23 16:44 - -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("accounts", "0002_alter_user_managers_remove_user_username_and_more"), - ] - - operations = [ - migrations.AlterModelOptions( - name="user", - options={}, - ), - migrations.AlterField( - model_name="user", - name="id", - field=models.UUIDField( - default=uuid.uuid4, editable=False, primary_key=True, serialize=False - ), - ), - ] diff --git a/accounts/tests/tests_api_user.py b/accounts/tests/tests_user.py similarity index 100% rename from accounts/tests/tests_api_user.py rename to accounts/tests/tests_user.py diff --git a/docker-compose.yaml b/docker-compose.yaml index f529da4..05d4ffb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,8 +2,10 @@ services: app: build: context: . - command: - "python manage.py runserver 0.0.0.0:8000" + command: > + sh -c "python manage.py wait_for_db && + python manage.py migrate && + python manage.py runserver 0.0.0.0:8000" ports: - "8000:8000" volumes: @@ -12,6 +14,7 @@ services: - .env depends_on: - redis + - db restart: always redis: @@ -23,3 +26,20 @@ services: - "5540:5540" depends_on: - redis + + db: + image: postgres:16.0-alpine + restart: always + env_file: + - .env + environment: + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + ports: + - "5432:5432" + volumes: + - my_db:$PGDATA + +volumes: + my_db: diff --git a/planetarium/management/commands/wait_for_db.py b/planetarium/management/commands/wait_for_db.py new file mode 100644 index 0000000..6c80f9a --- /dev/null +++ b/planetarium/management/commands/wait_for_db.py @@ -0,0 +1,19 @@ +import time +from django.core.management.base import BaseCommand +from django.db import connections +from django.db.utils import OperationalError + + +class Command(BaseCommand): + def handle(self, *args, **options): + self.stdout.write("Waiting for database...") + db_connection = None + while not db_connection: + try: + db_connection = connections["default"] + self.stdout.write(self.style.SUCCESS("Connected to database")) + except OperationalError: + self.stdout.write( + self.style.ERROR("Database unavailable, wait 1 second...") + ) + time.sleep(1) diff --git a/planetarium/migrations/0001_initial.py b/planetarium/migrations/0001_initial.py index 867d488..3b6091f 100644 --- a/planetarium/migrations/0001_initial.py +++ b/planetarium/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2024-12-25 15:41 +# Generated by Django 5.1.4 on 2024-12-27 14:47 import django.db.models.deletion from django.conf import settings diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 9ccd379..3d8f2e0 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -36,6 +36,7 @@ INSTALLED_APPS = [ "debug_toolbar", + "jazzmin", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -85,10 +86,17 @@ # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases + DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + # "ENGINE": "django.db.backends.sqlite3", + # "NAME": BASE_DIR / "db.sqlite3", + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("POSTGRES_DB"), + "USER": os.getenv("POSTGRES_USER"), + "PASSWORD": os.getenv("POSTGRES_PASSWORD"), + "HOST": os.getenv("POSTGRES_HOST"), + "PORT": os.getenv("POSTGRES_PORT"), } } @@ -129,7 +137,7 @@ # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" AUTH_USER_MODEL = "accounts.User" diff --git a/requirements.txt b/requirements.txt index 929bb6b..f9ad87a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,18 +6,23 @@ colorama==0.4.6 Django==5.1.4 django-debug-toolbar==4.4.6 django-filter==24.3 +django-jazzmin==3.0.1 django-redis==5.4.0 djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 +docopt==0.6.2 drf-spectacular==0.28.0 inflection==0.5.1 jsonschema==4.23.0 jsonschema-specifications==2024.10.1 +makefile==1.1.0 Markdown==3.7 mypy-extensions==1.0.0 packaging==24.2 pathspec==0.12.1 platformdirs==4.3.6 +psycopg2-binary==2.9.10 +py-make==0.1.2 PyJWT==2.10.1 python-dotenv==1.0.1 PyYAML==6.0.2 From ec25ab157175cc307e393354dfe403688ac29efe Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 27 Dec 2024 17:26:29 +0200 Subject: [PATCH 20/21] update readme.md for postgresql and features --- README.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b8f9b27..0590c06 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ ticket reservations, and viewing information about astronomy shows, available do - Manage show themes. - View, create, update, and delete astronomy shows. - Manage planetarium domes (seats, halls, etc.). -- Schedule astronomy show sessions. +- Advanced caching using **Redis**. - Ticket reservation system for shows. -- Authentication using JWT tokens. -- API response caching for improved performance. +- Authentication using **JWT tokens**. +- Support for custom user model with `UUID` as the primary key. ## Prerequisites @@ -20,6 +20,7 @@ Make sure you have the following tools installed on your system: - Docker and Docker Compose - Python 3.12+ - Redis for caching +- PostgreSQL ## Running the Project with Docker-Compose @@ -43,7 +44,15 @@ Make sure you have the following tools installed on your system: docker-compose up --build ``` -4. The API will be available at: [http://localhost:8000](http://localhost:8000). +4. All API caching is managed using **Redis**. Configure `REDIS_URL` in the `.env` file: + +``` env +REDIS_URL=redis://redis:6379/0 +``` + +Redis is automatically started via `docker-compose`. + +5. The API will be available at: [http://localhost:8000](http://localhost:8000). ### Docker-Compose Configuration @@ -53,8 +62,8 @@ Make sure you have the following tools installed on your system: ### Database Management -The project uses SQLite as the default database. You can modify the `settings.py` file to switch to PostgreSQL or -another database of your choice. +The project uses **PostgreSQL** as the database backend, +configured automatically with `docker-compose.yaml`. ## User Management @@ -98,10 +107,12 @@ make docker-test - Django Filters - Redis - drf-spectacular +- django-debug-toolbar +- psycopg2-binary - JWT (JSON Web Token) ## Future Enhancements -- Add support for other databases (e.g., PostgreSQL). -- Implement background tasks for booking management (e.g., Celery). -- Introduce monitoring and logging for API requests (e.g., via Sentry). +- Add Celery for background tasks like delayed booking confirmations. +- Real-time WebSocket Updates +- Payment Management From 7d3cb1db0bbf58d4a9c2d46b20d96ccc9678f401 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 30 Dec 2024 14:58:28 +0200 Subject: [PATCH 21/21] rm docker.readme, add isort, fix imports, add healthchecker for db, change method secret_key --- .isort.cfg | 7 ++++++ README.Docker.md | 22 ------------------- accounts/migrations/0001_initial.py | 6 +++-- accounts/urls.py | 1 + docker-compose.yaml | 6 +++++ planetarium/admin.py | 3 ++- .../management/commands/wait_for_db.py | 1 + planetarium/models.py | 7 +++++- planetarium/permissions.py | 2 +- planetarium/serializers.py | 19 ++++++++++++++-- planetarium/signals.py | 2 +- planetarium/tests/tests_api_ticket.py | 8 ++++--- planetarium/urls.py | 10 ++++----- planetarium/views.py | 18 +++++++-------- planetarium_service/settings.py | 5 ++--- 15 files changed, 66 insertions(+), 51 deletions(-) create mode 100644 .isort.cfg delete mode 100644 README.Docker.md diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..3dd6d50 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,7 @@ +[settings] +profile = pycharm +line_length = 88 +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +combine_as_imports = true \ No newline at end of file diff --git a/README.Docker.md b/README.Docker.md deleted file mode 100644 index 6dae561..0000000 --- a/README.Docker.md +++ /dev/null @@ -1,22 +0,0 @@ -### Building and running your application - -When you're ready, start your application by running: -`docker compose up --build`. - -Your application will be available at http://localhost:8000. - -### Deploying your application to the cloud - -First, build your image, e.g.: `docker build -t myapp .`. -If your cloud uses a different CPU architecture than your development -machine (e.g., you are on a Mac M1 and your cloud provider is amd64), -you'll want to build the image for that platform, e.g.: -`docker build --platform=linux/amd64 -t myapp .`. - -Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. - -Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) -docs for more detail on building and pushing. - -### References -* [Docker's Python guide](https://docs.docker.com/language/python/) \ No newline at end of file diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index 65d459c..be2cbb0 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -1,10 +1,12 @@ # Generated by Django 5.1.4 on 2024-12-27 14:47 -import accounts.models -import django.utils.timezone import uuid + +import django.utils.timezone from django.db import migrations, models +import accounts.models + class Migration(migrations.Migration): diff --git a/accounts/urls.py b/accounts/urls.py index cd57172..931609a 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -4,6 +4,7 @@ TokenRefreshView, TokenVerifyView, ) + from .views import CreateUserView diff --git a/docker-compose.yaml b/docker-compose.yaml index 05d4ffb..ba2224a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -40,6 +40,12 @@ services: - "5432:5432" volumes: - my_db:$PGDATA + healthcheck: + test: curl --fail http://localhost:5432 || exit 1 + interval: 40s + retries: 3 + start_period: 30s + timeout: 30s volumes: my_db: diff --git a/planetarium/admin.py b/planetarium/admin.py index 2750440..337be14 100644 --- a/planetarium/admin.py +++ b/planetarium/admin.py @@ -1,14 +1,15 @@ from django.contrib import admin from planetarium.models import ( - ShowTheme, AstronomyShow, PlanetariumDome, Reservation, ShowSession, + ShowTheme, Ticket, ) + admin.site.register(ShowTheme) admin.site.register(AstronomyShow) admin.site.register(PlanetariumDome) diff --git a/planetarium/management/commands/wait_for_db.py b/planetarium/management/commands/wait_for_db.py index 6c80f9a..bead53d 100644 --- a/planetarium/management/commands/wait_for_db.py +++ b/planetarium/management/commands/wait_for_db.py @@ -1,4 +1,5 @@ import time + from django.core.management.base import BaseCommand from django.db import connections from django.db.utils import OperationalError diff --git a/planetarium/models.py b/planetarium/models.py index 1436a69..eff812a 100644 --- a/planetarium/models.py +++ b/planetarium/models.py @@ -67,7 +67,12 @@ class Ticket(models.Model): ) class Meta: - unique_together = ("row", "seat", "show_session") + constraints = [ + models.UniqueConstraint( + fields=["row", "seat", "show_session"], + name="unique_row_seat_show_session", + ) + ] ordering = ["row", "seat"] def __str__(self): diff --git a/planetarium/permissions.py b/planetarium/permissions.py index 10a1199..778d3f6 100644 --- a/planetarium/permissions.py +++ b/planetarium/permissions.py @@ -1,4 +1,4 @@ -from rest_framework.permissions import BasePermission, SAFE_METHODS +from rest_framework.permissions import SAFE_METHODS, BasePermission class IsAdminAllORIsAuthenticatedOrReadOnly(BasePermission): diff --git a/planetarium/serializers.py b/planetarium/serializers.py index 4f6eaba..a06b6c9 100644 --- a/planetarium/serializers.py +++ b/planetarium/serializers.py @@ -1,11 +1,12 @@ from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator from planetarium.models import ( - ShowTheme, AstronomyShow, PlanetariumDome, - ShowSession, Reservation, + ShowSession, + ShowTheme, Ticket, ) @@ -71,6 +72,13 @@ class TicketSerializer(serializers.ModelSerializer): class Meta: model = Ticket fields = ["id", "row", "seat", "show_session", "reservation"] + validators = [ + UniqueTogetherValidator( + queryset=Ticket.objects.all(), + fields=["row", "seat", "show_session"], + message="This seat is already taken.", + ) + ] def validate(self, data): row = data.get("row") @@ -97,6 +105,13 @@ class TicketListSerializer(serializers.ModelSerializer): class Meta: model = Ticket fields = ["id", "row", "seat", "user", "astronomy_show"] + validators = [ + UniqueTogetherValidator( + queryset=Ticket.objects.all(), + fields=["row", "seat", "show_session"], + message="This seat is already taken.", + ) + ] class TickerRetrieveSerializer(TicketSerializer): diff --git a/planetarium/signals.py b/planetarium/signals.py index d098181..4f30287 100644 --- a/planetarium/signals.py +++ b/planetarium/signals.py @@ -1,5 +1,5 @@ from django.core.cache import cache -from django.db.models.signals import post_save, post_delete +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from planetarium.models import Ticket diff --git a/planetarium/tests/tests_api_ticket.py b/planetarium/tests/tests_api_ticket.py index d423882..b484a8d 100644 --- a/planetarium/tests/tests_api_ticket.py +++ b/planetarium/tests/tests_api_ticket.py @@ -10,11 +10,13 @@ from planetarium.models import ( AstronomyShow, PlanetariumDome, - ShowSession, Reservation, - Ticket, ShowTheme, + ShowSession, + ShowTheme, + Ticket, ) -from planetarium.serializers import TicketListSerializer, TickerRetrieveSerializer +from planetarium.serializers import TickerRetrieveSerializer, TicketListSerializer + TICKET_URL = reverse("planetarium:ticket-list") diff --git a/planetarium/urls.py b/planetarium/urls.py index d1f4634..bc11efd 100644 --- a/planetarium/urls.py +++ b/planetarium/urls.py @@ -1,15 +1,15 @@ -from django.urls import path, include from rest_framework import routers from planetarium.views import ( - ShowThemeViewSet, AstronomyShowViewSet, PlanetariumDomeViewSet, - ShowSessionViewSet, ReservationViewSet, + ShowSessionViewSet, + ShowThemeViewSet, TicketViewSet, ) + router = routers.DefaultRouter() router.register("show-themes", ShowThemeViewSet) router.register("astronomy-shows", AstronomyShowViewSet) @@ -18,8 +18,6 @@ router.register("reservations", ReservationViewSet) router.register("tickets", TicketViewSet) -urlpatterns = [ - path("", include(router.urls)), -] +urlpatterns = router.urls app_name = "planetarium" diff --git a/planetarium/views.py b/planetarium/views.py index 4cceb91..c97ec25 100644 --- a/planetarium/views.py +++ b/planetarium/views.py @@ -1,28 +1,28 @@ from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page -from rest_framework import viewsets, filters +from rest_framework import filters, viewsets from planetarium.models import ( - ShowTheme, AstronomyShow, PlanetariumDome, - ShowSession, Reservation, + ShowSession, + ShowTheme, Ticket, ) from planetarium.serializers import ( - ShowThemeSerializer, + AstronomyShowListSerializer, + AstronomyShowRetrieveSerializer, AstronomyShowSerializer, PlanetariumDomeSerializer, - ShowSessionSerializer, ReservationSerializer, - TicketSerializer, - AstronomyShowRetrieveSerializer, - AstronomyShowListSerializer, ShowSessionListSerializer, ShowSessionRetrieveSerializer, - TicketListSerializer, + ShowSessionSerializer, + ShowThemeSerializer, TickerRetrieveSerializer, + TicketListSerializer, + TicketSerializer, ) diff --git a/planetarium_service/settings.py b/planetarium_service/settings.py index 3d8f2e0..4899a99 100644 --- a/planetarium_service/settings.py +++ b/planetarium_service/settings.py @@ -16,6 +16,7 @@ from dotenv import load_dotenv + load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -25,7 +26,7 @@ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv("SECRET_KEY") +SECRET_KEY = os.environ["SECRET_KEY"] # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -89,8 +90,6 @@ DATABASES = { "default": { - # "ENGINE": "django.db.backends.sqlite3", - # "NAME": BASE_DIR / "db.sqlite3", "ENGINE": "django.db.backends.postgresql", "NAME": os.getenv("POSTGRES_DB"), "USER": os.getenv("POSTGRES_USER"),