-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Feature/like] 상품 좋아요 기능 구현
- Loading branch information
Showing
19 changed files
with
324 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from django.contrib import admin | ||
|
||
# Register your models here. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class LikeConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "apps.like" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from django.db import models | ||
|
||
from apps.common.models import BaseModel | ||
from apps.product.models import Product | ||
from apps.user.models import Account | ||
|
||
|
||
class Like(BaseModel): | ||
user = models.ForeignKey(Account, on_delete=models.CASCADE) | ||
product = models.ForeignKey(Product, on_delete=models.CASCADE) | ||
|
||
class Meta: | ||
constraints = [ | ||
models.UniqueConstraint(fields=["user", "product"], name="unique_user_product"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from typing import Any | ||
|
||
from rest_framework import permissions | ||
from rest_framework.request import Request | ||
from rest_framework.views import APIView | ||
|
||
|
||
class IsUserOrReadOnly(permissions.BasePermission): | ||
def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: | ||
if request.method in permissions.SAFE_METHODS: | ||
return True | ||
# return str(request.user) == obj.lender.email | ||
return request.user == getattr(obj, "user", None) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from rest_framework import serializers | ||
|
||
from apps.like.models import Like | ||
from apps.product.models import Product | ||
from apps.product.serializers import ProductSerializer | ||
|
||
|
||
class LikeSerializer(serializers.ModelSerializer[Like]): | ||
product_id = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all(), write_only=True) | ||
product = ProductSerializer(read_only=True) | ||
|
||
class Meta: | ||
model = Like | ||
fields = ("id", "product_id", "product") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
from django.test import TestCase | ||
from django.urls import reverse | ||
from rest_framework import status | ||
from rest_framework.test import APIClient, APITestCase | ||
from rest_framework_simplejwt.tokens import AccessToken | ||
|
||
from apps.category.models import Category | ||
from apps.like.models import Like | ||
from apps.product.models import Product | ||
from apps.user.models import Account | ||
|
||
|
||
class TestLikeListCreateView(APITestCase): | ||
def setUp(self) -> None: | ||
# self.client = APIClient() | ||
self.url = reverse("likes") | ||
data = { | ||
"email": "user@email.com", | ||
"password": "fels3570", | ||
"nickname": "nick", | ||
"phone": "1234", | ||
} | ||
self.user = Account.objects.create_user(**data) | ||
# self.client.force_login(user=self.user) | ||
# self.token = AccessToken.for_user(self.user) | ||
self.category = Category.objects.create(name="test category") | ||
self.product = Product.objects.create( | ||
name="test product", | ||
lender=self.user, | ||
brand="test brand", | ||
condition="good", | ||
purchase_date="2024-01-01", | ||
purchase_price=10000, | ||
rental_fee=1000, | ||
size="xs", | ||
product_category=self.category, | ||
) | ||
|
||
def test_list_likes(self) -> None: | ||
self.client.force_authenticate(user=self.user) | ||
Like.objects.create(user=self.user, product=self.product) | ||
res = self.client.get(self.url) | ||
self.assertEqual(res.status_code, status.HTTP_200_OK) | ||
self.assertEqual(res.data.get("count"), 1) | ||
|
||
def test_create_like(self) -> None: | ||
self.client.force_authenticate(user=self.user) | ||
data = {"product_id": self.product.uuid} | ||
res = self.client.post(self.url, data, format="json") | ||
self.assertEqual(res.status_code, status.HTTP_201_CREATED) | ||
self.assertTrue(Like.objects.filter(user=self.user, product=self.product).exists()) | ||
|
||
def test_create_like_without_product_id(self) -> None: | ||
self.client.force_authenticate(user=self.user) | ||
res = self.client.post(self.url, {}) | ||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertFalse(Like.objects.filter(user=self.user, product=self.product).exists()) | ||
|
||
def test_create_like_already_exists(self) -> None: | ||
self.client.force_authenticate(user=self.user) | ||
Like.objects.create(user=self.user, product=self.product) | ||
data = {"product_id": self.product.uuid} | ||
res = self.client.post(self.url, data, format="json") | ||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) | ||
self.assertEqual(res.data, ["Already liked this product."]) | ||
|
||
def test_create_list_without_login(self) -> None: | ||
data = {"product_id": self.product.uuid} | ||
res = self.client.post(self.url, data, format="json") | ||
self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) | ||
|
||
|
||
class TestLikeDestroyView(APITestCase): | ||
def setUp(self) -> None: | ||
data = { | ||
"email": "user@email.com", | ||
"password": "fels3570", | ||
"nickname": "nick", | ||
"phone": "1234", | ||
} | ||
self.user = Account.objects.create_user(**data) | ||
self.category = Category.objects.create(name="test category") | ||
self.product = Product.objects.create( | ||
name="test product", | ||
lender=self.user, | ||
brand="test brand", | ||
condition="good", | ||
purchase_date="2024-01-01", | ||
purchase_price=10000, | ||
rental_fee=1000, | ||
size="xs", | ||
product_category=self.category, | ||
likes=1, | ||
) | ||
self.like = Like.objects.create(user=self.user, product=self.product) | ||
self.url = reverse("like_delete", kwargs={"pk": self.product.pk}) | ||
|
||
def test_delete_like(self) -> None: | ||
self.client.force_authenticate(user=self.user) | ||
res = self.client.delete(self.url) | ||
self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) | ||
self.assertFalse(Like.objects.filter(user=self.user, product=self.product).exists()) | ||
|
||
# def test_like_count_decreased(self) -> None: | ||
# self.client.force_authenticate(user=self.user) | ||
# initial_count = self.product.likes | ||
# res = self.client.delete(self.url) | ||
# self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) | ||
# self.assertEqual(self.product.likes, initial_count - 1) | ||
|
||
def test_delete_like_not_found(self) -> None: | ||
self.client.force_authenticate(user=self.user) | ||
self.like.delete() | ||
res = self.client.delete(self.url) | ||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) | ||
|
||
def test_delete_like_by_other_user(self) -> None: | ||
data = { | ||
"email": "otuser@email.com", | ||
"password": "fels3570", | ||
"nickname": "dfk", | ||
"phone": "1234", | ||
} | ||
other_user = Account.objects.create_user(**data) | ||
self.client.force_authenticate(other_user) | ||
res = self.client.delete(self.url) | ||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) | ||
|
||
|
||
class TestPermission(APITestCase): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from django.urls import path | ||
|
||
from apps.like.views import LikeDestroyView, LikeListCreateView | ||
|
||
urlpatterns = [ | ||
path("", LikeListCreateView.as_view(), name="likes"), | ||
# path("<uuid:pk>/", LikeCreateView.as_view(), name="like_create"), | ||
path("<uuid:pk>/", LikeDestroyView.as_view(), name="like_delete"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
from typing import Union | ||
from uuid import UUID | ||
|
||
from django.db import IntegrityError, transaction | ||
from django.db.models import F, QuerySet | ||
from rest_framework import generics, permissions | ||
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError | ||
from rest_framework.serializers import BaseSerializer | ||
|
||
from apps.like.models import Like | ||
from apps.like.permissions import IsUserOrReadOnly | ||
from apps.like.serializers import LikeSerializer | ||
from apps.product.models import Product | ||
from apps.user.models import Account | ||
|
||
# class LikeListView(generics.ListAPIView): | ||
# serializer_class = LikeSerializer | ||
# permission_classes = [permissions.IsAuthenticated] | ||
# | ||
# def get_queryset(self) -> QuerySet[Like]: | ||
# user = self.request.user | ||
# return Like.objects.filter(user=user).order_by("-created_at") | ||
|
||
|
||
class LikeListCreateView(generics.ListCreateAPIView[Like]): | ||
serializer_class = LikeSerializer | ||
permissions_classes = [permissions.IsAuthenticated, IsUserOrReadOnly] | ||
|
||
def get_queryset(self) -> QuerySet[Like]: | ||
user = self.request.user | ||
if not isinstance(user, Account): | ||
raise PermissionDenied("You must be logged in to view your likes.") | ||
return Like.objects.filter(user=user).order_by("-created_at") | ||
|
||
def perform_create(self, serializer: BaseSerializer[Like]) -> None: | ||
product_id = self.request.data.get("product_id") | ||
# product = Product.objects.get(pk=product_id) | ||
if not product_id: | ||
raise ValidationError("You must provide a product ID.") | ||
|
||
try: | ||
with transaction.atomic(): | ||
serializer.save(user=self.request.user, product_id=product_id) | ||
Product.objects.filter(pk=product_id).update(likes=F("likes") + 1) | ||
# product.likes = F("likes") + 1 | ||
# product.save(update_fields=["likes"]) | ||
except IntegrityError: | ||
raise ValidationError("Already liked this product.") | ||
|
||
|
||
class LikeDestroyView(generics.DestroyAPIView[Like]): | ||
# serializer_class = LikeSerializer | ||
permissions_classes = [permissions.IsAuthenticated, IsUserOrReadOnly] | ||
|
||
def get_object(self) -> Like: | ||
product_id = self.kwargs.get("pk") | ||
user = self.request.user if isinstance(self.request.user, Account) else None | ||
# like = Like.objects.filter(user=user, product_id=product_id).first() | ||
# if not like: | ||
# raise NotFound("No Like matches the given query.") | ||
# return like | ||
try: | ||
like = Like.objects.get(user=user, product_id=product_id) | ||
return like | ||
except Like.DoesNotExist: | ||
raise NotFound("No Like matches the given query.") | ||
|
||
@transaction.atomic | ||
def perform_destroy(self, instance: Like) -> None: | ||
product_id = self.kwargs.get("pk") | ||
if instance: | ||
instance.delete() | ||
Product.objects.filter(pk=product_id).update(likes=F("likes") - 1) | ||
# product.likes = F("likes") - 1 | ||
# product.save(update_fields=["likes"]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.