From 7b216d55b38e75efc6b65facf76bde99b06b594f Mon Sep 17 00:00:00 2001 From: Okemwag Date: Thu, 13 Jun 2024 22:29:57 +0300 Subject: [PATCH] Chore: ( Documented api) --- apps/posts/mixins.py | 31 ------- apps/posts/selectors.py | 35 ++++++++ apps/posts/urls.py | 13 ++- apps/posts/views.py | 178 ++++++++++++++++++++++++++++++++++++++++ config/settings.py | 3 +- config/urls.py | 52 ++++++++---- 6 files changed, 260 insertions(+), 52 deletions(-) delete mode 100644 apps/posts/mixins.py create mode 100644 apps/posts/selectors.py diff --git a/apps/posts/mixins.py b/apps/posts/mixins.py deleted file mode 100644 index c376017..0000000 --- a/apps/posts/mixins.py +++ /dev/null @@ -1,31 +0,0 @@ -from rest_framework.mixins import CreateModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin -from rest_framework.renderers import JSONRenderer -from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated, AllowAny - - -class BaseMixin(CreateModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin): - renderer_classes = [JSONRenderer] - permission_classes = [IsAuthenticated] - - def create(self, request, *args, **kwargs): - return super().create(request, *args, **kwargs) - - def list(self, request, *args, **kwargs): - return super().list(request, *args, **kwargs) - - def retrieve(self, request, *args, **kwargs): - return super().retrieve(request, *args, **kwargs) - - def update(self, request, *args, **kwargs): - return super().update(request, *args, **kwargs) - - def destroy(self, request, *args, **kwargs): - return super().destroy(request, *args, **kwargs) - - def get_permissions(self): - if self.action == 'create': - self.permission_classes = [AllowAny] - return super().get_permissions() - - diff --git a/apps/posts/selectors.py b/apps/posts/selectors.py new file mode 100644 index 0000000..03b6900 --- /dev/null +++ b/apps/posts/selectors.py @@ -0,0 +1,35 @@ +from typing import Optional +from django.db.models import QuerySet, Q +from .models import Post, LikePost + + +def get_all_posts() -> QuerySet[Post]: + return Post.objects.all() + + +def get_user_posts(user_id: int) -> QuerySet[Post]: + return Post.objects.filter(author_id=user_id) + + +def search_posts(query: str) -> QuerySet[Post]: + return Post.objects.filter( + Q(caption__icontains=query) | Q(author__first_name__icontains(query)) + ) + + +def get_trending_posts(limit: int = 5) -> QuerySet[Post]: + return Post.objects.order_by("-date_posted")[:limit] + + +def get_post_by_id(post_id: int) -> Optional[Post]: + try: + return Post.objects.get(id=post_id) + except Post.DoesNotExist: + return None + + +def get_like_for_post(user_id: int, post_id: int) -> Optional[LikePost]: + try: + return LikePost.objects.get(user_id=user_id, post_id=post_id) + except LikePost.DoesNotExist: + return None diff --git a/apps/posts/urls.py b/apps/posts/urls.py index e2f3c6c..6b2545b 100644 --- a/apps/posts/urls.py +++ b/apps/posts/urls.py @@ -1,6 +1,15 @@ from rest_framework import routers -from .views import PostViewSet +from .views import PostViewSet, LikePostViewSet +app_name = 'apps.posts' + router = routers.DefaultRouter() -router.register(r'posts', PostViewSet) \ No newline at end of file +# router.register(r"posts", PostViewSet) +# router.register(r"likes", LikePostViewSet) + +router.register("posts", PostViewSet, basename="posts") +router.register("likes", LikePostViewSet, basename="likes") + + +urlpatterns = router.urls diff --git a/apps/posts/views.py b/apps/posts/views.py index e69de29..5c28115 100644 --- a/apps/posts/views.py +++ b/apps/posts/views.py @@ -0,0 +1,178 @@ +from rest_framework import viewsets, status, filters +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.response import Response +from rest_framework.decorators import action +from django_filters.rest_framework import DjangoFilterBackend +from django.core.exceptions import ObjectDoesNotExist +import logging +from typing import Any, Dict + +from .models import LikePost +from .serializers import PostSerializer, LikePostSerializer +from .exceptions import PostDoesNotExist +from .selectors import ( + get_all_posts, + get_user_posts, + search_posts, + get_trending_posts, + +) + +logger = logging.getLogger(__name__) + + +class PostViewSet(viewsets.ModelViewSet): + queryset = get_all_posts() + serializer_class = PostSerializer + permission_classes = [IsAuthenticated] + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + filterset_fields = ["author"] + search_fields = ["caption"] + + def create(self, request: Any, *args: Any, **kwargs: Any) -> Response: + try: + data: Dict[str, Any] = request.data + data["author"] = request.user.id + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + except Exception as e: + logger.error(f"Error creating post: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=True, methods=["post"], permission_classes=[IsAuthenticated]) + def like(self, request: Any, pk: int = None) -> Response: + try: + post = self.get_object() + like, created = LikePost.objects.get_or_create(user=request.user, post=post) + if not created: + like.delete() + return Response({"message": "Unliked"}, status=status.HTTP_200_OK) + return Response({"message": "Liked"}, status=status.HTTP_201_CREATED) + except ObjectDoesNotExist: + logger.error(f"Post with id {pk} does not exist") + raise PostDoesNotExist() + except Exception as e: + logger.error(f"Error liking post: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=["get"], permission_classes=[IsAuthenticated]) + def my_posts(self, request: Any) -> Response: + try: + queryset = get_user_posts(request.user.id) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + logger.error(f"Error retrieving my posts: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=["get"], permission_classes=[AllowAny]) + def search(self, request: Any) -> Response: + try: + query: str = request.query_params.get("q", "") + queryset = search_posts(query) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + logger.error(f"Error searching posts: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=["get"], permission_classes=[AllowAny]) + def trending(self, request: Any) -> Response: + try: + queryset = get_trending_posts() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + logger.error(f"Error retrieving trending posts: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class LikePostViewSet(viewsets.ModelViewSet): + queryset = LikePost.objects.all() + serializer_class = LikePostSerializer + permission_classes = [IsAuthenticated] + filter_backends = [DjangoFilterBackend] + filterset_fields = ["user", "post"] + + def create(self, request: Any, *args: Any, **kwargs: Any) -> Response: + try: + data: Dict[str, Any] = request.data + data["user"] = request.user.id + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + except Exception as e: + logger.error(f"Error creating like: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request: Any, *args: Any, **kwargs: Any) -> Response: + try: + instance = self.get_object() + if instance.user == request.user: + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + {"message": "You are not allowed to delete this"}, + status=status.HTTP_403_FORBIDDEN, + ) + except ObjectDoesNotExist: + logger.error(f"Like with id {kwargs['pk']} does not exist") + return Response( + {"message": "Like does not exist"}, status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + logger.error(f"Error deleting like: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + def list(self, request: Any, *args: Any, **kwargs: Any) -> Response: + try: + queryset = self.queryset.filter(user=request.user) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + logger.error(f"Error listing likes: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request: Any, *args: Any, **kwargs: Any) -> Response: + try: + instance = self.get_object() + if instance.user == request.user: + serializer = self.get_serializer(instance) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response( + {"message": "You are not allowed to view this"}, + status=status.HTTP_403_FORBIDDEN, + ) + except ObjectDoesNotExist: + logger.error(f"Like with id {kwargs['pk']} does not exist") + return Response( + {"message": "Like does not exist"}, status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + logger.error(f"Error retrieving like: {e}") + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + def update(self, request: Any, *args: Any, **kwargs: Any) -> Response: + try: + instance = self.get_object() + if instance.user == request.user: + serializer = self.get_serializer( + instance, data=request.data, partial=True + ) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response( + {"message": "You are not allowed to update this"}, + status=status.HTTP_403_FORBIDDEN, + ) + except ObjectDoesNotExist: + logger.error(f"Like with id {kwargs['pk']} does not exist") + return Response( + {"message": "Like does not exist"}, status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + logger diff --git a/config/settings.py b/config/settings.py index 0aa5592..f28c761 100644 --- a/config/settings.py +++ b/config/settings.py @@ -211,7 +211,8 @@ MEDIA_FILE_MAX_AGE = 90 -MEDIA_URL = BASE_DIR +MEDIA_URL = "/media/" +MEDIA_ROOT = BASE_DIR / "media" # Logging configuration diff --git a/config/urls.py b/config/urls.py index 9a92a59..901871b 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,22 +1,38 @@ -""" -URL configuration for config project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.0/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 +from django.urls import path, include +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +... + +schema_view = get_schema_view( + openapi.Info( + title="Snippets API", + default_version="v1", + description="Test description", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=(permissions.AllowAny,), +) urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), + path( + "swagger/", 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"), + path("api/v1/", include("apps.posts.urls")), ] + +admin.site.site_header = "Sharehub Admin" +admin.site.site_title = "Sharehub Admin" +admin.site.index_title = "Sharehub Admin"