SerializerMethodField: Serializer에 커스텀 필드를 추가하는 방법이다.
#내 room의 평균 리뷰 점수가 몇인지
#rooms. serializers.py
class RoomDetailSerializer(ModelSerializer):
owner = TinyUserSerializer(read_only = True) #users.serializers.py에서 만든 serializer
amenities = AmenitySerializer(read_only=True,many=True)
category = CategorySerializer(read_only=True)
rating = serializers.SerializerMethodField() #SerializerMethodField로 필드 형태를 설정해줘야함.
class Meta:
model = Room
fields = "__all__"
def get_rating(self,room): #이름은 위에서 설정한 필드의 'get_...'이 되어야함
return room.rating()
SerializerContext: 방을 보고있는 유저에 따라 필드를 다르게 계산하는데 쓰인다.
Context는 key와 value의 딕셔너리다.
views.py에서 GET, POST 메소드를 위해 serializer를 생성할 때, 원한다면 context를 추가할 수 있다. 이 context는 serializer에게 외부 세계의 정보를 보낼 때 사용한다.
context에게 원하는 것 뭐든지 전달 가능하고, 원하는 메소드 어떤 것이든 serializer 안에 있다면 serializer의 context에 접근 가능하다.
=> serializer 데이터에 데이터(중요한 객체, request객체 등)를 그냥 보낼 수 있다.
#rooms. views.py
class RoomDetail(APIView):
def get_object(self,pk):
try:
return Room.objects.get(pk=pk)
except Room.DoesNotExist:
raise NotFound
def get(self, request, pk):
room = self.get_object(pk)
serializer = RoomDetailSerializer( #views.py에서 serializer를 생성할 때 request객체가 달린 context를 추가한다.
room,
context = {'request':request}
)
return Response(serializer.data)
# rooms. serializers.py
class RoomDetailSerializer(ModelSerializer): #serializer 안의 메소드에서 받음
...
is_owner = serializer.SerializerMethodField()
...
def get_is_owner(self,room):
request=self.context['request'] #context에서 request를 꺼냄
return room.owner = request.user #그리고 계산하는데 사용
이렇게 context에 request 객체를 담아서 사용하면 방을 바라보고 있는 사람에 따라 'is_owner'의 True, False 값이 달라진다.
Reverse Serializers
우리 방에 대해 작성된 리뷰를 Room Detail에 역접근자를 추가해보기: 여러개의 리뷰는 하나의 유저, 하나의 방을 가리키고 있을 수 있다.
역접근자의 이름 변경은 'related_name='...''으로 했었고, 'related_name-reviews'로 해두어서 우리는 'room.reviews'를 가지고 있다.
하나의 방에 만들어진 모든 리뷰 보기: 먼저, reviews 앱 폴더의 serializers.py에 ReviewSerializer를 만든다.
-> rooms 모델 serializers.py
...
from reviews import ReviewSerializer
...
class RoomDetailSerializer(ModelSerializer):
...
reviews=ReviewSerializer(many=True, read_only=True)
역접근자가 있기 때문에, 사용할 serializer만 import 후에 적어주면 된다. 하지만 방 하나는 수만개가 넘는 리뷰를 가질 수 있고, 너무 커지면 pc에 부담이 된다. 그래서 pagination이 필요하다.
Pagination
#rooms.views.py Reviews Pagination
class RoomReviews(APIView):
def get_object(self,pk):
try:
return Room.objects.get(pk=pk)
except Room.DoesNotExist:
raise NotFound
def get(self,request, pk):
try:
page = request.query_params.get('page',1) #page를 찾으면 page 저장. 못찾으면 1 저장
page = int(page) #page는 int()를 하지 않으면 문자열 형태
except ValueError: #page가 정수 형태가 아닐 때.
page = 1
page_size = 3 #한 페이지에 3개씩 보여줌.
start = (page - 1) * page_size
end = start + page_size
room = self.get_object(pk)
serializer = ReviewSerializer(
room.reviews.all()[start:end], #[A:B] A부터 B전 까지
many=True,)
return Response(serializer.data)
모든 리뷰를 불러와서 한 페이지 만큼씩 자르는게 아니라 우리가 페이지를 불러오기 전까지 그 페이지의 데이터를 불러오지 않는다. 그래서 Pagination을 하면 한페이지에 불러오려는 만큼만 불러와져서 하나의 방에 아무리 많은 리뷰가 있어도 pc에 부담이 되지 않는다.
#rooms. views.py Amenities Pagination
class RoomAmenities(APIView):
def get_object(self,pk):
try:
return Room.objects.get(pk=pk)
except Room.DoesNotExist:
raise NotFound
def get(self, request, pk):
try:
page = request.query_params.get('page',1) #page를 찾으면 page 저장. 못찾으면 1 저장
page = int(page) #page는 int()를 하지 않으면 문자열 형태
except ValueError: #page가 정수 형태가 아닐 때.
page = 1
page_size = 3 #한 페이지에 3개씩 보여줌.
start = (page - 1) * page_size
end = start + page_size
room = self.get_object(pk)
serializer = AmenitySerializer(room.amenities.all()[start:end], many=True)
return Response(serializer.data)
File Uploads
이전에 media 앱에 photo모델을 만들었다.
어드민 패널에서 사진이나 비디오를 업로드하려면, 파일 선택을 해야한다. 선택되어 업로드된 파일들의 저장될 위치는 'config/settings.py'에서 'MEDIA_ROOT="uploads"'로 설정한다.
이 파일을 유저에게 노출시키려면, 어떤 url에서 파일을 노출시킬지 정해야 한다. 이것은 'MEDIA_URL="user-uploads/"'로 설정한다. '/'는 필수다.
#config/ urls.py
from django.cof.urls.static import static
...
urlpatterns = [ ... ]
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
위의 코드를 작성해주어야 한다.
하지만 이 방법은 안전하지 않다. 모르는 사람, 신뢰할 수 없는 사람이 업로드할 수 있다. 이런 유저가 업로드한 파일들이 내 코드 옆, 내 디스크 공간을 사용하는건 옳지 않다.
그래서 유저가 업로드하는 파일들은 다른 서버에 저장시킬 것이다.
django는 그 파일들의 url만 알게될 것이다.
=> medias/models.py에서 'file=models.URLField()로 바꿔준다.
permission_classes
'permission_class = [...]'로 'if request.user.is_authenticated' 구문을 대신할 수 있다.
'IsAuthenticated'는 인증받아야 GET,POST 등 모든 메소드를 이용 가능하다.
'IsAuthenticatedOrReadOnly'는 인증을 받지 않으면 GET 메소드만 이용 가능하다.
Reviews: /rooms/{room_id}/reviews
#reviews/serializers.py
class ReviewSerializer(serializers.ModelSerializer):
user = TinyUserSerializer(read_only=True) #유저가 없어도 유효성 검사를 통과시키기 위해
#post를 할 때는 작성자를 물어볼 필요없다. 작성하는 유저가 작성자기 때문이다.
class Meta:
model = Review
fields = (
"user",
"payload",
"rating",
)
#rooms/views.py
class RoomReviews(APIView):
permission_classes = [IsAuthenticatedOrReadOnly]
def get_object(self,pk):
try:
return Room.objects.get(pk=pk)
except Room.DoesNotExist:
raise NotFound
def get(self,request, pk):
try:
page = request.query_params.get('page',1) #page를 찾으면 page 저장. 못찾으면 1 저장
page = int(page) #page는 int()를 하지 않으면 문자열 형태
except ValueError: #page가 정수 형태가 아닐 때.
page = 1
page_size = settings.PAGE_SIZE #한 페이지에 3개씩 보여줌.
start = (page - 1) * page_size
end = start + page_size
room = self.get_object(pk)
serializer = ReviewSerializer(
room.reviews.all()[start:end], #[A:B] A부터 B전 까지
many=True,)
return Response(serializer.data)
def post(self, request, pk):
serializer = ReviewSerializer(data=request.data)
if serializer.is_valid():
review = serializer.save(user=request.user, room = self.get_object(pk))
serializer = ReviewSerializer(review)
return Response(serializer.data)
모든 리뷰는 아무나 봐도 되지만 리뷰 작성은 아무나하면 안된다.
그래서 'permission_classes=[IsAuthenticatedOrReadOnly]'로 하였다.
Wishlists
#wishlists.views.py
from rest_framework.views import APIView
from rest_framework.status import HTTP_200_OK
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rooms.models import Room
from .models import Wishlist
from .serializers import WishlistSerializer
class Wishlists(APIView):
permisson_classes = [IsAuthenticated] #위시리스트는 개인용이기 때문에, 모든 위시리스트를 보려면 인증된 사람이어야함.
def get(self,request):
all_wishlists = Wishlist.objects.filter(user=request.user) #request.user와 동일한 유저가 가지고 있는 위시리스트만 찾기 위해 filter 사용
serializer = WishlistSerializer(all_wishlists, many=True, context = {"request": request},) #rooms.serializers.py의 get_is_owner함수 때문에 serializer의 context에서 request를 넘겨줘야한다.
return Response(serializer.data)
def post(self,request): #이름만 보내면 됨
serializer = WishlistSerializer(data=request.data)
if serializer.is_valid():
wishlist = serializer.save(user=request.user) #user는 따로 보내주기
serializer = WishlistSerializer(wishlist)
return Response(serializer.data)
else:
return Response(serializer.errors)
class WishlistDetail(APIView):
permission_classes = [IsAuthenticated]
def get_object(self, pk, user):
try:
return Wishlist.objects.get(pk=pk, user=user) #wishlist는 개인적인것이므로 유저가 같은지 확인
except Wishlist.DoesNotExist:
raise NotFound
def get(self,request,pk):
wishlist = self.get_object(pk, request.user)
serializer = WishlistSerializer(wishlist,context={"request":request})
return Response(serializer.data)
def delete(self, request, pk):
wishlist = self.get_object(pk, request.user)
wishlist.delete()
return Response(status=HTTP_200_OK)
def put(self, request, pk):
wishlist = self.get_object(pk, request.user)
serializer = WishlistSerializer(wishlist, data= request.data, partial=True)
if serializer.is_valid():
wishlist = serializer.save()
serializer = WishlistSerializer(wishlist)
return Response(serializer.data)
else:
return Response(serializer.errors)
class WishlistToggle(APIView):
def get_list(self, pk, user):
try:
return Wishlist.objects.get(pk=pk, user=user) #wishlist는 개인적인것이므로 유저가 같은지 확인
except Wishlist.DoesNotExist:
raise NotFound
def get_room(self, pk):
try:
return Room.objects.get(pk=pk)
except Room.DoesNotExist:
raise NotFound
def put(self, request,pk, room_pk):
wishlist = self.get_list(pk, request.user)
room = self.get_room(room_pk)
if wishlist.rooms.filter(pk=room.pk).exists(): #조건에 맞는 방이 존재하는지. True or False를 받게 됨. room_pk가 리스트에 있으면 삭제.
wishlist.rooms.remove(room)
else: #room_pk가 리스트에 없으면 추가.
wishlist.rooms.add(room)
return Response(status=HTTP_200_OK)
Wishlist는 MantToMany필드인 room 필드를 가지고 있다. 그래서 room list를 가지고 있고, 이를 이용해 room_list에 room이 있는지 확인해볼 수 있다.
유저는 PUT request를 사용해서 list에 넣고 싶은 room의 pk를 보낸다. 만약에 그 room의 pk가 위시리스트에 없으면 리스트에 추가하고, 위시리스트에 있다면 삭제하는 토글 기능이 필요하다. 이 기능을 'WishlistToggle'로 만들었다.
is_liked
RoomDetail에서 만들었던 'is_owner'와 비슷한 속성이다. 이것도 바라보는 유저에 따라 값이 달라져야 한다.
#rooms.serializers.py
class RoomDetailSerializer(ModelSerializer):
owner = TinyUserSerializer(read_only = True) #users.serializers.py에서 만든 serializer
amenities = AmenitySerializer(read_only=True,many=True)
category = CategorySerializer(read_only=True)
rating = serializers.SerializerMethodField()
is_owner = serializers.SerializerMethodField()
is_liked = serializers.SerializerMethodField()
photos = PhotoSerializer(many=True, read_only=True)
class Meta:
model = Room
fields = "__all__"
def get_rating(self,room):
return room.rating()
def get_is_owner(self, room):
request = self.context['request'] #어떤 유저가 이 방을 보고 있는지 확인하기 위함.
return room.owner == request.user
def get_is_liked(self,room):
request = self.context['request'] #어떤 유저가 이 방을 보고 있는지 확인하기 위함.
return Wishlist.objects.filter(user=request.user, rooms__pk=room.pk).exists() #한 유저가 여러개의 위시리스트를 가질 수 있으므로 get대신 filter 사용.
'wishlist.objects.filter(user=request.user,rooms__pk=room.pk).exist()'에서
user=request는 요청한 유저의 위시리스트를 가져오는 역할.
rooms__pk=room.pk는 유저의 위시리스트들 중 현재 보고있는 room의 pk가 있는 위시리스트를 가져오는 역할을 한다.
Bookings
#rooms. views.py
class RoomBookings(APIView):
permission_class = [IsAuthenticatedOrReadOnly] #로그인해야 예약가능하게
def get_object(self, pk):
try:
return Room.objects.get(pk=pk)
except:
raise NotFound
def get(self, request, pk):
room = self.get_object(pk)
now = timezone.localtime(timezone.now()).date() #시간은 필요없고 날짜만 받으려고
bookings = Booking.objects.filter(
room=room,
kind=Booking.BookingKindChoices.ROOM, #Room으로 예약된 것만
check_in__gt=now, #현재 날짜보다 이후인 날짜만
)
serializer = PublicBookingSerializer(bookings, many=True)
return Response(serializer.data)
def post(self, request, pk):
room = self.get_object(pk)
serializer = CreateRoomBookingSerializer(data=request.data)
if serializer.is_valid():
booking = serializer.save(
room=room,
user=request.user,
kind=Booking.BookingKindChoices.ROOM,
)
serializer = PublicBookingSerializer(booking)
return Response(serializer.data)
else:
return Response(serializer.errors)
bookings 앱에서 pk는 읽기 전용, check_in, check_out,experience_time은 옵션이다. 그래서 serializer는 유저가 필수 값인 guests만 보내도 유효하다고 판단할 수 있다. 하지만 guest정보만 가지고 예약이 되면 안된다.
그래서 새로운 serializer를 만들어서 거기에 check_in, check_out 필드가 필수 값이 되도록 덮어쓰기를 해주어야 한다.
그리고 체크인 날짜가 체크아웃 날짜보다 커야하고, 예약하려는 날짜에 이미 잡힌 예약이 없어야한다.
이것은 views.py에서 save()를 할 때, serializer에서 유효성 검사를 하는 것을 이용하여 유저의 잘못을 잡을 수 있다.
validate를 커스텀하여 views.py에서 is_validated()를 더욱 간편하게 쓸 수 있고, 유효성 검사의 기능을 더 향상시킬 수 있다.
#bookings. serializers.py
from django.utils import timezone
from rest_framework import serializers
from .models import Booking
class CreateRoomBookingSerializer(serializers.ModelSerializer):
check_in = serializers.DateField() #필수 값이 되도록 덮어쓰기
check_out = serializers.DateField() #필수 값이 되도록 덮어쓰기
class Meta:
model = Booking
fields = (
"check_in",
"check_out",
"guests",
)
#validate의 두번째 매개변수는 모든 데이터를 받아들인다.
def validate_check_in(self, value): #이를 이기면 serializer가 validate를 할 때 유효하지 않다고 보냄.
now = timezone.localtime(timezone.now()).date()
if now > value:
raise serializers.ValidationError("Can't book in the past!")
return value
def validate_check_out(self, value):
now = timezone.localtime(timezone.now()).date()
if now > value:
raise serializers.ValidationError("Can't book in the past!")
return value
def validate(self, data):
if data['check_out'] <= data['check_in']: #체크인이 체크아웃보다 커야함.
raise serializers.ValidationError("Check in should be smaller than Check out.")
if Booking.objects.filter( #체크인, 체크아웃을 예약하려는 날짜에 이미 예약이 있는지 알아보기 위헤.
check_in__lt = data["check_out"], #check_in날짜가 이미 잡힌 다른 예약의 check_out보다 작으면 안됨.
check_out__gte = data["check_in"], #check_out날짜가 이미 잡힌 다른 예약의 check_in보다 크면 안됨.
).exists():
raise serializers.ValidationError("Those(or some) of those datas are already taken.")
return data
class PublicBookingSerializer(serializers.ModelSerializer): #공개적으로 보여주기 위한 serializer
class Meta:
model = Booking
fields = (
"pk",
"check_in",
"check_out",
"experience_time",
"guests",
)
'노마드 코더 Airbnb 클론 코딩' 카테고리의 다른 글
노마드 코더 에어비앤비 클론 코딩 #15 AUTHENTICATION (0) | 2022.12.01 |
---|---|
노마드 코더 에어비앤비 클론 코딩 #12 USERS API (0) | 2022.11.23 |
노마드 코더 에어비앤비 클론 코딩 #11 Rest API - 1 (0) | 2022.11.08 |
노마드 코더 에어비앤비 클론 코딩 #10 Django Rest Framework (1) | 2022.11.05 |
노마드 코더 에어비앤비 클론 코딩 #9 Urls and Views (0) | 2022.10.26 |