Skip to content

Commit e660b36

Browse files
authored
Merge pull request #35 from pythonkr/feat/event-filter-public-api
feat: 공개 API에 이벤트별 필터링 추가 및 최신 이벤트 기본값 적용
2 parents 40d760d + fabe927 commit e660b36

File tree

8 files changed

+172
-37
lines changed

8 files changed

+172
-37
lines changed

app/event/filters.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from django.db.models import Q
2+
from django_filters import rest_framework as filters
3+
from django_filters.constants import EMPTY_VALUES
4+
from event.models import Event
5+
6+
7+
class EventFilterMixin(filters.FilterSet):
8+
event = filters.CharFilter(method="filter_by_event_name")
9+
event_field_prefix = "event"
10+
11+
def filter_by_event_name(self, queryset, name, value):
12+
if value in EMPTY_VALUES:
13+
return queryset
14+
15+
prefix = self.event_field_prefix
16+
return queryset.filter(Q(**{f"{prefix}__name_ko": value}) | Q(**{f"{prefix}__name_en": value}))
17+
18+
def filter_queryset(self, queryset):
19+
queryset = super().filter_queryset(queryset)
20+
21+
if self.data.get("event") in EMPTY_VALUES:
22+
latest = Event.objects.filter_active().first()
23+
if latest:
24+
queryset = queryset.filter(**{self.event_field_prefix: latest})
25+
26+
return queryset

app/event/presentation/filters.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from core.models import BaseAbstractModelQuerySet
2+
from django.db.models import Q
3+
from django_filters import rest_framework as filters
4+
from django_filters.constants import EMPTY_VALUES
5+
from event.filters import EventFilterMixin
6+
7+
8+
class PresentationFilterSet(EventFilterMixin):
9+
event_field_prefix = "type__event"
10+
types = filters.BaseCSVFilter(method="filter_by_type_names")
11+
12+
def filter_by_type_names(self, queryset: BaseAbstractModelQuerySet, name: str, values: list[str]) -> Q:
13+
if values in EMPTY_VALUES:
14+
return queryset
15+
16+
return queryset.filter(Q(type__name_ko__in=values) | Q(type__name_en__in=values))

app/event/presentation/test/api_test.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import http
22
import urllib.parse
3+
from datetime import datetime
34

