Skip to content

Commit 07aa192

Browse files
committed
Merge remote-tracking branch 'origin/feature/add-presentation-organization-admin'
2 parents 5f5d2df + b583e0c commit 07aa192

39 files changed

+1419
-109
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from core.const.serializer import COMMON_ADMIN_FIELDS
2+
from core.serializer.base_abstract_serializer import BaseAbstractSerializer
3+
from core.serializer.json_schema_serializer import JsonSchemaSerializer
4+
from event.models import Event
5+
from rest_framework import serializers
6+
7+
8+
class EventAdminSerializer(BaseAbstractSerializer, JsonSchemaSerializer, serializers.ModelSerializer):
9+
class Meta:
10+
model = Event
11+
fields = COMMON_ADMIN_FIELDS + ("organization", "name_ko", "name_en")
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from core.const.serializer import COMMON_ADMIN_FIELDS
2+
from core.serializer.base_abstract_serializer import BaseAbstractSerializer
3+
from core.serializer.json_schema_serializer import JsonSchemaSerializer
4+
from event.presentation.models import Presentation, PresentationCategory, PresentationSpeaker, PresentationType
5+
from rest_framework import serializers
6+
7+
8+
class PresentationTypeAdminSerializer(BaseAbstractSerializer, JsonSchemaSerializer, serializers.ModelSerializer):
9+
class Meta:
10+
model = PresentationType
11+
fields = COMMON_ADMIN_FIELDS + ("event", "name_ko", "name_en")
12+
13+
14+
class PresentationCategoryAdminSerializer(BaseAbstractSerializer, JsonSchemaSerializer, serializers.ModelSerializer):
15+
class Meta:
16+
model = PresentationCategory
17+
fields = COMMON_ADMIN_FIELDS + ("type", "name_ko", "name_en")
18+
19+
20+
class PresentationAdminSerializer(BaseAbstractSerializer, JsonSchemaSerializer, serializers.ModelSerializer):
21+
class Meta:
22+
model = Presentation
23+
fields = COMMON_ADMIN_FIELDS + ("title_ko", "title_en")
24+
25+
26+
class PresentationSpeakerAdminSerializer(BaseAbstractSerializer, JsonSchemaSerializer, serializers.ModelSerializer):
27+
class Meta:
28+
model = PresentationSpeaker
29+
fields = COMMON_ADMIN_FIELDS + ("presentation", "user", "biography_ko", "biography_en")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from core.const.serializer import COMMON_ADMIN_FIELDS
2+
from core.serializer.base_abstract_serializer import BaseAbstractSerializer
3+
from core.serializer.json_schema_serializer import JsonSchemaSerializer
4+
from event.sponsor.models import Sponsor, SponsorTier
5+
from rest_framework import serializers
6+
7+
8+
class SponsorTierAdminSerializer(BaseAbstractSerializer, JsonSchemaSerializer, serializers.ModelSerializer):
9+
class Meta:
10+
model = SponsorTier
11+
fields = COMMON_ADMIN_FIELDS + ("event", "name_ko", "name_en", "order")
12+
13+
14+
class SponsorAdminSerializer(BaseAbstractSerializer, JsonSchemaSerializer, serializers.ModelSerializer):
15+
class Meta:
16+
model = Sponsor
17+
fields = COMMON_ADMIN_FIELDS + ("event", "logo", "page", "name_ko", "name_en")

app/admin_api/serializers/user.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,28 @@
77
from user.models import UserExt
88

99

10-
class UserAdminSerializer(JsonSchemaSerializer, ReadOnlyModelSerializer, serializers.ModelSerializer):
10+
class UserAdminSerializer(JsonSchemaSerializer, serializers.ModelSerializer):
11+
str_repr = serializers.CharField(source="__str__", read_only=True)
12+
1113
class Meta:
1214
model = UserExt
13-
fields = ("id", "username", "email", "first_name", "last_name", "is_staff", "is_active", "date_joined")
15+
fields = (
16+
"id",
17+
"is_active",
18+
"username",
19+
"nickname_ko",
20+
"nickname_en",
21+
"email",
22+
"is_superuser",
23+
"str_repr",
24+
"date_joined",
25+
"last_login",
26+
)
27+
extra_kwargs = {
28+
"id": {"read_only": True},
29+
"date_joined": {"read_only": True},
30+
"last_login": {"read_only": True},
31+
}
1432

