From c521486451fa55ddddbe5198d378eceb68623003 Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Thu, 26 Oct 2023 10:51:00 +0900 Subject: [PATCH 01/10] =?UTF-8?q?:sparkles:Feat=20:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EB=AC=BC=20=EC=A2=8B=EC=95=84=EC=9A=94=20api=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게시물 좋아요, /likes// 의 post method에 대한 APIView Related to #11 --- config/urls.py | 4 +++- likes/serializers.py | 12 ++++++++++++ likes/urls.py | 12 ++++++++++++ likes/views.py | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 likes/serializers.py create mode 100644 likes/urls.py diff --git a/config/urls.py b/config/urls.py index acb037d..19708db 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,7 +1,7 @@ from rest_framework import permissions from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.conf import settings from django.conf.urls.static import static @@ -24,6 +24,8 @@ path("admin/", admin.site.urls), # Swagger path("swagger/docs/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), + # Likes + path("likes/", include("likes.urls")), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/likes/serializers.py b/likes/serializers.py new file mode 100644 index 0000000..f718ec1 --- /dev/null +++ b/likes/serializers.py @@ -0,0 +1,12 @@ +from rest_framework.serializers import ModelSerializer + +from posts.models import Post + + +class PostLikeIncrementSerializer(ModelSerializer): + class Meta: + model = Post + fields = "__all__" + + def save(self, **kwargs): + return super().save(**kwargs) diff --git a/likes/urls.py b/likes/urls.py new file mode 100644 index 0000000..c4ca557 --- /dev/null +++ b/likes/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from rest_framework import routers + +from . import views + +router = routers.SimpleRouter() +router.register(r"", views.LikesAPIViewSet, basename="recruits") + +urlpatterns = [ + path("/", views.LikesAPIView.as_view()), +] +# + router.urls diff --git a/likes/views.py b/likes/views.py index 60f00ef..3bb4abe 100644 --- a/likes/views.py +++ b/likes/views.py @@ -1 +1,32 @@ -# Create your views here. +from rest_framework.exceptions import NotFound +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework.views import APIView + +from posts.models import Post + +from .serializers import PostLikeIncrementSerializer + + +class LikesAPIView(APIView): + permission_classes = [IsAuthenticatedOrReadOnly] + + def get_post(self, pk): + try: + return Post.objects.get(pk=pk) + except Post.DoesNotExist: + raise NotFound("Post Not Found.") + + def post(self, request, content_id): + post = self.get_post(content_id) + like_count = post.like_count + serializer = PostLikeIncrementSerializer( + post, + data={"like_count": like_count + 1}, + partial=True, + ) + + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(status=200) From f10bf1c5d482f3e59a9433d4628fd2063f7f57df Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Thu, 26 Oct 2023 10:52:46 +0900 Subject: [PATCH 02/10] =?UTF-8?q?:white=5Fcheck=5Fmark:Test=20:=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EC=A2=8B=EC=95=84=EC=9A=94=20api?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - authenticated 되지 않은 경우Unauthorized(401) - authenticated 된 경우 like_count를 증가하며 Ok(200) Related to #11 --- likes/tests.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/likes/tests.py b/likes/tests.py index a39b155..78d9c69 100644 --- a/likes/tests.py +++ b/likes/tests.py @@ -1 +1,37 @@ -# Create your tests here. +from rest_framework.test import APIClient, APITestCase + +from posts.models import Post +from users.models import User + + +class LikeAPITestCase(APITestCase): + client = APIClient(enforce_csrf_checks=True) + url = "/likes/" + + def setUp(self): + self.user = User.objects.create(email="user") + + self.post = Post.objects.create(title="title", post_type="facebook", content="content") + + def test_post_without_auth(self): + """logout and post like""" + + self.client.logout() + + url = f"{self.url}{self.post.pk}/" + response = self.client.post(url) + + self.assertEqual(response.status_code, 401) + + def test_post_with_auth(self): + """login and post like""" + + # self.client.force_login(self.user) + self.client.force_authenticate(user=self.user) + + url = f"{self.url}{self.post.pk}/" + + response = self.client.post(url) + + self.assertEqual(response.status_code, 200) + self.assertEqual(self.post.like_count, Post.objects.first().like_count - 1) From 844dfdccb3a844c011c1904c3c4760503ca37e2c Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Thu, 26 Oct 2023 14:12:55 +0900 Subject: [PATCH 03/10] =?UTF-8?q?:fire:Remove=20:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EB=AC=BC=20=EC=A2=8B=EC=95=84=EC=9A=94=20api=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=B6=88=ED=95=84=EC=9A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to #11 --- likes/serializers.py | 3 --- likes/urls.py | 5 ----- 2 files changed, 8 deletions(-) diff --git a/likes/serializers.py b/likes/serializers.py index f718ec1..bb9d954 100644 --- a/likes/serializers.py +++ b/likes/serializers.py @@ -7,6 +7,3 @@ class PostLikeIncrementSerializer(ModelSerializer): class Meta: model = Post fields = "__all__" - - def save(self, **kwargs): - return super().save(**kwargs) diff --git a/likes/urls.py b/likes/urls.py index c4ca557..bedd216 100644 --- a/likes/urls.py +++ b/likes/urls.py @@ -1,12 +1,7 @@ from django.urls import path -from rest_framework import routers from . import views -router = routers.SimpleRouter() -router.register(r"", views.LikesAPIViewSet, basename="recruits") - urlpatterns = [ path("/", views.LikesAPIView.as_view()), ] -# + router.urls From de812cac288f1efda20749bdb54edd10d62fd14e Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Thu, 26 Oct 2023 20:13:10 +0900 Subject: [PATCH 04/10] =?UTF-8?q?:recycle:Refact=20:=20Post=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20#4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - content_id 필드 추가, social post에 대한 구분 식별자 Related to #4 --- posts/migrations/0002_post_content_id.py | 19 +++++++++++++++++++ posts/models.py | 3 +++ 2 files changed, 22 insertions(+) create mode 100644 posts/migrations/0002_post_content_id.py diff --git a/posts/migrations/0002_post_content_id.py b/posts/migrations/0002_post_content_id.py new file mode 100644 index 0000000..4fd24d0 --- /dev/null +++ b/posts/migrations/0002_post_content_id.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.6 on 2023-10-26 11:12 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='post', + name='content_id', + field=models.UUIDField(default=uuid.uuid4, editable=False), + ), + ] diff --git a/posts/models.py b/posts/models.py index bcd9eb9..5c66c5c 100644 --- a/posts/models.py +++ b/posts/models.py @@ -1,3 +1,5 @@ +import uuid + from django.db import models from users.models import User @@ -10,6 +12,7 @@ class PostType(models.TextChoices): INSTAGRAM = "instagram" THREADS = "threads" + content_id = models.UUIDField(default=uuid.uuid4, editable=False) post_type = models.CharField(max_length=16, choices=PostType.choices) title = models.CharField(max_length=32) content = models.TextField() From 5758afcf9d2187130156d6f1fdbcbb5b4b9daea5 Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Thu, 26 Oct 2023 20:28:25 +0900 Subject: [PATCH 05/10] =?UTF-8?q?:recycle:Refact=20:=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20api=EC=97=90=20=EB=8C=80=ED=95=9C=20content=5Fid=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - post 모델의 content_id 필드가 추가됨에 따라서 likes// 리소스 형태로 변경 Related to #11 --- likes/serializers.py | 2 +- likes/tests.py | 4 ++-- likes/urls.py | 2 +- likes/views.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/likes/serializers.py b/likes/serializers.py index bb9d954..14f2d7b 100644 --- a/likes/serializers.py +++ b/likes/serializers.py @@ -6,4 +6,4 @@ class PostLikeIncrementSerializer(ModelSerializer): class Meta: model = Post - fields = "__all__" + fields = ("like_count",) diff --git a/likes/tests.py b/likes/tests.py index 78d9c69..7430ea8 100644 --- a/likes/tests.py +++ b/likes/tests.py @@ -18,7 +18,7 @@ def test_post_without_auth(self): self.client.logout() - url = f"{self.url}{self.post.pk}/" + url = f"{self.url}{self.post.content_id}/" response = self.client.post(url) self.assertEqual(response.status_code, 401) @@ -29,7 +29,7 @@ def test_post_with_auth(self): # self.client.force_login(self.user) self.client.force_authenticate(user=self.user) - url = f"{self.url}{self.post.pk}/" + url = f"{self.url}{self.post.content_id}/" response = self.client.post(url) diff --git a/likes/urls.py b/likes/urls.py index bedd216..d9478e6 100644 --- a/likes/urls.py +++ b/likes/urls.py @@ -3,5 +3,5 @@ from . import views urlpatterns = [ - path("/", views.LikesAPIView.as_view()), + path("/", views.LikesAPIView.as_view()), ] diff --git a/likes/views.py b/likes/views.py index 3bb4abe..3ea89da 100644 --- a/likes/views.py +++ b/likes/views.py @@ -11,9 +11,9 @@ class LikesAPIView(APIView): permission_classes = [IsAuthenticatedOrReadOnly] - def get_post(self, pk): + def get_post(self, content_id): try: - return Post.objects.get(pk=pk) + return Post.objects.get(content_id=content_id) except Post.DoesNotExist: raise NotFound("Post Not Found.") From 646f084d820c80e73dcf18abc43bb00cf0a7bea8 Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Fri, 27 Oct 2023 06:45:07 +0900 Subject: [PATCH 06/10] =?UTF-8?q?:recycle:Refact=20:=20LikeApi=EC=9D=98=20?= =?UTF-8?q?post=20object=20getter=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - post 모델에 대한 getter는 django의 내장 함수로 대체 가능 - 가독성 증가 Related to #11 --- likes/views.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/likes/views.py b/likes/views.py index 3ea89da..e40d117 100644 --- a/likes/views.py +++ b/likes/views.py @@ -1,4 +1,4 @@ -from rest_framework.exceptions import NotFound +from django.shortcuts import get_object_or_404 from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response from rest_framework.views import APIView @@ -11,18 +11,12 @@ class LikesAPIView(APIView): permission_classes = [IsAuthenticatedOrReadOnly] - def get_post(self, content_id): - try: - return Post.objects.get(content_id=content_id) - except Post.DoesNotExist: - raise NotFound("Post Not Found.") - def post(self, request, content_id): - post = self.get_post(content_id) - like_count = post.like_count + post = get_object_or_404(Post, content_id=content_id) + serializer = PostLikeIncrementSerializer( post, - data={"like_count": like_count + 1}, + data={"like_count": post.like_count + 1}, partial=True, ) From bd40d3d7510672e1d4115fd7aeb8b4a9ca8990e6 Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Fri, 27 Oct 2023 06:53:38 +0900 Subject: [PATCH 07/10] =?UTF-8?q?:recycle:Refact=20:=20api=20url=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EC=9D=BC=EC=B9=98=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 해당 프로젝트의 url format을 api/{application}/ 로 사용하기로 정함. Related to #11 --- config/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/urls.py b/config/urls.py index 19708db..01ace9f 100644 --- a/config/urls.py +++ b/config/urls.py @@ -25,7 +25,7 @@ # Swagger path("swagger/docs/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), # Likes - path("likes/", include("likes.urls")), + path("api/likes/", include("likes.urls")), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) From 5c2632e9cb41301cbe17199a9f296793dcc288b0 Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Fri, 27 Oct 2023 07:06:38 +0900 Subject: [PATCH 08/10] =?UTF-8?q?:recycle:Refact=20:=20API=20name=20?= =?UTF-8?q?=EB=B6=80=EC=97=AC=20=EB=B0=8F=20testcase=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 뷰에 대한 이름 지정 - API 테스트에서 url을 reverse를 통해 가져올 수 있도록 - 추후 뷰에 대한 변경이 파급이 적도록 Related to #11 --- likes/tests.py | 24 ++++++++++++++++++------ likes/urls.py | 2 +- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/likes/tests.py b/likes/tests.py index 7430ea8..cab7496 100644 --- a/likes/tests.py +++ b/likes/tests.py @@ -1,3 +1,4 @@ +from django.urls import reverse from rest_framework.test import APIClient, APITestCase from posts.models import Post @@ -6,7 +7,7 @@ class LikeAPITestCase(APITestCase): client = APIClient(enforce_csrf_checks=True) - url = "/likes/" + viewname = "likes" def setUp(self): self.user = User.objects.create(email="user") @@ -18,8 +19,14 @@ def test_post_without_auth(self): self.client.logout() - url = f"{self.url}{self.post.content_id}/" - response = self.client.post(url) + response = self.client.post( + path=reverse( + viewname=self.viewname, + kwargs={ + "content_id": self.post.content_id, + }, + ), + ) self.assertEqual(response.status_code, 401) @@ -29,9 +36,14 @@ def test_post_with_auth(self): # self.client.force_login(self.user) self.client.force_authenticate(user=self.user) - url = f"{self.url}{self.post.content_id}/" - - response = self.client.post(url) + response = self.client.post( + path=reverse( + viewname=self.viewname, + kwargs={ + "content_id": self.post.content_id, + }, + ), + ) self.assertEqual(response.status_code, 200) self.assertEqual(self.post.like_count, Post.objects.first().like_count - 1) diff --git a/likes/urls.py b/likes/urls.py index d9478e6..c6d14da 100644 --- a/likes/urls.py +++ b/likes/urls.py @@ -3,5 +3,5 @@ from . import views urlpatterns = [ - path("/", views.LikesAPIView.as_view()), + path("/", views.LikesAPIView.as_view(), name="likes"), ] From 88ac02b2732fb4b18fb375ef8995353f53eb8fb8 Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Fri, 27 Oct 2023 07:14:45 +0900 Subject: [PATCH 09/10] =?UTF-8?q?:recycle:Refact=20:=20=EC=A0=88=EB=8C=80?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20import=20=EB=B3=80=EA=B2=BD=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PEP8 권장에 따른 상대경로에서 절대경로로 변경함 Related to #11 --- likes/urls.py | 2 +- likes/views.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/likes/urls.py b/likes/urls.py index c6d14da..69c972d 100644 --- a/likes/urls.py +++ b/likes/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from . import views +from likes import views urlpatterns = [ path("/", views.LikesAPIView.as_view(), name="likes"), diff --git a/likes/views.py b/likes/views.py index e40d117..d498ee4 100644 --- a/likes/views.py +++ b/likes/views.py @@ -3,10 +3,9 @@ from rest_framework.response import Response from rest_framework.views import APIView +from likes.serializers import PostLikeIncrementSerializer from posts.models import Post -from .serializers import PostLikeIncrementSerializer - class LikesAPIView(APIView): permission_classes = [IsAuthenticatedOrReadOnly] From e7ed99b4aa59ca5206963e88e03bb51406b3cd9f Mon Sep 17 00:00:00 2001 From: chestnut90 Date: Fri, 27 Oct 2023 07:35:45 +0900 Subject: [PATCH 10/10] =?UTF-8?q?:recycle:Refact=20:=20likes=20api?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20swagger=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api 문서화를 위한 swagger 적용 Related to #11 --- likes/views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/likes/views.py b/likes/views.py index d498ee4..18492c2 100644 --- a/likes/views.py +++ b/likes/views.py @@ -1,6 +1,9 @@ from django.shortcuts import get_object_or_404 +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response +from rest_framework.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED from rest_framework.views import APIView from likes.serializers import PostLikeIncrementSerializer @@ -10,6 +13,13 @@ class LikesAPIView(APIView): permission_classes = [IsAuthenticatedOrReadOnly] + @swagger_auto_schema( + operation_summary="게시물에 좋아요", + responses={ + HTTP_200_OK: openapi.Response(description="ok"), + HTTP_401_UNAUTHORIZED: openapi.Response(description="unauthorized"), + }, + ) def post(self, request, content_id): post = get_object_or_404(Post, content_id=content_id)