Skip to content

Commit eeec32c

Browse files
committed
Feat: 등급 별 혜택 관리 API 구현
1 parent e9b08f7 commit eeec32c

File tree

6 files changed

+195
-47
lines changed

6 files changed

+195
-47
lines changed

pyconkr/settings-localtest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import os
2+
3+
from pyconkr.settings import * # noqa
4+
5+
DEBUG = True
6+
7+
ALLOWED_HOSTS += [
8+
"*",
9+
]
10+
11+
12+
# RDS
13+
DATABASES = {
14+
"default": {
15+
"ENGINE": "django.db.backends.sqlite3",
16+
"NAME": BASE_DIR / "local.sqlite3",
17+
}
18+
}
19+
20+
# django-storages: TODO fix to in memory?
21+
del MEDIA_ROOT # noqa
22+
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
23+
STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage"
24+
AWS_S3_ACCESS_KEY_ID = os.getenv("AWS_S3_ACCESS_KEY_ID")
25+
AWS_S3_SECRET_ACCESS_KEY = os.getenv("AWS_S3_SECRET_ACCESS_KEY")
26+
AWS_STORAGE_BUCKET_NAME = "pyconkr-api-v2-static-dev"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Generated by Django 4.1.5 on 2024-08-06 12:41
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("sponsor", "0006_sponsorbenefit"),
11+
]
12+
13+
operations = [
14+
migrations.RemoveField(
15+
model_name="sponsorbenefit",
16+
name="offer",
17+
),
18+
migrations.RemoveField(
19+
model_name="sponsorbenefit",
20+
name="sponsor_level",
21+
),
22+
migrations.CreateModel(
23+
name="BenefitByLevel",
24+
fields=[
25+
(
26+
"id",
27+
models.BigAutoField(
28+
auto_created=True,
29+
primary_key=True,
30+
serialize=False,
31+
verbose_name="ID",
32+
),
33+
),
34+
("offer", models.PositiveIntegerField(help_text="제공 하는 혜택 개수")),
35+
(
36+
"benefit",
37+
models.ForeignKey(
38+
on_delete=django.db.models.deletion.CASCADE,
39+
related_name="benefit_by_level",
40+
to="sponsor.sponsorbenefit",
41+
),
42+
),
43+
(
44+
"level",
45+
models.ForeignKey(
46+
on_delete=django.db.models.deletion.CASCADE,
47+
related_name="benefit_by_level",
48+
to="sponsor.sponsorlevel",
49+
),
50+
),
51+
],
52+
),
53+
migrations.AddConstraint(
54+
model_name="benefitbylevel",
55+
constraint=models.UniqueConstraint(
56+
fields=("benefit_id", "level_id"), name="IX_BENEFIT_BY_LEVEL_1"
57+
),
58+
),
59+
]

sponsor/models.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ def get_queryset(self):
1010
return super(SponsorLevelManager, self).get_queryset().all().order_by("order")
1111

1212

13+
class SponsorBenefit(models.Model):
14+
class Meta:
15+
verbose_name = "후원사 등급 별 혜택"
16+
verbose_name_plural = "후원사 등급 별 혜택 목록"
17+
18+
name = models.CharField(max_length=255, help_text="혜택 이름")
19+
desc = models.TextField(null=True, blank=True, help_text="기타")
20+
unit = models.CharField(max_length=10, help_text="혜택 단위")
21+
is_countable = models.BooleanField(
22+
default=True, help_text="제공 하는 혜택이 셀 수 있는지 여부"
23+
)
24+
25+
1326
class SponsorLevel(models.Model):
1427
class Meta:
1528
verbose_name = "후원사 등급"
@@ -30,6 +43,10 @@ class Meta:
3043
created_at = models.DateTimeField(auto_now_add=True)
3144
updated_at = models.DateTimeField(auto_now=True)
3245

46+
benefits = models.ManyToManyField(
47+
SponsorBenefit, through="BenefitByLevel", related_name="level"
48+
)
49+
3350
objects = SponsorLevelManager()
3451

3552
@property
@@ -54,22 +71,21 @@ def __str__(self):
5471
return self.name
5572

5673

57-
class SponsorBenefit(models.Model):
74+
class BenefitByLevel(models.Model):
5875
class Meta:
59-
verbose_name = "후원사 등급 별 혜택"
60-
verbose_name_plural = "후원사 등급 별 혜택 목록"
76+
constraints = [
77+
models.UniqueConstraint(
78+
fields=["benefit_id", "level_id"], name="IX_BENEFIT_BY_LEVEL_1"
79+
)
80+
]
6181

