Skip to content

Commit

Permalink
Merge pull request #24 from OZ-Coding-School/feature-favorite-api
Browse files Browse the repository at this point in the history
Feature favorite api 구현
  • Loading branch information
Gomnonix authored Sep 2, 2024
2 parents a171f61 + 4254a21 commit 4a0d496
Show file tree
Hide file tree
Showing 20 changed files with 412 additions and 63 deletions.
1 change: 1 addition & 0 deletions customk/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"reviews",
"reactions",
"corsheaders",
"favorites",
]


Expand Down
3 changes: 2 additions & 1 deletion customk/config/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.contrib import admin
from django.urls import include, path, re_path
from django.urls import include, re_path
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
Expand Down Expand Up @@ -27,5 +27,6 @@
re_path(r"^v1/classes/?", include("classes.urls")),
re_path(r"^v1/question/?", include("questions.urls")),
re_path(r"^v1/reviews/?", include("reviews.urls")),
re_path(r"^v1/favorites/?", include("favorites.urls")),
re_path(r"^v1/reactions/?", include("reactions.urls")),
]
Empty file added customk/favorites/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions customk/favorites/admin.py
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
6 changes: 6 additions & 0 deletions customk/favorites/apps.py
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"
49 changes: 49 additions & 0 deletions customk/favorites/migrations/0001_initial.py
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.
21 changes: 21 additions & 0 deletions customk/favorites/models.py
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}"
21 changes: 21 additions & 0 deletions customk/favorites/serializers.py
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
12 changes: 12 additions & 0 deletions customk/favorites/services.py
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.
31 changes: 31 additions & 0 deletions customk/favorites/tests/conftest.py
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"},
)
41 changes: 41 additions & 0 deletions customk/favorites/tests/test_favorite_api.py
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
)
7 changes: 7 additions & 0 deletions customk/favorites/urls.py
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"),
]
154 changes: 154 additions & 0 deletions customk/favorites/views.py
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
)
4 changes: 2 additions & 2 deletions customk/users/serializers/user_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class Meta:
"profile_image_url",
)

def get_profile_image_url(self, obj):
def get_profile_image_url(self, obj) -> str:
return obj.profile_image

def velidate(self, data):
Expand Down Expand Up @@ -107,7 +107,7 @@ class Meta:
model = User
fields = ("name", "profile_image", "profile_image_url")

def get_profile_image_url(self, obj):
def get_profile_image_url(self, obj) -> str:
return obj.profile_image

def update(self, instance, validated_data):
Expand Down
Loading

0 comments on commit 4a0d496

Please sign in to comment.