노마드 코더 Airbnb 클론 코딩

노마드 코더 에어비앤비 클론 코딩 #11 Rest API - 2

gogi masidda 2022. 11. 19. 19:49

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 값이 달라진다.

 

is_owner.

 

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)

review pagination
amenity pagination

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()로 바꿔준다.

URLField()로 바꿔줌
serializers.py / views.py
URL로 보이는 파일명

 

permission_classes

'permission_class = [...]'로 'if request.user.is_authenticated' 구문을 대신할 수 있다. 

'IsAuthenticated'는 인증받아야 GET,POST 등 모든 메소드를 이용 가능하다.

'IsAuthenticatedOrReadOnly'는 인증을 받지 않으면 GET 메소드만 이용 가능하다.

delete가 잘 작동하는 모습

 

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",
        )

 

serializers.py에 추가한 validate로 오류를 잡는 어드민 패널
오류 없이 잘 작동할 때

728x90