62-
name = models.CharField(max_length=255, help_text="혜택 이름")
63-
desc = models.TextField(null=True, blank=True, help_text="기타")
64-
offer = models.PositiveIntegerField(help_text="제공 하는 혜택 개수")
65-
unit = models.CharField(max_length=10, help_text="혜택 단위")
66-
is_countable = models.BooleanField(
67-
default=True, help_text="제공 하는 혜택이 셀 수 있는지 여부"
82+
benefit = models.ForeignKey(
83+
SponsorBenefit, on_delete=models.CASCADE, related_name="benefit_by_level"
6884
)
69-
70-
sponsor_level = models.ForeignKey(
71-
SponsorLevel, related_name="benefits", on_delete=models.CASCADE
85+
level = models.ForeignKey(
86+
SponsorLevel, on_delete=models.CASCADE, related_name="benefit_by_level"
7287
)
88+
offer = models.PositiveIntegerField(help_text="제공 하는 혜택 개수")
7389

7490

7591
def registration_file_upload_to(instance, filename):

sponsor/serializers.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
import rest_framework.serializers as serializers
22
from rest_framework.fields import SerializerMethodField
33

4-
from sponsor.models import Patron, Sponsor, SponsorLevel, SponsorBenefit
4+
from sponsor.models import Patron, Sponsor, SponsorLevel, SponsorBenefit, BenefitByLevel
5+
6+
7+
class BenefitByLevelSerializer(serializers.ModelSerializer):
8+
benefit_id = serializers.PrimaryKeyRelatedField(
9+
queryset=SponsorBenefit.objects.all(), source="benefit"
10+
)
11+
level_id = serializers.PrimaryKeyRelatedField(
12+
queryset=SponsorLevel.objects.get_queryset(), source="level", write_only=True
13+
)
14+
15+
class Meta:
16+
model = BenefitByLevel
17+
fields = ["benefit_id", "offer", "level_id"]
518

619

720
class SponsorBenefitSerializer(serializers.ModelSerializer):
821
class Meta:
922
model = SponsorBenefit
10-
fields = ["name", "desc", "offer", "unit", "is_countable"]
23+
fields = ["id", "name", "desc", "unit", "is_countable"]
1124
read_only_fields = ["id"]
1225

1326

27+
class SponsorBenefitWithOfferSerializer(SponsorBenefitSerializer):
28+
offer = serializers.SerializerMethodField()
29+
30+
class Meta(SponsorBenefitSerializer.Meta):
31+
fields = SponsorBenefitSerializer.Meta.fields + ["offer"]
32+
33+
def get_offer(self, obj):
34+
return obj.benefit_by_level.filter(benefit_id=obj.id).get().offer
35+
36+
1437
class SponsorSerializer(serializers.ModelSerializer):
1538
class Meta:
1639
model = Sponsor
@@ -36,24 +59,12 @@ class Meta:
3659

3760

3861
class SponsorLevelSerializer(serializers.ModelSerializer):
62+
benefits = SponsorBenefitWithOfferSerializer(many=True, read_only=True)
63+
3964
class Meta:
4065
model = SponsorLevel
4166
fields = [
42-
"name",
43-
"desc",
44-
"visible",
45-
"price",
46-
"limit",
47-
"order",
48-
]
49-
read_only_fields = ["id"]
50-
51-
52-
class SponsorLevelDetailSerializer(SponsorLevelSerializer):
53-
benefits = SponsorBenefitSerializer(many=True)
54-
55-
class Meta(SponsorLevelSerializer.Meta):
56-
fields = [
67+
"id",
5768
"name",
5869
"desc",
5970
"visible",
@@ -62,7 +73,7 @@ class Meta(SponsorLevelSerializer.Meta):
6273
"order",
6374
"benefits",
6475
]
65-
read_only_fields = ["id", "benefits"]
76+
read_only_fields = ["id"]
6677

6778

6879
class SponsorDetailSerializer(serializers.ModelSerializer):

sponsor/urls.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
from django.urls import path
22

3-
from sponsor.viewsets import PatronListViewSet, SponsorViewSet, SponsorLevelViewSet
3+
from sponsor.viewsets import (
4+
PatronListViewSet,
5+
SponsorViewSet,
6+
SponsorLevelViewSet,
7+
SponsorBenefitViewSet,
8+
)
49

510
urlpatterns = [
611
path("list/", SponsorViewSet.as_view({"get": "list"})),
@@ -22,4 +27,17 @@
2227
{"get": "retrieve", "delete": "destroy", "put": "update"}
2328
),
2429
),
30+
path(
31+
"levels/benefits/",
32+
SponsorLevelViewSet.as_view(
33+
{"post": "assign_benefits", "put": "create_or_update_benefits"}
34+
),
35+
),
36+
path("benefits/", SponsorBenefitViewSet.as_view({"get": "list", "post": "create"})),
37+
path(
38+
"benefits/<int:id>/",
39+
SponsorBenefitViewSet.as_view(
40+
{"get": "retrieve", "delete": "destroy", "put": "update"}
41+
),
42+
),
2543
]

