From 30c09b736109f8e06085a18441353a4c5f233c0a Mon Sep 17 00:00:00 2001 From: jungmir Date: Sun, 28 Jul 2024 21:03:36 +0900 Subject: [PATCH 01/11] =?UTF-8?q?Feat:=20=ED=9B=84=EC=9B=90=20=EB=93=B1?= =?UTF-8?q?=EA=B8=89=20=EA=B4=80=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sponsor/serializers.py | 13 +++++++++++++ sponsor/urls.py | 12 +++++++++++- sponsor/viewsets.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/sponsor/serializers.py b/sponsor/serializers.py index f133dfe..9ef602c 100644 --- a/sponsor/serializers.py +++ b/sponsor/serializers.py @@ -28,6 +28,19 @@ class Meta: ] +class SponsorLevelSerializer(serializers.ModelSerializer): + class Meta: + model = SponsorLevel + fields = [ + "name", + "desc", + "visible", + "price", + "limit", + "order", + ] + + class SponsorDetailSerializer(serializers.ModelSerializer): creator_userid = serializers.SerializerMethodField() diff --git a/sponsor/urls.py b/sponsor/urls.py index 245dabb..8dc5631 100644 --- a/sponsor/urls.py +++ b/sponsor/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from sponsor.viewsets import PatronListViewSet, SponsorViewSet +from sponsor.viewsets import PatronListViewSet, SponsorViewSet, SponsorLevelViewSet urlpatterns = [ path("list/", SponsorViewSet.as_view({"get": "list"})), @@ -12,4 +12,14 @@ "patron/list/", PatronListViewSet.as_view({"get": "list"}), ), + path( + "levels", + SponsorLevelViewSet.as_view({"get": "list", "post": "create"}), + ), + path( + "levels//", + SponsorLevelViewSet.as_view( + {"get": "retrieve", "delete": "destroy", "put": "update"} + ), + ), ] diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index 95cd135..7c20339 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -14,11 +14,50 @@ SponsorListSerializer, SponsorRemainingAccountSerializer, SponsorSerializer, + SponsorLevelSerializer, ) from sponsor.slack import send_new_sponsor_notification from sponsor.validators import SponsorValidater +class SponsorLevelViewSet(ModelViewSet): + lookup_field = "id" + serializer_class = SponsorLevelSerializer + http_method_names = ["get", "post", "put", "delete"] + + def get_queryset(self): + return SponsorLevel.objects.get_queryset() + + def list(self, request, *args, **kwagrs): + queryset = self.get_queryset() + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + def create(self, request, *args, **kwagrs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def retrieve(self, request, id, *args, **kwargs): + sponsor_level = self.get_object() + serializer = self.get_serializer(sponsor_level) + return Response(serializer.data) + + # def update(self, request, id, *args, **kwargs): + # sponsor_level = self.get_object() + # serializer = self.get_serializer(sponsor_level, data=request.data, partial=True) + # serializer.is_valid(raise_exception=True) + # serializer.save() + # return Response(serializer.data) + + # def delete(self, request, id, *args, **kwagrs): + # sponsor_level = self.get_object() + # sponsor_level.delete() + # return Response(status=status.HTTP_204_NO_CONTENT) + + class SponsorViewSet(ModelViewSet): serializer_class = SponsorSerializer permission_classes = [IsOwnerOrReadOnly] # 본인 소유만 수정 가능 From 8e54abb6c08c7835c0b49d6bb975739ffeaeeb35 Mon Sep 17 00:00:00 2001 From: jungmir Date: Sun, 28 Jul 2024 21:13:16 +0900 Subject: [PATCH 02/11] =?UTF-8?q?Feat:=20=ED=9B=84=EC=9B=90=20=ED=98=9C?= =?UTF-8?q?=ED=83=9D=20=EB=AA=A8=EB=8D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sponsor/models.py | 52 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/sponsor/models.py b/sponsor/models.py index 6547ce3..b967258 100644 --- a/sponsor/models.py +++ b/sponsor/models.py @@ -15,9 +15,13 @@ class Meta: verbose_name = "후원사 등급" verbose_name_plural = "후원사 등급" - name = models.CharField(max_length=255, blank=True, default="", help_text="후원 등급명") + name = models.CharField( + max_length=255, blank=True, default="", help_text="후원 등급명" + ) desc = models.TextField( - null=True, blank=True, help_text="후원 혜택을 입력하면 될 거 같아요 :) 후원사가 등급을 정할 때 볼 문구입니다." + null=True, + blank=True, + help_text="후원 혜택을 입력하면 될 거 같아요 :) 후원사가 등급을 정할 때 볼 문구입니다.", ) visible = models.BooleanField(default=True) price = models.IntegerField(default=0) @@ -50,6 +54,24 @@ def __str__(self): return self.name +class SponsorBenefit(models.Model): + class Meta: + verbose_name = "후원사 등급 별 혜택" + verbose_name_plural = "후원사 등급 별 혜택 목록" + + name = models.CharField(max_length=255, help_text="혜택 이름") + desc = models.TextField(null=True, blank=True, help_text="기타") + offer = models.PositiveIntegerField(help_text="제공 하는 혜택 개수") + unit = models.CharField(max_length=10, help_text="혜택 단위") + is_countable = models.BooleanField( + default=True, help_text="제공 하는 혜택이 셀 수 있는지 여부" + ) + + sponsor_level = models.ForeignKey( + SponsorLevel, related_name="benefits", on_delete=models.CASCADE + ) + + def registration_file_upload_to(instance, filename): return f"sponsor/business_registration/{instance.id}/{filename}" @@ -77,7 +99,8 @@ class Meta: related_name="sponsor_creator", ) name = models.CharField( - max_length=255, help_text="후원사의 이름입니다. 서비스나 회사 이름이 될 수 있습니다." + max_length=255, + help_text="후원사의 이름입니다. 서비스나 회사 이름이 될 수 있습니다.", ) level = models.ForeignKey( SponsorLevel, @@ -87,13 +110,18 @@ class Meta: help_text="후원을 원하시는 등급을 선택해주십시오. 모두 판매된 등급은 선택할 수 없습니다.", ) desc = models.TextField( - null=True, blank=True, help_text="후원사 설명입니다. 이 설명은 국문 홈페이지에 게시됩니다." + null=True, + blank=True, + help_text="후원사 설명입니다. 이 설명은 국문 홈페이지에 게시됩니다.", ) eng_desc = models.TextField( - null=True, blank=True, help_text="후원사 영문 설명입니다. 이 설명은 영문 홈페이지에 게시됩니다." + null=True, + blank=True, + help_text="후원사 영문 설명입니다. 이 설명은 영문 홈페이지에 게시됩니다.", ) manager_name = models.CharField( - max_length=100, help_text="준비위원회와 후원과 관련된 논의를 진행할 담당자의 이름을 입력해주십시오." + max_length=100, + help_text="준비위원회와 후원과 관련된 논의를 진행할 담당자의 이름을 입력해주십시오.", ) manager_email = models.CharField( max_length=100, @@ -148,10 +176,13 @@ class Meta: help_text="사용자가 제출했는지 여부를 저장합니다. 요청이 제출되면 준비위원회에서 검토하고 받아들일지를 결정합니다.", ) accepted = models.BooleanField( - default=False, help_text="후원사 신청이 접수되었고, 입금 대기 상태인 경우 True로 설정됩니다." + default=False, + help_text="후원사 신청이 접수되었고, 입금 대기 상태인 경우 True로 설정됩니다.", ) paid_at = models.DateTimeField( - null=True, blank=True, help_text="후원금이 입금된 일시입니다. 아직 입금되지 않았을 경우 None이 들어갑니다." + null=True, + blank=True, + help_text="후원금이 입금된 일시입니다. 아직 입금되지 않았을 경우 None이 들어갑니다.", ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -160,7 +191,6 @@ def __str__(self): return f"{self.name}/{self.level}" - class Patron(models.Model): class Meta: ordering = ["-total_contribution", "contribution_datetime"] @@ -176,7 +206,9 @@ class Meta: help_text="개인후원을 등록한 유저", related_name="patron_user", ) - total_contribution = models.IntegerField(default=0, help_text="개인후원한 금액입니다.") + total_contribution = models.IntegerField( + default=0, help_text="개인후원한 금액입니다." + ) contribution_datetime = models.DateTimeField( help_text="개인후원 결제한 일시입니다." ) From 7c00a0c720e0883032dc7118ec3ba6803022441c Mon Sep 17 00:00:00 2001 From: jungmir Date: Sun, 28 Jul 2024 21:22:42 +0900 Subject: [PATCH 03/11] =?UTF-8?q?Feat:=20=ED=9B=84=EC=9B=90=20=ED=98=9C?= =?UTF-8?q?=ED=83=9D=20=EA=B4=80=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sponsor/migrations/0006_sponsorbenefit.py | 50 +++++++++++++++++++++++ sponsor/serializers.py | 26 +++++++++++- sponsor/viewsets.py | 21 ++++------ 3 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 sponsor/migrations/0006_sponsorbenefit.py diff --git a/sponsor/migrations/0006_sponsorbenefit.py b/sponsor/migrations/0006_sponsorbenefit.py new file mode 100644 index 0000000..e117e49 --- /dev/null +++ b/sponsor/migrations/0006_sponsorbenefit.py @@ -0,0 +1,50 @@ +# Generated by Django 4.1.5 on 2024-07-28 12:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("sponsor", "0005_patron"), + ] + + operations = [ + migrations.CreateModel( + name="SponsorBenefit", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(help_text="혜택 이름", max_length=255)), + ("desc", models.TextField(blank=True, help_text="기타", null=True)), + ("offer", models.PositiveIntegerField(help_text="제공 하는 혜택 개수")), + ("unit", models.CharField(help_text="혜택 단위", max_length=10)), + ( + "is_countable", + models.BooleanField( + default=True, help_text="제공 하는 혜택이 셀 수 있는지 여부" + ), + ), + ( + "sponsor_level", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="benefits", + to="sponsor.sponsorlevel", + ), + ), + ], + options={ + "verbose_name": "후원사 등급 별 혜택", + "verbose_name_plural": "후원사 등급 별 혜택 목록", + }, + ), + ] diff --git a/sponsor/serializers.py b/sponsor/serializers.py index 9ef602c..71319b0 100644 --- a/sponsor/serializers.py +++ b/sponsor/serializers.py @@ -1,7 +1,14 @@ import rest_framework.serializers as serializers from rest_framework.fields import SerializerMethodField -from sponsor.models import Patron, Sponsor, SponsorLevel +from sponsor.models import Patron, Sponsor, SponsorLevel, SponsorBenefit + + +class SponsorBenefitSerializer(serializers.ModelSerializer): + class Meta: + model = SponsorBenefit + fields = ["name", "desc", "offer", "unit", "is_countable"] + read_only_fields = ["id"] class SponsorSerializer(serializers.ModelSerializer): @@ -39,6 +46,23 @@ class Meta: "limit", "order", ] + read_only_fields = ["id"] + + +class SponsorLevelDetailSerializer(SponsorLevelSerializer): + benefits = SponsorBenefitSerializer(many=True) + + class Meta(SponsorLevelSerializer.Meta): + fields = [ + "name", + "desc", + "visible", + "price", + "limit", + "order", + "benefits", + ] + read_only_fields = ["id", "benefits"] class SponsorDetailSerializer(serializers.ModelSerializer): diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index 7c20339..a988ccc 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -15,6 +15,7 @@ SponsorRemainingAccountSerializer, SponsorSerializer, SponsorLevelSerializer, + SponsorLevelDetailSerializer, ) from sponsor.slack import send_new_sponsor_notification from sponsor.validators import SponsorValidater @@ -22,12 +23,18 @@ class SponsorLevelViewSet(ModelViewSet): lookup_field = "id" - serializer_class = SponsorLevelSerializer http_method_names = ["get", "post", "put", "delete"] def get_queryset(self): return SponsorLevel.objects.get_queryset() + def get_serializer(self, *args, **kwargs): + match self.action: + case "list" | "create": + return SponsorLevelSerializer(*args, **kwargs) + case _: + return SponsorLevelDetailSerializer(*args, **kwargs) + def list(self, request, *args, **kwagrs): queryset = self.get_queryset() serializer = self.get_serializer(queryset, many=True) @@ -45,18 +52,6 @@ def retrieve(self, request, id, *args, **kwargs): serializer = self.get_serializer(sponsor_level) return Response(serializer.data) - # def update(self, request, id, *args, **kwargs): - # sponsor_level = self.get_object() - # serializer = self.get_serializer(sponsor_level, data=request.data, partial=True) - # serializer.is_valid(raise_exception=True) - # serializer.save() - # return Response(serializer.data) - - # def delete(self, request, id, *args, **kwagrs): - # sponsor_level = self.get_object() - # sponsor_level.delete() - # return Response(status=status.HTTP_204_NO_CONTENT) - class SponsorViewSet(ModelViewSet): serializer_class = SponsorSerializer From fa3ac9191bb314a273d366374cbcba3f293effe6 Mon Sep 17 00:00:00 2001 From: jungmir Date: Sun, 4 Aug 2024 00:27:07 +0900 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20get=5Fserializer=EB=A5=BC=20g?= =?UTF-8?q?et=5Fserializer=5Fclass=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyconkr/settings-localtest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyconkr/settings-localtest.py b/pyconkr/settings-localtest.py index 1b7ccd6..04df28f 100644 --- a/pyconkr/settings-localtest.py +++ b/pyconkr/settings-localtest.py @@ -10,7 +10,12 @@ # RDS -DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "local.sqlite3", + } +} # django-storages: TODO fix to in memory? del MEDIA_ROOT From df609754ff3a0a308090502bc067f52335d7cac6 Mon Sep 17 00:00:00 2001 From: jungmir Date: Sun, 4 Aug 2024 00:30:37 +0900 Subject: [PATCH 05/11] =?UTF-8?q?remove:=20=EB=A1=9C=EC=BB=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyconkr/settings-localtest.py | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 pyconkr/settings-localtest.py diff --git a/pyconkr/settings-localtest.py b/pyconkr/settings-localtest.py deleted file mode 100644 index 04df28f..0000000 --- a/pyconkr/settings-localtest.py +++ /dev/null @@ -1,26 +0,0 @@ -import os - -from pyconkr.settings import * - -DEBUG = True - -ALLOWED_HOSTS += [ - "*", -] - - -# RDS -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "local.sqlite3", - } -} - -# django-storages: TODO fix to in memory? -del MEDIA_ROOT -DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" -STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage" -AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID") -AWS_S3_SECRET_ACCESS_KEY = os.getenv("AWS_S3_SECRET_ACCESS_KEY") -AWS_STORAGE_BUCKET_NAME = "pyconkr-api-v2-static-dev" From e9b08f72a420230d2cb04e034572b8dd3d578b7c Mon Sep 17 00:00:00 2001 From: jungmir Date: Sun, 4 Aug 2024 00:30:46 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refactor:=20get=5Fserializer=EB=A5=BC=20g?= =?UTF-8?q?et=5Fserializer=5Fclass=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sponsor/viewsets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index a988ccc..1aa9206 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -28,12 +28,12 @@ class SponsorLevelViewSet(ModelViewSet): def get_queryset(self): return SponsorLevel.objects.get_queryset() - def get_serializer(self, *args, **kwargs): + def get_serializer_class(self): match self.action: case "list" | "create": - return SponsorLevelSerializer(*args, **kwargs) + return SponsorLevelSerializer case _: - return SponsorLevelDetailSerializer(*args, **kwargs) + return SponsorLevelDetailSerializer def list(self, request, *args, **kwagrs): queryset = self.get_queryset() From eeec32cc0ca88c1eddc1216862c687f165714583 Mon Sep 17 00:00:00 2001 From: jungmir Date: Tue, 6 Aug 2024 21:57:41 +0900 Subject: [PATCH 07/11] =?UTF-8?q?Feat:=20=EB=93=B1=EA=B8=89=20=EB=B3=84=20?= =?UTF-8?q?=ED=98=9C=ED=83=9D=20=EA=B4=80=EB=A6=AC=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyconkr/settings-localtest.py | 26 ++++++++ ...07_remove_sponsorbenefit_offer_and_more.py | 59 +++++++++++++++++++ sponsor/models.py | 40 +++++++++---- sponsor/serializers.py | 47 +++++++++------ sponsor/urls.py | 20 ++++++- sponsor/viewsets.py | 50 +++++++++++----- 6 files changed, 195 insertions(+), 47 deletions(-) create mode 100644 pyconkr/settings-localtest.py create mode 100644 sponsor/migrations/0007_remove_sponsorbenefit_offer_and_more.py diff --git a/pyconkr/settings-localtest.py b/pyconkr/settings-localtest.py new file mode 100644 index 0000000..b9a0e74 --- /dev/null +++ b/pyconkr/settings-localtest.py @@ -0,0 +1,26 @@ +import os + +from pyconkr.settings import * # noqa + +DEBUG = True + +ALLOWED_HOSTS += [ + "*", +] + + +# RDS +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "local.sqlite3", + } +} + +# django-storages: TODO fix to in memory? +del MEDIA_ROOT # noqa +DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" +STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage" +AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID") +AWS_S3_SECRET_ACCESS_KEY = os.getenv("AWS_S3_SECRET_ACCESS_KEY") +AWS_STORAGE_BUCKET_NAME = "pyconkr-api-v2-static-dev" diff --git a/sponsor/migrations/0007_remove_sponsorbenefit_offer_and_more.py b/sponsor/migrations/0007_remove_sponsorbenefit_offer_and_more.py new file mode 100644 index 0000000..90dc57d --- /dev/null +++ b/sponsor/migrations/0007_remove_sponsorbenefit_offer_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 4.1.5 on 2024-08-06 12:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("sponsor", "0006_sponsorbenefit"), + ] + + operations = [ + migrations.RemoveField( + model_name="sponsorbenefit", + name="offer", + ), + migrations.RemoveField( + model_name="sponsorbenefit", + name="sponsor_level", + ), + migrations.CreateModel( + name="BenefitByLevel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("offer", models.PositiveIntegerField(help_text="제공 하는 혜택 개수")), + ( + "benefit", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="benefit_by_level", + to="sponsor.sponsorbenefit", + ), + ), + ( + "level", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="benefit_by_level", + to="sponsor.sponsorlevel", + ), + ), + ], + ), + migrations.AddConstraint( + model_name="benefitbylevel", + constraint=models.UniqueConstraint( + fields=("benefit_id", "level_id"), name="IX_BENEFIT_BY_LEVEL_1" + ), + ), + ] diff --git a/sponsor/models.py b/sponsor/models.py index b967258..8380451 100644 --- a/sponsor/models.py +++ b/sponsor/models.py @@ -10,6 +10,19 @@ def get_queryset(self): return super(SponsorLevelManager, self).get_queryset().all().order_by("order") +class SponsorBenefit(models.Model): + class Meta: + verbose_name = "후원사 등급 별 혜택" + verbose_name_plural = "후원사 등급 별 혜택 목록" + + name = models.CharField(max_length=255, help_text="혜택 이름") + desc = models.TextField(null=True, blank=True, help_text="기타") + unit = models.CharField(max_length=10, help_text="혜택 단위") + is_countable = models.BooleanField( + default=True, help_text="제공 하는 혜택이 셀 수 있는지 여부" + ) + + class SponsorLevel(models.Model): class Meta: verbose_name = "후원사 등급" @@ -30,6 +43,10 @@ class Meta: created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + benefits = models.ManyToManyField( + SponsorBenefit, through="BenefitByLevel", related_name="level" + ) + objects = SponsorLevelManager() @property @@ -54,22 +71,21 @@ def __str__(self): return self.name -class SponsorBenefit(models.Model): +class BenefitByLevel(models.Model): class Meta: - verbose_name = "후원사 등급 별 혜택" - verbose_name_plural = "후원사 등급 별 혜택 목록" + constraints = [ + models.UniqueConstraint( + fields=["benefit_id", "level_id"], name="IX_BENEFIT_BY_LEVEL_1" + ) + ] - name = models.CharField(max_length=255, help_text="혜택 이름") - desc = models.TextField(null=True, blank=True, help_text="기타") - offer = models.PositiveIntegerField(help_text="제공 하는 혜택 개수") - unit = models.CharField(max_length=10, help_text="혜택 단위") - is_countable = models.BooleanField( - default=True, help_text="제공 하는 혜택이 셀 수 있는지 여부" + benefit = models.ForeignKey( + SponsorBenefit, on_delete=models.CASCADE, related_name="benefit_by_level" ) - - sponsor_level = models.ForeignKey( - SponsorLevel, related_name="benefits", on_delete=models.CASCADE + level = models.ForeignKey( + SponsorLevel, on_delete=models.CASCADE, related_name="benefit_by_level" ) + offer = models.PositiveIntegerField(help_text="제공 하는 혜택 개수") def registration_file_upload_to(instance, filename): diff --git a/sponsor/serializers.py b/sponsor/serializers.py index 71319b0..e3ad23f 100644 --- a/sponsor/serializers.py +++ b/sponsor/serializers.py @@ -1,16 +1,39 @@ import rest_framework.serializers as serializers from rest_framework.fields import SerializerMethodField -from sponsor.models import Patron, Sponsor, SponsorLevel, SponsorBenefit +from sponsor.models import Patron, Sponsor, SponsorLevel, SponsorBenefit, BenefitByLevel + + +class BenefitByLevelSerializer(serializers.ModelSerializer): + benefit_id = serializers.PrimaryKeyRelatedField( + queryset=SponsorBenefit.objects.all(), source="benefit" + ) + level_id = serializers.PrimaryKeyRelatedField( + queryset=SponsorLevel.objects.get_queryset(), source="level", write_only=True + ) + + class Meta: + model = BenefitByLevel + fields = ["benefit_id", "offer", "level_id"] class SponsorBenefitSerializer(serializers.ModelSerializer): class Meta: model = SponsorBenefit - fields = ["name", "desc", "offer", "unit", "is_countable"] + fields = ["id", "name", "desc", "unit", "is_countable"] read_only_fields = ["id"] +class SponsorBenefitWithOfferSerializer(SponsorBenefitSerializer): + offer = serializers.SerializerMethodField() + + class Meta(SponsorBenefitSerializer.Meta): + fields = SponsorBenefitSerializer.Meta.fields + ["offer"] + + def get_offer(self, obj): + return obj.benefit_by_level.filter(benefit_id=obj.id).get().offer + + class SponsorSerializer(serializers.ModelSerializer): class Meta: model = Sponsor @@ -36,24 +59,12 @@ class Meta: class SponsorLevelSerializer(serializers.ModelSerializer): + benefits = SponsorBenefitWithOfferSerializer(many=True, read_only=True) + class Meta: model = SponsorLevel fields = [ - "name", - "desc", - "visible", - "price", - "limit", - "order", - ] - read_only_fields = ["id"] - - -class SponsorLevelDetailSerializer(SponsorLevelSerializer): - benefits = SponsorBenefitSerializer(many=True) - - class Meta(SponsorLevelSerializer.Meta): - fields = [ + "id", "name", "desc", "visible", @@ -62,7 +73,7 @@ class Meta(SponsorLevelSerializer.Meta): "order", "benefits", ] - read_only_fields = ["id", "benefits"] + read_only_fields = ["id"] class SponsorDetailSerializer(serializers.ModelSerializer): diff --git a/sponsor/urls.py b/sponsor/urls.py index 8dc5631..b53e3f7 100644 --- a/sponsor/urls.py +++ b/sponsor/urls.py @@ -1,6 +1,11 @@ from django.urls import path -from sponsor.viewsets import PatronListViewSet, SponsorViewSet, SponsorLevelViewSet +from sponsor.viewsets import ( + PatronListViewSet, + SponsorViewSet, + SponsorLevelViewSet, + SponsorBenefitViewSet, +) urlpatterns = [ path("list/", SponsorViewSet.as_view({"get": "list"})), @@ -22,4 +27,17 @@ {"get": "retrieve", "delete": "destroy", "put": "update"} ), ), + path( + "levels/benefits/", + SponsorLevelViewSet.as_view( + {"post": "assign_benefits", "put": "create_or_update_benefits"} + ), + ), + path("benefits/", SponsorBenefitViewSet.as_view({"get": "list", "post": "create"})), + path( + "benefits//", + SponsorBenefitViewSet.as_view( + {"get": "retrieve", "delete": "destroy", "put": "update"} + ), + ), ] diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index 1aa9206..ab8fc0b 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -1,12 +1,15 @@ from typing import Type from django.db.transaction import atomic + from django.shortcuts import get_object_or_404 +from django.db.utils import IntegrityError from rest_framework import status from rest_framework.response import Response +from rest_framework.decorators import action from rest_framework.viewsets import ModelViewSet, ViewSet -from sponsor.models import Patron, Sponsor, SponsorLevel +from sponsor.models import Patron, Sponsor, SponsorLevel, SponsorBenefit, BenefitByLevel from sponsor.permissions import IsOwnerOrReadOnly, OwnerOnly from sponsor.serializers import ( PatronListSerializer, @@ -15,12 +18,22 @@ SponsorRemainingAccountSerializer, SponsorSerializer, SponsorLevelSerializer, - SponsorLevelDetailSerializer, + SponsorBenefitSerializer, + BenefitByLevelSerializer, ) from sponsor.slack import send_new_sponsor_notification from sponsor.validators import SponsorValidater +class SponsorBenefitViewSet(ModelViewSet): + lookup_field = "id" + http_method_names = ["get", "post", "put", "delete"] + serializer_class = SponsorBenefitSerializer + + def get_queryset(self): + return SponsorBenefit.objects.all() + + class SponsorLevelViewSet(ModelViewSet): lookup_field = "id" http_method_names = ["get", "post", "put", "delete"] @@ -30,26 +43,31 @@ def get_queryset(self): def get_serializer_class(self): match self.action: - case "list" | "create": - return SponsorLevelSerializer + case "create_or_update_benefits" | "assign_benefits": + return BenefitByLevelSerializer case _: - return SponsorLevelDetailSerializer + return SponsorLevelSerializer - def list(self, request, *args, **kwagrs): - queryset = self.get_queryset() - serializer = self.get_serializer(queryset, many=True) + @action(detail=False, methods=["POST"]) + def assign_benefits(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + try: + serializer.save() + except IntegrityError: + return Response("Already assigned", status=status.HTTP_400_BAD_REQUEST) return Response(serializer.data) - def create(self, request, *args, **kwagrs): - serializer = self.get_serializer(data=request.data) + @action(detail=True, methods=["PUT"]) + def create_or_update_benefits(self, request): + level_id = request.data.get("level_id", None) + benefit_id = request.data.get("benefit_id", None) + benefit_by_level = get_object_or_404( + BenefitByLevel, level_id=level_id, benefit_id=benefit_id + ) + serializer = self.get_serializer(benefit_by_level, data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def retrieve(self, request, id, *args, **kwargs): - sponsor_level = self.get_object() - serializer = self.get_serializer(sponsor_level) return Response(serializer.data) From 2c34c870e59ca8aba313ec135989a241e8f31c94 Mon Sep 17 00:00:00 2001 From: jungmir Date: Tue, 6 Aug 2024 22:06:47 +0900 Subject: [PATCH 08/11] =?UTF-8?q?Feat:=20=EC=97=B0=EB=8F=84=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sponsor/migrations/0008_merge_20240806_2206.py | 13 +++++++++++++ sponsor/viewsets.py | 11 ++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 sponsor/migrations/0008_merge_20240806_2206.py diff --git a/sponsor/migrations/0008_merge_20240806_2206.py b/sponsor/migrations/0008_merge_20240806_2206.py new file mode 100644 index 0000000..e9c699a --- /dev/null +++ b/sponsor/migrations/0008_merge_20240806_2206.py @@ -0,0 +1,13 @@ +# Generated by Django 4.1.5 on 2024-08-06 13:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("sponsor", "0006_sponsorlevel_year"), + ("sponsor", "0007_remove_sponsorbenefit_offer_and_more"), + ] + + operations = [] diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index 3a03f15..053f10e 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -25,14 +25,13 @@ from sponsor.validators import SponsorValidater - class SponsorBenefitViewSet(ModelViewSet): lookup_field = "id" http_method_names = ["get", "post", "put", "delete"] serializer_class = SponsorBenefitSerializer def get_queryset(self): - return SponsorBenefit.objects.all() + return SponsorBenefit.objects.filter(level__year=self.request.version).all() class SponsorLevelViewSet(ModelViewSet): @@ -71,6 +70,7 @@ def create_or_update_benefits(self, request): serializer.save() return Response(serializer.data) + class SponsorViewSet( mixins.CreateModelMixin, mixins.RetrieveModelMixin, @@ -84,7 +84,12 @@ class SponsorViewSet( validator = SponsorValidater() def get_queryset(self): - return super().get_queryset().filter(paid_at__isnull=False, level__year=self.request.version).order_by("level__order", "paid_at") + return ( + super() + .get_queryset() + .filter(paid_at__isnull=False, level__year=self.request.version) + .order_by("level__order", "paid_at") + ) def get_serializer_class(self): if self.action == "list": From 2f30a55c8a7b5864616905e593c03f402156ed90 Mon Sep 17 00:00:00 2001 From: jungmir Date: Tue, 6 Aug 2024 23:20:10 +0900 Subject: [PATCH 09/11] =?UTF-8?q?Fix:=20relation=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20query=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sponsor/serializers.py | 30 +++++++++++++++++++++++++----- sponsor/viewsets.py | 4 ++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/sponsor/serializers.py b/sponsor/serializers.py index e3ad23f..e942c9f 100644 --- a/sponsor/serializers.py +++ b/sponsor/serializers.py @@ -25,13 +25,31 @@ class Meta: class SponsorBenefitWithOfferSerializer(SponsorBenefitSerializer): - offer = serializers.SerializerMethodField() + id = serializers.SlugRelatedField(slug_field="id", source="benefit", read_only=True) + name = serializers.SlugRelatedField( + slug_field="name", source="benefit", read_only=True + ) + desc = serializers.SlugRelatedField( + slug_field="desc", source="benefit", read_only=True + ) + unit = serializers.SlugRelatedField( + slug_field="unit", source="benefit", read_only=True + ) + is_countable = serializers.SlugRelatedField( + slug_field="is_countable", source="benefit", read_only=True + ) - class Meta(SponsorBenefitSerializer.Meta): - fields = SponsorBenefitSerializer.Meta.fields + ["offer"] + class Meta: + model = BenefitByLevel + fields = ["id", "name", "desc", "unit", "is_countable", "offer"] + + def get_benefit_id(self, obj): + breakpoint() + return def get_offer(self, obj): - return obj.benefit_by_level.filter(benefit_id=obj.id).get().offer + breakpoint() + return obj.benefit_by_level.filter(benefit_id=obj.id) class SponsorSerializer(serializers.ModelSerializer): @@ -59,7 +77,9 @@ class Meta: class SponsorLevelSerializer(serializers.ModelSerializer): - benefits = SponsorBenefitWithOfferSerializer(many=True, read_only=True) + benefits = SponsorBenefitWithOfferSerializer( + many=True, read_only=True, source="benefit_by_level" + ) class Meta: model = SponsorLevel diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index 053f10e..8d78e9e 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -49,7 +49,7 @@ def get_serializer_class(self): return SponsorLevelSerializer @action(detail=False, methods=["POST"]) - def assign_benefits(self, request): + def assign_benefits(self, request, version): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) try: @@ -59,7 +59,7 @@ def assign_benefits(self, request): return Response(serializer.data) @action(detail=True, methods=["PUT"]) - def create_or_update_benefits(self, request): + def create_or_update_benefits(self, request, version): level_id = request.data.get("level_id", None) benefit_id = request.data.get("benefit_id", None) benefit_by_level = get_object_or_404( From 0f4719a8d8803f1f10c82d974a2af5fb270e3509 Mon Sep 17 00:00:00 2001 From: jungmir Date: Tue, 6 Aug 2024 23:29:28 +0900 Subject: [PATCH 10/11] =?UTF-8?q?Fix:=20version=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20=EC=A4=91=EB=B3=B5=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EB=90=98=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...onsorbenefit_year_sponsorlevel_benefits.py | 27 +++++++++++++++++++ sponsor/models.py | 1 + sponsor/serializers.py | 3 ++- sponsor/viewsets.py | 2 +- 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 sponsor/migrations/0009_sponsorbenefit_year_sponsorlevel_benefits.py diff --git a/sponsor/migrations/0009_sponsorbenefit_year_sponsorlevel_benefits.py b/sponsor/migrations/0009_sponsorbenefit_year_sponsorlevel_benefits.py new file mode 100644 index 0000000..9104a84 --- /dev/null +++ b/sponsor/migrations/0009_sponsorbenefit_year_sponsorlevel_benefits.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.5 on 2024-08-06 14:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sponsor", "0008_merge_20240806_2206"), + ] + + operations = [ + migrations.AddField( + model_name="sponsorbenefit", + name="year", + field=models.IntegerField(default=2023), + ), + migrations.AddField( + model_name="sponsorlevel", + name="benefits", + field=models.ManyToManyField( + related_name="level", + through="sponsor.BenefitByLevel", + to="sponsor.sponsorbenefit", + ), + ), + ] diff --git a/sponsor/models.py b/sponsor/models.py index 45daac0..4328acf 100644 --- a/sponsor/models.py +++ b/sponsor/models.py @@ -18,6 +18,7 @@ class Meta: name = models.CharField(max_length=255, help_text="혜택 이름") desc = models.TextField(null=True, blank=True, help_text="기타") unit = models.CharField(max_length=10, help_text="혜택 단위") + year = models.IntegerField(default=2023) is_countable = models.BooleanField( default=True, help_text="제공 하는 혜택이 셀 수 있는지 여부" ) diff --git a/sponsor/serializers.py b/sponsor/serializers.py index e942c9f..0daaf16 100644 --- a/sponsor/serializers.py +++ b/sponsor/serializers.py @@ -20,8 +20,9 @@ class Meta: class SponsorBenefitSerializer(serializers.ModelSerializer): class Meta: model = SponsorBenefit - fields = ["id", "name", "desc", "unit", "is_countable"] + fields = ["id", "year", "name", "desc", "unit", "is_countable"] read_only_fields = ["id"] + extra_kwargs = {"year": {"write_only": True}} class SponsorBenefitWithOfferSerializer(SponsorBenefitSerializer): diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index 8d78e9e..9adf7f3 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -31,7 +31,7 @@ class SponsorBenefitViewSet(ModelViewSet): serializer_class = SponsorBenefitSerializer def get_queryset(self): - return SponsorBenefit.objects.filter(level__year=self.request.version).all() + return SponsorBenefit.objects.filter(year=self.request.version).all() class SponsorLevelViewSet(ModelViewSet): From 45347c867a060e8237d4d43ee141762ae96c5c17 Mon Sep 17 00:00:00 2001 From: jungmir Date: Wed, 7 Aug 2024 00:10:03 +0900 Subject: [PATCH 11/11] =?UTF-8?q?Fix:=20=ED=9B=84=EC=9B=90=EC=82=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=93=B1?= =?UTF-8?q?=EA=B8=89=20=EB=B3=84=EB=A1=9C=20=EC=A1=B0=ED=9A=8C=20=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyconkr/settings.py | 14 +++++++++----- sponsor/serializers.py | 8 ++++++++ sponsor/urls.py | 2 +- sponsor/viewsets.py | 7 +++---- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/pyconkr/settings.py b/pyconkr/settings.py index 42f7222..5baf2ca 100644 --- a/pyconkr/settings.py +++ b/pyconkr/settings.py @@ -9,6 +9,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.1/ref/settings/ """ + import os import pathlib @@ -81,9 +82,10 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "templates" , - BASE_DIR / "accounts/templates", - ], + "DIRS": [ + BASE_DIR / "templates", + BASE_DIR / "accounts/templates", + ], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -139,7 +141,9 @@ # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" + }, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, @@ -219,7 +223,7 @@ "https://2023.pycon.kr", "https://pycon-dev2023.pycon.kr", "https://pycon-prod2023.pycon.kr", - "https://ticket-2023.pycon.kr", # PG 심사 대비 임시 도메인 + "https://ticket-2023.pycon.kr", # PG 심사 대비 임시 도메인 "https://127.0.0.1:3000", "https://localhost:3000", "http://2023.pycon.kr", diff --git a/sponsor/serializers.py b/sponsor/serializers.py index 0daaf16..9e291ad 100644 --- a/sponsor/serializers.py +++ b/sponsor/serializers.py @@ -97,6 +97,14 @@ class Meta: read_only_fields = ["id"] +class SponsorWithLevelSerializer(serializers.ModelSerializer): + sponsor = SponsorSerializer(read_only=True, many=True, source="sponsor_set") + + class Meta: + model = SponsorLevel + fields = ["name", "order", "sponsor"] + + class SponsorDetailSerializer(serializers.ModelSerializer): creator_userid = serializers.SerializerMethodField() diff --git a/sponsor/urls.py b/sponsor/urls.py index b53e3f7..fc24249 100644 --- a/sponsor/urls.py +++ b/sponsor/urls.py @@ -8,7 +8,7 @@ ) urlpatterns = [ - path("list/", SponsorViewSet.as_view({"get": "list"})), + path("list/", SponsorViewSet.as_view({"get": "list", "post": "create"})), path( "list//", SponsorViewSet.as_view({"get": "retrieve", "put": "update"}), diff --git a/sponsor/viewsets.py b/sponsor/viewsets.py index 9adf7f3..ffc7f31 100644 --- a/sponsor/viewsets.py +++ b/sponsor/viewsets.py @@ -14,7 +14,7 @@ from sponsor.serializers import ( PatronListSerializer, SponsorDetailSerializer, - SponsorListSerializer, + SponsorWithLevelSerializer, SponsorRemainingAccountSerializer, SponsorSerializer, SponsorLevelSerializer, @@ -79,7 +79,6 @@ class SponsorViewSet( viewsets.GenericViewSet, ): queryset = Sponsor.objects.all() - serializer_class = SponsorSerializer permission_classes = [IsOwnerOrReadOnly] # 본인 소유만 수정 가능 validator = SponsorValidater() @@ -93,7 +92,7 @@ def get_queryset(self): def get_serializer_class(self): if self.action == "list": - return SponsorListSerializer + return SponsorWithLevelSerializer return SponsorSerializer @atomic @@ -102,7 +101,7 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) self.validator.assert_create(serializer.validated_data) - new_sponsor = serializer.save() + serializer.save() # slack 알림을 실패하더라도 transaction 전체를 롤백하지는 않아야 함 # TODO 람다 외부 인터넷 접근 확인 후 활성화