-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24 from OZ-Coding-School/feature-favorite-api
Feature favorite api 구현
- Loading branch information
Showing
20 changed files
with
412 additions
and
63 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,7 @@ | |
"reviews", | ||
"reactions", | ||
"corsheaders", | ||
"favorites", | ||
] | ||
|
||
|
||
|
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,8 @@ | ||
from django.contrib import admin | ||
|
||
from .models import Favorite | ||
|
||
|
||
@admin.register(Favorite) | ||
class FavoriteAdmin(admin.ModelAdmin): # type: ignore | ||
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,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class FavoritesConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "favorites" |
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,49 @@ | ||
# Generated by Django 5.1 on 2024-09-02 04:59 | ||
|
||
import django.db.models.deletion | ||
import django.utils.timezone | ||
from django.conf import settings | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
initial = True | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="Favorite", | ||
fields=[ | ||
( | ||
"id", | ||
models.BigAutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
("created_at", models.DateTimeField(default=django.utils.timezone.now)), | ||
("updated_at", models.DateTimeField(auto_now=True)), | ||
("class_id", models.IntegerField()), | ||
( | ||
"user", | ||
models.ForeignKey( | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="favorites", | ||
to=settings.AUTH_USER_MODEL, | ||
), | ||
), | ||
], | ||
options={ | ||
"constraints": [ | ||
models.UniqueConstraint( | ||
fields=("user", "class_id"), name="unique_users_class" | ||
) | ||
], | ||
}, | ||
), | ||
] |
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,21 @@ | ||
from django.db import models | ||
|
||
from classes.models import Class | ||
from common.models import CommonModel | ||
from users.models import User | ||
|
||
|
||
class Favorite(CommonModel): | ||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="favorites") | ||
class_id = models.IntegerField() | ||
|
||
class Meta: | ||
constraints = [ | ||
models.UniqueConstraint( | ||
fields=["user", "class_id"], name="unique_users_class" | ||
) | ||
] | ||
|
||
def __str__(self): | ||
klass = Class.objects.filter(pk=self.class_id).first() | ||
return f"{self.user.username} likes {klass.title}" |
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,21 @@ | ||
from rest_framework import serializers | ||
|
||
from classes.models import Class | ||
from classes.serializers import ClassSerializer | ||
|
||
from .models import Favorite | ||
|
||
|
||
class FavoriteSerializer(serializers.ModelSerializer): | ||
klass = serializers.SerializerMethodField() | ||
|
||
class Meta: | ||
model = Favorite | ||
fields = ["id", "user", "class_id", "klass"] | ||
|
||
def get_klass(self, obj): | ||
try: | ||
class_instance = Class.objects.get(id=obj.class_id) | ||
return ClassSerializer(class_instance).data | ||
except Class.DoesNotExist: | ||
return 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,12 @@ | ||
from django.db import transaction | ||
|
||
from .models import Favorite | ||
|
||
|
||
@transaction.atomic | ||
def add_favorite_class(user_id: int, class_id: int) -> tuple[Favorite, bool]: | ||
return Favorite.objects.get_or_create(user_id=user_id, class_id=class_id) | ||
|
||
|
||
def delete_favorite_class(favorite_id: int) -> None: | ||
Favorite.objects.filter(id=favorite_id).delete() |
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,31 @@ | ||
import pytest | ||
|
||
from classes.models import Class | ||
from classes.tests.conftest import sample_class | ||
from favorites.models import Favorite | ||
from favorites.services import add_favorite_class | ||
from users.tests.conftest import ( | ||
access_token, | ||
api_client_with_token, | ||
refresh_token, | ||
sample_user, | ||
) | ||
|
||
pytestmark = pytest.mark.django_db | ||
|
||
|
||
@pytest.fixture | ||
def favorite_instance(): | ||
return Favorite.objects.create(user=sample_user, class_id=sample_class.id) | ||
|
||
|
||
@pytest.fixture | ||
def sample_class2(): | ||
return Class.objects.create( | ||
title="Sample Class2", | ||
description="This is a sample class2", | ||
max_person=100, | ||
require_person=50, | ||
price=50000, | ||
address={"state": "Seoul", "city": "Gangnam", "street": "Teheran-ro"}, | ||
) |
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,41 @@ | ||
import pytest | ||
from django.urls import reverse | ||
from rest_framework import status | ||
|
||
from favorites.models import Favorite | ||
|
||
pytestmark = pytest.mark.django_db | ||
|
||
|
||
def test_get_favorites(api_client_with_token, sample_class, favorite_instance): | ||
url = reverse("favorite") | ||
response = api_client_with_token.get(url, {"page": 1, "size": 10}) | ||
|
||
assert response.status_code == status.HTTP_200_OK | ||
data = response.json() | ||
assert data["total_count"] == 1 | ||
assert len(data["results"]) == 1 | ||
|
||
|
||
def test_post_favorite(api_client_with_token, sample_user, sample_class2): | ||
url = reverse("favorite") + f"?class_id={sample_class2.id}" | ||
response = api_client_with_token.post(url, data={}) | ||
|
||
assert response.status_code == status.HTTP_201_CREATED | ||
|
||
|
||
def test_delete_favorite( | ||
api_client_with_token, sample_user, sample_class, favorite_instance | ||
): | ||
data, _ = favorite_instance | ||
|
||
url = reverse("favorite") + f"?favorite_id={data.id}" | ||
response = api_client_with_token.delete(url) | ||
|
||
assert response.status_code == status.HTTP_204_NO_CONTENT | ||
assert ( | ||
Favorite.objects.filter( | ||
user_id=sample_user.id, class_id=sample_class.id | ||
).count() | ||
== 0 | ||
) |
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,7 @@ | ||
from django.urls import re_path | ||
|
||
from .views import FavoriteView | ||
|
||
urlpatterns = [ | ||
re_path(r"^$", FavoriteView.as_view(), name="favorite"), | ||
] |
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,154 @@ | ||
from drf_spectacular.types import OpenApiTypes | ||
from drf_spectacular.utils import ( | ||
OpenApiParameter, | ||
OpenApiResponse, | ||
extend_schema, | ||
inline_serializer, | ||
) | ||
from rest_framework import serializers, status | ||
from rest_framework.request import Request | ||
from rest_framework.response import Response | ||
from rest_framework.views import APIView | ||
|
||
from classes.serializers import ClassSerializer | ||
|
||
from .models import Favorite | ||
from .serializers import FavoriteSerializer | ||
from .services import add_favorite_class, delete_favorite_class | ||
|
||
|
||
class FavoriteView(APIView): | ||
@extend_schema( | ||
methods=["GET"], | ||
summary="찜한 클래스 목록 조회", | ||
description="유저가 찜한 클래스 목록을 페이지네이션형태로 가져옵니다", | ||
parameters=[ | ||
OpenApiParameter( | ||
name="page", | ||
description="페이지", | ||
required=False, | ||
default=1, | ||
type=OpenApiTypes.INT, | ||
location=OpenApiParameter.QUERY, | ||
), | ||
OpenApiParameter( | ||
name="size", | ||
description="사이즈", | ||
required=False, | ||
default=10, | ||
type=OpenApiTypes.INT, | ||
location=OpenApiParameter.QUERY, | ||
), | ||
], | ||
responses={ | ||
200: inline_serializer( | ||
name="FavoriteListResponse", | ||
fields={ | ||
"total_count": serializers.IntegerField(), | ||
"total_pages": serializers.IntegerField(), | ||
"current_page": serializers.IntegerField(), | ||
"results": ClassSerializer(many=True), | ||
}, | ||
), | ||
400: OpenApiResponse(description="Page input error"), | ||
}, | ||
) | ||
def get(self, request: Request, *args, **kwargs) -> Response: | ||
page = int(request.GET.get("page", "1")) | ||
size = int(request.GET.get("size", "10")) | ||
offset = (page - 1) * size | ||
user = request.user | ||
if page < 1: | ||
return Response("page input error", status=status.HTTP_400_BAD_REQUEST) | ||
|
||
total_count = Favorite.objects.filter(user_id=user.id).count() | ||
total_pages = (total_count // size) + 1 | ||
|
||
favorites = Favorite.objects.filter(user_id=user.id).order_by("-id")[ | ||
offset : offset + size | ||
] | ||
|
||
serializer = FavoriteSerializer(favorites, many=True) | ||
|
||
return Response( | ||
{ | ||
"total_count": total_count, | ||
"total_pages": total_pages, | ||
"current_page": page, | ||
"results": serializer.data, | ||
}, | ||
status=status.HTTP_200_OK, | ||
) | ||
|
||
@extend_schema( | ||
methods=["POST"], | ||
summary="찜 클래스 생성", | ||
description="찜 할 클래스를 생성하는 API", | ||
parameters=[ | ||
OpenApiParameter( | ||
name="class_id", | ||
description="찜할 클래스 id", | ||
required=True, | ||
type=OpenApiTypes.INT, | ||
location=OpenApiParameter.QUERY, | ||
) | ||
], | ||
request=None, | ||
responses={ | ||
200: OpenApiResponse(description="이미 찜한 클래스입니다."), | ||
201: OpenApiResponse( | ||
description="클래스 생성 성공", response=ClassSerializer | ||
), | ||
400: OpenApiResponse(description="class_id is required"), | ||
}, | ||
) | ||
def post(self, request: Request, *args, **kwargs) -> Response: | ||
class_id = int(request.GET.get("class_id", 0)) | ||
|
||
if class_id == 0: | ||
return Response( | ||
{"error": "class_id is required"}, status=status.HTTP_400_BAD_REQUEST | ||
) | ||
|
||
favorite, created = add_favorite_class( | ||
user_id=request.user.id, class_id=class_id | ||
) | ||
serializer = FavoriteSerializer(favorite) | ||
|
||
if created: | ||
return Response(serializer.data, status=status.HTTP_201_CREATED) | ||
else: | ||
return Response({"message": "Already favorited"}, status=status.HTTP_200_OK) | ||
|
||
@extend_schema( | ||
methods=["DELETE"], | ||
summary="찜 한 클래스 삭제", | ||
description="찜 한 클래스를 삭제하는 API", | ||
parameters=[ | ||
OpenApiParameter( | ||
name="favorite_id", | ||
description="삭제할 찜 id", | ||
required=True, | ||
type=OpenApiTypes.INT, | ||
location=OpenApiParameter.QUERY, | ||
) | ||
], | ||
request=None, | ||
responses={ | ||
204: OpenApiResponse(description="클래스 삭제 성공"), | ||
400: OpenApiResponse(description="class_id is required"), | ||
}, | ||
) | ||
def delete(self, request: Request, *args, **kwargs) -> Response: | ||
favorite_id = int(request.GET.get("favorite_id", 0)) | ||
|
||
if favorite_id == 0: | ||
return Response( | ||
{"error": "class_id is required"}, status=status.HTTP_400_BAD_REQUEST | ||
) | ||
|
||
delete_favorite_class(favorite_id=favorite_id) | ||
|
||
return Response( | ||
{"message": "success delete"}, status=status.HTTP_204_NO_CONTENT | ||
) |
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.