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..26e5efcd --- /dev/null +++ b/apps/calendar/admin.py @@ -0,0 +1,37 @@ +from django.contrib import admin + +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", + ) diff --git a/apps/calendar/apps.py b/apps/calendar/apps.py new file mode 100644 index 00000000..68394ea3 --- /dev/null +++ b/apps/calendar/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CalendarConfig(AppConfig): + 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..e1e3e6a8 --- /dev/null +++ b/apps/calendar/migrations/0001_initial.py @@ -0,0 +1,124 @@ +# Generated by Django 4.2.3 on 2024-01-03 07:17 + +import datetime + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Tag", + fields=[ + ( + "id", + 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, + 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_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.TextField( + blank=True, max_length=512, null=True, verbose_name="한글 설명" + ), + ), + ( + "en_description", + models.TextField( + blank=True, max_length=512, null=True, verbose_name="영어 설명" + ), + ), + ( + "location", + models.CharField( + blank=True, max_length=512, null=True, verbose_name="장소" + ), + ), + ("url", models.URLField(blank=True, null=True, verbose_name="URL")), + ( + "tags", + models.ManyToManyField( + blank=True, to="calendar.tag", verbose_name="태그" + ), + ), + ], + options={ + "verbose_name": "일정", + "verbose_name_plural": "일정 목록", + "ordering": ("-created_at",), + "abstract": False, + }, + ), + ] 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/__init__.py b/apps/calendar/models/__init__.py new file mode 100644 index 00000000..f0de8b33 --- /dev/null +++ b/apps/calendar/models/__init__.py @@ -0,0 +1,2 @@ +from .event import Event +from .tag import Tag diff --git a/apps/calendar/models/event.py b/apps/calendar/models/event.py new file mode 100644 index 00000000..2b487ac3 --- /dev/null +++ b/apps/calendar/models/event.py @@ -0,0 +1,62 @@ +from django.db import models + +from ara.db.models import MetaDataModel + + +class Event(MetaDataModel): + is_all_day = models.BooleanField( + verbose_name="하루 종일", + default=False, + ) + start_at = models.DateTimeField( + verbose_name="시작 시각", + ) + end_at = models.DateTimeField( + verbose_name="종료 시각", + ) + + ko_title = models.CharField( + verbose_name="한글 제목", + max_length=512, + ) + en_title = models.CharField( + verbose_name="영어 제목", + max_length=512, + ) + ko_description = models.TextField( + verbose_name="한글 설명", + max_length=512, + blank=True, + null=True, + ) + en_description = models.TextField( + verbose_name="영어 설명", + max_length=512, + blank=True, + null=True, + ) + + location = models.CharField( + verbose_name="장소", + max_length=512, + blank=True, + null=True, + ) + url = models.URLField( + verbose_name="URL", + max_length=200, + blank=True, + null=True, + ) + tags = models.ManyToManyField( + verbose_name="태그", + to="calendar.Tag", + blank=True, + ) + + class Meta(MetaDataModel.Meta): + verbose_name = "일정" + verbose_name_plural = "일정 목록" + + def __str__(self) -> str: + return self.ko_title diff --git a/apps/calendar/models/tag.py b/apps/calendar/models/tag.py new file mode 100644 index 00000000..e8f6529e --- /dev/null +++ b/apps/calendar/models/tag.py @@ -0,0 +1,36 @@ +from django.contrib import admin +from django.db import models +from django.utils.html import format_html + + +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, + ) 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/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__" diff --git a/apps/calendar/urls.py b/apps/calendar/urls.py new file mode 100644 index 00000000..2060ea98 --- /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 import EventViewSet, TagViewSet + +router = DefaultRouter() +router.register(r"events", EventViewSet, basename="event") +router.register(r"tags", TagViewSet, basename="tag") + +urlpatterns = [ + path("api/calendar/", include(router.urls)), +] diff --git a/apps/calendar/views/__init__.py b/apps/calendar/views/__init__.py new file mode 100644 index 00000000..4ce49873 --- /dev/null +++ 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/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 50e0990a..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/", 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 new file mode 100644 index 00000000..ebe63034 --- /dev/null +++ b/tests/test_calendar.py @@ -0,0 +1,47 @@ +import pytest +from dateutil import parser +from django.utils import timezone +from rest_framework import status + +from apps.calendar.models import Event, Tag +from tests.conftest import RequestSetting, TestCase + + +@pytest.fixture(scope="class", autouse=True) +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="장소", + 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_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)