diff --git a/.isort.cfg b/.isort.cfg index a6d68c2..3c2df58 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,4 +2,5 @@ line_length = 88 multi_line_output = 3 include_trailing_comma = True -known_third_party = django,django_filters,djchoices,factory,moviewarehouse,requests,rest_framework +known_first_party = moviewarehouse +known_third_party = django,django_filters,djchoices,drf_yasg,factory,requests,rest_framework diff --git a/README.md b/README.md index 3a606f8..43bd5fb 100644 --- a/README.md +++ b/README.md @@ -1 +1,45 @@ -# movie-warehouse \ No newline at end of file +# Welcome to movie-warehouse + +Simple test application to storing and looking for data about movies. You can also add some comments to your downloaded movies and get top movies(ranking is determined by count of comments). + +## Requirements + +* Docker +* Docker Compose + +## Setup +* Clone the repository +* Go to project directory +* mv example.env to .env and replace `xxx` values for your own(OMDB_API_KEY you can get from http://www.omdbapi.com/) +* make up + +## Quickstart guide + +Start the project: + + make up + +Bring project down: + + make down + +To test the project run: + + make test + +To build project: + + make build + +## Documentation + +You can easily review all endpoints using these links: + +* /swagger/ - http://localhost:8000/swagger/ +* /redoc/ - http://localhost:8000/redoc/ + +### Additional packages + +* django-choices - Choices are much cleaner, than standard - some additional functions +* factory-boy - Transparent fixtures, speeding up writing tests +* drf-yasg - Complex documentation without any effor diff --git a/app/moviewarehouse/movies/admin.py b/app/moviewarehouse/movies/admin.py index 1d5afa5..5a20d94 100644 --- a/app/moviewarehouse/movies/admin.py +++ b/app/moviewarehouse/movies/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.template.defaultfilters import truncatechars from django.utils.translation import gettext as _ + from moviewarehouse.movies.models import Comment, Movie diff --git a/app/moviewarehouse/movies/filters.py b/app/moviewarehouse/movies/filters.py index deae560..e6cf432 100644 --- a/app/moviewarehouse/movies/filters.py +++ b/app/moviewarehouse/movies/filters.py @@ -1,4 +1,5 @@ from django_filters import rest_framework as filters + from moviewarehouse.movies.models import Movie diff --git a/app/moviewarehouse/movies/models.py b/app/moviewarehouse/movies/models.py index f4140f4..5543476 100644 --- a/app/moviewarehouse/movies/models.py +++ b/app/moviewarehouse/movies/models.py @@ -1,6 +1,7 @@ from django.db import models from django.template.defaultfilters import truncatechars from django.utils.translation import gettext as _ + from moviewarehouse.movies.choices import RatingChoices diff --git a/app/moviewarehouse/movies/serializers.py b/app/moviewarehouse/movies/serializers.py index e134a9c..382e336 100644 --- a/app/moviewarehouse/movies/serializers.py +++ b/app/moviewarehouse/movies/serializers.py @@ -1,8 +1,9 @@ import re -from moviewarehouse.movies.models import Comment, Movie from rest_framework import serializers +from moviewarehouse.movies.models import Comment, Movie + NUMBER_PATTERN = re.compile(r"\d+") FLOAT_NUMBER_PATTERN = re.compile(r"\d+\.\d+") diff --git a/app/moviewarehouse/movies/tests/test_endpoints.py b/app/moviewarehouse/movies/tests/test_endpoints.py index 1c21a16..0b23fe2 100644 --- a/app/moviewarehouse/movies/tests/test_endpoints.py +++ b/app/moviewarehouse/movies/tests/test_endpoints.py @@ -1,6 +1,10 @@ from datetime import datetime, timedelta from unittest.mock import patch +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase + from moviewarehouse.movies.models import Comment, Movie from moviewarehouse.movies.tests.factories import ( CommentFactory, @@ -9,9 +13,6 @@ mocked_movie_full_data, mocked_movie_not_found, ) -from rest_framework import status -from rest_framework.reverse import reverse -from rest_framework.test import APITestCase class MovieViewSetTest(APITestCase): diff --git a/app/moviewarehouse/movies/utils.py b/app/moviewarehouse/movies/utils.py index 57787f6..bd6a9e1 100644 --- a/app/moviewarehouse/movies/utils.py +++ b/app/moviewarehouse/movies/utils.py @@ -1,16 +1,18 @@ import logging +from typing import Tuple import requests from django.conf import settings +from rest_framework import status + from moviewarehouse.movies.models import Movie from moviewarehouse.movies.serializers import MovieSerializer -from rest_framework import status logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -def get_movie_details(title: str) -> [dict, int]: +def get_movie_details(title: str) -> Tuple[dict, int]: try: movie = Movie.objects.get(title__iexact=title) return MovieSerializer(instance=movie).data, status.HTTP_200_OK diff --git a/app/moviewarehouse/movies/viewsets.py b/app/moviewarehouse/movies/viewsets.py index 7f4cbea..0baf3d4 100644 --- a/app/moviewarehouse/movies/viewsets.py +++ b/app/moviewarehouse/movies/viewsets.py @@ -1,6 +1,11 @@ from django.db.models import Count, F, Q, Window from django.db.models.functions.window import DenseRank from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import status as rest_status +from rest_framework.mixins import CreateModelMixin, ListModelMixin +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + from moviewarehouse.movies.filters import MovieFilter from moviewarehouse.movies.models import Comment, Movie from moviewarehouse.movies.serializers import ( @@ -11,21 +16,18 @@ TopMovieSerializer, ) from moviewarehouse.movies.utils import get_movie_details -from rest_framework import status as rest_status -from rest_framework.mixins import CreateModelMixin, ListModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet class TopMovieViewSet(ListModelMixin, GenericViewSet): queryset = Movie.objects.all() serializer_class = TopMovieSerializer - def list(self, request, *args, **kwargs): - serializer = TopMovieSearchSerializer(data=request.GET) + def get_queryset(self): + queryset = super().get_queryset() + + serializer = TopMovieSearchSerializer(data=self.request.GET) serializer.is_valid(raise_exception=True) - queryset = self.filter_queryset(self.get_queryset()) queryset = queryset.annotate( total_comments=Count( "comments", @@ -39,13 +41,7 @@ def list(self, request, *args, **kwargs): rank=Window(expression=DenseRank(), order_by=F("total_comments").desc()), ) - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) - - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) + return queryset class MovieViewSet(ListModelMixin, CreateModelMixin, GenericViewSet): diff --git a/app/moviewarehouse/urls.py b/app/moviewarehouse/urls.py index 9870629..7bbf5e2 100644 --- a/app/moviewarehouse/urls.py +++ b/app/moviewarehouse/urls.py @@ -1,13 +1,40 @@ from django.contrib import admin -from django.urls import path -from moviewarehouse.movies.viewsets import CommentViewSet, MovieViewSet, TopMovieViewSet +from django.urls import path, re_path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions from rest_framework.routers import DefaultRouter +from moviewarehouse.movies.viewsets import CommentViewSet, MovieViewSet, TopMovieViewSet + +schema_view = get_schema_view( + openapi.Info( + title="Snippets API", + default_version="v1", + contact=openapi.Contact(email="pmlynarek1@gmail.com"), + license=openapi.License(name="MIT License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) router = DefaultRouter() router.register(r"top", TopMovieViewSet, basename="top_movie") router.register(r"movies", MovieViewSet, basename="movie") router.register(r"comments", CommentViewSet, basename="comment") -urlpatterns = [path("admin/", admin.site.urls)] +urlpatterns = [ + path("admin/", admin.site.urls), + re_path( + r"^swagger(?P\.json|\.yaml)$", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + path( + "swagger/", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), +] urlpatterns += router.urls diff --git a/app/requirements/staging.txt b/app/requirements/staging.txt index cf287e2..d480047 100644 --- a/app/requirements/staging.txt +++ b/app/requirements/staging.txt @@ -9,3 +9,4 @@ django-extensions==2.2.9 ipython==7.11.1 django-filter==2.2.0 factory-boy==2.12.0 +drf-yasg==1.17.1 diff --git a/app/settings/dev.py b/app/settings/dev.py index f711d1e..013c24a 100644 --- a/app/settings/dev.py +++ b/app/settings/dev.py @@ -32,6 +32,7 @@ # Third party "django_extensions", "django_filters", + "drf_yasg", # Local "moviewarehouse.movies", ] diff --git a/example.env b/example.env new file mode 100644 index 0000000..7a29ff8 --- /dev/null +++ b/example.env @@ -0,0 +1,9 @@ +DJANGO_SETTINGS_MODULE=settings.dev +DJANGO_SECRET_KEY=xxx + +POSTGRES_HOST=db +POSTGRES_DB=moviewarehouse_dev_db +POSTGRES_USER=moviewarehouse_dev_db_user +POSTGRES_PASSWORD=xxx + +OMDB_API_KEY=xxx