From 4966481b1977fc736a73ffb81a99e54f8c67b53a Mon Sep 17 00:00:00 2001 From: earthyoung Date: Tue, 3 Jun 2025 22:16:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20models=20&=20list=20API=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 8 + app/core/settings.py | 2 + app/core/urls.py | 5 +- app/event/__init__.py | 0 app/event/admin.py | 1 + app/event/apps.py | 16 + app/event/migrations/0001_initial.py | 209 ++++++ app/event/migrations/__init__.py | 0 app/event/models.py | 27 + app/event/presentation/__init__.py | 0 app/event/presentation/admin.py | 1 + app/event/presentation/apps.py | 19 + .../presentation/migrations/0001_initial.py | 657 ++++++++++++++++++ app/event/presentation/migrations/__init__.py | 0 app/event/presentation/models.py | 70 ++ app/event/presentation/serializers.py | 36 + app/event/presentation/test/api_test.py | 23 + app/event/presentation/test/conftest.py | 71 ++ .../presentation/test/count_queries_test.py | 15 + app/event/presentation/translation.py | 20 + app/event/presentation/urls.py | 25 + app/event/presentation/views.py | 13 + app/event/tests.py | 1 + app/event/translation.py | 7 + app/event/views.py | 1 + app/user/apps.py | 11 + ...organization_historicaluserext_and_more.py | 267 +++++++ app/user/models.py | 21 + app/user/translation.py | 7 + pyproject.toml | 2 + 30 files changed, 1534 insertions(+), 1 deletion(-) create mode 100644 app/event/__init__.py create mode 100644 app/event/admin.py create mode 100644 app/event/apps.py create mode 100644 app/event/migrations/0001_initial.py create mode 100644 app/event/migrations/__init__.py create mode 100644 app/event/models.py create mode 100644 app/event/presentation/__init__.py create mode 100644 app/event/presentation/admin.py create mode 100644 app/event/presentation/apps.py create mode 100644 app/event/presentation/migrations/0001_initial.py create mode 100644 app/event/presentation/migrations/__init__.py create mode 100644 app/event/presentation/models.py create mode 100644 app/event/presentation/serializers.py create mode 100644 app/event/presentation/test/api_test.py create mode 100644 app/event/presentation/test/conftest.py create mode 100644 app/event/presentation/test/count_queries_test.py create mode 100644 app/event/presentation/translation.py create mode 100644 app/event/presentation/urls.py create mode 100644 app/event/presentation/views.py create mode 100644 app/event/tests.py create mode 100644 app/event/translation.py create mode 100644 app/event/views.py create mode 100644 app/user/migrations/0002_historicalorganization_historicaluserext_and_more.py create mode 100644 app/user/translation.py diff --git a/Makefile b/Makefile index efb5d11..6a939b2 100644 --- a/Makefile +++ b/Makefile @@ -68,10 +68,18 @@ local-makemigrations: local-migrate: @ENV_PATH=envfile/.env.local uv run python app/manage.py migrate +# Show django makemigrations +local-showmigrations: + @ENV_PATH=envfile/.env.local uv run python app/manage.py showmigrations + # Create admin superuser local-createsuperuser: @ENV_PATH=envfile/.env.local uv run python app/manage.py createsuperuser +# Reverse django migrations +local-reverse-migrations: + @ENV_PATH=envfile/.env.local uv run python app/manage.py migrate $(app) $(number) + # Run pytest local-test: @ENV_PATH=envfile/.env.local cd app && uv run pytest -v diff --git a/app/core/settings.py b/app/core/settings.py index c78c8e4..6c44624 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -157,6 +157,8 @@ "user", "file", "cms", + "event", + "event.presentation", # django-constance "constance", ] diff --git a/app/core/urls.py b/app/core/urls.py index d347ee9..6bedb66 100644 --- a/app/core/urls.py +++ b/app/core/urls.py @@ -23,7 +23,10 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView # type: ignore[assignment] -v1_apis: list[resolvers.URLPattern | resolvers.URLResolver] = [path("cms/", include("cms.urls"))] +v1_apis: list[resolvers.URLPattern | resolvers.URLResolver] = [ + path("cms/", include("cms.urls")), + path("event/presentations/", include("event.presentation.urls")), +] urlpatterns = [ # Health Check diff --git a/app/event/__init__.py b/app/event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/event/admin.py b/app/event/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/app/event/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/app/event/apps.py b/app/event/apps.py new file mode 100644 index 0000000..fb00bfc --- /dev/null +++ b/app/event/apps.py @@ -0,0 +1,16 @@ +import importlib + +from django.apps import AppConfig + + +class EventConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "event" + + def ready(self): + importlib.import_module("event.translation") + + from event.models import Event + from simple_history import register + + register(Event) diff --git a/app/event/migrations/0001_initial.py b/app/event/migrations/0001_initial.py new file mode 100644 index 0000000..1e4302c --- /dev/null +++ b/app/event/migrations/0001_initial.py @@ -0,0 +1,209 @@ +# Generated by Django 5.2 on 2025-06-03 04:16 + +import uuid + +import django.db.models.deletion +import simple_history.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("user", "0002_historicalorganization_historicaluserext_and_more"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Event", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("banner_image", models.TextField(blank=True, null=True)), + ("slogan", models.CharField(blank=True, max_length=1000, null=True)), + ("slogan_ko", models.CharField(blank=True, max_length=1000, null=True)), + ("slogan_en", models.CharField(blank=True, max_length=1000, null=True)), + ( + "description", + models.CharField(blank=True, max_length=1000, null=True), + ), + ( + "description_ko", + models.CharField(blank=True, max_length=1000, null=True), + ), + ( + "description_en", + models.CharField(blank=True, max_length=1000, null=True), + ), + ("event_start_at", models.DateTimeField(blank=True, null=True)), + ("event_end_at", models.DateTimeField(blank=True, null=True)), + ( + "banner_display_start_at", + models.DateTimeField(blank=True, null=True), + ), + ("banner_display_end_at", models.DateTimeField(blank=True, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_deleted_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="events", + to="user.organization", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="HistoricalEvent", + fields=[ + ( + "id", + models.UUIDField(db_index=True, default=uuid.uuid4, editable=False), + ), + ("created_at", models.DateTimeField(blank=True, editable=False)), + ("updated_at", models.DateTimeField(blank=True, editable=False)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("banner_image", models.TextField(blank=True, null=True)), + ("slogan", models.CharField(blank=True, max_length=1000, null=True)), + ("slogan_ko", models.CharField(blank=True, max_length=1000, null=True)), + ("slogan_en", models.CharField(blank=True, max_length=1000, null=True)), + ( + "description", + models.CharField(blank=True, max_length=1000, null=True), + ), + ( + "description_ko", + models.CharField(blank=True, max_length=1000, null=True), + ), + ( + "description_en", + models.CharField(blank=True, max_length=1000, null=True), + ), + ("event_start_at", models.DateTimeField(blank=True, null=True)), + ("event_end_at", models.DateTimeField(blank=True, null=True)), + ( + "banner_display_start_at", + models.DateTimeField(blank=True, null=True), + ), + ("banner_display_end_at", models.DateTimeField(blank=True, null=True)), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "organization", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="user.organization", + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "historical event", + "verbose_name_plural": "historical events", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/app/event/migrations/__init__.py b/app/event/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/event/models.py b/app/event/models.py new file mode 100644 index 0000000..4ae19a8 --- /dev/null +++ b/app/event/models.py @@ -0,0 +1,27 @@ +from core.models import BaseAbstractModel +from django.core.exceptions import ValidationError +from django.db import models +from user.models import Organization + + +class Event(BaseAbstractModel): + organization = models.ForeignKey(Organization, on_delete=models.PROTECT, related_name="events") + name = models.CharField(max_length=256, null=True, blank=True) + banner_image = models.TextField(null=True, blank=True) + slogan = models.CharField(max_length=1000, null=True, blank=True) + description = models.CharField(max_length=1000, null=True, blank=True) + event_start_at = models.DateTimeField(null=True, blank=True) + event_end_at = models.DateTimeField(null=True, blank=True) + banner_display_start_at = models.DateTimeField(null=True, blank=True) + banner_display_end_at = models.DateTimeField(null=True, blank=True) + + def clean(self) -> None: + super().clean() + if self.event_start_at and self.event_end_at and self.event_start_at > self.event_end_at: + raise ValidationError("event의 종료 날짜는 시작 날짜보다 이전일 수 없습니다.") + if ( + self.banner_display_start_at + and self.banner_display_end_at + and self.banner_display_start_at > self.banner_display_end_at + ): + raise ValidationError("banner 전시 종료 날짜는 시작 날짜보다 이전일 수 없습니다.") diff --git a/app/event/presentation/__init__.py b/app/event/presentation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/event/presentation/admin.py b/app/event/presentation/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/app/event/presentation/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/app/event/presentation/apps.py b/app/event/presentation/apps.py new file mode 100644 index 0000000..0751a4c --- /dev/null +++ b/app/event/presentation/apps.py @@ -0,0 +1,19 @@ +import importlib + +from django.apps import AppConfig + + +class PresentationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "event.presentation" + + def ready(self): + importlib.import_module("event.presentation.translation") + + from event.presentation.models import Presentation, PresentationCategory, PresentationSpeaker, PresentationType + from simple_history import register + + register(PresentationType) + register(PresentationCategory) + register(Presentation) + register(PresentationSpeaker) diff --git a/app/event/presentation/migrations/0001_initial.py b/app/event/presentation/migrations/0001_initial.py new file mode 100644 index 0000000..6083502 --- /dev/null +++ b/app/event/presentation/migrations/0001_initial.py @@ -0,0 +1,657 @@ +# Generated by Django 5.2 on 2025-06-03 04:16 + +import uuid + +import django.db.models.deletion +import simple_history.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("event", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="HistoricalPresentationType", + fields=[ + ( + "id", + models.UUIDField(db_index=True, default=uuid.uuid4, editable=False), + ), + ("created_at", models.DateTimeField(blank=True, editable=False)), + ("updated_at", models.DateTimeField(blank=True, editable=False)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "event", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="event.event", + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "historical presentation type", + "verbose_name_plural": "historical presentation types", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name="Presentation", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_deleted_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="HistoricalPresentationSpeaker", + fields=[ + ( + "id", + models.UUIDField(db_index=True, default=uuid.uuid4, editable=False), + ), + ("created_at", models.DateTimeField(blank=True, editable=False)), + ("updated_at", models.DateTimeField(blank=True, editable=False)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("biography", models.CharField(blank=True, max_length=256, null=True)), + ( + "biography_ko", + models.CharField(blank=True, max_length=256, null=True), + ), + ( + "biography_en", + models.CharField(blank=True, max_length=256, null=True), + ), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "presentation", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="presentation.presentation", + ), + ), + ], + options={ + "verbose_name": "historical presentation speaker", + "verbose_name_plural": "historical presentation speakers", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name="PresentationCategory", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_deleted_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="PresentationCategoryRelation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "category", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="relations", + to="presentation.presentationcategory", + ), + ), + ( + "presentation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="relations", + to="presentation.presentation", + ), + ), + ], + ), + migrations.CreateModel( + name="PresentationSpeaker", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("biography", models.CharField(blank=True, max_length=256, null=True)), + ( + "biography_ko", + models.CharField(blank=True, max_length=256, null=True), + ), + ( + "biography_en", + models.CharField(blank=True, max_length=256, null=True), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_deleted_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "presentation", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="presentation_speakers", + to="presentation.presentation", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="presentation_speakers", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="PresentationType", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_deleted_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "event", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="presentation_types", + to="event.event", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="presentationcategory", + name="presentation_type", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="presentation_categories", + to="presentation.presentationtype", + ), + ), + migrations.AddField( + model_name="presentation", + name="presentation_type", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="presentations", + to="presentation.presentationtype", + ), + ), + migrations.CreateModel( + name="HistoricalPresentationCategory", + fields=[ + ( + "id", + models.UUIDField(db_index=True, default=uuid.uuid4, editable=False), + ), + ("created_at", models.DateTimeField(blank=True, editable=False)), + ("updated_at", models.DateTimeField(blank=True, editable=False)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "presentation_type", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="presentation.presentationtype", + ), + ), + ], + options={ + "verbose_name": "historical presentation category", + "verbose_name_plural": "historical presentation categorys", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name="HistoricalPresentation", + fields=[ + ( + "id", + models.UUIDField(db_index=True, default=uuid.uuid4, editable=False), + ), + ("created_at", models.DateTimeField(blank=True, editable=False)), + ("updated_at", models.DateTimeField(blank=True, editable=False)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "presentation_type", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="presentation.presentationtype", + ), + ), + ], + options={ + "verbose_name": "historical presentation", + "verbose_name_plural": "historical presentations", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/app/event/presentation/migrations/__init__.py b/app/event/presentation/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/event/presentation/models.py b/app/event/presentation/models.py new file mode 100644 index 0000000..72d06f7 --- /dev/null +++ b/app/event/presentation/models.py @@ -0,0 +1,70 @@ +from core.models import BaseAbstractModel, BaseAbstractModelQuerySet +from django.contrib.auth import get_user_model +from django.db import models +from django.db.models import Prefetch +from event.models import Event + +User = get_user_model() + + +class PresentationQuerySet(BaseAbstractModelQuerySet): + def get_all_nested_data(self): + return ( + super() + .all() + .select_related("presentation_type") + .prefetch_related( + Prefetch(lookup="presentation_speakers"), + Prefetch( + lookup="relations", + queryset=PresentationCategoryRelation.objects.select_related("category"), + to_attr="presentation_category_relation", + ), + ) + ) + + def filter_by_category(self, category_name): + return ( + super() + .all() + .select_related("presentation_type") + .prefetch_related( + Prefetch(lookup="presentation_speakers"), + Prefetch( + lookup="relations", + queryset=PresentationCategoryRelation.objects.select_related("category").filter( + category__name=category_name + ), + to_attr="presentation_category_relation", + ), + ) + ) + + +class PresentationType(BaseAbstractModel): + event = models.ForeignKey(Event, on_delete=models.PROTECT, related_name="presentation_types", null=True, blank=True) + name = models.CharField(max_length=256, null=True, blank=True) + + +class PresentationCategory(BaseAbstractModel): + presentation_type = models.ForeignKey( + PresentationType, on_delete=models.PROTECT, related_name="presentation_categories" + ) + name = models.CharField(max_length=256, null=True, blank=True) + + +class Presentation(BaseAbstractModel): + presentation_type = models.ForeignKey(PresentationType, on_delete=models.PROTECT, related_name="presentations") + objects: PresentationQuerySet = PresentationQuerySet.as_manager() + + +class PresentationCategoryRelation(models.Model): + presentation = models.ForeignKey(Presentation, on_delete=models.CASCADE, related_name="relations") + category = models.ForeignKey(PresentationCategory, on_delete=models.CASCADE, related_name="relations") + + +class PresentationSpeaker(BaseAbstractModel): + presentation = models.ForeignKey(Presentation, on_delete=models.PROTECT, related_name="presentation_speakers") + user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="presentation_speakers") + name = models.CharField(max_length=256, null=True, blank=True) + biography = models.CharField(max_length=256, null=True, blank=True) diff --git a/app/event/presentation/serializers.py b/app/event/presentation/serializers.py new file mode 100644 index 0000000..c52b981 --- /dev/null +++ b/app/event/presentation/serializers.py @@ -0,0 +1,36 @@ +from event.presentation.models import Presentation, PresentationCategory, PresentationSpeaker, PresentationType +from rest_framework import serializers + + +class PresentationTypeSerializer(serializers.ModelSerializer): + class Meta: + model = PresentationType + fields = ("id", "event", "name") + + +class PresentationCategorySerializer(serializers.ModelSerializer): + class Meta: + model = PresentationCategory + fields = ("id", "presentation_type", "name") + + +class PresentationSpeakerSerializer(serializers.ModelSerializer): + class Meta: + model = PresentationSpeaker + fields = ("id", "presentation", "user", "name", "biography") + + +class PresentationSerializer(serializers.ModelSerializer): + presentation_type = PresentationTypeSerializer(read_only=True) + presentation_categories = serializers.SerializerMethodField() + presentation_speakers = PresentationSpeakerSerializer(many=True, read_only=True) + + def get_presentation_categories(self, obj): + relations = obj.presentation_category_relation + return PresentationCategorySerializer( + instance=[rel.category for rel in relations], many=True, read_only=True + ).data + + class Meta: + model = Presentation + fields = ("id", "presentation_type", "presentation_categories", "presentation_speakers") diff --git a/app/event/presentation/test/api_test.py b/app/event/presentation/test/api_test.py new file mode 100644 index 0000000..cf8eebe --- /dev/null +++ b/app/event/presentation/test/api_test.py @@ -0,0 +1,23 @@ +import http + +import pytest +from django.urls import reverse +from event.presentation.test.conftest import PresentationTestEntity +from rest_framework.test import APIClient + + +@pytest.mark.django_db +def test_presentation_api(api_client: APIClient, create_presentation_set: PresentationTestEntity): + url = reverse("v1:presentation-list") + response = api_client.get(url) + assert response.status_code == http.HTTPStatus.OK + assert len(response.json()) > 0 + + +@pytest.mark.django_db +def test_presentation_category_filter_api(api_client: APIClient, create_presentation_set: PresentationTestEntity): + presentation_category = create_presentation_set.presentation_category + url = reverse("v1:presentation-list") + f"?category={presentation_category.name}" + response = api_client.get(url) + assert response.status_code == http.HTTPStatus.OK + assert len(response.json()) > 0 diff --git a/app/event/presentation/test/conftest.py b/app/event/presentation/test/conftest.py new file mode 100644 index 0000000..e8f0964 --- /dev/null +++ b/app/event/presentation/test/conftest.py @@ -0,0 +1,71 @@ +import dataclasses + +import pytest +from event.models import Event +from event.presentation.models import ( + Presentation, + PresentationCategory, + PresentationCategoryRelation, + PresentationSpeaker, + PresentationType, +) +from faker import Faker +from model_bakery import baker +from rest_framework.test import APIClient +from user.models import Organization, OrganizationUserRelation, UserExt + + +@dataclasses.dataclass +class PresentationTestEntity: + user: UserExt + organization: Organization + organization_user_relation: OrganizationUserRelation + presentation_type: PresentationType + presentation: Presentation + presentation_category: PresentationCategory + presentation_category_relation: PresentationCategoryRelation + presentation_speaker: PresentationSpeaker + + +@pytest.fixture +def create_user_with_organization_and_relation(): + user = baker.make(UserExt) + organization = baker.make(Organization) + relation = baker.make(OrganizationUserRelation, organization=organization, user=user) + return user, organization, relation + + +@pytest.fixture() +def create_event(create_user_with_organization_and_relation): + user, organization, relation = create_user_with_organization_and_relation + event = baker.make(Event, organization=organization) + return user, organization, relation, event + + +@pytest.fixture +def create_presentation_set(create_event): + fake = Faker() + user, organization, relation, event = create_event + presentation_type = baker.make(PresentationType, event=event) + presentation = baker.make(Presentation, presentation_type=presentation_type) + presentation_category = baker.make(PresentationCategory, presentation_type=presentation_type, name=fake.name()) + presentation_category_relation = baker.make( + PresentationCategoryRelation, presentation=presentation, category=presentation_category + ) + presentation_speaker = baker.make(PresentationSpeaker, presentation=presentation, user=user) + + return PresentationTestEntity( + user=user, + organization=organization, + organization_user_relation=relation, + presentation_type=presentation_type, + presentation=presentation, + presentation_category=presentation_category, + presentation_category_relation=presentation_category_relation, + presentation_speaker=presentation_speaker, + ) + + +@pytest.fixture +def api_client(): + return APIClient() diff --git a/app/event/presentation/test/count_queries_test.py b/app/event/presentation/test/count_queries_test.py new file mode 100644 index 0000000..666575c --- /dev/null +++ b/app/event/presentation/test/count_queries_test.py @@ -0,0 +1,15 @@ +import pytest +from django.db import reset_queries +from event.presentation.models import Presentation +from event.presentation.test.conftest import PresentationTestEntity +from pytest_django import DjangoAssertNumQueries + + +@pytest.mark.django_db +def test_count_queries( + django_assert_max_num_queries: DjangoAssertNumQueries, create_presentation_set: PresentationTestEntity +): + reset_queries() + with django_assert_max_num_queries(3): + queryset = Presentation.objects.get_all_nested_data() + list(queryset) # query evaluation diff --git a/app/event/presentation/translation.py b/app/event/presentation/translation.py new file mode 100644 index 0000000..b235682 --- /dev/null +++ b/app/event/presentation/translation.py @@ -0,0 +1,20 @@ +from event.presentation.models import PresentationCategory, PresentationSpeaker, PresentationType +from modeltranslation.translator import TranslationOptions, register + + +@register(PresentationType) +class PresentationTypeTranslationOptions(TranslationOptions): + fields = ("name",) + + +@register(PresentationCategory) +class PresentationCategoryTranslationOptions(TranslationOptions): + fields = ("name",) + + +@register(PresentationSpeaker) +class PresentationSpeakerTranslationOptions(TranslationOptions): + fields = ( + "name", + "biography", + ) diff --git a/app/event/presentation/urls.py b/app/event/presentation/urls.py new file mode 100644 index 0000000..36a1985 --- /dev/null +++ b/app/event/presentation/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for core project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.urls import include, path +from event.presentation import views +from rest_framework import routers + +cms_router = routers.SimpleRouter() +cms_router.register("presentation", views.PresentationViewSet, basename="presentation") + +urlpatterns = [path("", include(cms_router.urls))] diff --git a/app/event/presentation/views.py b/app/event/presentation/views.py new file mode 100644 index 0000000..215943e --- /dev/null +++ b/app/event/presentation/views.py @@ -0,0 +1,13 @@ +from event.presentation.models import Presentation +from event.presentation.serializers import PresentationSerializer +from rest_framework import mixins, viewsets + + +class PresentationViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = Presentation.objects.get_all_nested_data() + serializer_class = PresentationSerializer + + def get_queryset(self): + if category_name := self.request.query_params.get("category"): + return Presentation.objects.filter_by_category(category_name) + return self.queryset diff --git a/app/event/tests.py b/app/event/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/app/event/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/app/event/translation.py b/app/event/translation.py new file mode 100644 index 0000000..2dee690 --- /dev/null +++ b/app/event/translation.py @@ -0,0 +1,7 @@ +from event.models import Event +from modeltranslation.translator import TranslationOptions, register + + +@register(Event) +class EventTranslationOptions(TranslationOptions): + fields = ("name", "slogan", "description") diff --git a/app/event/views.py b/app/event/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/app/event/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/app/user/apps.py b/app/user/apps.py index 578292c..c2d7ddc 100644 --- a/app/user/apps.py +++ b/app/user/apps.py @@ -1,6 +1,17 @@ +import importlib + from django.apps import AppConfig class UserConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "user" + + def ready(self): + importlib.import_module("user.translation") + + from simple_history import register + from user.models import Organization, UserExt + + register(UserExt) + register(Organization) diff --git a/app/user/migrations/0002_historicalorganization_historicaluserext_and_more.py b/app/user/migrations/0002_historicalorganization_historicaluserext_and_more.py new file mode 100644 index 0000000..871ecca --- /dev/null +++ b/app/user/migrations/0002_historicalorganization_historicaluserext_and_more.py @@ -0,0 +1,267 @@ +# Generated by Django 5.2 on 2025-06-03 04:16 + +import uuid + +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +import simple_history.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("user", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="HistoricalOrganization", + fields=[ + ( + "id", + models.UUIDField(db_index=True, default=uuid.uuid4, editable=False), + ), + ("created_at", models.DateTimeField(blank=True, editable=False)), + ("updated_at", models.DateTimeField(blank=True, editable=False)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "historical organization", + "verbose_name_plural": "historical organizations", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name="HistoricalUserExt", + fields=[ + ( + "id", + models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name="ID"), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField(blank=True, null=True, verbose_name="last login"), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + db_index=True, + error_messages={"unique": "A user with that username already exists."}, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField(blank=True, max_length=150, verbose_name="first name"), + ), + ( + "last_name", + models.CharField(blank=True, max_length=150, verbose_name="last name"), + ), + ( + "email", + models.EmailField(blank=True, max_length=254, verbose_name="email address"), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. " + "Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined"), + ), + ("history_id", models.AutoField(primary_key=True, serialize=False)), + ("history_date", models.DateTimeField(db_index=True)), + ("history_change_reason", models.CharField(max_length=100, null=True)), + ( + "history_type", + models.CharField( + choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], + max_length=1, + ), + ), + ( + "history_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "historical user", + "verbose_name_plural": "historical users", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name="Organization", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "deleted_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_deleted_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="OrganizationUserRelation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("start_at", models.DateTimeField(blank=True, null=True)), + ("end_at", models.DateTimeField(blank=True, null=True)), + ( + "organization", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="organization_user_relations", + to="user.organization", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="organization_user_relations", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/app/user/models.py b/app/user/models.py index 83817dc..7aaa0c1 100644 --- a/app/user/models.py +++ b/app/user/models.py @@ -1,5 +1,26 @@ from django.contrib.auth.models import AbstractUser +from django.core.exceptions import ValidationError +from django.db import models class UserExt(AbstractUser): pass + + +from core.models import BaseAbstractModel # noqa: E402 + + +class Organization(BaseAbstractModel): + name = models.CharField(max_length=256, null=True, blank=True) + + +class OrganizationUserRelation(models.Model): + organization = models.ForeignKey(Organization, on_delete=models.PROTECT, related_name="organization_user_relations") + user = models.ForeignKey(UserExt, on_delete=models.PROTECT, related_name="organization_user_relations") + start_at = models.DateTimeField(null=True, blank=True) + end_at = models.DateTimeField(null=True, blank=True) + + def clean(self) -> None: + super().clean() + if self.start_at and self.end_at and self.start_at > self.end_at: + raise ValidationError("종료 날짜는 시작 날짜보다 이전일 수 없습니다.") diff --git a/app/user/translation.py b/app/user/translation.py new file mode 100644 index 0000000..2bb7295 --- /dev/null +++ b/app/user/translation.py @@ -0,0 +1,7 @@ +from modeltranslation.translator import TranslationOptions, register +from user.models import Organization + + +@register(Organization) +class OrganizationTranslationOptions(TranslationOptions): + fields = ("name",) diff --git a/pyproject.toml b/pyproject.toml index d615534..7265df8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,8 @@ default-groups = ["dev", "deployment"] pytest = ">=8.3.5,<9.0.0" pytest-django = "^4.11.1" pytest-socket = "^0.7.0" +faker = "^37.3.0" +model-bakery = "^1.20.4" [build-system] requires = ["hatchling"] From 2ce78b5683d54c196ecdbb42f8c0b8a9bd0ed993 Mon Sep 17 00:00:00 2001 From: earthyoung Date: Thu, 5 Jun 2025 20:09:54 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20PR=20review=20comment=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/urls.py | 1 - app/event/apps.py | 1 - app/event/migrations/0001_initial.py | 4 +- app/event/presentation/apps.py | 1 - .../presentation/migrations/0001_initial.py | 15 ++- app/event/presentation/models.py | 32 +---- app/event/presentation/serializers.py | 8 +- app/event/presentation/views.py | 11 +- app/user/migrations/0002_create_superuser.py | 28 ----- ...zation_historicalorganization_and_more.py} | 115 ++++-------------- app/user/models.py | 11 +- pyproject.toml | 2 + uv.lock | 29 +++++ 13 files changed, 90 insertions(+), 168 deletions(-) delete mode 100644 app/user/migrations/0002_create_superuser.py rename app/user/migrations/{0002_historicalorganization_historicaluserext_and_more.py => 0002_organization_historicalorganization_and_more.py} (76%) diff --git a/app/core/urls.py b/app/core/urls.py index 7e0bb92..dd2e0e9 100644 --- a/app/core/urls.py +++ b/app/core/urls.py @@ -26,7 +26,6 @@ v1_apis: list[resolvers.URLPattern | resolvers.URLResolver] = [ path("cms/", include("cms.urls")), path("admin-api/", include("admin_api.urls")), - path("cms/", include("cms.urls")), path("event/presentations/", include("event.presentation.urls")), ] diff --git a/app/event/apps.py b/app/event/apps.py index fb00bfc..ca9b413 100644 --- a/app/event/apps.py +++ b/app/event/apps.py @@ -4,7 +4,6 @@ class EventConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" name = "event" def ready(self): diff --git a/app/event/migrations/0001_initial.py b/app/event/migrations/0001_initial.py index 1e4302c..25d0d87 100644 --- a/app/event/migrations/0001_initial.py +++ b/app/event/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-06-03 04:16 +# Generated by Django 5.2 on 2025-06-05 11:04 import uuid @@ -12,7 +12,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("user", "0002_historicalorganization_historicaluserext_and_more"), + ("user", "0002_organization_historicalorganization_and_more"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] diff --git a/app/event/presentation/apps.py b/app/event/presentation/apps.py index 0751a4c..c435948 100644 --- a/app/event/presentation/apps.py +++ b/app/event/presentation/apps.py @@ -4,7 +4,6 @@ class PresentationConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" name = "event.presentation" def ready(self): diff --git a/app/event/presentation/migrations/0001_initial.py b/app/event/presentation/migrations/0001_initial.py index 6083502..aa8b8a1 100644 --- a/app/event/presentation/migrations/0001_initial.py +++ b/app/event/presentation/migrations/0001_initial.py @@ -1,7 +1,8 @@ -# Generated by Django 5.2 on 2025-06-03 04:16 +# Generated by Django 5.2 on 2025-06-05 11:04 import uuid +import core.fields import django.db.models.deletion import simple_history.models from django.conf import settings @@ -309,8 +310,10 @@ class Migration(migrations.Migration): fields=[ ( "id", - models.BigAutoField( + core.fields.UUIDAutoField( auto_created=True, + default=uuid.uuid4, + editable=False, primary_key=True, serialize=False, verbose_name="ID", @@ -334,6 +337,14 @@ class Migration(migrations.Migration): ), ], ), + migrations.AddField( + model_name="presentation", + name="presentation_categories", + field=models.ManyToManyField( + through="presentation.PresentationCategoryRelation", + to="presentation.presentationcategory", + ), + ), migrations.CreateModel( name="PresentationSpeaker", fields=[ diff --git a/app/event/presentation/models.py b/app/event/presentation/models.py index 72d06f7..96ba766 100644 --- a/app/event/presentation/models.py +++ b/app/event/presentation/models.py @@ -1,7 +1,6 @@ from core.models import BaseAbstractModel, BaseAbstractModelQuerySet from django.contrib.auth import get_user_model from django.db import models -from django.db.models import Prefetch from event.models import Event User = get_user_model() @@ -10,34 +9,9 @@ class PresentationQuerySet(BaseAbstractModelQuerySet): def get_all_nested_data(self): return ( - super() - .all() + self.filter_active() .select_related("presentation_type") - .prefetch_related( - Prefetch(lookup="presentation_speakers"), - Prefetch( - lookup="relations", - queryset=PresentationCategoryRelation.objects.select_related("category"), - to_attr="presentation_category_relation", - ), - ) - ) - - def filter_by_category(self, category_name): - return ( - super() - .all() - .select_related("presentation_type") - .prefetch_related( - Prefetch(lookup="presentation_speakers"), - Prefetch( - lookup="relations", - queryset=PresentationCategoryRelation.objects.select_related("category").filter( - category__name=category_name - ), - to_attr="presentation_category_relation", - ), - ) + .prefetch_related("presentation_speakers", "presentation_categories") ) @@ -55,6 +29,8 @@ class PresentationCategory(BaseAbstractModel): class Presentation(BaseAbstractModel): presentation_type = models.ForeignKey(PresentationType, on_delete=models.PROTECT, related_name="presentations") + presentation_categories = models.ManyToManyField(to="PresentationCategory", through="PresentationCategoryRelation") + objects: PresentationQuerySet = PresentationQuerySet.as_manager() diff --git a/app/event/presentation/serializers.py b/app/event/presentation/serializers.py index c52b981..3a04fb0 100644 --- a/app/event/presentation/serializers.py +++ b/app/event/presentation/serializers.py @@ -22,15 +22,9 @@ class Meta: class PresentationSerializer(serializers.ModelSerializer): presentation_type = PresentationTypeSerializer(read_only=True) - presentation_categories = serializers.SerializerMethodField() + presentation_categories = PresentationCategorySerializer(many=True, read_only=True) presentation_speakers = PresentationSpeakerSerializer(many=True, read_only=True) - def get_presentation_categories(self, obj): - relations = obj.presentation_category_relation - return PresentationCategorySerializer( - instance=[rel.category for rel in relations], many=True, read_only=True - ).data - class Meta: model = Presentation fields = ("id", "presentation_type", "presentation_categories", "presentation_speakers") diff --git a/app/event/presentation/views.py b/app/event/presentation/views.py index 215943e..f9ae2a9 100644 --- a/app/event/presentation/views.py +++ b/app/event/presentation/views.py @@ -1,13 +1,14 @@ +from django_filters import rest_framework as filters from event.presentation.models import Presentation from event.presentation.serializers import PresentationSerializer from rest_framework import mixins, viewsets +class PresentationFilterSet(filters.FilterSet): + category = filters.CharFilter(field_name="presentation_categories__name", lookup_expr="exact") + + class PresentationViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): queryset = Presentation.objects.get_all_nested_data() serializer_class = PresentationSerializer - - def get_queryset(self): - if category_name := self.request.query_params.get("category"): - return Presentation.objects.filter_by_category(category_name) - return self.queryset + filterset_class = PresentationFilterSet diff --git a/app/user/migrations/0002_create_superuser.py b/app/user/migrations/0002_create_superuser.py deleted file mode 100644 index fb98154..0000000 --- a/app/user/migrations/0002_create_superuser.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.2 on 2025-05-24 14:30 -import typing -import uuid - -from core.const.system import SYSTEM_EMAIL, SYSTEM_ID, SYSTEM_USERNAME -from django.db import migrations -from django.db.backends.base.schema import BaseDatabaseSchemaEditor -from django.db.migrations.state import StateApps - -if typing.TYPE_CHECKING: - from user.models import UserExt as UserExtType - - -def create_superuser(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None: - UserExt: type["UserExtType"] = apps.get_model("user", "UserExt") - - if not UserExt.objects.filter(id=SYSTEM_ID).exists(): - UserExt.objects.create_superuser( - id=SYSTEM_ID, - username=SYSTEM_USERNAME, - email=SYSTEM_EMAIL, - password=uuid.uuid4().hex, - ) - - -class Migration(migrations.Migration): - dependencies = [("user", "0001_initial")] - operations = [migrations.RunPython(create_superuser, migrations.RunPython.noop)] diff --git a/app/user/migrations/0002_historicalorganization_historicaluserext_and_more.py b/app/user/migrations/0002_organization_historicalorganization_and_more.py similarity index 76% rename from app/user/migrations/0002_historicalorganization_historicaluserext_and_more.py rename to app/user/migrations/0002_organization_historicalorganization_and_more.py index 871ecca..6f72fcb 100644 --- a/app/user/migrations/0002_historicalorganization_historicaluserext_and_more.py +++ b/app/user/migrations/0002_organization_historicalorganization_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-06-03 04:16 +# Generated by Django 5.2 on 2025-06-05 11:04 import uuid @@ -16,6 +16,26 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name="Organization", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.CharField(blank=True, max_length=256, null=True)), + ("name_ko", models.CharField(blank=True, max_length=256, null=True)), + ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), + ], + ), migrations.CreateModel( name="HistoricalOrganization", fields=[ @@ -23,12 +43,12 @@ class Migration(migrations.Migration): "id", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False), ), - ("created_at", models.DateTimeField(blank=True, editable=False)), - ("updated_at", models.DateTimeField(blank=True, editable=False)), - ("deleted_at", models.DateTimeField(blank=True, null=True)), ("name", models.CharField(blank=True, max_length=256, null=True)), ("name_ko", models.CharField(blank=True, max_length=256, null=True)), ("name_en", models.CharField(blank=True, max_length=256, null=True)), + ("created_at", models.DateTimeField(blank=True, editable=False)), + ("updated_at", models.DateTimeField(blank=True, editable=False)), + ("deleted_at", models.DateTimeField(blank=True, null=True)), ("history_id", models.AutoField(primary_key=True, serialize=False)), ("history_date", models.DateTimeField(db_index=True)), ("history_change_reason", models.CharField(max_length=100, null=True)), @@ -39,28 +59,6 @@ class Migration(migrations.Migration): max_length=1, ), ), - ( - "created_by", - models.ForeignKey( - blank=True, - db_constraint=False, - null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "deleted_by", - models.ForeignKey( - blank=True, - db_constraint=False, - null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to=settings.AUTH_USER_MODEL, - ), - ), ( "history_user", models.ForeignKey( @@ -70,17 +68,6 @@ class Migration(migrations.Migration): to=settings.AUTH_USER_MODEL, ), ), - ( - "updated_by", - models.ForeignKey( - blank=True, - db_constraint=False, - null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to=settings.AUTH_USER_MODEL, - ), - ), ], options={ "verbose_name": "historical organization", @@ -145,8 +132,8 @@ class Migration(migrations.Migration): "is_active", models.BooleanField( default=True, - help_text="Designates whether this user should be treated as active. " - "Unselect this instead of deleting accounts.", + help_text="Designates whether this user should be treated as active." + + "Unselect this instead of deleting accounts.", verbose_name="active", ), ), @@ -182,56 +169,6 @@ class Migration(migrations.Migration): }, bases=(simple_history.models.HistoricalChanges, models.Model), ), - migrations.CreateModel( - name="Organization", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ("deleted_at", models.DateTimeField(blank=True, null=True)), - ("name", models.CharField(blank=True, max_length=256, null=True)), - ("name_ko", models.CharField(blank=True, max_length=256, null=True)), - ("name_en", models.CharField(blank=True, max_length=256, null=True)), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="%(class)s_created_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "deleted_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="%(class)s_deleted_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "updated_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="%(class)s_updated_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), migrations.CreateModel( name="OrganizationUserRelation", fields=[ diff --git a/app/user/models.py b/app/user/models.py index 7aaa0c1..b3a5bd0 100644 --- a/app/user/models.py +++ b/app/user/models.py @@ -1,3 +1,5 @@ +import uuid + from django.contrib.auth.models import AbstractUser from django.core.exceptions import ValidationError from django.db import models @@ -7,11 +9,12 @@ class UserExt(AbstractUser): pass -from core.models import BaseAbstractModel # noqa: E402 - - -class Organization(BaseAbstractModel): +class Organization(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=256, null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(null=True, blank=True) class OrganizationUserRelation(models.Model): diff --git a/pyproject.toml b/pyproject.toml index fef661e..8e5fc95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,9 @@ dependencies = [ "djangorestframework>=3.16.0", "drf-spectacular>=0.28.0", "drf-standardized-errors[openapi]>=0.14.1", + "faker>=37.3.0", "httpx>=0.28.1", + "model-bakery>=1.20.4", "packaging>=24.2", "psycopg[binary]>=3.2.6", "py-openapi-schema-to-json-schema>=0.0.3", diff --git a/uv.lock b/uv.lock index de00d26..d970124 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = "==3.12.*" [[package]] @@ -119,7 +120,9 @@ dependencies = [ { name = "djangorestframework" }, { name = "drf-spectacular" }, { name = "drf-standardized-errors", extra = ["openapi"] }, + { name = "faker" }, { name = "httpx" }, + { name = "model-bakery" }, { name = "packaging" }, { name = "psycopg", extra = ["binary"] }, { name = "py-openapi-schema-to-json-schema" }, @@ -168,7 +171,9 @@ requires-dist = [ { name = "djangorestframework", specifier = ">=3.16.0" }, { name = "drf-spectacular", specifier = ">=0.28.0" }, { name = "drf-standardized-errors", extras = ["openapi"], specifier = ">=0.14.1" }, + { name = "faker", specifier = ">=37.3.0" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "model-bakery", specifier = ">=1.20.4" }, { name = "packaging", specifier = ">=24.2" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.6" }, { name = "py-openapi-schema-to-json-schema", specifier = ">=0.0.3" }, @@ -628,6 +633,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] +[[package]] +name = "faker" +version = "37.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/4b/5354912eaff922876323f2d07e21408b10867f3295d5f917748341cb6f53/faker-37.3.0.tar.gz", hash = "sha256:77b79e7a2228d57175133af0bbcdd26dc623df81db390ee52f5104d46c010f2f", size = 1901376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/99/045b2dae19a01b9fbb23b9971bc04f4ef808e7f3a213d08c81067304a210/faker-37.3.0-py3-none-any.whl", hash = "sha256:48c94daa16a432f2d2bc803c7ff602509699fca228d13e97e379cd860a7e216e", size = 1942203 }, +] + [[package]] name = "filelock" version = "3.18.0" @@ -842,6 +859,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, ] +[[package]] +name = "model-bakery" +version = "1.20.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/dc/6d6260fa30c4d041958f71d6790b722e6f2588fbbca0534779b81a83b66d/model_bakery-1.20.4.tar.gz", hash = "sha256:a0c97e8a27329ecad78136f9d8f573ae392e4282326ea5c5f6daed1173013c4e", size = 21147 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/23/9b30a9c70e1a6df5100ae8c440b98fc1da6a4ce195327a7fba399569a2ec/model_bakery-1.20.4-py3-none-any.whl", hash = "sha256:30ad372604f326a1ba9f949bad9d0f85e6a510db4ef6a0b07be2d6bd7485008b", size = 24154 }, +] + [[package]] name = "mypy" version = "1.15.0"