diff --git a/apps/books/api/__init__.py b/apps/books/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/books/api/v1/__init__.py b/apps/books/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/books/api/v1/filters.py b/apps/books/api/v1/filters.py new file mode 100644 index 0000000..fb1d0e6 --- /dev/null +++ b/apps/books/api/v1/filters.py @@ -0,0 +1,36 @@ +from django_filters import CharFilter, DateFilter, FilterSet + +from ...models import Book + + +class BookFilter(FilterSet): + title = CharFilter( + lookup_expr="icontains", + ) + author = CharFilter( + field_name="author__last_name", + lookup_expr="icontains", + ) + publisher = CharFilter( + field_name="publisher__name", + lookup_expr="icontains", + ) + tag = CharFilter( + field_name="tags__name", + lookup_expr="iexact", + ) + language = CharFilter( + lookup_expr="iexact", + ) + published_after = DateFilter( + field_name="published_at", + lookup_expr="gte", + ) + published_before = DateFilter( + field_name="published_at", + lookup_expr="lte", + ) + + class Meta: + model = Book + fields = [] diff --git a/apps/books/api/v1/serializers.py b/apps/books/api/v1/serializers.py new file mode 100644 index 0000000..c7040e6 --- /dev/null +++ b/apps/books/api/v1/serializers.py @@ -0,0 +1,71 @@ +from rest_framework import serializers + +from ...models import ( + Author, + Book, + Comment, + Publisher, + Tag, +) + + +class PublisherSerializer(serializers.ModelSerializer): + class Meta: + model = Publisher + fields = "__all__" + + +class AuthorSerializer(serializers.ModelSerializer): + class Meta: + model = Author + fields = "__all__" + + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = "__all__" + + +class BookSerializer(serializers.ModelSerializer): + author = AuthorSerializer(many=True, read_only=True) + publisher = PublisherSerializer(read_only=True) + + class Meta: + model = Book + fields = [ + "id", + "title", + "author", + "publisher", + "published_at", + ] + + +class BookDetailSerializer(BookSerializer): + tags = TagSerializer(many=True, read_only=True) + comments = serializers.SerializerMethodField() + + class Meta(BookSerializer.Meta): + fields = BookSerializer.Meta.fields + [ + "description", + "isbn_code", + "total_pages", + "cover_image", + "language", + "tags", + "comments", + ] + + def get_comments(self, obj): + comments = obj.comments.all()[:5] + return CommentSerializer(comments, many=True).data + + +class CommentSerializer(serializers.ModelSerializer): + user = serializers.StringRelatedField() + + class Meta: + model = Comment + fields = ["id", "text", "user", "created", "modified"] + read_only_fields = ["user", "created", "modified"] diff --git a/apps/books/api/v1/urls.py b/apps/books/api/v1/urls.py new file mode 100644 index 0000000..72fddb9 --- /dev/null +++ b/apps/books/api/v1/urls.py @@ -0,0 +1,22 @@ +from django.urls import include, path +from rest_framework import routers + +from .views import ( + AuthorViewSet, + BookViewSet, + CommentViewSet, + PublisherViewSet, + TagViewSet, +) + +router = routers.DefaultRouter() + +router.register(r"authors", AuthorViewSet) +router.register(r"books", BookViewSet) +router.register(r"comments", CommentViewSet) +router.register(r"publishers", PublisherViewSet) +router.register(r"tags", TagViewSet) + +urlpatterns = [ + path("", include(router.urls)), +] diff --git a/apps/books/api/v1/views.py b/apps/books/api/v1/views.py new file mode 100644 index 0000000..e1da3f1 --- /dev/null +++ b/apps/books/api/v1/views.py @@ -0,0 +1,55 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import viewsets + +from ...models import ( + Author, + Book, + Comment, + Publisher, + Tag, +) +from .filters import BookFilter +from .serializers import ( + AuthorSerializer, + BookDetailSerializer, + BookSerializer, + CommentSerializer, + PublisherSerializer, + TagSerializer, +) + + +class PublisherViewSet(viewsets.ModelViewSet): + queryset = Publisher.objects.all() + serializer_class = PublisherSerializer + + +class AuthorViewSet(viewsets.ModelViewSet): + queryset = Author.objects.all() + serializer_class = AuthorSerializer + + +class TagViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer + + +class BookViewSet(viewsets.ModelViewSet): + queryset = Book.objects.select_related("publisher").prefetch_related( + "author__books", "tags" + ) + filter_backends = [DjangoFilterBackend] + filterset_class = BookFilter + + def get_serializer_class(self): + if self.action == "retrieve": + return BookDetailSerializer + return BookSerializer + + +class CommentViewSet(viewsets.ModelViewSet): + queryset = Comment.objects.select_related("user", "book") + serializer_class = CommentSerializer + + def perform_create(self, serializer): + serializer.save(user=self.request.user) diff --git a/apps/books/management/commands/parse_books.py b/apps/books/management/commands/parse_books.py index 3cb2238..457a64f 100644 --- a/apps/books/management/commands/parse_books.py +++ b/apps/books/management/commands/parse_books.py @@ -1,14 +1,15 @@ import asyncio +from urllib.parse import urljoin + from asgiref.sync import sync_to_async from django.core.management.base import BaseCommand -from urllib.parse import urljoin -from apps.books.models import Book, Author, Publisher -from apps.books.services.book_saver import BookSaver +from apps.books.models import Author, Book, Publisher +from apps.books.scrapers.base_scraper import BaseScraper from apps.books.scrapers.piter_publ.book_parser import BookParser from apps.books.scrapers.piter_publ.piter_scraper import PiterScraper -from apps.books.scrapers.base_scraper import BaseScraper from apps.books.services.author_service import AuthorService +from apps.books.services.book_saver import BookSaver from apps.books.services.publisher_service import PublisherService from logger.books.log import get_logger diff --git a/apps/books/models.py b/apps/books/models.py index 3e69fb7..8306b54 100644 --- a/apps/books/models.py +++ b/apps/books/models.py @@ -1,5 +1,5 @@ -from django.db import models from django.contrib.auth import get_user_model +from django.db import models from django_extensions.db.models import TimeStampedModel User = get_user_model() diff --git a/apps/books/scrapers/base_scraper.py b/apps/books/scrapers/base_scraper.py index e47d582..4ea165d 100644 --- a/apps/books/scrapers/base_scraper.py +++ b/apps/books/scrapers/base_scraper.py @@ -1,6 +1,6 @@ import asyncio -import httpx +import httpx from bs4 import BeautifulSoup diff --git a/apps/books/scrapers/piter_publ/book_parser.py b/apps/books/scrapers/piter_publ/book_parser.py index 74f5883..13427cf 100644 --- a/apps/books/scrapers/piter_publ/book_parser.py +++ b/apps/books/scrapers/piter_publ/book_parser.py @@ -1,7 +1,8 @@ -from bs4 import BeautifulSoup -from typing import List, Dict +from typing import Dict, List from urllib.parse import urljoin +from bs4 import BeautifulSoup + from logger.books.log import get_logger logger = get_logger(__name__) diff --git a/apps/books/scrapers/piter_publ/link_extractor.py b/apps/books/scrapers/piter_publ/link_extractor.py index 843d202..99da1e1 100644 --- a/apps/books/scrapers/piter_publ/link_extractor.py +++ b/apps/books/scrapers/piter_publ/link_extractor.py @@ -1,6 +1,7 @@ -from bs4 import BeautifulSoup -from urllib.parse import urljoin from typing import List +from urllib.parse import urljoin + +from bs4 import BeautifulSoup from logger.books.log import get_logger diff --git a/apps/books/scrapers/piter_publ/paginator.py b/apps/books/scrapers/piter_publ/paginator.py index 55fcf62..44745cb 100644 --- a/apps/books/scrapers/piter_publ/paginator.py +++ b/apps/books/scrapers/piter_publ/paginator.py @@ -1,7 +1,8 @@ import re -from bs4 import BeautifulSoup from urllib.parse import urljoin +from bs4 import BeautifulSoup + from logger.books.log import get_logger logger = get_logger(__name__) diff --git a/apps/books/scrapers/piter_publ/piter_scraper.py b/apps/books/scrapers/piter_publ/piter_scraper.py index a5035c1..c390103 100644 --- a/apps/books/scrapers/piter_publ/piter_scraper.py +++ b/apps/books/scrapers/piter_publ/piter_scraper.py @@ -1,9 +1,10 @@ import asyncio +from logger.books.log import get_logger + from ..base_scraper import BaseScraper -from .paginator import Paginator from .link_extractor import LinkExtractor -from logger.books.log import get_logger +from .paginator import Paginator logger = get_logger(__name__) BASE_DOMAIN = "https://www.piter.com" diff --git a/apps/books/services/book_saver.py b/apps/books/services/book_saver.py index 25dbfde..292e4ac 100644 --- a/apps/books/services/book_saver.py +++ b/apps/books/services/book_saver.py @@ -1,11 +1,12 @@ from datetime import datetime + from django.db import transaction from pydantic import ValidationError -from apps.books.models import Book, Author, Publisher -from apps.books.validators.validators import BookInput +from apps.books.models import Author, Book, Publisher from apps.books.services.author_service import AuthorService from apps.books.services.publisher_service import PublisherService +from apps.books.validators.validators import BookInput from logger.books.log import get_logger logger = get_logger(__name__) diff --git a/apps/books/tasks.py b/apps/books/tasks.py index c043edf..5ecfe1a 100644 --- a/apps/books/tasks.py +++ b/apps/books/tasks.py @@ -3,12 +3,12 @@ from asgiref.sync import sync_to_async from celery import shared_task -from apps.books.scrapers.piter_publ.piter_scraper import PiterScraper from apps.books.management.commands.parse_books import ( AsyncBookFetcher, book_saver, logger, ) +from apps.books.scrapers.piter_publ.piter_scraper import PiterScraper @shared_task diff --git a/apps/books/validators/validators.py b/apps/books/validators/validators.py index 9dfb915..fd6e7d3 100644 --- a/apps/books/validators/validators.py +++ b/apps/books/validators/validators.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, field_validator from typing import List, Optional +from pydantic import BaseModel, Field, field_validator + class AuthorInput(BaseModel): first_name: str = "" diff --git a/config/celery_app.py b/config/celery_app.py index 58f5eaa..bd41a1c 100644 --- a/config/celery_app.py +++ b/config/celery_app.py @@ -1,6 +1,6 @@ import os -from celery import Celery +from celery import Celery os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") diff --git a/config/settings.py b/config/settings.py index 63b4a7e..b8c78ae 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,16 +1,15 @@ import os - from pathlib import Path -from celery.schedules import crontab import environ +from celery.schedules import crontab # Initialize environment variables env = environ.Env() BASE_DIR = Path(__file__).resolve().parent.parent -# Take environment variables from .env file +# Take environment variables from .env.example file environ.Env.read_env(os.path.join(BASE_DIR, ".env")) # ======================== @@ -34,6 +33,8 @@ "django.contrib.staticfiles", "apps.books.apps.BooksConfig", "django_extensions", + "rest_framework", + "django_filters", ] MIDDLEWARE = [ @@ -121,6 +122,20 @@ # ==================== DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# ==================== +# DJANGO REST FRAMEWORK +# ==================== +REST_FRAMEWORK = { + "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 20, + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.TokenAuthentication", + ], +} + # ==================== # CELERY SETTINGS # ==================== diff --git a/config/urls.py b/config/urls.py index 083932c..f513c10 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,6 +1,7 @@ from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), + path("api/v1/", include("apps.books.api.v1.urls")), ]