1533

1634
class UserAdminSignInSerializerData(typing.TypedDict):
@@ -35,4 +53,42 @@ def validate(self, attrs: UserAdminSignInSerializerData) -> UserAdminSignInSeria
3553
if not (self.user and self.user.check_password(attrs["password"])):
3654
raise serializers.ValidationError("User not found or inactive or wrong password.")
3755

56+
if not self.user.is_superuser:
57+
raise serializers.PermissionDenied("Only permissioned users can sign in using this route.")
58+
3859
return attrs
60+
61+
62+
class UserAdminPasswordChangeSerializerData(typing.TypedDict):
63+
old_password: str
64+
new_password: str
65+
new_password_confirm: str
66+
67+
68+
class UserAdminPasswordChangeSerializer(JsonSchemaSerializer, ReadOnlyModelSerializer):
69+
old_password = serializers.CharField(write_only=True, required=True)
70+
new_password = serializers.CharField(write_only=True, required=True)
71+
new_password_confirm = serializers.CharField(write_only=True, required=True)
72+
73+
class Meta:
74+
model = UserExt
75+
fields = ("old_password", "new_password", "new_password_confirm")
76+
77+
def validate(self, attrs: UserAdminPasswordChangeSerializerData) -> UserAdminPasswordChangeSerializerData:
78+
user: UserExt = self.instance
79+
if not user.check_password(attrs["old_password"]):
80+
raise serializers.ValidationError("Old password is incorrect.")
81+
82+
if attrs["old_password"] == attrs["new_password"]:
83+
raise serializers.ValidationError("New password cannot be the same as the old password.")
84+
85+
if attrs["new_password"] != attrs["new_password_confirm"]:
86+
raise serializers.ValidationError("New password and confirmation do not match.")
87+
88+
return attrs
89+
90+
def save(self, **kwargs: typing.Any) -> UserExt:
91+
user: UserExt = self.instance
92+
user.set_password(self.validated_data["new_password"])
93+
user.save(update_fields=["password"])
94+
return user

app/admin_api/urls.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
from admin_api.views.cms import PageAdminViewSet, SitemapAdminViewSet
2+
from admin_api.views.event.event import EventAdminViewSet
3+
from admin_api.views.event.presentation import (
4+
PresentationAdminViewSet,
5+
PresentationSpeakerAdminViewSet,
6+
PresentationTypeAdminViewSet,
7+
)
8+
from admin_api.views.event.sponsor import SponsorAdminViewSet, SponsorTierAdminViewSet
29
from admin_api.views.file import PublicFileAdminViewSet
310
from admin_api.views.user import UserAdminViewSet
411
from django.urls import include, path
@@ -14,8 +21,20 @@
1421
admin_file_router = routers.SimpleRouter()
1522
admin_file_router.register("publicfile", PublicFileAdminViewSet, basename="admin-public-file")
1623

24+
admin_event_router = routers.SimpleRouter()
25+
admin_event_router.register("event", EventAdminViewSet)
26+
admin_event_router.register("sponsortier", SponsorTierAdminViewSet)
27+
admin_event_router.register("sponsor", SponsorAdminViewSet)
28+
admin_event_router.register("presentationtype", PresentationTypeAdminViewSet)
29+
admin_event_router.register("presentation", PresentationAdminViewSet)
30+
admin_event_router.register(
31+
"presentation/(?P<presentation_id>{UUID_V4_PATTERN})/speaker",
32+
PresentationSpeakerAdminViewSet,
33+
)
34+
1735
urlpatterns = [
1836
path("cms/", include(admin_cms_router.urls)),
1937
path("file/", include(admin_file_router.urls)),
2038
path("user/", include(admin_user_router.urls)),
39+
path("event/", include(admin_event_router.urls)),
2140
]