45
import pytest
56
from django.urls import reverse
@@ -22,18 +23,20 @@ def test_presentation_api(api_client: APIClient, create_presentation_set: Presen
2223
def test_presentation_event_type_filter_api(api_client: APIClient):
2324
# Given: 행사 2개에 각각 2 종류의 발표 유형이 있고, 각 발표 유형마다 1개의 발표가 있음.
2425
organization = Organization.objects.create(name="Test Organization")
25-
event_1: Event = Event.objects.create(organization=organization, name="Test Event 1")
26-
event_2: Event = Event.objects.create(organization=organization, name="Test Event 2")
26+
event_1: Event = Event.objects.create(
27+
organization=organization, name="Test Event 1", event_start_at=datetime(2025, 8, 1)
28+
)
29+
event_2: Event = Event.objects.create(
30+
organization=organization, name="Test Event 2", event_start_at=datetime(2026, 8, 1)
31+
)
2732

2833
event_1_prst_type_1 = PresentationType.objects.create(event=event_1, name="Type 1")
2934
event_1_prst_type_2 = PresentationType.objects.create(event=event_1, name="Type 2")
30-
event_2_prst_type_1 = PresentationType.objects.create(event=event_2, name="Type 1")
31-
event_2_prst_type_2 = PresentationType.objects.create(event=event_2, name="Type 2")
35+
PresentationType.objects.create(event=event_2, name="Type 1")
36+
PresentationType.objects.create(event=event_2, name="Type 2")
3237

3338
event_1_prst_type_1_prst = Presentation.objects.create(type=event_1_prst_type_1, title="Presentation 1")
3439
event_1_prst_type_2_prst = Presentation.objects.create(type=event_1_prst_type_2, title="Presentation 2")
35-
event_2_prst_type_1_prst = Presentation.objects.create(type=event_2_prst_type_1, title="Presentation 3")
36-
Presentation.objects.create(type=event_2_prst_type_2, title="Presentation 4")
3740

3841
# When: API 요청을 통해 행사 1의 발표 유형 1과 2에 해당하는 발표를 요청할 시
3942
qs = urllib.parse.urlencode(
@@ -51,16 +54,29 @@ def test_presentation_event_type_filter_api(api_client: APIClient):
5154
str(event_1_prst_type_2_prst.id),
5255
}
5356

54-
# When: API 요청을 통해 행사 유형은 지정하지 않고 유형 1에 해당하는 발표를 요청할 시
55-
qs = urllib.parse.urlencode({"types": event_1_prst_type_1.name})
56-
response = api_client.get(f"{reverse('v1:presentation-list')}?{qs}")
5757

58-
# Then: 행사 1의 발표 유형 1과 행사 2의 발표 유형 1에 해당하는 발표가 반환되어야 함.
59-
assert response.status_code == http.HTTPStatus.OK
58+
@pytest.mark.django_db
59+
def test_presentation_defaults_to_latest_event(api_client: APIClient):
60+
# Given: 2개의 행사가 있고, 각각 발표가 있음.
61+
organization = Organization.objects.create(name="Test Organization")
62+
old_event = Event.objects.create(
63+
organization=organization, name="PyCon Korea 2025", event_start_at=datetime(2025, 8, 1)
64+
)
65+
new_event = Event.objects.create(
66+
organization=organization, name="PyCon Korea 2026", event_start_at=datetime(2026, 8, 1)
67+
)
6068

69+
old_type = PresentationType.objects.create(event=old_event, name="Talk")
70+
new_type = PresentationType.objects.create(event=new_event, name="Talk")
71+
72+
Presentation.objects.create(type=old_type, title="Old Presentation")
73+
new_prst = Presentation.objects.create(type=new_type, title="New Presentation")
74+
75+
# When: event 파라미터 없이 요청
76+
response = api_client.get(reverse("v1:presentation-list"))
77+
78+
# Then: 최신 행사(2026)의 발표만 반환
79+
assert response.status_code == http.HTTPStatus.OK
6180
response_data = response.json()
62-
assert len(response_data) == 2, "Should return presentations for type 1 across all events"
63-
assert {datum["id"] for datum in response_data} == {
64-
str(event_1_prst_type_1_prst.id),
65-
str(event_2_prst_type_1_prst.id),
66-
}
81+
assert len(response_data) == 1
82+
assert response_data[0]["id"] == str(new_prst.id)

app/event/presentation/views.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,12 @@
11
from core.const.tag import OpenAPITag
2-
from core.models import BaseAbstractModelQuerySet
3-
from django.db.models import Q
42
from django.utils.decorators import method_decorator
5-
from django_filters import rest_framework as filters
6-
from django_filters.constants import EMPTY_VALUES
73
from drf_spectacular.utils import extend_schema
4+
from event.presentation.filters import PresentationFilterSet
85
from event.presentation.models import Presentation, PresentationCategory
96
from event.presentation.serializers import PresentationSerializer
107
from rest_framework import mixins, viewsets
118

129

13-
class PresentationFilterSet(filters.FilterSet):
14-
event = filters.CharFilter(method="filter_by_event_name")
15-
types = filters.BaseCSVFilter(method="filter_by_type_names")
16-
17-
def filter_by_event_name(self, queryset: BaseAbstractModelQuerySet, name: str, value: str) -> Q:
18-
if value in EMPTY_VALUES:
19-
return queryset
20-
21-
return queryset.filter(Q(type__event__name_ko=value) | Q(type__event__name_en=value))
22-
23-
def filter_by_type_names(self, queryset: BaseAbstractModelQuerySet, name: str, values: list[str]) -> Q:
24-
if values in EMPTY_VALUES:
25-
return queryset
26-
27-
return queryset.filter(Q(type__name_ko__in=values) | Q(type__name_en__in=values))
28-
29-
3010
@method_decorator(name="list", decorator=extend_schema(tags=[OpenAPITag.EVENT_PRESENTATION]))
3111
class PresentationCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
3212
queryset = PresentationCategory.objects.filter_active()

app/event/sponsor/filters.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from event.filters import EventFilterMixin
2+
3+
4+
class SponsorTierFilterSet(EventFilterMixin):
5+
event_field_prefix = "event"

app/event/sponsor/test/__init__.py

Whitespace-only changes.

app/event/sponsor/test/api_test.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import http
2+
from datetime import datetime
3+
4+
import pytest
5+
from django.urls import reverse
6+
from event.models import Event
7+
from event.sponsor.models import Sponsor, SponsorTier, SponsorTierSponsorRelation
8+
from file.models import PublicFile
9+
from rest_framework.test import APIClient
10+
from user.models.organization import Organization
11+
12+
13+
@pytest.fixture
14+
def api_client():
15+
return APIClient()
16+
17+
18+
@pytest.fixture
19+
def two_events():
20+
organization = Organization.objects.create(name="Test Organization")
21+
old_event = Event.objects.create(
22+
organization=organization, name="PyCon Korea 2025", event_start_at=datetime(2025, 8, 1)
23+
)
24+
new_event = Event.objects.create(
25+
organization=organization, name="PyCon Korea 2026", event_start_at=datetime(2026, 8, 1)
26+
)
27+
return old_event, new_event
28+
29+
30+
def _make_sponsor(event, name, tier):
31+
logo = PublicFile.objects.create(
32+
file=f"public/{name}.png",
33+
mimetype="image/png",
34+
hash=name,
35+
size=0,
36+
)
37+
sponsor = Sponsor.objects.create(event=event, name=name, logo=logo)
38+
SponsorTierSponsorRelation.objects.create(tier=tier, sponsor=sponsor)
39+
return sponsor
40+
41+
42+
@pytest.mark.django_db
43+
def test_sponsor_defaults_to_latest_event(api_client: APIClient, two_events):
44+
old_event, new_event = two_events
45+
46+
# Given: 각 행사에 후원 등급과 후원사가 있음
47+
old_tier = SponsorTier.objects.create(event=old_event, name="Gold", order=0)
48+
new_tier = SponsorTier.objects.create(event=new_event, name="Gold", order=0)
49+
50+
_make_sponsor(old_event, "Old Sponsor", old_tier)
51+
_make_sponsor(new_event, "New Sponsor", new_tier)
52+
53+
# When: event 파라미터 없이 요청
54+
response = api_client.get(reverse("v1:sponsor-list"))
55+
56+
# Then: 최신 행사(2026)의 후원 등급만 반환
57+
assert response.status_code == http.HTTPStatus.OK
58+
response_data = response.json()
59+
assert len(response_data) == 1
60+
assert response_data[0]["id"] == str(new_tier.id)
61+
62+
63+
@pytest.mark.django_db
64+
def test_sponsor_filter_by_event_name(api_client: APIClient, two_events):
65+
old_event, new_event = two_events
66+
67+
old_tier = SponsorTier.objects.create(event=old_event, name="Gold", order=0)
68+
new_tier = SponsorTier.objects.create(event=new_event, name="Gold", order=0)
69+
70+
_make_sponsor(old_event, "Old Sponsor", old_tier)
71+
_make_sponsor(new_event, "New Sponsor", new_tier)
72+
73+
# When: 2025 행사를 명시적으로 지정
74+
response = api_client.get(reverse("v1:sponsor-list"), {"event": "PyCon Korea 2025"})
75+
76+
# Then: 2025 행사의 후원 등급만 반환
77+
assert response.status_code == http.HTTPStatus.OK
78+
response_data = response.json()
79+
assert len(response_data) == 1
80+
assert response_data[0]["id"] == str(old_tier.id)
81+
82+
83+
@pytest.mark.django_db
84+
def test_sponsor_no_events_returns_empty(api_client: APIClient):
85+
# When: 이벤트가 없을 때 요청
86+
response = api_client.get(reverse("v1:sponsor-list"))
87+
88+
# Then: 빈 응답
89+
assert response.status_code == http.HTTPStatus.OK
90+
assert response.json() == []

app/event/sponsor/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.db import models
33
from django.utils.decorators import method_decorator
44
from drf_spectacular.utils import extend_schema
5+
from event.sponsor.filters import SponsorTierFilterSet
56
from event.sponsor.models import Sponsor, SponsorTier
67
from event.sponsor.serializers import SponsorTierSerializer
78
from rest_framework import mixins, viewsets
@@ -17,3 +18,4 @@ class SponsorTierViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
1718
)
1819
)
1920
serializer_class = SponsorTierSerializer
21+
filterset_class = SponsorTierFilterSet

0 commit comments

Comments
 (0)