From aeab43aa8aa96377d5d859132d360275c64343c1 Mon Sep 17 00:00:00 2001 From: maciejrusek Date: Sun, 22 Mar 2026 14:19:17 +0100 Subject: [PATCH] Changed work of shelf books --- README.md | 3 - .../apps/authors/migrations/0001_initial.py | 25 ++++++ backend/apps/books/migrations/0001_initial.py | 28 +++++++ backend/apps/books/models.py | 1 - backend/apps/books/serializers.py | 29 ++----- backend/apps/books/views.py | 5 +- .../{shelf_items => shelf_books}/__init__.py | 0 .../{shelf_items => shelf_books}/admin.py | 0 backend/apps/shelf_books/apps.py | 5 ++ .../shelf_books/migrations/0001_initial.py | 29 +++++++ .../0002_rename_shelfitem_shelfbook.py | 19 +++++ .../migrations/__init__.py | 0 .../{shelf_items => shelf_books}/models.py | 2 +- backend/apps/shelf_books/serializers.py | 84 +++++++++++++++++++ .../{shelf_items => shelf_books}/tests.py | 0 backend/apps/shelf_books/urls.py | 10 +++ backend/apps/shelf_books/views.py | 84 +++++++++++++++++++ backend/apps/shelf_items/apps.py | 5 -- backend/apps/shelf_items/serializers.py | 77 ----------------- backend/apps/shelf_items/urls.py | 9 -- backend/apps/shelf_items/views.py | 20 ----- backend/apps/urls.py | 1 + backend/config/django/base.py | 11 +++ 23 files changed, 307 insertions(+), 140 deletions(-) create mode 100644 backend/apps/authors/migrations/0001_initial.py create mode 100644 backend/apps/books/migrations/0001_initial.py rename backend/apps/{shelf_items => shelf_books}/__init__.py (100%) rename backend/apps/{shelf_items => shelf_books}/admin.py (100%) create mode 100644 backend/apps/shelf_books/apps.py create mode 100644 backend/apps/shelf_books/migrations/0001_initial.py create mode 100644 backend/apps/shelf_books/migrations/0002_rename_shelfitem_shelfbook.py rename backend/apps/{shelf_items => shelf_books}/migrations/__init__.py (100%) rename backend/apps/{shelf_items => shelf_books}/models.py (94%) create mode 100644 backend/apps/shelf_books/serializers.py rename backend/apps/{shelf_items => shelf_books}/tests.py (100%) create mode 100644 backend/apps/shelf_books/urls.py create mode 100644 backend/apps/shelf_books/views.py delete mode 100644 backend/apps/shelf_items/apps.py delete mode 100644 backend/apps/shelf_items/serializers.py delete mode 100644 backend/apps/shelf_items/urls.py delete mode 100644 backend/apps/shelf_items/views.py diff --git a/README.md b/README.md index 964f005..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,3 +0,0 @@ - - -shelves/id/items !!!! \ No newline at end of file diff --git a/backend/apps/authors/migrations/0001_initial.py b/backend/apps/authors/migrations/0001_initial.py new file mode 100644 index 0000000..c40ed05 --- /dev/null +++ b/backend/apps/authors/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 6.0.3 on 2026-03-22 12:16 + +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='Author', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='authors', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/apps/books/migrations/0001_initial.py b/backend/apps/books/migrations/0001_initial.py new file mode 100644 index 0000000..e4714da --- /dev/null +++ b/backend/apps/books/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.3 on 2026-03-22 12:16 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('authors', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('isbn', models.CharField(max_length=13)), + ('authors', models.ManyToManyField(related_name='books', to='authors.author')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='books', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/apps/books/models.py b/backend/apps/books/models.py index 24a4cfc..b6ffa68 100644 --- a/backend/apps/books/models.py +++ b/backend/apps/books/models.py @@ -1,6 +1,5 @@ from django.db import models -from apps.shelves.models import Shelf from apps.authors.models import Author from apps.user.models import User diff --git a/backend/apps/books/serializers.py b/backend/apps/books/serializers.py index d98f383..d69a8ec 100644 --- a/backend/apps/books/serializers.py +++ b/backend/apps/books/serializers.py @@ -1,26 +1,17 @@ from rest_framework import serializers -from apps.shelves.serializers import ShelfSerializer from apps.authors.serializers import AuthorSerializer from apps.books.models import Book -from apps.shelves.models import Shelf from apps.authors.models import Author class BookSerializer(serializers.ModelSerializer): - shelf = ShelfSerializer(read_only=True) - shelf_id = serializers.PrimaryKeyRelatedField( - queryset=Shelf.objects.all(), - source="shelf", - write_only=True - ) - authors = AuthorSerializer(many=True, read_only=True) authors_id = serializers.PrimaryKeyRelatedField( queryset=Author.objects.all(), source="authors", many=True, - allow_null=True, - write_only=True + write_only=True, + required=False ) class Meta: @@ -28,27 +19,23 @@ class BookSerializer(serializers.ModelSerializer): fields = [ "id", "name", - "isbn", - "shelf", - "shelf_id", + "isbn", "authors", "authors_id" ] read_only_fields = ["id"] - def __init__(self, instance=None, data=..., **kwargs): - super().__init__(instance, data, **kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) request = self.context.get("request") - if request and request.user: - self.fields["shelf_id"].queryset = Shelf.objects.filter( - user=request.user - ) - + if request and request.user and request.user.is_authenticated: self.fields["authors_id"].queryset = Author.objects.filter( user=request.user ) + else: + self.fields["authors_id"].queryset = Author.objects.none() def create(self, validated_data): diff --git a/backend/apps/books/views.py b/backend/apps/books/views.py index 67aa063..1c422a9 100644 --- a/backend/apps/books/views.py +++ b/backend/apps/books/views.py @@ -14,6 +14,5 @@ class BooksViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): - return self.queryset.filter( - shelf__user_id=self.request.user.pk - ).prefetch_related("authors") + return self.queryset.filter(user=self.request.user) + diff --git a/backend/apps/shelf_items/__init__.py b/backend/apps/shelf_books/__init__.py similarity index 100% rename from backend/apps/shelf_items/__init__.py rename to backend/apps/shelf_books/__init__.py diff --git a/backend/apps/shelf_items/admin.py b/backend/apps/shelf_books/admin.py similarity index 100% rename from backend/apps/shelf_items/admin.py rename to backend/apps/shelf_books/admin.py diff --git a/backend/apps/shelf_books/apps.py b/backend/apps/shelf_books/apps.py new file mode 100644 index 0000000..609c2f6 --- /dev/null +++ b/backend/apps/shelf_books/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ShelfBooksConfig(AppConfig): + name = 'apps.shelf_books' diff --git a/backend/apps/shelf_books/migrations/0001_initial.py b/backend/apps/shelf_books/migrations/0001_initial.py new file mode 100644 index 0000000..e869de3 --- /dev/null +++ b/backend/apps/shelf_books/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0.3 on 2026-03-22 12:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('books', '0001_initial'), + ('shelves', '0002_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ShelfItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=0)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shelf_items', to='books.book')), + ('shelf', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='book_items', to='shelves.shelf')), + ], + options={ + 'constraints': [models.UniqueConstraint(fields=('book', 'shelf'), name='unique_book_per_shelf')], + }, + ), + ] diff --git a/backend/apps/shelf_books/migrations/0002_rename_shelfitem_shelfbook.py b/backend/apps/shelf_books/migrations/0002_rename_shelfitem_shelfbook.py new file mode 100644 index 0000000..bd28ccc --- /dev/null +++ b/backend/apps/shelf_books/migrations/0002_rename_shelfitem_shelfbook.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.3 on 2026-03-22 12:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('books', '0001_initial'), + ('shelf_books', '0001_initial'), + ('shelves', '0002_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='ShelfItem', + new_name='ShelfBook', + ), + ] diff --git a/backend/apps/shelf_items/migrations/__init__.py b/backend/apps/shelf_books/migrations/__init__.py similarity index 100% rename from backend/apps/shelf_items/migrations/__init__.py rename to backend/apps/shelf_books/migrations/__init__.py diff --git a/backend/apps/shelf_items/models.py b/backend/apps/shelf_books/models.py similarity index 94% rename from backend/apps/shelf_items/models.py rename to backend/apps/shelf_books/models.py index 2922bcc..c873ac0 100644 --- a/backend/apps/shelf_items/models.py +++ b/backend/apps/shelf_books/models.py @@ -4,7 +4,7 @@ from django.db import models from apps.books.models import Book from apps.shelves.models import Shelf -class ShelfItem(models.Model): +class ShelfBook(models.Model): book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="shelf_items") shelf = models.ForeignKey(Shelf, on_delete=models.CASCADE, related_name="book_items") diff --git a/backend/apps/shelf_books/serializers.py b/backend/apps/shelf_books/serializers.py new file mode 100644 index 0000000..4d50a9f --- /dev/null +++ b/backend/apps/shelf_books/serializers.py @@ -0,0 +1,84 @@ +from rest_framework import serializers + +from apps.shelf_books.models import ShelfBook +from apps.books.models import Book +from apps.shelves.models import Shelf +from apps.shelves.serializers import ShelfSerializer +from apps.books.serializers import BookSerializer + +class ShelfBookSerializer(serializers.ModelSerializer): + shelf = ShelfSerializer(read_only=True) + + book = BookSerializer(read_only=True) + book_id = serializers.PrimaryKeyRelatedField( + queryset=Book.objects.all(), + source="book", + write_only=True + ) + + class Meta: + model = ShelfBook + fields = [ + "id", + "shelf", + "book", + "book_id", + "quantity" + ] + read_only_fields = ["id"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + request = self.context.get("request") + if request and request.user and request.user.is_authenticated: + self.fields["book_id"].queryset = Book.objects.filter( + user=request.user + ) + else: + self.fields["book_id"].queryset = Book.objects.none() + + + def validate(self, attrs): + attrs = super().validate(attrs) + + book = attrs.get("book") + shelf = self.context.get("shelf") + + if not book: + book = self.instance.book + + if not shelf: + return attrs + + qs = ShelfBook.objects.filter( + book=book, + shelf=shelf + ) + + if self.instance: + qs = qs.exclude(pk=self.instance.pk) + + if qs.exists(): + raise serializers.ValidationError( + "This Book already exists in shelf!" + ) + + if book.user_id != shelf.user_id: + raise serializers.ValidationError( + "Book and Shelf must be from the same user!" + ) + + return attrs + + def create(self, validated_data): + shelf = self.context["shelf"] + validated_data["shelf"] = shelf + + return super().create(validated_data) + + def update(self, instance, validated_data): + shelf = self.context["shelf"] + validated_data["shelf"] = shelf + + return super().update(instance, validated_data) \ No newline at end of file diff --git a/backend/apps/shelf_items/tests.py b/backend/apps/shelf_books/tests.py similarity index 100% rename from backend/apps/shelf_items/tests.py rename to backend/apps/shelf_books/tests.py diff --git a/backend/apps/shelf_books/urls.py b/backend/apps/shelf_books/urls.py new file mode 100644 index 0000000..3744475 --- /dev/null +++ b/backend/apps/shelf_books/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from apps.shelf_books.views import ShelfBooks, SelectedShelfBook + +urlpatterns = [ + path("shelves//books/", ShelfBooks.as_view()), + path("shelves//books//", SelectedShelfBook.as_view()) +] + + diff --git a/backend/apps/shelf_books/views.py b/backend/apps/shelf_books/views.py new file mode 100644 index 0000000..0ba934f --- /dev/null +++ b/backend/apps/shelf_books/views.py @@ -0,0 +1,84 @@ +from rest_framework import generics + +from apps.shelf_books.models import ShelfBook +from apps.shelves.models import Shelf +from apps.shelf_books.serializers import ShelfBookSerializer +from rest_framework.permissions import IsAuthenticated +from rest_framework_simplejwt.authentication import JWTAuthentication +from drf_spectacular.utils import extend_schema, extend_schema_view + + +@extend_schema_view( + get=extend_schema(tags=["Shelf Books"]), + post=extend_schema(tags=["Shelf Books"]), +) +class ShelfBooks(generics.ListCreateAPIView): + queryset = ShelfBook.objects.all() + serializer_class = ShelfBookSerializer + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + shelf_id = self.kwargs["shelf_id"] + + return ShelfBook.objects.filter( + shelf__id=shelf_id, + shelf__user_id=self.request.user.pk, + ).select_related("book", "shelf").prefetch_related("book__authors") + + def get_serializer_context(self): + context = super().get_serializer_context() + + shelf_id = self.kwargs["shelf_id"] + + shelf = Shelf.objects.filter( + id=shelf_id, + user=self.request.user + ).first() + + context["shelf"] = shelf + return context + + +@extend_schema_view( + get=extend_schema(tags=["Shelf Books"]), + put=extend_schema(tags=["Shelf Books"]), + patch=extend_schema(tags=["Shelf Books"]), + delete=extend_schema(tags=["Shelf Books"]), +) +class SelectedShelfBook(generics.RetrieveUpdateDestroyAPIView): + queryset = ShelfBook.objects.all() + serializer_class = ShelfBookSerializer + + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + lookup_url_kwarg = "book_id" + + def get_queryset(self): + return ShelfBook.objects.filter( + shelf_id=self.kwargs["shelf_id"], + shelf__user=self.request.user, + ).select_related("book", "shelf").prefetch_related("book__authors") + + def get_object(self): + queryset = self.get_queryset() + + return generics.get_object_or_404( + queryset, + book_id=self.kwargs["book_id"] + ) + + def get_serializer_context(self): + context = super().get_serializer_context() + + shelf = Shelf.objects.filter( + id=self.kwargs["shelf_id"], + user=self.request.user + ).first() + + context["shelf"] = shelf + return context + + \ No newline at end of file diff --git a/backend/apps/shelf_items/apps.py b/backend/apps/shelf_items/apps.py deleted file mode 100644 index 35e40bb..0000000 --- a/backend/apps/shelf_items/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ShelfItemsConfig(AppConfig): - name = 'apps.shelf_items' diff --git a/backend/apps/shelf_items/serializers.py b/backend/apps/shelf_items/serializers.py deleted file mode 100644 index 0440ad0..0000000 --- a/backend/apps/shelf_items/serializers.py +++ /dev/null @@ -1,77 +0,0 @@ -from rest_framework import serializers - -from apps.shelf_items.models import ShelfItem -from apps.books.models import Book -from apps.shelves.models import Shelf -from apps.shelves.serializers import ShelfSerializer -from apps.books.serializers import BookSerializer - -class ShelfItemSerializer(serializers.ModelSerializer): - shelf = ShelfSerializer(read_only=True) - shelf_id = serializers.PrimaryKeyRelatedField( - queryset=Shelf.objects.all(), - source="shelf", - write_only=True - ) - - book = BookSerializer(read_only=True) - book_id = serializers.PrimaryKeyRelatedField( - queryset=Book.objects.all(), - source="book", - write_only=True - ) - - class Meta: - model = ShelfItem - fields = [ - "id", - "shelf", - "shelf_id", - "book", - "book_id", - "quantity" - ] - read_only_fields = ["id"] - - def __init__(self, instance=None, data=..., **kwargs): - super().__init__(instance, data, **kwargs) - - request = self.context.get("request") - if request and request.user: - self.fields["shelf_id"].queryset = Shelf.objects.filter( - user=request.user - ) - - self.fields["book_id"].queryset = Book.objects.filter( - user=request.user - ) - - def validate(self, attrs): - attrs = super().validate(attrs) - - book = attrs.get("book") - shelf = attrs.get("shelf") - - if book and shelf: - qs = ShelfItem.objects.filter( - book=book, - shelf=shelf - ) - - if self.instance: - qs = qs.exclude(pk=self.instance.pk) - - if qs.exists(): - raise serializers.ValidationError( - "This Book already exists in shelf!" - ) - - if book.user_id != shelf.user_id: - raise serializers.ValidationError( - "Book and Shelf must be from the same user!" - ) - - return attrs - - - diff --git a/backend/apps/shelf_items/urls.py b/backend/apps/shelf_items/urls.py deleted file mode 100644 index d2b1b22..0000000 --- a/backend/apps/shelf_items/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework.routers import DefaultRouter - -from apps.shelf_items.views import ShelfItemsViewSet - -urlpatterns = [] - -router = DefaultRouter() -router.register("", viewset=ShelfItemsViewSet) -urlpatterns += router.urls \ No newline at end of file diff --git a/backend/apps/shelf_items/views.py b/backend/apps/shelf_items/views.py deleted file mode 100644 index 7e9bf26..0000000 --- a/backend/apps/shelf_items/views.py +++ /dev/null @@ -1,20 +0,0 @@ -from rest_framework import viewsets - -from apps.shelf_items.models import ShelfItem -from apps.shelf_items.serializers import ShelfItemSerializer -from rest_framework import viewsets -from rest_framework.permissions import IsAuthenticated -from rest_framework_simplejwt.authentication import JWTAuthentication - - -class ShelfItemsViewSet(viewsets.ModelViewSet): - queryset = ShelfItem.objects.all() - serializer_class = ShelfItemSerializer - - authentication_classes = [JWTAuthentication] - permission_classes = [IsAuthenticated] - - def get_queryset(self): - return ShelfItem.objects.filter( - shelf__user_id=self.request.user.pk, - ).select_related("book", "shelf").prefetch_related("book__authors") diff --git a/backend/apps/urls.py b/backend/apps/urls.py index b8ab880..aee703f 100644 --- a/backend/apps/urls.py +++ b/backend/apps/urls.py @@ -6,6 +6,7 @@ urlpatterns = [ path("shelves/", include("apps.shelves.urls")), path("books/", include("apps.books.urls")), path("authors/", include("apps.authors.urls")), + path("", include("apps.shelf_books.urls")), path('schema/', SpectacularAPIView.as_view(), name='schema'), path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), diff --git a/backend/config/django/base.py b/backend/config/django/base.py index dc548ad..7623546 100644 --- a/backend/config/django/base.py +++ b/backend/config/django/base.py @@ -1,4 +1,5 @@ import os +from datetime import timedelta from config.env import BASE_DIR, env @@ -21,6 +22,7 @@ INSTALLED_APPS = [ "apps.shelves.apps.ShelvesConfig", "apps.books.apps.BooksConfig", "apps.authors.apps.AuthorsConfig", + "apps.shelf_books.apps.ShelfBooksConfig", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -112,9 +114,18 @@ SPECTACULAR_SETTINGS = { 'DESCRIPTION': 'Library project', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, + "COMPONENT_SPLIT_REQUEST": True, + "SCHEMA_PATH_PREFIX": "/api/", } +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(days=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=7), +} + + + # Internationalization # https://docs.djangoproject.com/en/6.0/topics/i18n/