From 6f70bec397d1f35ebac3afd95c7bc9ebdd3069a9 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Thu, 2 Nov 2023 15:48:10 +0000 Subject: [PATCH 01/22] feat(calendar): add model, viewset, serializer --- apps/calendar/__init__.py | 0 apps/calendar/admin.py | 3 + apps/calendar/apps.py | 6 ++ apps/calendar/migrations/__init__.py | 0 apps/calendar/models.py | 33 +++++++ apps/calendar/serializers/__init__.py | 0 apps/calendar/serializers/calendar.py | 93 ++++++++++++++++++++ apps/calendar/tests.py | 3 + apps/calendar/views/__init__.py | 0 apps/calendar/views/viewsets/__init__.py | 1 + apps/calendar/views/viewsets/calendar.py | 45 ++++++++++ apps/calendar/views/viewsets/user_profile.py | 45 ++++++++++ 12 files changed, 229 insertions(+) create mode 100644 apps/calendar/__init__.py create mode 100644 apps/calendar/admin.py create mode 100644 apps/calendar/apps.py create mode 100644 apps/calendar/migrations/__init__.py create mode 100644 apps/calendar/models.py create mode 100644 apps/calendar/serializers/__init__.py create mode 100644 apps/calendar/serializers/calendar.py create mode 100644 apps/calendar/tests.py create mode 100644 apps/calendar/views/__init__.py create mode 100644 apps/calendar/views/viewsets/__init__.py create mode 100644 apps/calendar/views/viewsets/calendar.py create mode 100644 apps/calendar/views/viewsets/user_profile.py diff --git a/apps/calendar/__init__.py b/apps/calendar/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/calendar/admin.py b/apps/calendar/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/calendar/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/calendar/apps.py b/apps/calendar/apps.py new file mode 100644 index 00000000..79dbdc1a --- /dev/null +++ b/apps/calendar/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CalendarConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "Calendar" diff --git a/apps/calendar/migrations/__init__.py b/apps/calendar/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/calendar/models.py b/apps/calendar/models.py new file mode 100644 index 00000000..ad5bd1c6 --- /dev/null +++ b/apps/calendar/models.py @@ -0,0 +1,33 @@ +from django.db import models + +from ara.db.models import MetaDataModel + + +class Calendar(MetaDataModel): + tag = models.ForeignKey( + verbose_name="태그", + to="core.Tag", + on_delete=models.CASCADE, + related_name="event_set", + db_index=True, + ) + is_allday = models.BooleanField( + verbose_name="하루종일", + default=False, + ) + start_at = models.DateTimeField( + verbose_name="시작 시간", + blank=True, + null=True, + default=None, + ) + end_at = models.DateTimeField( + verbose_name="종료 시간", + blank=True, + null=True, + default=None, + ) + title = models.CharField( + verbose_name="제목", + max_length=512, + ) diff --git a/apps/calendar/serializers/__init__.py b/apps/calendar/serializers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/calendar/serializers/calendar.py b/apps/calendar/serializers/calendar.py new file mode 100644 index 00000000..271a9a85 --- /dev/null +++ b/apps/calendar/serializers/calendar.py @@ -0,0 +1,93 @@ +from Calendar.models import Calendar +from rest_framework import serializers + + +class CalendarSerializer(serializers.ModelSerializer): + class Meta: + model = Calendar + fields = "__all__" + + +from dateutil.relativedelta import relativedelta +from django.utils import timezone +from django.utils.translation import gettext +from rest_framework import serializers + +from apps.user.models import UserProfile +from ara.classes.serializers import MetaDataModelSerializer + + +class UserProfileSerializer(MetaDataModelSerializer): + ... + + +class UserProfileUpdateActionSerializer(MetaDataModelSerializer): + class Meta(MetaDataModelSerializer.Meta): + read_only_fields = ( + "sid", + "user", + ) + + def validate_nickname(self, value) -> str: + nickname_changed = self.instance and value != self.instance.nickname + if nickname_changed and not self.instance.can_change_nickname(): + next_change_date = self.instance.nickname_updated_at + relativedelta( + months=3 + ) + raise serializers.ValidationError( + gettext( + "Nicknames can only be changed every 3 months. (can't change until %(date)s)" + ) + % {"date": next_change_date.strftime("%Y/%m/%d")} + ) + return value + + def update(self, instance, validated_data): + new_nickname = validated_data.get("nickname") + old_nickname = instance.nickname if instance else None + if instance and new_nickname and old_nickname != new_nickname: + validated_data["nickname_updated_at"] = timezone.now() + return super(BaseUserProfileSerializer, self).update(instance, validated_data) + + +class PublicUserProfileSerializer(BaseUserProfileSerializer): + class Meta(BaseUserProfileSerializer.Meta): + fields = ( + "picture", + "nickname", + "user", + "is_official", + "is_school_admin", + ) + + +class MyPageUserProfileSerializer(BaseUserProfileSerializer): + num_articles = serializers.SerializerMethodField() + num_comments = serializers.SerializerMethodField() + num_positive_votes = serializers.SerializerMethodField() + + @staticmethod + def get_num_articles(obj): + from apps.core.models import Article + + num_articles = Article.objects.filter(created_by=obj.user).count() + return num_articles + + @staticmethod + def get_num_comments(obj): + from apps.core.models import Comment + + num_comments = Comment.objects.filter(created_by=obj.user).count() + return num_comments + + @staticmethod + def get_num_positive_votes(obj): + from apps.core.models import Vote + + num_article_votes = Vote.objects.filter( + parent_article__created_by=obj.user, is_positive=True + ).count() + num_comment_votes = Vote.objects.filter( + parent_comment__created_by=obj.user, is_positive=True + ).count() + return num_article_votes + num_comment_votes diff --git a/apps/calendar/tests.py b/apps/calendar/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/calendar/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/calendar/views/__init__.py b/apps/calendar/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/calendar/views/viewsets/__init__.py b/apps/calendar/views/viewsets/__init__.py new file mode 100644 index 00000000..8b2bb321 --- /dev/null +++ b/apps/calendar/views/viewsets/__init__.py @@ -0,0 +1 @@ +from .calendar import * diff --git a/apps/calendar/views/viewsets/calendar.py b/apps/calendar/views/viewsets/calendar.py new file mode 100644 index 00000000..d72281b5 --- /dev/null +++ b/apps/calendar/views/viewsets/calendar.py @@ -0,0 +1,45 @@ +from django.utils import timezone +from rest_framework import decorators, mixins, response, status + +from apps.user.models import UserProfile +from apps.user.permissions.user_profile import UserProfilePermission +from apps.user.serializers.user_profile import ( + PublicUserProfileSerializer, + UserProfileSerializer, + UserProfileUpdateActionSerializer, +) +from ara.classes.viewset import ActionAPIViewSet + + +class CalendarViewSet( + mixins.RetrieveModelMixin, mixins.UpdateModelMixin, ActionAPIViewSet +): + queryset = UserProfile.objects.all() + serializer_class = UserProfileSerializer + action_serializer_class = { + "update": UserProfileUpdateActionSerializer, + "partial_update": UserProfileUpdateActionSerializer, + } + permission_classes = (UserProfilePermission,) + + def retrieve(self, request, *args, **kwargs): + profile = self.get_object() + if request.user == profile.user: + return super().retrieve(request, *args, **kwargs) + else: + return response.Response(PublicUserProfileSerializer(profile).data) + + @decorators.action(detail=True, methods=["patch"]) + def agree_terms_of_service(self, request, *args, **kwargs): + # BAD_REQUEST if user already agree with the terms of service + if request.user.profile.agree_terms_of_service_at is not None: + return response.Response( + status=status.HTTP_400_BAD_REQUEST, + ) + + request.user.profile.agree_terms_of_service_at = timezone.now() + request.user.profile.save() + + return response.Response( + status=status.HTTP_200_OK, + ) diff --git a/apps/calendar/views/viewsets/user_profile.py b/apps/calendar/views/viewsets/user_profile.py new file mode 100644 index 00000000..dc1febdc --- /dev/null +++ b/apps/calendar/views/viewsets/user_profile.py @@ -0,0 +1,45 @@ +from django.utils import timezone +from rest_framework import decorators, mixins, response, status + +from apps.user.models import UserProfile +from apps.user.permissions.user_profile import UserProfilePermission +from apps.user.serializers.user_profile import ( + PublicUserProfileSerializer, + UserProfileSerializer, + UserProfileUpdateActionSerializer, +) +from ara.classes.viewset import ActionAPIViewSet + + +class UserProfileViewSet( + mixins.RetrieveModelMixin, mixins.UpdateModelMixin, ActionAPIViewSet +): + queryset = UserProfile.objects.all() + serializer_class = UserProfileSerializer + action_serializer_class = { + "update": UserProfileUpdateActionSerializer, + "partial_update": UserProfileUpdateActionSerializer, + } + permission_classes = (UserProfilePermission,) + + def retrieve(self, request, *args, **kwargs): + profile = self.get_object() + if request.user == profile.user: + return super().retrieve(request, *args, **kwargs) + else: + return response.Response(PublicUserProfileSerializer(profile).data) + + @decorators.action(detail=True, methods=["patch"]) + def agree_terms_of_service(self, request, *args, **kwargs): + # BAD_REQUEST if user already agree with the terms of service + if request.user.profile.agree_terms_of_service_at is not None: + return response.Response( + status=status.HTTP_400_BAD_REQUEST, + ) + + request.user.profile.agree_terms_of_service_at = timezone.now() + request.user.profile.save() + + return response.Response( + status=status.HTTP_200_OK, + ) From 7026ce04a489e71d69f73ac373c3628c77ccb221 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Wed, 15 Nov 2023 14:03:53 +0000 Subject: [PATCH 02/22] fix: models --- apps/calendar/models.py | 17 ++++++------ apps/calendar/views/viewsets/calendar.py | 33 ++++++++++-------------- apps/user/serializers/user_profile.py | 6 +---- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/apps/calendar/models.py b/apps/calendar/models.py index ad5bd1c6..f84ae85d 100644 --- a/apps/calendar/models.py +++ b/apps/calendar/models.py @@ -1,16 +1,11 @@ from django.db import models +from django.utils.translation import gettext_lazy from ara.db.models import MetaDataModel class Calendar(MetaDataModel): - tag = models.ForeignKey( - verbose_name="태그", - to="core.Tag", - on_delete=models.CASCADE, - related_name="event_set", - db_index=True, - ) + tag = models.IntegerField() is_allday = models.BooleanField( verbose_name="하루종일", default=False, @@ -27,7 +22,11 @@ class Calendar(MetaDataModel): null=True, default=None, ) - title = models.CharField( - verbose_name="제목", + ko_title = models.CharField( + verbose_name="한글 제목", + max_length=512, + ) + en_title = models.CharField( + verbose_name="영어 제목", max_length=512, ) diff --git a/apps/calendar/views/viewsets/calendar.py b/apps/calendar/views/viewsets/calendar.py index d72281b5..5664c25b 100644 --- a/apps/calendar/views/viewsets/calendar.py +++ b/apps/calendar/views/viewsets/calendar.py @@ -1,11 +1,8 @@ -from django.utils import timezone -from rest_framework import decorators, mixins, response, status - -from apps.user.models import UserProfile +from apps.calendar.models import Calendar +from apps.calendar.serializers.calendar import CalendarSerializer from apps.user.permissions.user_profile import UserProfilePermission from apps.user.serializers.user_profile import ( PublicUserProfileSerializer, - UserProfileSerializer, UserProfileUpdateActionSerializer, ) from ara.classes.viewset import ActionAPIViewSet @@ -14,8 +11,8 @@ class CalendarViewSet( mixins.RetrieveModelMixin, mixins.UpdateModelMixin, ActionAPIViewSet ): - queryset = UserProfile.objects.all() - serializer_class = UserProfileSerializer + queryset = Calendar.objects.all() + serializer_class = CalendarSerializer action_serializer_class = { "update": UserProfileUpdateActionSerializer, "partial_update": UserProfileUpdateActionSerializer, @@ -29,17 +26,13 @@ def retrieve(self, request, *args, **kwargs): else: return response.Response(PublicUserProfileSerializer(profile).data) - @decorators.action(detail=True, methods=["patch"]) - def agree_terms_of_service(self, request, *args, **kwargs): - # BAD_REQUEST if user already agree with the terms of service - if request.user.profile.agree_terms_of_service_at is not None: - return response.Response( - status=status.HTTP_400_BAD_REQUEST, - ) - - request.user.profile.agree_terms_of_service_at = timezone.now() - request.user.profile.save() + def list(self, request): + queryset = User.objects.all() + serializer = UserSerializer(queryset, many=True) + return Response(serializer.data) - return response.Response( - status=status.HTTP_200_OK, - ) + def retrieve(self, request, pk=None): + queryset = User.objects.all() + user = get_object_or_404(queryset, pk=pk) + serializer = UserSerializer(user) + return Response(serializer.data) diff --git a/apps/user/serializers/user_profile.py b/apps/user/serializers/user_profile.py index 40dff639..3fbd9f6f 100644 --- a/apps/user/serializers/user_profile.py +++ b/apps/user/serializers/user_profile.py @@ -26,11 +26,7 @@ def get_is_official(obj) -> bool: return obj.is_official -class UserProfileSerializer(BaseUserProfileSerializer): - ... - - -class UserProfileUpdateActionSerializer(BaseUserProfileSerializer): +class CalendarUpdateActionSerializer(BaseUserProfileSerializer): class Meta(BaseUserProfileSerializer.Meta): read_only_fields = ( "sid", From edf1d43f24b493cf78987cc218f1946f681e1616 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Wed, 15 Nov 2023 14:54:57 +0000 Subject: [PATCH 03/22] fix: models --- apps/calendar/models.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/calendar/models.py b/apps/calendar/models.py index f84ae85d..e2cd47c2 100644 --- a/apps/calendar/models.py +++ b/apps/calendar/models.py @@ -1,11 +1,14 @@ from django.db import models -from django.utils.translation import gettext_lazy from ara.db.models import MetaDataModel +class Tag(models.Model): + name = models.CharField(max_length=255, unique=True) + + class Calendar(MetaDataModel): - tag = models.IntegerField() + id = models.AutoField(primary_key=True) is_allday = models.BooleanField( verbose_name="하루종일", default=False, @@ -30,3 +33,7 @@ class Calendar(MetaDataModel): verbose_name="영어 제목", max_length=512, ) + tags = models.ManyToManyField(Tag, related_name="calendars") + + def __str__(self): + return self.ko_title From e65399fce4fc1bd47788339efc3c308b5ee834c0 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Wed, 15 Nov 2023 15:17:26 +0000 Subject: [PATCH 04/22] fix: models --- apps/calendar/models.py | 14 ++-- apps/calendar/serializers/calendar.py | 94 +++------------------------ 2 files changed, 16 insertions(+), 92 deletions(-) diff --git a/apps/calendar/models.py b/apps/calendar/models.py index e2cd47c2..b0d2feef 100644 --- a/apps/calendar/models.py +++ b/apps/calendar/models.py @@ -3,12 +3,7 @@ from ara.db.models import MetaDataModel -class Tag(models.Model): - name = models.CharField(max_length=255, unique=True) - - class Calendar(MetaDataModel): - id = models.AutoField(primary_key=True) is_allday = models.BooleanField( verbose_name="하루종일", default=False, @@ -33,7 +28,14 @@ class Calendar(MetaDataModel): verbose_name="영어 제목", max_length=512, ) - tags = models.ManyToManyField(Tag, related_name="calendars") + tags = models.ManyToManyField("Tag", related_name="calendars") def __str__(self): return self.ko_title + + +class Tag(models.Model): + name = models.CharField(max_length=255, unique=True) + calendar = models.ForeignKey( + Calendar, on_delete=models.SET_NULL, null=True, blank=True + ) diff --git a/apps/calendar/serializers/calendar.py b/apps/calendar/serializers/calendar.py index 271a9a85..797a6e94 100644 --- a/apps/calendar/serializers/calendar.py +++ b/apps/calendar/serializers/calendar.py @@ -1,93 +1,15 @@ -from Calendar.models import Calendar +from calendar.models import Calendar, Tag + from rest_framework import serializers +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = ["id", "name", "calendar"] + + class CalendarSerializer(serializers.ModelSerializer): class Meta: model = Calendar fields = "__all__" - - -from dateutil.relativedelta import relativedelta -from django.utils import timezone -from django.utils.translation import gettext -from rest_framework import serializers - -from apps.user.models import UserProfile -from ara.classes.serializers import MetaDataModelSerializer - - -class UserProfileSerializer(MetaDataModelSerializer): - ... - - -class UserProfileUpdateActionSerializer(MetaDataModelSerializer): - class Meta(MetaDataModelSerializer.Meta): - read_only_fields = ( - "sid", - "user", - ) - - def validate_nickname(self, value) -> str: - nickname_changed = self.instance and value != self.instance.nickname - if nickname_changed and not self.instance.can_change_nickname(): - next_change_date = self.instance.nickname_updated_at + relativedelta( - months=3 - ) - raise serializers.ValidationError( - gettext( - "Nicknames can only be changed every 3 months. (can't change until %(date)s)" - ) - % {"date": next_change_date.strftime("%Y/%m/%d")} - ) - return value - - def update(self, instance, validated_data): - new_nickname = validated_data.get("nickname") - old_nickname = instance.nickname if instance else None - if instance and new_nickname and old_nickname != new_nickname: - validated_data["nickname_updated_at"] = timezone.now() - return super(BaseUserProfileSerializer, self).update(instance, validated_data) - - -class PublicUserProfileSerializer(BaseUserProfileSerializer): - class Meta(BaseUserProfileSerializer.Meta): - fields = ( - "picture", - "nickname", - "user", - "is_official", - "is_school_admin", - ) - - -class MyPageUserProfileSerializer(BaseUserProfileSerializer): - num_articles = serializers.SerializerMethodField() - num_comments = serializers.SerializerMethodField() - num_positive_votes = serializers.SerializerMethodField() - - @staticmethod - def get_num_articles(obj): - from apps.core.models import Article - - num_articles = Article.objects.filter(created_by=obj.user).count() - return num_articles - - @staticmethod - def get_num_comments(obj): - from apps.core.models import Comment - - num_comments = Comment.objects.filter(created_by=obj.user).count() - return num_comments - - @staticmethod - def get_num_positive_votes(obj): - from apps.core.models import Vote - - num_article_votes = Vote.objects.filter( - parent_article__created_by=obj.user, is_positive=True - ).count() - num_comment_votes = Vote.objects.filter( - parent_comment__created_by=obj.user, is_positive=True - ).count() - return num_article_votes + num_comment_votes From 002d840531a9ea01068f1a1a3ad64d01a20966be Mon Sep 17 00:00:00 2001 From: Qndndn Date: Wed, 15 Nov 2023 15:31:42 +0000 Subject: [PATCH 05/22] fix: serializers and viewsets --- apps/calendar/serializers/calendar.py | 10 ++++- apps/calendar/views/viewsets/calendar.py | 42 ++++-------------- apps/calendar/views/viewsets/user_profile.py | 45 -------------------- 3 files changed, 18 insertions(+), 79 deletions(-) delete mode 100644 apps/calendar/views/viewsets/user_profile.py diff --git a/apps/calendar/serializers/calendar.py b/apps/calendar/serializers/calendar.py index 797a6e94..ad6afbf9 100644 --- a/apps/calendar/serializers/calendar.py +++ b/apps/calendar/serializers/calendar.py @@ -12,4 +12,12 @@ class Meta: class CalendarSerializer(serializers.ModelSerializer): class Meta: model = Calendar - fields = "__all__" + fields = [ + "id", + "is_allday", + "start_at", + "end_at", + "ko_title", + "en_title", + "tags", + ] diff --git a/apps/calendar/views/viewsets/calendar.py b/apps/calendar/views/viewsets/calendar.py index 5664c25b..d2195967 100644 --- a/apps/calendar/views/viewsets/calendar.py +++ b/apps/calendar/views/viewsets/calendar.py @@ -1,38 +1,14 @@ -from apps.calendar.models import Calendar -from apps.calendar.serializers.calendar import CalendarSerializer -from apps.user.permissions.user_profile import UserProfilePermission -from apps.user.serializers.user_profile import ( - PublicUserProfileSerializer, - UserProfileUpdateActionSerializer, -) -from ara.classes.viewset import ActionAPIViewSet +from rest_framework import viewsets +from apps.calendar.models import Calendar, Tag +from apps.calendar.serializers.calendar import CalendarSerializer, TagSerializer -class CalendarViewSet( - mixins.RetrieveModelMixin, mixins.UpdateModelMixin, ActionAPIViewSet -): - queryset = Calendar.objects.all() - serializer_class = CalendarSerializer - action_serializer_class = { - "update": UserProfileUpdateActionSerializer, - "partial_update": UserProfileUpdateActionSerializer, - } - permission_classes = (UserProfilePermission,) - def retrieve(self, request, *args, **kwargs): - profile = self.get_object() - if request.user == profile.user: - return super().retrieve(request, *args, **kwargs) - else: - return response.Response(PublicUserProfileSerializer(profile).data) +class TagViewSet(viewsets.ModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer - def list(self, request): - queryset = User.objects.all() - serializer = UserSerializer(queryset, many=True) - return Response(serializer.data) - def retrieve(self, request, pk=None): - queryset = User.objects.all() - user = get_object_or_404(queryset, pk=pk) - serializer = UserSerializer(user) - return Response(serializer.data) +class CalendarViewSet(viewsets.ModelViewSet): + queryset = Calendar.objects.all() + serializer_class = CalendarSerializer diff --git a/apps/calendar/views/viewsets/user_profile.py b/apps/calendar/views/viewsets/user_profile.py deleted file mode 100644 index dc1febdc..00000000 --- a/apps/calendar/views/viewsets/user_profile.py +++ /dev/null @@ -1,45 +0,0 @@ -from django.utils import timezone -from rest_framework import decorators, mixins, response, status - -from apps.user.models import UserProfile -from apps.user.permissions.user_profile import UserProfilePermission -from apps.user.serializers.user_profile import ( - PublicUserProfileSerializer, - UserProfileSerializer, - UserProfileUpdateActionSerializer, -) -from ara.classes.viewset import ActionAPIViewSet - - -class UserProfileViewSet( - mixins.RetrieveModelMixin, mixins.UpdateModelMixin, ActionAPIViewSet -): - queryset = UserProfile.objects.all() - serializer_class = UserProfileSerializer - action_serializer_class = { - "update": UserProfileUpdateActionSerializer, - "partial_update": UserProfileUpdateActionSerializer, - } - permission_classes = (UserProfilePermission,) - - def retrieve(self, request, *args, **kwargs): - profile = self.get_object() - if request.user == profile.user: - return super().retrieve(request, *args, **kwargs) - else: - return response.Response(PublicUserProfileSerializer(profile).data) - - @decorators.action(detail=True, methods=["patch"]) - def agree_terms_of_service(self, request, *args, **kwargs): - # BAD_REQUEST if user already agree with the terms of service - if request.user.profile.agree_terms_of_service_at is not None: - return response.Response( - status=status.HTTP_400_BAD_REQUEST, - ) - - request.user.profile.agree_terms_of_service_at = timezone.now() - request.user.profile.save() - - return response.Response( - status=status.HTTP_200_OK, - ) From 09814863c2068f6d96246709eab9937e9ffe48f0 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Wed, 15 Nov 2023 15:35:28 +0000 Subject: [PATCH 06/22] fix: urls --- ara/urls.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ara/urls.py b/ara/urls.py index 50e0990a..667e8b56 100644 --- a/ara/urls.py +++ b/ara/urls.py @@ -21,6 +21,13 @@ SpectacularRedocView, SpectacularSwaggerView, ) +from rest_framework.routers import DefaultRouter + +from apps.calendar.views import CalendarViewSet, TagViewSet + +router = DefaultRouter() +router.register(r"tags", TagViewSet, basename="tag") +router.register(r"calendars", CalendarViewSet, basename="calendar") urlpatterns = [ path("api/admin/", admin.site.urls), @@ -37,6 +44,7 @@ SpectacularRedocView.as_view(url_name="schema"), name="redoc", ), + path("api/", include(router.urls)), ] if settings.DEBUG: From d8b09cf2f1c4406dbece9537f6f3e383675794b7 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Thu, 16 Nov 2023 13:17:40 +0000 Subject: [PATCH 07/22] fix: urls --- ara/urls.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ara/urls.py b/ara/urls.py index 667e8b56..b2a4bcc0 100644 --- a/ara/urls.py +++ b/ara/urls.py @@ -21,18 +21,13 @@ SpectacularRedocView, SpectacularSwaggerView, ) -from rest_framework.routers import DefaultRouter - -from apps.calendar.views import CalendarViewSet, TagViewSet - -router = DefaultRouter() -router.register(r"tags", TagViewSet, basename="tag") -router.register(r"calendars", CalendarViewSet, basename="calendar") urlpatterns = [ path("api/admin/", admin.site.urls), path("", include(("apps.core.urls", "core"))), path("", include(("apps.user.urls", "user"))), + path("", include(("apps.calendar.urls", "calendar"))), + path("", include(("apps.tag.urls", "tag"))), path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path( "api/schema/swagger/", @@ -44,7 +39,6 @@ SpectacularRedocView.as_view(url_name="schema"), name="redoc", ), - path("api/", include(router.urls)), ] if settings.DEBUG: From 73899c93ba45677b1825f3a4d3c2fb4c3e01ff4c Mon Sep 17 00:00:00 2001 From: Qndndn Date: Thu, 16 Nov 2023 13:53:20 +0000 Subject: [PATCH 08/22] fix: models --- apps/calendar/models.py | 22 +++++++++++++++++++++- apps/calendar/serializers/calendar.py | 14 ++++---------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/apps/calendar/models.py b/apps/calendar/models.py index b0d2feef..a2d6a65d 100644 --- a/apps/calendar/models.py +++ b/apps/calendar/models.py @@ -28,6 +28,25 @@ class Calendar(MetaDataModel): verbose_name="영어 제목", max_length=512, ) + ko_description = models.CharField( + verbose_name="한글 설명", + max_length=512, + ) + en_description = models.CharField( + verbose_name="영어 설명", + max_length=512, + ) + location = models.CharField( + verbose_name="위치", + max_length=512, + ) + url = models.URLField( + verbose_name="포탈 링크", + max_length=200, + blank=True, + null=True, + default=None, + ) tags = models.ManyToManyField("Tag", related_name="calendars") def __str__(self): @@ -36,6 +55,7 @@ def __str__(self): class Tag(models.Model): name = models.CharField(max_length=255, unique=True) + color = models.CharField(max_length=7, default="#000000") calendar = models.ForeignKey( - Calendar, on_delete=models.SET_NULL, null=True, blank=True + "Calendar", on_delete=models.SET_NULL, null=True, blank=True ) diff --git a/apps/calendar/serializers/calendar.py b/apps/calendar/serializers/calendar.py index ad6afbf9..da700f6d 100644 --- a/apps/calendar/serializers/calendar.py +++ b/apps/calendar/serializers/calendar.py @@ -6,18 +6,12 @@ class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag - fields = ["id", "name", "calendar"] + fields = ["name", "color", "calendar"] class CalendarSerializer(serializers.ModelSerializer): + tags = TagSerializer(many=True) + class Meta: model = Calendar - fields = [ - "id", - "is_allday", - "start_at", - "end_at", - "ko_title", - "en_title", - "tags", - ] + fields = "__all__" From 879cc8e0b9870a06d21ab9a528628f76e4db067a Mon Sep 17 00:00:00 2001 From: Qndndn Date: Thu, 16 Nov 2023 13:56:36 +0000 Subject: [PATCH 09/22] fix: serializers --- apps/calendar/serializers/calendar.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/calendar/serializers/calendar.py b/apps/calendar/serializers/calendar.py index da700f6d..37fdc6a3 100644 --- a/apps/calendar/serializers/calendar.py +++ b/apps/calendar/serializers/calendar.py @@ -1,12 +1,12 @@ -from calendar.models import Calendar, Tag - from rest_framework import serializers +from .models import Calendar, Tag + class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag - fields = ["name", "color", "calendar"] + fields = ["name", "color"] class CalendarSerializer(serializers.ModelSerializer): @@ -15,3 +15,23 @@ class CalendarSerializer(serializers.ModelSerializer): class Meta: model = Calendar fields = "__all__" + + +class RecursiveTagSerializer(serializers.Serializer): + def to_representation(self, value): + serializer = TagSerializer(value, context=self.context) + return serializer.data + + +class RecursiveCalendarSerializer(serializers.Serializer): + def to_representation(self, value): + serializer = CalendarSerializer(value, context=self.context) + return serializer.data + + +class TagSerializer(serializers.ModelSerializer): + calendars = RecursiveCalendarSerializer(many=True) + + class Meta: + model = Tag + fields = ["name", "color", "calendars"] From 03c583878bb465830ec64a15c45c19a54cfab152 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Thu, 16 Nov 2023 13:57:43 +0000 Subject: [PATCH 10/22] fix: serializers --- apps/calendar/serializers/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/calendar/serializers/calendar.py b/apps/calendar/serializers/calendar.py index 37fdc6a3..55e0e481 100644 --- a/apps/calendar/serializers/calendar.py +++ b/apps/calendar/serializers/calendar.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from .models import Calendar, Tag +from apps.calendar.models import Calendar, Tag class TagSerializer(serializers.ModelSerializer): From 910e9a27a9e369be5c617b84988b53fe0779391b Mon Sep 17 00:00:00 2001 From: Qndndn Date: Thu, 16 Nov 2023 15:31:35 +0000 Subject: [PATCH 11/22] fix: urls --- apps/calendar/urls | 11 +++++++++++ apps/user/serializers/user_profile.py | 6 +++++- ara/urls.py | 4 ++-- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 apps/calendar/urls diff --git a/apps/calendar/urls b/apps/calendar/urls new file mode 100644 index 00000000..2fd4599a --- /dev/null +++ b/apps/calendar/urls @@ -0,0 +1,11 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import CalendarViewSet, TagViewSet + +router = DefaultRouter() +router.register(r'calendars', CalendarViewSet, basename='calendar') +router.register(r'tags', TagViewSet, basename='tag') + +urlpatterns = [ + path('api/', include(router.urls)), +] diff --git a/apps/user/serializers/user_profile.py b/apps/user/serializers/user_profile.py index 3fbd9f6f..40dff639 100644 --- a/apps/user/serializers/user_profile.py +++ b/apps/user/serializers/user_profile.py @@ -26,7 +26,11 @@ def get_is_official(obj) -> bool: return obj.is_official -class CalendarUpdateActionSerializer(BaseUserProfileSerializer): +class UserProfileSerializer(BaseUserProfileSerializer): + ... + + +class UserProfileUpdateActionSerializer(BaseUserProfileSerializer): class Meta(BaseUserProfileSerializer.Meta): read_only_fields = ( "sid", diff --git a/ara/urls.py b/ara/urls.py index b2a4bcc0..7b26595e 100644 --- a/ara/urls.py +++ b/ara/urls.py @@ -26,8 +26,6 @@ path("api/admin/", admin.site.urls), path("", include(("apps.core.urls", "core"))), path("", include(("apps.user.urls", "user"))), - path("", include(("apps.calendar.urls", "calendar"))), - path("", include(("apps.tag.urls", "tag"))), path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path( "api/schema/swagger/", @@ -39,6 +37,8 @@ SpectacularRedocView.as_view(url_name="schema"), name="redoc", ), + path("admin/", admin.site.urls), + path("calendar/", include("apps.calendar.urls")), ] if settings.DEBUG: From 7c16b08a3587f02211c05ddf3bda4c20c7325132 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Thu, 23 Nov 2023 02:17:56 +0000 Subject: [PATCH 12/22] feat: implement new calendar testcode --- apps/calendar/apps.py | 3 +- apps/calendar/migrations/0001_initial.py | 118 ++++++++++++++++++ .../0002_alter_calendar_id_alter_tag_id.py | 26 ++++ apps/calendar/urls | 11 -- apps/calendar/urls.py | 12 ++ ara/settings/django.py | 1 + ara/urls.py | 3 +- tests/test_calendar.py | 31 +++++ 8 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 apps/calendar/migrations/0001_initial.py create mode 100644 apps/calendar/migrations/0002_alter_calendar_id_alter_tag_id.py delete mode 100644 apps/calendar/urls create mode 100644 apps/calendar/urls.py create mode 100644 tests/test_calendar.py diff --git a/apps/calendar/apps.py b/apps/calendar/apps.py index 79dbdc1a..68394ea3 100644 --- a/apps/calendar/apps.py +++ b/apps/calendar/apps.py @@ -2,5 +2,4 @@ class CalendarConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "Calendar" + name = "apps.calendar" diff --git a/apps/calendar/migrations/0001_initial.py b/apps/calendar/migrations/0001_initial.py new file mode 100644 index 00000000..c6190f94 --- /dev/null +++ b/apps/calendar/migrations/0001_initial.py @@ -0,0 +1,118 @@ +# Generated by Django 4.2.6 on 2023-11-16 15:42 + +import datetime + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Calendar", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + verbose_name="생성 시간", + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, db_index=True, verbose_name="수정 시간" + ), + ), + ( + "deleted_at", + models.DateTimeField( + db_index=True, + default=datetime.datetime( + 1, 1, 1, 0, 0, tzinfo=datetime.timezone.utc + ), + verbose_name="삭제 시간", + ), + ), + ("is_allday", models.BooleanField(default=False, verbose_name="하루종일")), + ( + "start_at", + models.DateTimeField( + blank=True, default=None, null=True, verbose_name="시작 시간" + ), + ), + ( + "end_at", + models.DateTimeField( + blank=True, default=None, null=True, verbose_name="종료 시간" + ), + ), + ("ko_title", models.CharField(max_length=512, verbose_name="한글 제목")), + ("en_title", models.CharField(max_length=512, verbose_name="영어 제목")), + ( + "ko_description", + models.CharField(max_length=512, verbose_name="한글 설명"), + ), + ( + "en_description", + models.CharField(max_length=512, verbose_name="영어 설명"), + ), + ("location", models.CharField(max_length=512, verbose_name="위치")), + ( + "url", + models.URLField( + blank=True, default=None, null=True, verbose_name="포탈 링크" + ), + ), + ], + options={ + "ordering": ("-created_at",), + "abstract": False, + }, + ), + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255, unique=True)), + ("color", models.CharField(default="#000000", max_length=7)), + ( + "calendar", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="calendar.calendar", + ), + ), + ], + ), + migrations.AddField( + model_name="calendar", + name="tags", + field=models.ManyToManyField(related_name="calendars", to="calendar.tag"), + ), + ] diff --git a/apps/calendar/migrations/0002_alter_calendar_id_alter_tag_id.py b/apps/calendar/migrations/0002_alter_calendar_id_alter_tag_id.py new file mode 100644 index 00000000..2d0e25e9 --- /dev/null +++ b/apps/calendar/migrations/0002_alter_calendar_id_alter_tag_id.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.6 on 2023-11-16 15:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("calendar", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="calendar", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="tag", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ] diff --git a/apps/calendar/urls b/apps/calendar/urls deleted file mode 100644 index 2fd4599a..00000000 --- a/apps/calendar/urls +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter -from .views import CalendarViewSet, TagViewSet - -router = DefaultRouter() -router.register(r'calendars', CalendarViewSet, basename='calendar') -router.register(r'tags', TagViewSet, basename='tag') - -urlpatterns = [ - path('api/', include(router.urls)), -] diff --git a/apps/calendar/urls.py b/apps/calendar/urls.py new file mode 100644 index 00000000..af40389f --- /dev/null +++ b/apps/calendar/urls.py @@ -0,0 +1,12 @@ +from django.urls import include, path +from rest_framework.routers import DefaultRouter + +from .views.viewsets.calendar import CalendarViewSet, TagViewSet + +router = DefaultRouter() +router.register(r"calendars", CalendarViewSet, basename="calendar") +router.register(r"tags", TagViewSet, basename="tag") + +urlpatterns = [ + path("api/", include(router.urls)), +] diff --git a/ara/settings/django.py b/ara/settings/django.py index 49409645..86792c29 100644 --- a/ara/settings/django.py +++ b/ara/settings/django.py @@ -30,6 +30,7 @@ "django_filters", "apps.core", "apps.user", + "apps.calendar", ] MIDDLEWARE = [ diff --git a/ara/urls.py b/ara/urls.py index 7b26595e..2c8a2e94 100644 --- a/ara/urls.py +++ b/ara/urls.py @@ -26,6 +26,7 @@ path("api/admin/", admin.site.urls), path("", include(("apps.core.urls", "core"))), path("", include(("apps.user.urls", "user"))), + path("", include(("apps.calendar.urls", "calendar"))), path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path( "api/schema/swagger/", @@ -37,8 +38,6 @@ SpectacularRedocView.as_view(url_name="schema"), name="redoc", ), - path("admin/", admin.site.urls), - path("calendar/", include("apps.calendar.urls")), ] if settings.DEBUG: diff --git a/tests/test_calendar.py b/tests/test_calendar.py new file mode 100644 index 00000000..337882f6 --- /dev/null +++ b/tests/test_calendar.py @@ -0,0 +1,31 @@ +import pytest +from django.utils import timezone + +from apps.calendar.models import Calendar +from tests.conftest import RequestSetting, TestCase + + +class TestCalendar(TestCase, RequestSetting): + @pytest.fixture(scope="class") + def set_calendar(self, request): + request.cls.calendar = Calendar.objects.create( + is_allday=True, + start_at=timezone.now(), + end_at=timezone.now() + timezone.timedelta(hours=1), + ko_title="테스트 캘린더", + en_title="Test Calendar", + ko_description="한글 설명", + en_description="English Description", + location="테스트 위치", + url="http://example.com/test", + ) + + def test_calendar_creation(self): + assert self.calendar.is_allday + assert self.calendar.start_at < self.calendar.end_at + assert self.calendar.ko_title == "테스트 캘린더" + assert self.calendar.en_title == "Test Calendar" + assert self.calendar.ko_description == "한글 설명" + assert self.calendar.en_description == "English Description" + assert self.calendar.location == "테스트 위치" + assert self.calendar.url == "http://example.com/test" From e03d686e24c0e69aa5ce9b282081b7f59ad89967 Mon Sep 17 00:00:00 2001 From: Qndndn Date: Thu, 23 Nov 2023 14:28:57 +0000 Subject: [PATCH 13/22] feat: calendar api test code --- tests/test_calendar.py | 72 ++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/tests/test_calendar.py b/tests/test_calendar.py index 337882f6..9323ae5f 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -2,26 +2,28 @@ from django.utils import timezone from apps.calendar.models import Calendar -from tests.conftest import RequestSetting, TestCase +from tests.conftest import RequestSetting, TestCase, Utils -class TestCalendar(TestCase, RequestSetting): - @pytest.fixture(scope="class") - def set_calendar(self, request): - request.cls.calendar = Calendar.objects.create( - is_allday=True, - start_at=timezone.now(), - end_at=timezone.now() + timezone.timedelta(hours=1), - ko_title="테스트 캘린더", - en_title="Test Calendar", - ko_description="한글 설명", - en_description="English Description", - location="테스트 위치", - url="http://example.com/test", - ) +@pytest.fixture(scope="class", autouse=True) +def set_calendar(request): + request.cls.calendar = Calendar.objects.create( + is_allday=False, + start_at=timezone.now(), + end_at=timezone.now() + timezone.timedelta(hours=1), + ko_title="테스트 캘린더", + en_title="Test Calendar", + ko_description="한글 설명", + en_description="English Description", + location="테스트 위치", + url="http://example.com/test", + ) + +class TestCalendar(TestCase, RequestSetting): + # 모델의 생성 및 필드 확인 def test_calendar_creation(self): - assert self.calendar.is_allday + assert self.calendar.is_allday == False assert self.calendar.start_at < self.calendar.end_at assert self.calendar.ko_title == "테스트 캘린더" assert self.calendar.en_title == "Test Calendar" @@ -29,3 +31,41 @@ def test_calendar_creation(self): assert self.calendar.en_description == "English Description" assert self.calendar.location == "테스트 위치" assert self.calendar.url == "http://example.com/test" + + +class TestCalendarAPI(TestCase, RequestSetting, Utils): + def test_create_calendar(self): + tags_data = [ + {"name": "Tag1", "color": "#FF0000"}, + {"name": "Tag2", "color": "#00FF00"}, + ] + + data = { + "is_allday": False, + "start_at": timezone.now(), + "end_at": timezone.now() + timezone.timedelta(hours=1), + "ko_title": "테스트 캘린더", + "en_title": "Test Calendar", + "ko_description": "한글 설명", + "en_description": "English Description", + "location": "테스트 위치", + "url": "http://example.com/test", + "tags": tags_data, + } + response = self.http_request(self.admin, "post", "calendars/", data=data) + assert response.status_code == 201 + assert Calendar.objects.filter(ko_title="테스트 캘린더").exists() + + calendar = Calendar.objects.get(ko_title="테스트 캘린더") + assert calendar.tags.count() == len(tags_data) + + for tag_data in tags_data: + assert calendar.tags.filter(name=tag_data["name"]).exists() + + def test_get_calendar_list(self): + self.create_calendar("Calendar 1") + self.create_calendar("Calendar 2") + + response = self.http_request(self.admin, "get", "calendars/") + assert response.status_code == 200 + assert len(response.data) == 2 From 4fb20bf136c4bcafe9a8a9ea9b4a9d7e44a94214 Mon Sep 17 00:00:00 2001 From: yuwol Date: Tue, 2 Jan 2024 13:03:13 +0000 Subject: [PATCH 14/22] refactor(calendar): split models into separate files --- apps/calendar/models/__init__.py | 2 ++ apps/calendar/{models.py => models/calendar.py} | 8 -------- apps/calendar/models/tag.py | 11 +++++++++++ apps/calendar/tests.py | 3 --- 4 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 apps/calendar/models/__init__.py rename apps/calendar/{models.py => models/calendar.py} (83%) create mode 100644 apps/calendar/models/tag.py delete mode 100644 apps/calendar/tests.py diff --git a/apps/calendar/models/__init__.py b/apps/calendar/models/__init__.py new file mode 100644 index 00000000..f48afca6 --- /dev/null +++ b/apps/calendar/models/__init__.py @@ -0,0 +1,2 @@ +from .calendar import Calendar +from .tag import Tag diff --git a/apps/calendar/models.py b/apps/calendar/models/calendar.py similarity index 83% rename from apps/calendar/models.py rename to apps/calendar/models/calendar.py index a2d6a65d..d6c4c49c 100644 --- a/apps/calendar/models.py +++ b/apps/calendar/models/calendar.py @@ -51,11 +51,3 @@ class Calendar(MetaDataModel): def __str__(self): return self.ko_title - - -class Tag(models.Model): - name = models.CharField(max_length=255, unique=True) - color = models.CharField(max_length=7, default="#000000") - calendar = models.ForeignKey( - "Calendar", on_delete=models.SET_NULL, null=True, blank=True - ) diff --git a/apps/calendar/models/tag.py b/apps/calendar/models/tag.py new file mode 100644 index 00000000..d7b10772 --- /dev/null +++ b/apps/calendar/models/tag.py @@ -0,0 +1,11 @@ +from django.db import models + +from ara.db.models import MetaDataModel + + +class Tag(MetaDataModel): + name = models.CharField(max_length=255, unique=True) + color = models.CharField(max_length=7, default="#000000") + calendar = models.ForeignKey( + "Calendar", on_delete=models.SET_NULL, null=True, blank=True + ) diff --git a/apps/calendar/tests.py b/apps/calendar/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/calendar/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. From b51ad62be655837adb21525e9cd493c91a4489e6 Mon Sep 17 00:00:00 2001 From: yuwol Date: Wed, 3 Jan 2024 07:20:36 +0000 Subject: [PATCH 15/22] feat(calendar): rename calendar model to event --- apps/calendar/models/__init__.py | 2 +- .../calendar/models/{calendar.py => event.py} | 52 ++++++++++++------- apps/calendar/models/tag.py | 39 +++++++++++--- 3 files changed, 67 insertions(+), 26 deletions(-) rename apps/calendar/models/{calendar.py => event.py} (51%) diff --git a/apps/calendar/models/__init__.py b/apps/calendar/models/__init__.py index f48afca6..f0de8b33 100644 --- a/apps/calendar/models/__init__.py +++ b/apps/calendar/models/__init__.py @@ -1,2 +1,2 @@ -from .calendar import Calendar +from .event import Event from .tag import Tag diff --git a/apps/calendar/models/calendar.py b/apps/calendar/models/event.py similarity index 51% rename from apps/calendar/models/calendar.py rename to apps/calendar/models/event.py index d6c4c49c..0a5926eb 100644 --- a/apps/calendar/models/calendar.py +++ b/apps/calendar/models/event.py @@ -1,25 +1,27 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.db import models from ara.db.models import MetaDataModel +if TYPE_CHECKING: + from .tag import Tag + -class Calendar(MetaDataModel): - is_allday = models.BooleanField( - verbose_name="하루종일", +class Event(MetaDataModel): + is_all_day = models.BooleanField( + verbose_name="하루 종일", default=False, ) start_at = models.DateTimeField( - verbose_name="시작 시간", - blank=True, - null=True, - default=None, + verbose_name="시작 시각", ) end_at = models.DateTimeField( - verbose_name="종료 시간", - blank=True, - null=True, - default=None, + verbose_name="종료 시각", ) + ko_title = models.CharField( verbose_name="한글 제목", max_length=512, @@ -28,26 +30,40 @@ class Calendar(MetaDataModel): verbose_name="영어 제목", max_length=512, ) - ko_description = models.CharField( + ko_description = models.TextField( verbose_name="한글 설명", max_length=512, + blank=True, + null=True, ) - en_description = models.CharField( + en_description = models.TextField( verbose_name="영어 설명", max_length=512, + blank=True, + null=True, ) + location = models.CharField( - verbose_name="위치", + verbose_name="장소", max_length=512, + blank=True, + null=True, ) url = models.URLField( - verbose_name="포탈 링크", + verbose_name="URL", max_length=200, blank=True, null=True, - default=None, ) - tags = models.ManyToManyField("Tag", related_name="calendars") + tags: list[Tag] = models.ManyToManyField( + verbose_name="태그", + to="calendar.Tag", + blank=True, + ) + + class Meta(MetaDataModel.Meta): + verbose_name = "일정" + verbose_name_plural = "일정 목록" - def __str__(self): + def __str__(self) -> str: return self.ko_title diff --git a/apps/calendar/models/tag.py b/apps/calendar/models/tag.py index d7b10772..e8f6529e 100644 --- a/apps/calendar/models/tag.py +++ b/apps/calendar/models/tag.py @@ -1,11 +1,36 @@ +from django.contrib import admin from django.db import models +from django.utils.html import format_html -from ara.db.models import MetaDataModel - -class Tag(MetaDataModel): - name = models.CharField(max_length=255, unique=True) - color = models.CharField(max_length=7, default="#000000") - calendar = models.ForeignKey( - "Calendar", on_delete=models.SET_NULL, null=True, blank=True +class Tag(models.Model): + ko_name = models.CharField( + verbose_name="한글 이름", + max_length=255, + unique=True, + ) + en_name = models.CharField( + verbose_name="영어 이름", + max_length=255, + unique=True, + ) + color = models.CharField( + verbose_name="HEX 코드", + max_length=7, + default="#000000", ) + + class Meta: + verbose_name = "태그" + verbose_name_plural = "태그 목록" + + def __str__(self) -> str: + return self.ko_name + + @admin.display(description="한글 이름") + def colored_ko_name(self): + return format_html( + "{}", + self.color, + self.ko_name, + ) From 9f349817e8283da86d984be6b5da32ddbb28b8ab Mon Sep 17 00:00:00 2001 From: yuwol Date: Wed, 3 Jan 2024 07:23:23 +0000 Subject: [PATCH 16/22] refactor(calendar): split serializers into separate files --- apps/calendar/serializers/calendar.py | 37 --------------------------- apps/calendar/serializers/event.py | 22 ++++++++++++++++ apps/calendar/serializers/tag.py | 9 +++++++ 3 files changed, 31 insertions(+), 37 deletions(-) delete mode 100644 apps/calendar/serializers/calendar.py create mode 100644 apps/calendar/serializers/event.py create mode 100644 apps/calendar/serializers/tag.py diff --git a/apps/calendar/serializers/calendar.py b/apps/calendar/serializers/calendar.py deleted file mode 100644 index 55e0e481..00000000 --- a/apps/calendar/serializers/calendar.py +++ /dev/null @@ -1,37 +0,0 @@ -from rest_framework import serializers - -from apps.calendar.models import Calendar, Tag - - -class TagSerializer(serializers.ModelSerializer): - class Meta: - model = Tag - fields = ["name", "color"] - - -class CalendarSerializer(serializers.ModelSerializer): - tags = TagSerializer(many=True) - - class Meta: - model = Calendar - fields = "__all__" - - -class RecursiveTagSerializer(serializers.Serializer): - def to_representation(self, value): - serializer = TagSerializer(value, context=self.context) - return serializer.data - - -class RecursiveCalendarSerializer(serializers.Serializer): - def to_representation(self, value): - serializer = CalendarSerializer(value, context=self.context) - return serializer.data - - -class TagSerializer(serializers.ModelSerializer): - calendars = RecursiveCalendarSerializer(many=True) - - class Meta: - model = Tag - fields = ["name", "color", "calendars"] diff --git a/apps/calendar/serializers/event.py b/apps/calendar/serializers/event.py new file mode 100644 index 00000000..e6e517e7 --- /dev/null +++ b/apps/calendar/serializers/event.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + +from apps.calendar.models import Event + + +class EventSerializer(serializers.ModelSerializer): + class Meta: + model = Event + fields = [ + "id", + "is_all_day", + "start_at", + "end_at", + "ko_title", + "en_title", + "ko_description", + "en_description", + "location", + "url", + "tags", + ] + depth = 1 diff --git a/apps/calendar/serializers/tag.py b/apps/calendar/serializers/tag.py new file mode 100644 index 00000000..b314149a --- /dev/null +++ b/apps/calendar/serializers/tag.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from apps.calendar.models import Tag + + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = "__all__" From 4eda968af4f806f5e6cc77f262cb337c19614c3b Mon Sep 17 00:00:00 2001 From: yuwol Date: Wed, 3 Jan 2024 07:23:48 +0000 Subject: [PATCH 17/22] refactor(calendar): split viewsets into separate files --- apps/calendar/views/__init__.py | 2 ++ apps/calendar/views/event.py | 9 +++++++++ apps/calendar/views/tag.py | 9 +++++++++ apps/calendar/views/viewsets/__init__.py | 1 - apps/calendar/views/viewsets/calendar.py | 14 -------------- 5 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 apps/calendar/views/event.py create mode 100644 apps/calendar/views/tag.py delete mode 100644 apps/calendar/views/viewsets/__init__.py delete mode 100644 apps/calendar/views/viewsets/calendar.py diff --git a/apps/calendar/views/__init__.py b/apps/calendar/views/__init__.py index e69de29b..4ce49873 100644 --- a/apps/calendar/views/__init__.py +++ b/apps/calendar/views/__init__.py @@ -0,0 +1,2 @@ +from .event import EventViewSet +from .tag import TagViewSet diff --git a/apps/calendar/views/event.py b/apps/calendar/views/event.py new file mode 100644 index 00000000..24d62877 --- /dev/null +++ b/apps/calendar/views/event.py @@ -0,0 +1,9 @@ +from rest_framework import viewsets + +from apps.calendar.models import Event +from apps.calendar.serializers.event import EventSerializer + + +class EventViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Event.objects.all() + serializer_class = EventSerializer diff --git a/apps/calendar/views/tag.py b/apps/calendar/views/tag.py new file mode 100644 index 00000000..47db28b9 --- /dev/null +++ b/apps/calendar/views/tag.py @@ -0,0 +1,9 @@ +from rest_framework import viewsets + +from apps.calendar.models import Tag +from apps.calendar.serializers.tag import TagSerializer + + +class TagViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer diff --git a/apps/calendar/views/viewsets/__init__.py b/apps/calendar/views/viewsets/__init__.py deleted file mode 100644 index 8b2bb321..00000000 --- a/apps/calendar/views/viewsets/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .calendar import * diff --git a/apps/calendar/views/viewsets/calendar.py b/apps/calendar/views/viewsets/calendar.py deleted file mode 100644 index d2195967..00000000 --- a/apps/calendar/views/viewsets/calendar.py +++ /dev/null @@ -1,14 +0,0 @@ -from rest_framework import viewsets - -from apps.calendar.models import Calendar, Tag -from apps.calendar.serializers.calendar import CalendarSerializer, TagSerializer - - -class TagViewSet(viewsets.ModelViewSet): - queryset = Tag.objects.all() - serializer_class = TagSerializer - - -class CalendarViewSet(viewsets.ModelViewSet): - queryset = Calendar.objects.all() - serializer_class = CalendarSerializer From fd79b144f1016602a51315487d67a14769eff6c6 Mon Sep 17 00:00:00 2001 From: yuwol Date: Wed, 3 Jan 2024 07:30:19 +0000 Subject: [PATCH 18/22] feat(calendar): migrate calendar models --- apps/calendar/migrations/0001_initial.py | 108 +++++++++--------- .../0002_alter_calendar_id_alter_tag_id.py | 26 ----- 2 files changed, 57 insertions(+), 77 deletions(-) delete mode 100644 apps/calendar/migrations/0002_alter_calendar_id_alter_tag_id.py diff --git a/apps/calendar/migrations/0001_initial.py b/apps/calendar/migrations/0001_initial.py index c6190f94..e1e3e6a8 100644 --- a/apps/calendar/migrations/0001_initial.py +++ b/apps/calendar/migrations/0001_initial.py @@ -1,8 +1,7 @@ -# Generated by Django 4.2.6 on 2023-11-16 15:42 +# Generated by Django 4.2.3 on 2024-01-03 07:17 import datetime -import django.db.models.deletion import django.utils.timezone from django.db import migrations, models @@ -14,11 +13,43 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="Calendar", + name="Tag", fields=[ ( "id", - models.BigAutoField( + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "ko_name", + models.CharField(max_length=255, unique=True, verbose_name="한글 이름"), + ), + ( + "en_name", + models.CharField(max_length=255, unique=True, verbose_name="영어 이름"), + ), + ( + "color", + models.CharField( + default="#000000", max_length=7, verbose_name="HEX 코드" + ), + ), + ], + options={ + "verbose_name": "태그", + "verbose_name_plural": "태그 목록", + }, + ), + migrations.CreateModel( + name="Event", + fields=[ + ( + "id", + models.AutoField( auto_created=True, primary_key=True, serialize=False, @@ -49,70 +80,45 @@ class Migration(migrations.Migration): verbose_name="삭제 시간", ), ), - ("is_allday", models.BooleanField(default=False, verbose_name="하루종일")), - ( - "start_at", - models.DateTimeField( - blank=True, default=None, null=True, verbose_name="시작 시간" - ), - ), ( - "end_at", - models.DateTimeField( - blank=True, default=None, null=True, verbose_name="종료 시간" - ), + "is_all_day", + models.BooleanField(default=False, verbose_name="하루 종일"), ), + ("start_at", models.DateTimeField(verbose_name="시작 시각")), + ("end_at", models.DateTimeField(verbose_name="종료 시각")), ("ko_title", models.CharField(max_length=512, verbose_name="한글 제목")), ("en_title", models.CharField(max_length=512, verbose_name="영어 제목")), ( "ko_description", - models.CharField(max_length=512, verbose_name="한글 설명"), + models.TextField( + blank=True, max_length=512, null=True, verbose_name="한글 설명" + ), ), ( "en_description", - models.CharField(max_length=512, verbose_name="영어 설명"), - ), - ("location", models.CharField(max_length=512, verbose_name="위치")), - ( - "url", - models.URLField( - blank=True, default=None, null=True, verbose_name="포탈 링크" + models.TextField( + blank=True, max_length=512, null=True, verbose_name="영어 설명" ), ), - ], - options={ - "ordering": ("-created_at",), - "abstract": False, - }, - ), - migrations.CreateModel( - name="Tag", - fields=[ ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", + "location", + models.CharField( + blank=True, max_length=512, null=True, verbose_name="장소" ), ), - ("name", models.CharField(max_length=255, unique=True)), - ("color", models.CharField(default="#000000", max_length=7)), + ("url", models.URLField(blank=True, null=True, verbose_name="URL")), ( - "calendar", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="calendar.calendar", + "tags", + models.ManyToManyField( + blank=True, to="calendar.tag", verbose_name="태그" ), ), ], - ), - migrations.AddField( - model_name="calendar", - name="tags", - field=models.ManyToManyField(related_name="calendars", to="calendar.tag"), + options={ + "verbose_name": "일정", + "verbose_name_plural": "일정 목록", + "ordering": ("-created_at",), + "abstract": False, + }, ), ] diff --git a/apps/calendar/migrations/0002_alter_calendar_id_alter_tag_id.py b/apps/calendar/migrations/0002_alter_calendar_id_alter_tag_id.py deleted file mode 100644 index 2d0e25e9..00000000 --- a/apps/calendar/migrations/0002_alter_calendar_id_alter_tag_id.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.2.6 on 2023-11-16 15:46 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("calendar", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="calendar", - name="id", - field=models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - migrations.AlterField( - model_name="tag", - name="id", - field=models.AutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ] From d9ef2ee47cf54f2b7b554703b6d924b3f7bcbafb Mon Sep 17 00:00:00 2001 From: yuwol Date: Wed, 3 Jan 2024 07:31:38 +0000 Subject: [PATCH 19/22] feat(calendar): add admin page --- apps/calendar/admin.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/calendar/admin.py b/apps/calendar/admin.py index 8c38f3f3..26e5efcd 100644 --- a/apps/calendar/admin.py +++ b/apps/calendar/admin.py @@ -1,3 +1,37 @@ from django.contrib import admin -# Register your models here. +from ara.classes.admin import MetaDataModelAdmin + +from .models import Event, Tag + + +@admin.register(Event) +class EventAdmin(MetaDataModelAdmin): + list_display = ( + "ko_title", + "en_title", + "is_all_day", + "start_at", + "end_at", + "location", + "url", + ) + fields = ( + ("ko_title", "ko_description"), + ("en_title", "en_description"), + ("start_at", "end_at"), + "is_all_day", + "location", + "url", + "tags", + ) + filter_horizontal = ("tags",) + + +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + list_display = ( + "colored_ko_name", + "en_name", + "color", + ) From f79ebf3ac8169d4059fe1265adcebb7571af8911 Mon Sep 17 00:00:00 2001 From: yuwol Date: Wed, 3 Jan 2024 07:37:25 +0000 Subject: [PATCH 20/22] feat(calendar): add endpoints --- apps/calendar/urls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/calendar/urls.py b/apps/calendar/urls.py index af40389f..2060ea98 100644 --- a/apps/calendar/urls.py +++ b/apps/calendar/urls.py @@ -1,12 +1,12 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views.viewsets.calendar import CalendarViewSet, TagViewSet +from .views import EventViewSet, TagViewSet router = DefaultRouter() -router.register(r"calendars", CalendarViewSet, basename="calendar") +router.register(r"events", EventViewSet, basename="event") router.register(r"tags", TagViewSet, basename="tag") urlpatterns = [ - path("api/", include(router.urls)), + path("api/calendar/", include(router.urls)), ] From 371b6e9318cd906e068cf1517ca253f5c68fbf94 Mon Sep 17 00:00:00 2001 From: yuwol Date: Wed, 3 Jan 2024 10:32:33 +0000 Subject: [PATCH 21/22] fix(calendar): remove type hint for tags --- apps/calendar/models/event.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/calendar/models/event.py b/apps/calendar/models/event.py index 0a5926eb..2b487ac3 100644 --- a/apps/calendar/models/event.py +++ b/apps/calendar/models/event.py @@ -1,14 +1,7 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - from django.db import models from ara.db.models import MetaDataModel -if TYPE_CHECKING: - from .tag import Tag - class Event(MetaDataModel): is_all_day = models.BooleanField( @@ -55,7 +48,7 @@ class Event(MetaDataModel): blank=True, null=True, ) - tags: list[Tag] = models.ManyToManyField( + tags = models.ManyToManyField( verbose_name="태그", to="calendar.Tag", blank=True, From 1a41c8a69578b87b023644a788944cd97451f66f Mon Sep 17 00:00:00 2001 From: yuwol Date: Wed, 3 Jan 2024 10:35:04 +0000 Subject: [PATCH 22/22] feat(calendar): add tests about retrievals --- tests/conftest.py | 2 +- tests/test_calendar.py | 92 ++++++++++++++++-------------------------- 2 files changed, 35 insertions(+), 59 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7ab8a049..b5fa62db 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,7 +26,7 @@ def set_admin_client(request): agree_terms_of_service_at=timezone.now(), ) client = APIClient() - client.force_authenticate(user=request.cls.user) + client.force_authenticate(user=request.cls.admin) request.cls.api_client = client diff --git a/tests/test_calendar.py b/tests/test_calendar.py index 9323ae5f..ebe63034 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -1,71 +1,47 @@ import pytest +from dateutil import parser from django.utils import timezone +from rest_framework import status -from apps.calendar.models import Calendar -from tests.conftest import RequestSetting, TestCase, Utils +from apps.calendar.models import Event, Tag +from tests.conftest import RequestSetting, TestCase @pytest.fixture(scope="class", autouse=True) -def set_calendar(request): - request.cls.calendar = Calendar.objects.create( - is_allday=False, - start_at=timezone.now(), - end_at=timezone.now() + timezone.timedelta(hours=1), - ko_title="테스트 캘린더", - en_title="Test Calendar", +def set_event(request): + now = timezone.now() + request.cls.event = Event.objects.create( + is_all_day=False, + start_at=now, + end_at=now + timezone.timedelta(hours=1), + ko_title="한글 제목", + en_title="English Title", ko_description="한글 설명", en_description="English Description", - location="테스트 위치", + location="장소", url="http://example.com/test", ) + request.cls.event.tags.add( + Tag.objects.get_or_create(ko_name="아라", en_name="Ara", color="#ED3A3A")[0], + Tag.objects.get_or_create(ko_name="스팍스", en_name="SPARCS", color="#EBA12A")[0], + ) +@pytest.mark.usefixtures("set_user_client", "set_event") class TestCalendar(TestCase, RequestSetting): - # 모델의 생성 및 필드 확인 - def test_calendar_creation(self): - assert self.calendar.is_allday == False - assert self.calendar.start_at < self.calendar.end_at - assert self.calendar.ko_title == "테스트 캘린더" - assert self.calendar.en_title == "Test Calendar" - assert self.calendar.ko_description == "한글 설명" - assert self.calendar.en_description == "English Description" - assert self.calendar.location == "테스트 위치" - assert self.calendar.url == "http://example.com/test" - - -class TestCalendarAPI(TestCase, RequestSetting, Utils): - def test_create_calendar(self): - tags_data = [ - {"name": "Tag1", "color": "#FF0000"}, - {"name": "Tag2", "color": "#00FF00"}, - ] - - data = { - "is_allday": False, - "start_at": timezone.now(), - "end_at": timezone.now() + timezone.timedelta(hours=1), - "ko_title": "테스트 캘린더", - "en_title": "Test Calendar", - "ko_description": "한글 설명", - "en_description": "English Description", - "location": "테스트 위치", - "url": "http://example.com/test", - "tags": tags_data, - } - response = self.http_request(self.admin, "post", "calendars/", data=data) - assert response.status_code == 201 - assert Calendar.objects.filter(ko_title="테스트 캘린더").exists() - - calendar = Calendar.objects.get(ko_title="테스트 캘린더") - assert calendar.tags.count() == len(tags_data) - - for tag_data in tags_data: - assert calendar.tags.filter(name=tag_data["name"]).exists() - - def test_get_calendar_list(self): - self.create_calendar("Calendar 1") - self.create_calendar("Calendar 2") - - response = self.http_request(self.admin, "get", "calendars/") - assert response.status_code == 200 - assert len(response.data) == 2 + def test_list_count(self) -> None: + res = self.http_request(self.user, "get", "calendar/events") + assert res.status_code == status.HTTP_200_OK + assert res.data.get("num_items") == Event.objects.count() + + def test_get(self) -> None: + res = self.http_request(self.user, "get", f"calendar/events/{self.event.id}") + assert res.status_code == status.HTTP_200_OK + + for el in res.data: + if el == "tags": + continue + elif el == "start_at" or el == "end_at": + assert parser.parse(res.data.get(el)) == getattr(self.event, el) + else: + assert res.data.get(el) == getattr(self.event, el)