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 5da5696..322238f 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -157,6 +157,8 @@ "user", "file", "cms", + "event", + "event.presentation", "admin_api", # django-constance "constance", diff --git a/app/core/urls.py b/app/core/urls.py index b3aa710..dd2e0e9 100644 --- a/app/core/urls.py +++ b/app/core/urls.py @@ -24,8 +24,9 @@ # type: ignore[assignment] v1_apis: list[resolvers.URLPattern | resolvers.URLResolver] = [ - path("admin-api/", include("admin_api.urls")), path("cms/", include("cms.urls")), + path("admin-api/", include("admin_api.urls")), + path("event/presentations/", include("event.presentation.urls")), ] urlpatterns = [ 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..ca9b413 --- /dev/null +++ b/app/event/apps.py @@ -0,0 +1,15 @@ +import importlib + +from django.apps import AppConfig + + +class EventConfig(AppConfig): + 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..25d0d87 --- /dev/null +++ b/app/event/migrations/0001_initial.py @@ -0,0 +1,209 @@ +# Generated by Django 5.2 on 2025-06-05 11:04 + +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_organization_historicalorganization_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..c435948 --- /dev/null +++ b/app/event/presentation/apps.py @@ -0,0 +1,18 @@ +import importlib + +from django.apps import AppConfig + + +class PresentationConfig(AppConfig): + 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..aa8b8a1 --- /dev/null +++ b/app/event/presentation/migrations/0001_initial.py @@ -0,0 +1,668 @@ +# 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 +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", + core.fields.UUIDAutoField( + auto_created=True, + default=uuid.uuid4, + editable=False, + 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.AddField( + model_name="presentation", + name="presentation_categories", + field=models.ManyToManyField( + through="presentation.PresentationCategoryRelation", + to="presentation.presentationcategory", + ), + ), + 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..96ba766 --- /dev/null +++ b/app/event/presentation/models.py @@ -0,0 +1,46 @@ +from core.models import BaseAbstractModel, BaseAbstractModelQuerySet +from django.contrib.auth import get_user_model +from django.db import models +from event.models import Event + +User = get_user_model() + + +class PresentationQuerySet(BaseAbstractModelQuerySet): + def get_all_nested_data(self): + return ( + self.filter_active() + .select_related("presentation_type") + .prefetch_related("presentation_speakers", "presentation_categories") + ) + + +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") + presentation_categories = models.ManyToManyField(to="PresentationCategory", through="PresentationCategoryRelation") + + 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..3a04fb0 --- /dev/null +++ b/app/event/presentation/serializers.py @@ -0,0 +1,30 @@ +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 = PresentationCategorySerializer(many=True, read_only=True) + presentation_speakers = PresentationSpeakerSerializer(many=True, read_only=True) + + 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..f9ae2a9 --- /dev/null +++ b/app/event/presentation/views.py @@ -0,0 +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 + filterset_class = PresentationFilterSet 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_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_organization_historicalorganization_and_more.py b/app/user/migrations/0002_organization_historicalorganization_and_more.py new file mode 100644 index 0000000..6f72fcb --- /dev/null +++ b/app/user/migrations/0002_organization_historicalorganization_and_more.py @@ -0,0 +1,204 @@ +# Generated by Django 5.2 on 2025-06-05 11:04 + +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="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=[ + ( + "id", + models.UUIDField(db_index=True, default=uuid.uuid4, editable=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(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, + ), + ), + ( + "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 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="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..b3a5bd0 100644 --- a/app/user/models.py +++ b/app/user/models.py @@ -1,5 +1,29 @@ +import uuid + from django.contrib.auth.models import AbstractUser +from django.core.exceptions import ValidationError +from django.db import models class UserExt(AbstractUser): pass + + +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): + 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 c5b4ce1..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", @@ -60,6 +62,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"] 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"