app/admin_api/views/event/event.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
from admin_api.serializers.event.event import EventAdminSerializer
4+
from core.const.tag import OpenAPITag
5+
from core.permissions import IsSuperUser
6+
from core.viewset.json_schema_viewset import JsonSchemaViewSet
7+
from drf_spectacular.utils import extend_schema, extend_schema_view
8+
from event.models import Event
9+
from rest_framework import viewsets
10+
11+
ADMIN_METHODS = ["list", "retrieve", "create", "update", "partial_update", "destroy"]
12+
13+
14+
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_EVENT_EVENT]) for m in ADMIN_METHODS})
15+
class EventAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
16+
http_method_names = ["get", "post", "patch", "delete"]
17+
serializer_class = EventAdminSerializer
18+
permission_classes = [IsSuperUser]
19+
queryset = Event.objects.filter_active().select_related("created_by", "updated_by", "deleted_by")
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from __future__ import annotations
2+
3+
from admin_api.serializers.event.presentation import (
4+
PresentationAdminSerializer,
5+
PresentationCategoryAdminSerializer,
6+
PresentationSpeakerAdminSerializer,
7+
PresentationTypeAdminSerializer,
8+
)
9+
from core.const.regex import UUID_V4_PATTERN
10+
from core.const.tag import OpenAPITag
11+
from core.permissions import IsSuperUser
12+
from core.viewset.json_schema_viewset import JsonSchemaViewSet
13+
from django.db import models
14+
from drf_spectacular.utils import extend_schema, extend_schema_view
15+
from event.presentation.models import (
16+
Presentation,
17+
PresentationCategory,
18+
PresentationCategoryRelation,
19+
PresentationSpeaker,
20+
PresentationType,
21+
)
22+
from rest_framework import decorators, exceptions, request, response, status, viewsets
23+
24+
ADMIN_METHODS = ["list", "retrieve", "create", "update", "partial_update", "destroy"]
25+
26+
27+
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION]) for m in ADMIN_METHODS})
28+
class PresentationTypeAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
29+
http_method_names = ["get", "post", "patch", "delete"]
30+
serializer_class = PresentationTypeAdminSerializer
31+
permission_classes = [IsSuperUser]
32+
queryset = PresentationType.objects.filter_active().select_related("created_by", "updated_by", "deleted_by")
33+
34+
@extend_schema(
35+
tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION],
36+
responses={status.HTTP_200_OK: PresentationCategoryAdminSerializer(many=True)},
37+
)
38+
@decorators.action(detail=True, methods=["get"], url_path="categories")
39+
def list_categories(self, *args: tuple, **kwargs: dict) -> response.Response:
40+
categories = PresentationCategory.objects.filter_active().filter(type=self.get_object())
41+
return response.Response(data=PresentationCategoryAdminSerializer(instance=categories, many=True).data)
42+
43+
@extend_schema(
44+
tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION],
45+
request=PresentationCategoryAdminSerializer,
46+
responses={status.HTTP_201_CREATED: PresentationCategoryAdminSerializer},
47+
)
48+
@decorators.action(detail=True, methods=["post"], url_path="categories")
49+
def add_category(self, request: request.Request, *args: tuple, **kwargs: dict) -> response.Response:
50+
serializer = PresentationCategoryAdminSerializer(data=request.data)
51+
serializer.is_valid(raise_exception=True)
52+
serializer.save()
53+
return response.Response(data=serializer.data, status=status.HTTP_201_CREATED)
54+
55+
@extend_schema(
56+
tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION],
57+
request=PresentationCategoryAdminSerializer,
58+
responses={status.HTTP_200_OK: PresentationCategoryAdminSerializer},
59+
)
60+
@decorators.action(detail=True, methods=["patch"], url_path=f"categories/(?P<category_id>{UUID_V4_PATTERN})")
61+
def update_category(self, request: request.Request, pk: str, category_id: str, *args, **kwargs):
62+
if not (category := PresentationCategory.objects.filter_active().filter(type_id=pk, id=category_id).first()):
63+
raise exceptions.NotFound(detail="Category not found.")
64+
65+
serializer = PresentationCategoryAdminSerializer(instance=category, data=request.data, partial=True)
66+
serializer.is_valid(raise_exception=True)
67+
serializer.save()
68+
return response.Response(data=serializer.data)
69+
70+
@extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION], responses={status.HTTP_204_NO_CONTENT: None})
71+
@decorators.action(detail=True, methods=["delete"], url_path=f"categories/(?P<category_id>{UUID_V4_PATTERN})")
72+
def delete_category(self, pk: str, category_id: str, *args: tuple, **kwargs: dict) -> response.Response:
73+
if not (category := PresentationCategory.objects.filter_active().filter(type_id=pk, id=category_id).first()):
74+
raise exceptions.NotFound(detail="Category not found.")
75+
76+
category.delete()
77+
return response.Response(status=status.HTTP_204_NO_CONTENT)
78+
79+
80+
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION]) for m in ADMIN_METHODS})
81+
class PresentationAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
82+
http_method_names = ["get", "post", "patch", "delete"]
83+
serializer_class = PresentationAdminSerializer
84+
permission_classes = [IsSuperUser]
85+
queryset = Presentation.objects.get_all_nested_data().select_related("created_by", "updated_by", "deleted_by")
86+
87+
@extend_schema(
88+
tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION],
89+
responses={status.HTTP_200_OK: PresentationCategoryAdminSerializer(many=True)},
90+
)
91+
@decorators.action(detail=True, methods=["get"], url_path="categories")
92+
def list_categories(self, *args: tuple, **kwargs: dict) -> response.Response:
93+
categories = self.get_object().active_categories
94+
return response.Response(data=PresentationCategoryAdminSerializer(instance=categories, many=True).data)
95+
96+
@extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION], responses={status.HTTP_201_CREATED: None})
97+
@decorators.action(detail=True, methods=["post"], url_path="categories/(?P<category_id>{UUID_V4_PATTERN})")
98+
def add_category(self, pk: str, category_id: str, *args: tuple, **kwargs: dict) -> response.Response:
99+
PresentationCategoryRelation.objects.get_or_create(presentation_id=pk, category_id=category_id)
100+
return response.Response(status=status.HTTP_201_CREATED)
101+
102+
@extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION], responses={status.HTTP_204_NO_CONTENT: None})
103+
@decorators.action(detail=True, methods=["delete"], url_path="categories/(?P<category_id>{UUID_V4_PATTERN})")
104+
def remove_category(self, pk: str, category_id: str, *args: tuple, **kwargs: dict) -> response.Response:
105+
if not (
106+
relation := PresentationCategoryRelation.objects.filter(presentation_id=pk, category_id=category_id).first()
107+
):
108+
raise exceptions.NotFound(detail="Category is not associated with this presentation.")
109+
110+
relation.delete()
111+
return response.Response(status=status.HTTP_204_NO_CONTENT)
112+
113+
114+
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_EVENT_PRESENTATION]) for m in ADMIN_METHODS})
115+
class PresentationSpeakerAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
116+
http_method_names = ["get", "post", "patch", "delete"]
117+
serializer_class = PresentationSpeakerAdminSerializer
118+
permission_classes = [IsSuperUser]
119+
queryset = PresentationSpeaker.objects.filter_active().select_related("created_by", "updated_by", "deleted_by")
120+
121+
def get_queryset(self) -> models.QuerySet[PresentationSpeaker]:
122+
return super().get_queryset().filter(presentation_id=self.kwargs["presentation_id"])
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
from admin_api.serializers.event.sponsor import SponsorAdminSerializer, SponsorTierAdminSerializer
4+
from core.const.regex import UUID_V4_PATTERN
5+
from core.const.tag import OpenAPITag
6+
from core.permissions import IsSuperUser
7+
from core.viewset.json_schema_viewset import JsonSchemaViewSet
8+
from django.db import models
9+
from drf_spectacular.utils import extend_schema, extend_schema_view
10+
from event.sponsor.models import Sponsor, SponsorTier, SponsorTierSponsorRelation
11+
from rest_framework import decorators, exceptions, response, status, viewsets
12+
13+
ADMIN_METHODS = ["list", "retrieve", "create", "update", "partial_update", "destroy"]
14+
15+
16+
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_EVENT_SPONSOR]) for m in ADMIN_METHODS})
17+
class SponsorTierAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
18+
http_method_names = ["get", "post", "patch", "delete"]
19+
serializer_class = SponsorTierAdminSerializer
20+
permission_classes = [IsSuperUser]
21+
queryset = (
22+
SponsorTier.objects.filter_active()
23+
.prefetch_related(
24+
models.Prefetch(
25+
lookup="sponsors",
26+
queryset=Sponsor.objects.filter_active().select_related("created_by", "updated_by", "deleted_by"),
27+
to_attr="_prefetched_active_sponsors",
28+
),
29+
)
30+
.select_related("created_by", "updated_by", "deleted_by")
31+
)
32+
33+
@extend_schema(
34+
tags=[OpenAPITag.ADMIN_EVENT_SPONSOR],
35+
responses={status.HTTP_200_OK: SponsorAdminSerializer(many=True)},
36+
)
37+
@decorators.action(detail=True, methods=["get"], url_path="sponsors")
38+
def list_sponsors(self, *args: tuple, **kwargs: dict) -> response.Response:
39+
tier: SponsorTier = self.get_object()
40+
return response.Response(data=SponsorAdminSerializer(instance=tier.active_sponsors, many=True).data)
41+
42+
@extend_schema(tags=[OpenAPITag.ADMIN_EVENT_SPONSOR], responses={status.HTTP_201_CREATED: SponsorAdminSerializer})
43+
@decorators.action(detail=True, methods=["post"], url_path=f"sponsors/(?P<sponsor_id>{UUID_V4_PATTERN})")
44+
def add_sponsor(self, sponsor_id: str, *args: tuple, **kwargs: dict) -> response.Response:
45+
tier: SponsorTier = self.get_object()
46+
if not (sponsor := Sponsor.objects.filter_active().filter(id=sponsor_id).first()):
47+
raise exceptions.NotFound(detail="Sponsor not found")
48+
49+
SponsorTierSponsorRelation.objects.get_or_create(tier=tier, sponsor=sponsor)
50+
return response.Response(data=SponsorAdminSerializer(instance=sponsor).data, status=status.HTTP_201_CREATED)
51+
52+
@extend_schema(tags=[OpenAPITag.ADMIN_EVENT_SPONSOR], responses={status.HTTP_204_NO_CONTENT: None})
53+
@decorators.action(detail=True, methods=["delete"], url_path=f"sponsors/(?P<sponsor_id>{UUID_V4_PATTERN})")
54+
def remove_sponsor(self, pk: str, sponsor_id: str, *args: tuple, **kwargs: dict) -> response.Response:
55+
if not (relation := SponsorTierSponsorRelation.objects.filter(tier_id=pk, sponsor_id=sponsor_id).first()):
56+
raise exceptions.NotFound(detail="Sponsor is not associated with this tier")
57+
58+
relation.delete()
59+
return response.Response(status=status.HTTP_204_NO_CONTENT)
60+
61+
62+
@extend_schema_view(**{m: extend_schema(tags=[OpenAPITag.ADMIN_EVENT_SPONSOR]) for m in ADMIN_METHODS})
63+
class SponsorAdminViewSet(JsonSchemaViewSet, viewsets.ModelViewSet):
64+
http_method_names = ["get", "post", "patch", "delete"]
65+
serializer_class = SponsorAdminSerializer
66+
permission_classes = [IsSuperUser]
67+
queryset = Sponsor.objects.filter_active().select_related("created_by", "updated_by", "deleted_by")
68+
69+
@extend_schema(
70+
tags=[OpenAPITag.ADMIN_EVENT_SPONSOR],
71+
responses={status.HTTP_200_OK: SponsorTierAdminSerializer(many=True)},
72+
)
73+
@decorators.action(detail=True, methods=["get"], url_path="tiers")
74+
def list_tiers(self, *args: tuple, **kwargs: dict) -> response.Response:
75+
sponsor: Sponsor = self.get_object()
76+
tier_id_qs = SponsorTierSponsorRelation.objects.filter(sponsor=sponsor).values_list("tier_id", flat=True)
77+
tiers = SponsorTier.objects.filter_active().filter(id__in=tier_id_qs)
78+
return response.Response(data=SponsorTierAdminSerializer(instance=tiers, many=True).data)

0 commit comments

Comments
 (0)