Skip to content

Commit 2e3eed1

Browse files
authored
Merge pull request #139 from darjeeling/patron
add patron sponsor/patron model
2 parents 39699a9 + 73b070d commit 2e3eed1

File tree

6 files changed

+206
-7
lines changed

6 files changed

+206
-7
lines changed

sponsor/migrations/0005_patron.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Generated by Django 4.1.5 on 2023-07-26 15:59
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11+
("sponsor", "0004_alter_sponsor_options_alter_sponsorlevel_options"),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="Patron",
17+
fields=[
18+
(
19+
"id",
20+
models.BigAutoField(
21+
auto_created=True,
22+
primary_key=True,
23+
serialize=False,
24+
verbose_name="ID",
25+
),
26+
),
27+
("name", models.CharField(max_length=100)),
28+
(
29+
"total_contribution",
30+
models.IntegerField(default=0, help_text="개인후원한 금액입니다."),
31+
),
32+
(
33+
"contribution_datetime",
34+
models.DateTimeField(help_text="개인후원 결제한 일시입니다."),
35+
),
36+
(
37+
"contribution_message",
38+
models.TextField(
39+
help_text="후원메시지입니다. emoji 를 입력가능해야하고 html 태그가 들어갈 수 있습니다."
40+
),
41+
),
42+
("created_at", models.DateTimeField(auto_now_add=True)),
43+
("updated_at", models.DateTimeField(auto_now=True)),
44+
(
45+
"creator",
46+
models.ForeignKey(
47+
blank=True,
48+
help_text="개인후원을 등록한 유저",
49+
null=True,
50+
on_delete=django.db.models.deletion.CASCADE,
51+
related_name="patron_user",
52+
to=settings.AUTH_USER_MODEL,
53+
),
54+
),
55+
],
56+
options={
57+
"verbose_name": "개인후원자",
58+
"verbose_name_plural": "개인후원자 목록",
59+
"ordering": ["-total_contribution", "contribution_datetime"],
60+
},
61+
),
62+
]

sponsor/models.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,36 @@ class Meta:
158158

159159
def __str__(self):
160160
return f"{self.name}/{self.level}"
161+
162+
163+
164+
class Patron(models.Model):
165+
class Meta:
166+
ordering = ["-total_contribution", "contribution_datetime"]
167+
verbose_name = "개인후원자"
168+
verbose_name_plural = "개인후원자 목록"
169+
170+
name = models.CharField(max_length=100)
171+
creator = models.ForeignKey(
172+
User,
173+
null=True, # TODO: 추후 로그인 적용 후 입력
174+
blank=True, # TODO: 추후 로그인 적용 후 입력
175+
on_delete=models.CASCADE,
176+
help_text="개인후원을 등록한 유저",
177+
related_name="patron_user",
178+
)
179+
total_contribution = models.IntegerField(default=0, help_text="개인후원한 금액입니다.")
180+
contribution_datetime = models.DateTimeField(
181+
help_text="개인후원 결제한 일시입니다."
182+
)
183+
contribution_message = models.TextField(
184+
help_text="후원메시지입니다. emoji 를 입력가능해야하고 html 태그가 들어갈 수 있습니다."
185+
)
186+
# need html sanitizing before saving
187+
# but need to include emoji
188+
189+
created_at = models.DateTimeField(auto_now_add=True)
190+
updated_at = models.DateTimeField(auto_now=True)
191+
192+
def __str__(self):
193+
return f"{self.name}"

sponsor/serializers.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import rest_framework.serializers as serializers
22
from rest_framework.fields import SerializerMethodField
33

4-
from sponsor.models import Sponsor, SponsorLevel
4+
from sponsor.models import Patron, Sponsor, SponsorLevel
55

66

77
class SponsorSerializer(serializers.ModelSerializer):
@@ -74,3 +74,23 @@ def get_remaining(obj):
7474
@staticmethod
7575
def get_available(obj: SponsorLevel):
7676
return True if obj.current_remaining_number > 0 else False
77+
78+
79+
class PatronListSerializer(serializers.ModelSerializer):
80+
class Meta:
81+
model = Patron
82+
fields = [
83+
"name",
84+
"contribution_message",
85+
"sort_order",
86+
]
87+
88+
sort_order = serializers.SerializerMethodField()
89+
90+
def get_sort_order(self, obj: Patron):
91+
self._sort_order += 1
92+
return self._sort_order
93+
94+
def __init__(self, *args, **kwargs):
95+
super().__init__(*args, **kwargs)
96+
self._sort_order = 0

sponsor/tests.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import pytest
22
from django.contrib.auth import get_user_model
3+
from rest_framework.test import APIClient
34

4-
from sponsor.models import SponsorLevel
5+
from sponsor.models import Patron, SponsorLevel
56

67
pytestmark = pytest.mark.django_db
78

89
UserModel = get_user_model()
910

1011

12+
client = APIClient()
13+
14+
1115
@pytest.mark.django_db
1216
class TestSponsorLevelModel:
1317
pytestmark = pytest.mark.django_db
@@ -23,4 +27,70 @@ def test_sponsor_level_creation_success(self):
2327
assert SponsorLevel.objects.count() != 0
2428