sponsor/viewsets.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from typing import Type
22

33
from django.db.transaction import atomic
4+
45
from django.shortcuts import get_object_or_404
6+
from django.db.utils import IntegrityError
57
from rest_framework import status
68
from rest_framework.response import Response
9+
from rest_framework.decorators import action
710
from rest_framework.viewsets import ModelViewSet, ViewSet
811

9-
from sponsor.models import Patron, Sponsor, SponsorLevel
12+
from sponsor.models import Patron, Sponsor, SponsorLevel, SponsorBenefit, BenefitByLevel
1013
from sponsor.permissions import IsOwnerOrReadOnly, OwnerOnly
1114
from sponsor.serializers import (
1215
PatronListSerializer,
@@ -15,12 +18,22 @@
1518
SponsorRemainingAccountSerializer,
1619
SponsorSerializer,
1720
SponsorLevelSerializer,
18-
SponsorLevelDetailSerializer,
21+
SponsorBenefitSerializer,
22+
BenefitByLevelSerializer,
1923
)
2024
from sponsor.slack import send_new_sponsor_notification
2125
from sponsor.validators import SponsorValidater
2226

2327

28+
class SponsorBenefitViewSet(ModelViewSet):
29+
lookup_field = "id"
30+
http_method_names = ["get", "post", "put", "delete"]
31+
serializer_class = SponsorBenefitSerializer
32+
33+
def get_queryset(self):
34+
return SponsorBenefit.objects.all()
35+
36+
2437
class SponsorLevelViewSet(ModelViewSet):
2538
lookup_field = "id"
2639
http_method_names = ["get", "post", "put", "delete"]
@@ -30,26 +43,31 @@ def get_queryset(self):
3043

3144
def get_serializer_class(self):
3245
match self.action:
33-
case "list" | "create":
34-
return SponsorLevelSerializer
46+
case "create_or_update_benefits" | "assign_benefits":
47+
return BenefitByLevelSerializer
3548
case _:
36-
return SponsorLevelDetailSerializer
49+
return SponsorLevelSerializer
3750

38-
def list(self, request, *args, **kwagrs):
39-
queryset = self.get_queryset()
40-
serializer = self.get_serializer(queryset, many=True)
51+
@action(detail=False, methods=["POST"])
52+
def assign_benefits(self, request):
53+
serializer = self.get_serializer(data=request.data)
54+
serializer.is_valid(raise_exception=True)
55+
try:
56+
serializer.save()
57+
except IntegrityError:
58+
return Response("Already assigned", status=status.HTTP_400_BAD_REQUEST)
4159
return Response(serializer.data)
4260

43-
def create(self, request, *args, **kwagrs):
44-
serializer = self.get_serializer(data=request.data)
61+
@action(detail=True, methods=["PUT"])
62+
def create_or_update_benefits(self, request):
63+
level_id = request.data.get("level_id", None)
64+
benefit_id = request.data.get("benefit_id", None)
65+
benefit_by_level = get_object_or_404(
66+
BenefitByLevel, level_id=level_id, benefit_id=benefit_id
67+
)
68+
serializer = self.get_serializer(benefit_by_level, data=request.data)
4569
serializer.is_valid(raise_exception=True)
4670
serializer.save()
47-
48-
return Response(serializer.data, status=status.HTTP_201_CREATED)
49-
50-
def retrieve(self, request, id, *args, **kwargs):
51-
sponsor_level = self.get_object()
52-
serializer = self.get_serializer(sponsor_level)
5371
return Response(serializer.data)
5472

5573

0 commit comments

Comments
 (0)