From feab8c97df9a17c361702870b16a6b95c92588a1 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 2 Sep 2024 14:44:52 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=ED=8A=B9=EC=A0=95=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/classes/urls.py | 3 ++- customk/classes/views.py | 45 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/customk/classes/urls.py b/customk/classes/urls.py index 2b11138..c07f5dd 100644 --- a/customk/classes/urls.py +++ b/customk/classes/urls.py @@ -1,7 +1,8 @@ from django.urls import re_path -from classes.views import ClassListView +from classes.views import ClassListView, ClassDetailView urlpatterns = [ re_path(r"^$", ClassListView.as_view(), name="class-list"), + re_path(r"^(?P\d+)/$", ClassDetailView.as_view(), name="class-detail"), ] diff --git a/customk/classes/views.py b/customk/classes/views.py index f6ee55b..5870b40 100644 --- a/customk/classes/views.py +++ b/customk/classes/views.py @@ -1,10 +1,11 @@ from typing import Any -from drf_spectacular.utils import OpenApiResponse, extend_schema +from drf_spectacular.utils import OpenApiResponse, extend_schema, OpenApiParameter from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView +from drf_spectacular.types import OpenApiTypes from .models import Class from .serializers import ClassSerializer @@ -120,3 +121,45 @@ def delete(self, request: Request, *args: Any, **kwargs: Any) -> Response: return Response( {"status": "error", "message": "삭제 실패했습니다"}, status=404 ) + +class ClassDetailView(APIView): + def get_permissions(self): + if self.request.method == "GET": + return [AllowAny()] + return [IsAuthenticated()] + + @extend_schema( + methods=["GET"], + summary="특정 클래스 조회", + description="특정 클래스의 세부 정보를 조회하는 API입니다.", + parameters=[ + OpenApiParameter( + name="id", + description="클래스 ID", + required=True, + type=OpenApiTypes.INT, + location=OpenApiParameter.PATH, + ), + ], + responses={ + 200: OpenApiResponse( + description="클래스 조회 성공", response=ClassSerializer + ), + 404: OpenApiResponse(description="클래스가 존재하지 않음"), + }, + ) + def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: + class_id = kwargs.get("id") + try: + class_instance = Class.objects.get(id=class_id) + serializer = ClassSerializer(class_instance) + response_data = { + "status": "success", + "message": "Class fetched successfully", + "data": serializer.data, + } + return Response(response_data, status=200) + except Class.DoesNotExist: + return Response( + {"status": "error", "message": "Class not found"}, status=404 + ) \ No newline at end of file From 09ce898cdae80961d5ffbc5a39c99c5ae7c71ff9 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 2 Sep 2024 14:52:28 +0900 Subject: [PATCH 02/11] =?UTF-8?q?=ED=8A=B9=EC=A0=95=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/classes/urls.py | 4 ++-- customk/classes/views.py | 15 ++++++++++----- customk/users/services/token_service.py | 11 +++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/customk/classes/urls.py b/customk/classes/urls.py index c07f5dd..3013427 100644 --- a/customk/classes/urls.py +++ b/customk/classes/urls.py @@ -1,8 +1,8 @@ from django.urls import re_path -from classes.views import ClassListView, ClassDetailView +from classes.views import ClassDetailView, ClassListView urlpatterns = [ re_path(r"^$", ClassListView.as_view(), name="class-list"), - re_path(r"^(?P\d+)/$", ClassDetailView.as_view(), name="class-detail"), + re_path(r"^(?P\d+)/$", ClassDetailView.as_view(), name="class-detail"), ] diff --git a/customk/classes/views.py b/customk/classes/views.py index 5870b40..42be6e8 100644 --- a/customk/classes/views.py +++ b/customk/classes/views.py @@ -1,11 +1,11 @@ from typing import Any -from drf_spectacular.utils import OpenApiResponse, extend_schema, OpenApiParameter +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from drf_spectacular.types import OpenApiTypes from .models import Class from .serializers import ClassSerializer @@ -122,6 +122,7 @@ def delete(self, request: Request, *args: Any, **kwargs: Any) -> Response: {"status": "error", "message": "삭제 실패했습니다"}, status=404 ) + class ClassDetailView(APIView): def get_permissions(self): if self.request.method == "GET": @@ -134,7 +135,7 @@ def get_permissions(self): description="특정 클래스의 세부 정보를 조회하는 API입니다.", parameters=[ OpenApiParameter( - name="id", + name="class_id", description="클래스 ID", required=True, type=OpenApiTypes.INT, @@ -149,7 +150,11 @@ def get_permissions(self): }, ) def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: - class_id = kwargs.get("id") + class_id = kwargs.get("class_id") + if class_id is None: + return Response( + {"status": "error", "message": "Class ID not provided"}, status=400 + ) try: class_instance = Class.objects.get(id=class_id) serializer = ClassSerializer(class_instance) @@ -162,4 +167,4 @@ def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: except Class.DoesNotExist: return Response( {"status": "error", "message": "Class not found"}, status=404 - ) \ No newline at end of file + ) diff --git a/customk/users/services/token_service.py b/customk/users/services/token_service.py index 206a02e..f5f606a 100644 --- a/customk/users/services/token_service.py +++ b/customk/users/services/token_service.py @@ -65,10 +65,13 @@ def set_cookies(request: Request, response: Response, token: Token) -> Response: domain=domain, ) - if hasattr(response, 'data'): - response.data['access_token'] = token.access_token - response.data['refresh_token'] = token.refresh_token + if hasattr(response, "data"): + response.data["access_token"] = token.access_token + response.data["refresh_token"] = token.refresh_token else: - response.data = {'access_token': token.access_token, 'refresh_token': token.refresh_token} + response.data = { + "access_token": token.access_token, + "refresh_token": token.refresh_token, + } return response From 92a6f27fab1dee4614cf9aa2c139c58df9c7c09d Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 2 Sep 2024 16:35:15 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/classes/admin.py | 30 +++++++++++++---- customk/classes/forms.py | 6 ++-- ...6_remove_classimages_image_url_and_more.py | 33 +++++++++++++++++++ customk/classes/models.py | 4 ++- customk/classes/serializers.py | 29 +++++++++++++--- 5 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 customk/classes/migrations/0016_remove_classimages_image_url_and_more.py diff --git a/customk/classes/admin.py b/customk/classes/admin.py index 0db94a2..1000fec 100644 --- a/customk/classes/admin.py +++ b/customk/classes/admin.py @@ -84,17 +84,33 @@ class ExchangeRateAdmin(admin.ModelAdmin): # type: ignore @admin.register(ClassImages) class ClassImagesAdmin(admin.ModelAdmin): form = ClassImagesForm - list_display = ["class_id", "image_url"] + list_display = [ + "class_id", + "thumbnail_image_url", + "description_image_url", + "detail_image_url", + ] search_fields = ["class_id"] - def save_model(self, request, obj, form, change): # + def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) - if "images" in request.FILES: - image_file = request.FILES["images"] - image_url = upload_image_to_object_storage(image_file) - obj.image_url = image_url - obj.save() + if form.cleaned_data.get("thumbnail_image"): + thumbnail_image = form.cleaned_data["thumbnail_image"] + thumbnail_url = upload_image_to_object_storage(thumbnail_image) + obj.thumbnail_image_url = thumbnail_url + + if form.cleaned_data.get("description_image"): + description_image = form.cleaned_data["description_image"] + description_url = upload_image_to_object_storage(description_image) + obj.description_image_url = description_url + + if form.cleaned_data.get("detail_image"): + detail_image = form.cleaned_data["detail_image"] + detail_url = upload_image_to_object_storage(detail_image) + obj.detail_image_url = detail_url + + obj.save() @admin.register(Genre) diff --git a/customk/classes/forms.py b/customk/classes/forms.py index f9fb86d..59b7249 100644 --- a/customk/classes/forms.py +++ b/customk/classes/forms.py @@ -4,8 +4,10 @@ class ClassImagesForm(forms.ModelForm): - images = forms.ImageField() + thumbnail_image = forms.ImageField(required=False) + detail_image = forms.ImageField(required=False) + description_image = forms.ImageField(required=False) class Meta: model = ClassImages - fields = ["class_id", "images"] + fields = ["class_id", "thumbnail_image", "detail_image", "description_image"] diff --git a/customk/classes/migrations/0016_remove_classimages_image_url_and_more.py b/customk/classes/migrations/0016_remove_classimages_image_url_and_more.py new file mode 100644 index 0000000..1d136bd --- /dev/null +++ b/customk/classes/migrations/0016_remove_classimages_image_url_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1 on 2024-09-02 06:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("classes", "0015_class_discount_rate"), + ] + + operations = [ + migrations.RemoveField( + model_name="classimages", + name="image_url", + ), + migrations.AddField( + model_name="classimages", + name="description_image_url", + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name="classimages", + name="detail_image_url", + field=models.CharField(default="", max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name="classimages", + name="thumbnail_image_url", + field=models.CharField(default="", max_length=255), + preserve_default=False, + ), + ] diff --git a/customk/classes/models.py b/customk/classes/models.py index d318aa2..f4fc27c 100644 --- a/customk/classes/models.py +++ b/customk/classes/models.py @@ -82,7 +82,9 @@ class ClassDate(models.Model): class ClassImages(models.Model): class_id = models.ForeignKey(Class, related_name="images", on_delete=models.CASCADE) - image_url = models.CharField() + description_image_url = models.CharField(max_length=255, blank=True) + thumbnail_image_url = models.CharField(max_length=255) + detail_image_url = models.CharField(max_length=255) def __str__(self) -> str: return f"{self.class_id.title}" diff --git a/customk/classes/serializers.py b/customk/classes/serializers.py index 6343ce8..2727b42 100644 --- a/customk/classes/serializers.py +++ b/customk/classes/serializers.py @@ -55,13 +55,20 @@ class Meta: class ClassImagesSerializer(serializers.ModelSerializer): class_id = serializers.PrimaryKeyRelatedField(read_only=True) + thumbnail_image_url = serializers.CharField(required=False) + description_image_url = serializers.CharField(required=False) + detail_image_url = serializers.CharField(required=False) class Meta: model = ClassImages fields = "__all__" - def get_image_url(self, obj): - return obj.image_url + def get_image_urls(self, obj): + return { + "thumbnail_image_url": obj.thumbnail_image_url, + "description_image_url": obj.description_image_url, + "additional_image_url": obj.additional_image_url, + } class ClassSerializer(serializers.ModelSerializer): @@ -123,7 +130,21 @@ def create(self, validated_data): ClassDate.objects.create(class_id=class_instance, **date_data) for image_data64 in images_data64: - image_url = upload_image_to_object_storage(image_data64["image_url"]) - ClassImages.objects.create(class_id=class_instance, image_url=image_url) + thumbnail_url = upload_image_to_object_storage( + image_data64["thumbnail_image_url"] + ) + description_url = upload_image_to_object_storage( + image_data64["description_image_url"] + ) + detail_image_url = upload_image_to_object_storage( + image_data64["detail_image_url"] + ) + + ClassImages.objects.create( + class_id=class_instance, + thumbnail_image_url=thumbnail_url, + description_image_url=description_url, + detail_image_url=detail_image_url, + ) return class_instance From 333ebc2ddc60057394bbe89e8f76368620e3df7d Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 2 Sep 2024 17:06:32 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/classes/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/customk/classes/models.py b/customk/classes/models.py index f4fc27c..bb87cd8 100644 --- a/customk/classes/models.py +++ b/customk/classes/models.py @@ -2,7 +2,6 @@ from typing import Any, Dict, Optional from django.core.serializers.json import DjangoJSONEncoder -from django.core.validators import MinValueValidator from django.db import models from django.db.models import Avg From f68d9be93ee72152efddb977da4384169a465f00 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 2 Sep 2024 17:08:34 +0900 Subject: [PATCH 05/11] =?UTF-8?q?blank=20=EC=98=B5=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ssimages_description_image_url_and_more.py | 39 ++++++++++++++++++ ...simages_description_image_urls_and_more.py | 41 +++++++++++++++++++ ...r_classimages_detail_image_url_and_more.py | 23 +++++++++++ customk/classes/models.py | 4 +- 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 customk/classes/migrations/0017_remove_classimages_description_image_url_and_more.py create mode 100644 customk/classes/migrations/0018_remove_classimages_description_image_urls_and_more.py create mode 100644 customk/classes/migrations/0019_alter_classimages_detail_image_url_and_more.py diff --git a/customk/classes/migrations/0017_remove_classimages_description_image_url_and_more.py b/customk/classes/migrations/0017_remove_classimages_description_image_url_and_more.py new file mode 100644 index 0000000..e98a0cb --- /dev/null +++ b/customk/classes/migrations/0017_remove_classimages_description_image_url_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.1 on 2024-09-02 07:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("classes", "0016_remove_classimages_image_url_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="classimages", + name="description_image_url", + ), + migrations.RemoveField( + model_name="classimages", + name="detail_image_url", + ), + migrations.RemoveField( + model_name="classimages", + name="thumbnail_image_url", + ), + migrations.AddField( + model_name="classimages", + name="description_image_urls", + field=models.JSONField(blank=True, default=list), + ), + migrations.AddField( + model_name="classimages", + name="detail_image_urls", + field=models.JSONField(blank=True, default=list), + ), + migrations.AddField( + model_name="classimages", + name="thumbnail_image_urls", + field=models.JSONField(blank=True, default=list), + ), + ] diff --git a/customk/classes/migrations/0018_remove_classimages_description_image_urls_and_more.py b/customk/classes/migrations/0018_remove_classimages_description_image_urls_and_more.py new file mode 100644 index 0000000..7d6a8f9 --- /dev/null +++ b/customk/classes/migrations/0018_remove_classimages_description_image_urls_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1 on 2024-09-02 08:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("classes", "0017_remove_classimages_description_image_url_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="classimages", + name="description_image_urls", + ), + migrations.RemoveField( + model_name="classimages", + name="detail_image_urls", + ), + migrations.RemoveField( + model_name="classimages", + name="thumbnail_image_urls", + ), + migrations.AddField( + model_name="classimages", + name="description_image_url", + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name="classimages", + name="detail_image_url", + field=models.CharField(default="", max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name="classimages", + name="thumbnail_image_url", + field=models.CharField(default="", max_length=255), + preserve_default=False, + ), + ] diff --git a/customk/classes/migrations/0019_alter_classimages_detail_image_url_and_more.py b/customk/classes/migrations/0019_alter_classimages_detail_image_url_and_more.py new file mode 100644 index 0000000..97ef1ed --- /dev/null +++ b/customk/classes/migrations/0019_alter_classimages_detail_image_url_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1 on 2024-09-02 08:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('classes', '0018_remove_classimages_description_image_urls_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='classimages', + name='detail_image_url', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AlterField( + model_name='classimages', + name='thumbnail_image_url', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/customk/classes/models.py b/customk/classes/models.py index bb87cd8..b08375c 100644 --- a/customk/classes/models.py +++ b/customk/classes/models.py @@ -82,8 +82,8 @@ class ClassDate(models.Model): class ClassImages(models.Model): class_id = models.ForeignKey(Class, related_name="images", on_delete=models.CASCADE) description_image_url = models.CharField(max_length=255, blank=True) - thumbnail_image_url = models.CharField(max_length=255) - detail_image_url = models.CharField(max_length=255) + thumbnail_image_url = models.CharField(max_length=255, blank=True) + detail_image_url = models.CharField(max_length=255, blank=True) def __str__(self) -> str: return f"{self.class_id.title}" From a171f6139cd0949ef2c50e05a68877d67af99ec7 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 2 Sep 2024 17:08:58 +0900 Subject: [PATCH 06/11] =?UTF-8?q?blank=20=EC=98=B5=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...019_alter_classimages_detail_image_url_and_more.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/customk/classes/migrations/0019_alter_classimages_detail_image_url_and_more.py b/customk/classes/migrations/0019_alter_classimages_detail_image_url_and_more.py index 97ef1ed..8c4bfe3 100644 --- a/customk/classes/migrations/0019_alter_classimages_detail_image_url_and_more.py +++ b/customk/classes/migrations/0019_alter_classimages_detail_image_url_and_more.py @@ -4,20 +4,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('classes', '0018_remove_classimages_description_image_urls_and_more'), + ("classes", "0018_remove_classimages_description_image_urls_and_more"), ] operations = [ migrations.AlterField( - model_name='classimages', - name='detail_image_url', + model_name="classimages", + name="detail_image_url", field=models.CharField(blank=True, max_length=255), ), migrations.AlterField( - model_name='classimages', - name='thumbnail_image_url', + model_name="classimages", + name="thumbnail_image_url", field=models.CharField(blank=True, max_length=255), ), ] From 59c2080d0f51945f5782bde981e0c8cabec7ff64 Mon Sep 17 00:00:00 2001 From: rbwo552 Date: Mon, 2 Sep 2024 13:07:01 +0900 Subject: [PATCH 07/11] =?UTF-8?q?favorite=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/favorites/__init__.py | 0 customk/favorites/admin.py | 3 ++ customk/favorites/apps.py | 6 +++ customk/favorites/migrations/0001_initial.py | 31 +++++++++++ customk/favorites/migrations/__init__.py | 0 customk/favorites/models.py | 16 ++++++ customk/favorites/serializers.py | 11 ++++ customk/favorites/services.py | 11 ++++ customk/favorites/tests/__init__.py | 0 customk/favorites/tests/test_favorite_api.py | 0 customk/favorites/urls.py | 7 +++ customk/favorites/views.py | 56 ++++++++++++++++++++ 12 files changed, 141 insertions(+) create mode 100644 customk/favorites/__init__.py create mode 100644 customk/favorites/admin.py create mode 100644 customk/favorites/apps.py create mode 100644 customk/favorites/migrations/0001_initial.py create mode 100644 customk/favorites/migrations/__init__.py create mode 100644 customk/favorites/models.py create mode 100644 customk/favorites/serializers.py create mode 100644 customk/favorites/services.py create mode 100644 customk/favorites/tests/__init__.py create mode 100644 customk/favorites/tests/test_favorite_api.py create mode 100644 customk/favorites/urls.py create mode 100644 customk/favorites/views.py diff --git a/customk/favorites/__init__.py b/customk/favorites/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customk/favorites/admin.py b/customk/favorites/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/customk/favorites/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/customk/favorites/apps.py b/customk/favorites/apps.py new file mode 100644 index 0000000..437d6ed --- /dev/null +++ b/customk/favorites/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FavoritesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'favorites' diff --git a/customk/favorites/migrations/0001_initial.py b/customk/favorites/migrations/0001_initial.py new file mode 100644 index 0000000..dd755ec --- /dev/null +++ b/customk/favorites/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1 on 2024-09-01 12:44 + +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')], + }, + ), + ] diff --git a/customk/favorites/migrations/__init__.py b/customk/favorites/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customk/favorites/models.py b/customk/favorites/models.py new file mode 100644 index 0000000..fc56760 --- /dev/null +++ b/customk/favorites/models.py @@ -0,0 +1,16 @@ +from django.db import models +from common.models import CommonModel +from users.models import User +from classes.models import Class + + +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}" diff --git a/customk/favorites/serializers.py b/customk/favorites/serializers.py new file mode 100644 index 0000000..119546f --- /dev/null +++ b/customk/favorites/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers +from .models import Favorite +from classes.serializers import ClassSerializer + + +class FavoriteSerializer(serializers.ModelSerializer): # type: ignore + klass = ClassSerializer(read_only=True) + + class Meta: + model = Favorite + fields = ['id', 'user', 'klass'] diff --git a/customk/favorites/services.py b/customk/favorites/services.py new file mode 100644 index 0000000..c10aa38 --- /dev/null +++ b/customk/favorites/services.py @@ -0,0 +1,11 @@ +from .models import Favorite +from django.db import transaction + + +@transaction.atomic +def add_favorite_class(user_id: int, class_id: int) -> Favorite: + return Favorite.objects.create(user_id=user_id, klass=class_id) + + +def delete_favorite_class(user_id: int, class_id: int) -> None: + Favorite.objects.filter(user_id=user_id, class_id=class_id).delete() \ No newline at end of file diff --git a/customk/favorites/tests/__init__.py b/customk/favorites/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customk/favorites/tests/test_favorite_api.py b/customk/favorites/tests/test_favorite_api.py new file mode 100644 index 0000000..e69de29 diff --git a/customk/favorites/urls.py b/customk/favorites/urls.py new file mode 100644 index 0000000..286641e --- /dev/null +++ b/customk/favorites/urls.py @@ -0,0 +1,7 @@ +from django.urls import re_path +from .views import FavoriteView, FavoriteDeleteView + +urlpatterns = [ + re_path(r"^$", FavoriteView.as_view(), name="favorite"), + re_path(r"^(?P\d+)/?$", FavoriteDeleteView.as_view(), name="favorite_detail"), +] diff --git a/customk/favorites/views.py b/customk/favorites/views.py new file mode 100644 index 0000000..acfeb32 --- /dev/null +++ b/customk/favorites/views.py @@ -0,0 +1,56 @@ +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework import status +from rest_framework.views import APIView +from .models import Favorite +from .serializers import FavoriteSerializer +from classes.models import Class + + +class FavoriteView(APIView): + 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 + + if page < 1: + return Response("page input error", status=status.HTTP_400_BAD_REQUEST) + favorites = Favorite.objects.filter(user=request.user).order_by("-id")[offset: offset + size] + + class_ids = [favorite.class_id for favorite in favorites] + classes = Class.objects.filter(id__in=class_ids) + + serializer = FavoriteSerializer(classes, many=True) + + total_count = len(favorites) + total_pages = (total_count // size) + 1 + + # 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, + ) + + def post(self, request, *args, **kwargs): + class_id = request.GET.get("class_id", "") + + if not class_id: + return Response({"error": "klass_id is required"}, status=status.HTTP_400_BAD_REQUEST) + + favorite, created = Favorite.objects.get_or_create(user=request.user, klass_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) + + +class FavoriteDeleteView(APIView): + def delete(self, request: Request, *args, **kwargs) -> Response: + class_id = request.GET.get("class_id", "") + user_id = request.user.id + Favorite.objects.filter(user_id=user_id, id=class_id).delete() + + return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file From fbfac525ba07c99742f75f743d35229212dce6ff Mon Sep 17 00:00:00 2001 From: rbwo552 Date: Mon, 2 Sep 2024 13:07:31 +0900 Subject: [PATCH 08/11] =?UTF-8?q?token,=20setting,=20urls=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/config/settings.py | 1 + customk/config/urls.py | 4 ++-- customk/users/views/token.py | 14 +++++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/customk/config/settings.py b/customk/config/settings.py index 34795d5..2271c33 100644 --- a/customk/config/settings.py +++ b/customk/config/settings.py @@ -33,6 +33,7 @@ "reviews", "reactions", "corsheaders", + "favorites" ] diff --git a/customk/config/urls.py b/customk/config/urls.py index 436337a..11f40e8 100644 --- a/customk/config/urls.py +++ b/customk/config/urls.py @@ -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, @@ -27,5 +27,5 @@ 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/reactions/?", include("reactions.urls")), + re_path(r"^v1/favorites/?", include("favorites.urls")), ] diff --git a/customk/users/views/token.py b/customk/users/views/token.py index fe73dea..dea3d36 100644 --- a/customk/users/views/token.py +++ b/customk/users/views/token.py @@ -1,5 +1,7 @@ from typing import Any - +from datetime import timedelta +from typing import cast +from config import settings from drf_spectacular.utils import ( OpenApiExample, OpenApiParameter, @@ -68,14 +70,20 @@ def post(self, request: Request, *args: Any, **kwargs: Any) -> Response: if response.status_code == 200: access_token = response.data.get("access") + from users.services.token_service import get_domain + domain = get_domain(request=request) + access_max_age = int( + cast(timedelta, settings.SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"]).total_seconds() + ) response.set_cookie( key="access_token", value=access_token, + max_age=access_max_age, + domain=domain, httponly=True, secure=True, - samesite="Lax", ) - del response.data["access"] + # del response.data["access"] del response.data["refresh"] return response From 4254a219f92f86587b018987296819f92279d124 Mon Sep 17 00:00:00 2001 From: rbwo552 Date: Mon, 2 Sep 2024 17:31:36 +0900 Subject: [PATCH 09/11] =?UTF-8?q?favorite=20app=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20line=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/config/settings.py | 2 +- customk/config/urls.py | 1 + customk/favorites/admin.py | 7 +- customk/favorites/apps.py | 4 +- customk/favorites/migrations/0001_initial.py | 36 +++-- customk/favorites/models.py | 11 +- customk/favorites/serializers.py | 18 ++- customk/favorites/services.py | 11 +- customk/favorites/tests/conftest.py | 31 ++++ customk/favorites/tests/test_favorite_api.py | 41 ++++++ customk/favorites/urls.py | 4 +- customk/favorites/views.py | 144 ++++++++++++++++--- customk/users/serializers/user_serializer.py | 4 +- customk/users/tests/conftest.py | 4 +- customk/users/tests/test_token_api.py | 76 +++++----- customk/users/views/line.py | 20 +-- customk/users/views/token.py | 11 +- 17 files changed, 312 insertions(+), 113 deletions(-) create mode 100644 customk/favorites/tests/conftest.py diff --git a/customk/config/settings.py b/customk/config/settings.py index 2271c33..c67b686 100644 --- a/customk/config/settings.py +++ b/customk/config/settings.py @@ -33,7 +33,7 @@ "reviews", "reactions", "corsheaders", - "favorites" + "favorites", ] diff --git a/customk/config/urls.py b/customk/config/urls.py index 11f40e8..5a61b7e 100644 --- a/customk/config/urls.py +++ b/customk/config/urls.py @@ -28,4 +28,5 @@ 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")), ] diff --git a/customk/favorites/admin.py b/customk/favorites/admin.py index 8c38f3f..c16617a 100644 --- a/customk/favorites/admin.py +++ b/customk/favorites/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin -# Register your models here. +from .models import Favorite + + +@admin.register(Favorite) +class FavoriteAdmin(admin.ModelAdmin): # type: ignore + pass diff --git a/customk/favorites/apps.py b/customk/favorites/apps.py index 437d6ed..1fc39c6 100644 --- a/customk/favorites/apps.py +++ b/customk/favorites/apps.py @@ -2,5 +2,5 @@ class FavoritesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'favorites' + default_auto_field = "django.db.models.BigAutoField" + name = "favorites" diff --git a/customk/favorites/migrations/0001_initial.py b/customk/favorites/migrations/0001_initial.py index dd755ec..623775c 100644 --- a/customk/favorites/migrations/0001_initial.py +++ b/customk/favorites/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1 on 2024-09-01 12:44 +# Generated by Django 5.1 on 2024-09-02 04:59 import django.db.models.deletion import django.utils.timezone @@ -7,7 +7,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ @@ -16,16 +15,35 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Favorite', + 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)), + ( + "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')], + "constraints": [ + models.UniqueConstraint( + fields=("user", "class_id"), name="unique_users_class" + ) + ], }, ), ] diff --git a/customk/favorites/models.py b/customk/favorites/models.py index fc56760..411f018 100644 --- a/customk/favorites/models.py +++ b/customk/favorites/models.py @@ -1,15 +1,20 @@ from django.db import models + +from classes.models import Class from common.models import CommonModel from users.models import User -from classes.models import Class class Favorite(CommonModel): - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='favorites') + 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")] + constraints = [ + models.UniqueConstraint( + fields=["user", "class_id"], name="unique_users_class" + ) + ] def __str__(self): klass = Class.objects.filter(pk=self.class_id).first() diff --git a/customk/favorites/serializers.py b/customk/favorites/serializers.py index 119546f..409f259 100644 --- a/customk/favorites/serializers.py +++ b/customk/favorites/serializers.py @@ -1,11 +1,21 @@ from rest_framework import serializers -from .models import Favorite + +from classes.models import Class from classes.serializers import ClassSerializer +from .models import Favorite -class FavoriteSerializer(serializers.ModelSerializer): # type: ignore - klass = ClassSerializer(read_only=True) + +class FavoriteSerializer(serializers.ModelSerializer): + klass = serializers.SerializerMethodField() class Meta: model = Favorite - fields = ['id', 'user', 'klass'] + 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 diff --git a/customk/favorites/services.py b/customk/favorites/services.py index c10aa38..f7af511 100644 --- a/customk/favorites/services.py +++ b/customk/favorites/services.py @@ -1,11 +1,12 @@ -from .models import Favorite from django.db import transaction +from .models import Favorite + @transaction.atomic -def add_favorite_class(user_id: int, class_id: int) -> Favorite: - return Favorite.objects.create(user_id=user_id, klass=class_id) +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(user_id: int, class_id: int) -> None: - Favorite.objects.filter(user_id=user_id, class_id=class_id).delete() \ No newline at end of file +def delete_favorite_class(favorite_id: int) -> None: + Favorite.objects.filter(id=favorite_id).delete() diff --git a/customk/favorites/tests/conftest.py b/customk/favorites/tests/conftest.py new file mode 100644 index 0000000..34c835d --- /dev/null +++ b/customk/favorites/tests/conftest.py @@ -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"}, + ) diff --git a/customk/favorites/tests/test_favorite_api.py b/customk/favorites/tests/test_favorite_api.py index e69de29..cffee9f 100644 --- a/customk/favorites/tests/test_favorite_api.py +++ b/customk/favorites/tests/test_favorite_api.py @@ -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 + ) diff --git a/customk/favorites/urls.py b/customk/favorites/urls.py index 286641e..b15010a 100644 --- a/customk/favorites/urls.py +++ b/customk/favorites/urls.py @@ -1,7 +1,7 @@ from django.urls import re_path -from .views import FavoriteView, FavoriteDeleteView + +from .views import FavoriteView urlpatterns = [ re_path(r"^$", FavoriteView.as_view(), name="favorite"), - re_path(r"^(?P\d+)/?$", FavoriteDeleteView.as_view(), name="favorite_detail"), ] diff --git a/customk/favorites/views.py b/customk/favorites/views.py index acfeb32..4b946ad 100644 --- a/customk/favorites/views.py +++ b/customk/favorites/views.py @@ -1,44 +1,118 @@ +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 import status from rest_framework.views import APIView + +from classes.serializers import ClassSerializer + from .models import Favorite from .serializers import FavoriteSerializer -from classes.models import Class +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) - favorites = Favorite.objects.filter(user=request.user).order_by("-id")[offset: offset + size] - class_ids = [favorite.class_id for favorite in favorites] - classes = Class.objects.filter(id__in=class_ids) - - serializer = FavoriteSerializer(classes, many=True) - - total_count = len(favorites) + total_count = Favorite.objects.filter(user_id=user.id).count() total_pages = (total_count // size) + 1 - # serializer = FavoriteSerializer(favorites, many=True) + 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}, + { + "total_count": total_count, + "total_pages": total_pages, + "current_page": page, + "results": serializer.data, + }, status=status.HTTP_200_OK, ) - def post(self, request, *args, **kwargs): - class_id = request.GET.get("class_id", "") + @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 not class_id: - return Response({"error": "klass_id is required"}, status=status.HTTP_400_BAD_REQUEST) + if class_id == 0: + return Response( + {"error": "class_id is required"}, status=status.HTTP_400_BAD_REQUEST + ) - favorite, created = Favorite.objects.get_or_create(user=request.user, klass_id=class_id) + favorite, created = add_favorite_class( + user_id=request.user.id, class_id=class_id + ) serializer = FavoriteSerializer(favorite) if created: @@ -46,11 +120,35 @@ def post(self, request, *args, **kwargs): else: return Response({"message": "Already favorited"}, status=status.HTTP_200_OK) - -class FavoriteDeleteView(APIView): + @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: - class_id = request.GET.get("class_id", "") - user_id = request.user.id - Favorite.objects.filter(user_id=user_id, id=class_id).delete() + 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 + ) - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + delete_favorite_class(favorite_id=favorite_id) + + return Response( + {"message": "success delete"}, status=status.HTTP_204_NO_CONTENT + ) diff --git a/customk/users/serializers/user_serializer.py b/customk/users/serializers/user_serializer.py index dbe3312..34089a6 100644 --- a/customk/users/serializers/user_serializer.py +++ b/customk/users/serializers/user_serializer.py @@ -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): @@ -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): diff --git a/customk/users/tests/conftest.py b/customk/users/tests/conftest.py index 4672271..b74f410 100644 --- a/customk/users/tests/conftest.py +++ b/customk/users/tests/conftest.py @@ -4,6 +4,8 @@ from users.serializers.user_serializer import UserSerializer +pytestmark = pytest.mark.django_db + @pytest.fixture def api_client(): @@ -11,7 +13,7 @@ def api_client(): @pytest.fixture -def sample_user(django_db_setup): +def sample_user(): data = { "name": "testname", "email": "test@example.com", diff --git a/customk/users/tests/test_token_api.py b/customk/users/tests/test_token_api.py index c4e779a..0ca5519 100644 --- a/customk/users/tests/test_token_api.py +++ b/customk/users/tests/test_token_api.py @@ -5,53 +5,53 @@ pytestmark = pytest.mark.django_db -class TestCustomTokenRefreshView: - def test_refresh_token_success(self, api_client, refresh_token): - api_client.cookies["refresh_token"] = refresh_token - url = reverse("token_refresh") - response = api_client.post(url) +def test_refresh_token_success(api_client, refresh_token): + api_client.cookies["refresh_token"] = refresh_token + url = reverse("token_refresh") + response = api_client.post(url) - assert response.status_code == status.HTTP_200_OK - assert "access" not in response.data - assert "refresh" not in response.data - assert "access_token" in response.cookies + assert response.status_code == status.HTTP_200_OK + assert "access_token" in response.cookies - def test_refresh_token_missing(self, api_client): - url = reverse("token_refresh") - response = api_client.post(url) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.data["detail"] == "Refresh token not found in cookies." +def test_refresh_token_missing(api_client): + url = reverse("token_refresh") + response = api_client.post(url) - def test_refresh_token_invalid(self, api_client): - api_client.cookies["refresh_token"] = "invalid_token" - url = reverse("token_refresh") - response = api_client.post(url) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["detail"] == "Refresh token not found in cookies." - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.data["detail"] == "Invalid refresh token." +def test_refresh_token_invalid(api_client): + api_client.cookies["refresh_token"] = "invalid_token" + url = reverse("token_refresh") + response = api_client.post(url) -class TestCustomTokenVerifyView: - def test_verify_token_success(self, api_client, access_token): - api_client.cookies["access_token"] = access_token - url = reverse("token_verify") - response = api_client.post(url) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["detail"] == "Invalid refresh token." - assert response.status_code == status.HTTP_200_OK - assert response.data["detail"] == "Token is valid" - def test_verify_token_missing(self, api_client): - url = reverse("token_verify") - response = api_client.post(url) +def test_verify_token_success(api_client, access_token): + api_client.cookies["access_token"] = access_token + url = reverse("token_verify") + response = api_client.post(url) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.data["detail"] == "Access token not found in cookies." + assert response.status_code == status.HTTP_200_OK + assert response.data["detail"] == "Token is valid" - def test_verify_token_invalid(self, api_client): - api_client.cookies["access_token"] = "invalid_token" - url = reverse("token_verify") - response = api_client.post(url) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.data["detail"] == "Token is invalid or expired" +def test_verify_token_missing(api_client): + url = reverse("token_verify") + response = api_client.post(url) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["detail"] == "Access token not found in cookies." + + +def test_verify_token_invalid(api_client): + api_client.cookies["access_token"] = "invalid_token" + url = reverse("token_verify") + response = api_client.post(url) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["detail"] == "Token is invalid or expired" diff --git a/customk/users/views/line.py b/customk/users/views/line.py index b11e5f5..e6ab3e2 100644 --- a/customk/users/views/line.py +++ b/customk/users/views/line.py @@ -70,25 +70,9 @@ def callback(request: Request) -> Response: profile_response.raise_for_status() profile = profile_response.json() - email = profile.get("email") + email = profile.get("email", "") name = profile.get("name") - profile_image = profile.get("picture") - - if not profile_image: - profile_image = "default.image.uri" # TODO: 기본 이미지 필요 - - if not email: - logger.warning("line email empty") - return Response( - data={ - "message": "이메일이 존재하지 않습니다.", - "result": { - "name": name, - "profile_image": profile_image, - }, - }, - status=status.HTTP_400_BAD_REQUEST, - ) + profile_image = profile.get("picture", "") user_data = { "email": email, diff --git a/customk/users/views/token.py b/customk/users/views/token.py index dea3d36..e291034 100644 --- a/customk/users/views/token.py +++ b/customk/users/views/token.py @@ -1,7 +1,6 @@ -from typing import Any from datetime import timedelta -from typing import cast -from config import settings +from typing import Any, cast + from drf_spectacular.utils import ( OpenApiExample, OpenApiParameter, @@ -14,6 +13,7 @@ from rest_framework_simplejwt.exceptions import InvalidToken from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView +from config import settings from config.logger import logger @@ -71,9 +71,12 @@ def post(self, request: Request, *args: Any, **kwargs: Any) -> Response: if response.status_code == 200: access_token = response.data.get("access") from users.services.token_service import get_domain + domain = get_domain(request=request) access_max_age = int( - cast(timedelta, settings.SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"]).total_seconds() + cast( + timedelta, settings.SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"] + ).total_seconds() ) response.set_cookie( key="access_token", From 5ac38f65b7762a1c909c8b654da1c437e45fc441 Mon Sep 17 00:00:00 2001 From: Gomnonix Date: Mon, 2 Sep 2024 17:40:27 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20url=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/classes/admin.py | 21 +++++++--- ...ssimages_description_image_url_and_more.py | 39 +++++++++++++++++++ customk/classes/models.py | 8 ++-- 3 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 customk/classes/migrations/0020_remove_classimages_description_image_url_and_more.py diff --git a/customk/classes/admin.py b/customk/classes/admin.py index 1000fec..a03b22b 100644 --- a/customk/classes/admin.py +++ b/customk/classes/admin.py @@ -86,29 +86,38 @@ class ClassImagesAdmin(admin.ModelAdmin): form = ClassImagesForm list_display = [ "class_id", - "thumbnail_image_url", - "description_image_url", - "detail_image_url", + "thumbnail_image_urls", + "description_image_urls", + "detail_image_urls", ] search_fields = ["class_id"] def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) + # Handle thumbnail image if form.cleaned_data.get("thumbnail_image"): thumbnail_image = form.cleaned_data["thumbnail_image"] thumbnail_url = upload_image_to_object_storage(thumbnail_image) - obj.thumbnail_image_url = thumbnail_url + thumbnail_image_urls = obj.thumbnail_image_urls or [] + thumbnail_image_urls.append(thumbnail_url) + obj.thumbnail_image_urls = thumbnail_image_urls + # Handle description image if form.cleaned_data.get("description_image"): description_image = form.cleaned_data["description_image"] description_url = upload_image_to_object_storage(description_image) - obj.description_image_url = description_url + description_image_urls = obj.description_image_urls or [] + description_image_urls.append(description_url) + obj.description_image_urls = description_image_urls + # Handle detail image if form.cleaned_data.get("detail_image"): detail_image = form.cleaned_data["detail_image"] detail_url = upload_image_to_object_storage(detail_image) - obj.detail_image_url = detail_url + detail_image_urls = obj.detail_image_urls or [] + detail_image_urls.append(detail_url) + obj.detail_image_urls = detail_image_urls obj.save() diff --git a/customk/classes/migrations/0020_remove_classimages_description_image_url_and_more.py b/customk/classes/migrations/0020_remove_classimages_description_image_url_and_more.py new file mode 100644 index 0000000..c98b1e5 --- /dev/null +++ b/customk/classes/migrations/0020_remove_classimages_description_image_url_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.1 on 2024-09-02 08:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("classes", "0019_alter_classimages_detail_image_url_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="classimages", + name="description_image_url", + ), + migrations.RemoveField( + model_name="classimages", + name="detail_image_url", + ), + migrations.RemoveField( + model_name="classimages", + name="thumbnail_image_url", + ), + migrations.AddField( + model_name="classimages", + name="description_image_urls", + field=models.JSONField(blank=True, default=list), + ), + migrations.AddField( + model_name="classimages", + name="detail_image_urls", + field=models.JSONField(blank=True, default=list), + ), + migrations.AddField( + model_name="classimages", + name="thumbnail_image_urls", + field=models.JSONField(blank=True, default=list), + ), + ] diff --git a/customk/classes/models.py b/customk/classes/models.py index b08375c..96e96fd 100644 --- a/customk/classes/models.py +++ b/customk/classes/models.py @@ -3,7 +3,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.db import models -from django.db.models import Avg +from django.db.models import Avg, JSONField from common.models import CommonModel @@ -81,9 +81,9 @@ class ClassDate(models.Model): class ClassImages(models.Model): class_id = models.ForeignKey(Class, related_name="images", on_delete=models.CASCADE) - description_image_url = models.CharField(max_length=255, blank=True) - thumbnail_image_url = models.CharField(max_length=255, blank=True) - detail_image_url = models.CharField(max_length=255, blank=True) + description_image_urls = JSONField(blank=True, default=list) + detail_image_urls = JSONField(blank=True, default=list) + thumbnail_image_urls = JSONField(blank=True, default=list) def __str__(self) -> str: return f"{self.class_id.title}" From f97a07852a21ea72459b061bc894e3dba0a59df7 Mon Sep 17 00:00:00 2001 From: rbwo552 Date: Mon, 2 Sep 2024 18:20:14 +0900 Subject: [PATCH 11/11] =?UTF-8?q?pytest=20ruff=20ignore=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customk/favorites/tests/conftest.py | 12 ++---------- customk/favorites/tests/test_favorite_api.py | 9 +++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/customk/favorites/tests/conftest.py b/customk/favorites/tests/conftest.py index 34c835d..0170e2e 100644 --- a/customk/favorites/tests/conftest.py +++ b/customk/favorites/tests/conftest.py @@ -1,22 +1,14 @@ 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) +def favorite_instance(sample_user, sample_class): + return add_favorite_class(user_id=sample_user.id, class_id=sample_class.id) @pytest.fixture diff --git a/customk/favorites/tests/test_favorite_api.py b/customk/favorites/tests/test_favorite_api.py index cffee9f..618d5fb 100644 --- a/customk/favorites/tests/test_favorite_api.py +++ b/customk/favorites/tests/test_favorite_api.py @@ -1,8 +1,17 @@ +# ruff: noqa: F811 + import pytest from django.urls import reverse from rest_framework import status +from classes.tests.conftest import sample_class from favorites.models import Favorite +from users.tests.conftest import ( + access_token, + api_client_with_token, + refresh_token, + sample_user, +) pytestmark = pytest.mark.django_db