2529

26-
# Create your tests here.
30+
@pytest.mark.django_db
31+
class TestPatron:
32+
pytestmark = pytest.mark.django_db
33+
34+
def test_patron_list_api(self):
35+
assert Patron.objects.count() == 0
36+
response = client.get("/sponsors/patron/list/", format="json")
37+
assert response.status_code == 200
38+
assert len(response.data) == 0
39+
40+
Patron.objects.create(
41+
name="Python Lover 1",
42+
contribution_message="I love Python",
43+
total_contribution=1000000,
44+
contribution_datetime="2023-07-27 00:00:00+09:00",
45+
)
46+
assert Patron.objects.count() == 1
47+
response = client.get("/sponsors/patron/list/", format="json")
48+
assert response.status_code == 200
49+
assert len(response.data) == 1
50+
assert response.data[0]["name"] == "Python Lover 1"
51+
# check sort order
52+
assert response.data[0]["sort_order"] == 1
53+
54+
# add second patron
55+
Patron.objects.create(
56+
name="Python Lover 2",
57+
contribution_message="I love Python too",
58+
total_contribution=1000001,
59+
contribution_datetime="2023-07-27 00:00:00+09:00",
60+
)
61+
assert Patron.objects.count() == 2
62+
response = client.get("/sponsors/patron/list/", format="json")
63+
assert response.status_code == 200
64+
assert len(response.data) == 2
65+
assert response.data[0]["name"] == "Python Lover 2"
66+
# check sort order
67+
assert response.data[0]["sort_order"] == 1
68+
assert response.data[1]["name"] == "Python Lover 1"
69+
assert response.data[1]["sort_order"] == 2
70+
71+
# add third patron
72+
# check contribution_datetime is earlier than Python Lover 2
73+
Patron.objects.create(
74+
name="Python Lover 3",
75+
contribution_message="I love Python most",
76+
total_contribution=1000001,
77+
# earlier contribution then Python Lover 2
78+
contribution_datetime="2023-07-26 00:00:00+09:00",
79+
)
80+
assert Patron.objects.count() == 3
81+
response = client.get("/sponsors/patron/list/", format="json")
82+
assert response.status_code == 200
83+
assert len(response.data) == 3
84+
assert response.data[0]["name"] == "Python Lover 3"
85+
# check sort order
86+
assert response.data[0]["sort_order"] == 1
87+
assert response.data[1]["name"] == "Python Lover 2"
88+
assert response.data[1]["sort_order"] == 2
89+
90+
@pytest.skip("TODO: implement")
91+
def test_patron_message_html_sanitizer(self):
92+
assert Patron.objects.count() == 0
93+
# check patron save will sanitize html field
94+
# allow only <a> tag
95+
# allow emoji
96+
pass

sponsor/urls.py

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

3-
from sponsor.viewsets import SponsorViewSet
3+
from sponsor.viewsets import PatronListViewSet, SponsorViewSet
44

55
urlpatterns = [
66
path("list/", SponsorViewSet.as_view({"get": "list"})),
77
path(
88
"list/<int:id>/",
99
SponsorViewSet.as_view({"get": "retrieve", "put": "update"}),
1010
),
11+
path(
12+
"patron/list/",
13+
PatronListViewSet.as_view({"get": "list"}),
14+
),
1115
]

sponsor/viewsets.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
from django.shortcuts import get_object_or_404
55
from rest_framework import status
66
from rest_framework.response import Response
7-
from rest_framework.viewsets import ModelViewSet
7+
from rest_framework.viewsets import ModelViewSet, ViewSet
88

9-
from sponsor.models import Sponsor, SponsorLevel
9+
from sponsor.models import Patron, Sponsor, SponsorLevel
1010
from sponsor.permissions import IsOwnerOrReadOnly, OwnerOnly
1111
from sponsor.serializers import (
12+
PatronListSerializer,
1213
SponsorDetailSerializer,
1314
SponsorListSerializer,
1415
SponsorRemainingAccountSerializer,
@@ -28,7 +29,9 @@ def get_queryset(self):
2829
return Sponsor.objects.all().order_by("paid_at")
2930

3031
def list(self, request, *args, **kwargs):
31-
queryset = Sponsor.objects.filter(paid_at__isnull=False).order_by("level", "paid_at")
32+
queryset = Sponsor.objects.filter(paid_at__isnull=False).order_by(
33+
"level", "paid_at"
34+
)
3235
serializer = SponsorListSerializer(queryset, many=True)
3336
return Response(serializer.data)
3437

@@ -109,3 +112,10 @@ def list(self, request, *args, **kwargs):
109112
serializer = self.get_serializer(queryset, many=True)
110113

111114
return Response(serializer.data)
115+
116+
117+
class PatronListViewSet(ViewSet):
118+
def list(self, request):
119+
queryset = Patron.objects.all()
120+
serializer = PatronListSerializer(queryset, many=True)
121+
return Response(serializer.data)

0 commit comments

Comments
 (0)