All Amenities, Perks
Amenity와 Perk도 Category API를 만들 듯이 만들었다.
APIView를 쓰면, request.method가 GET인지 POST인지 확인하는 조건문이 필요없다.
serializers.py에서 ModelSerializer를 상속받아서 사용하면, 'id', 'created_at', 'updated_at'은 자동으로 read_only로 되어있다.
def ...
...
if serializer.is_valid():
amenity = serializer.save() #유효하다면 amenity에 저장
return Response(AmenitySerializer(amenity).data) #amenity를 번역하고 데이터 뽑아서 리턴
...
Rooms
Rooms는 이전과는 다르다. 새로운 Room을 만들려면, 그 유저가 host인지, 어떤 category인지, 어떤 amenity를 가지고 있는지 등을 알아야 한다. owner(user),category는 ForeignKey이고, amenity는 ManytoMany이다. 그래서 serializer간에 관계를 어떻게 만드는지 알아야 한다.
이전처럼 GET메소드를 만들면 관계를 가지고 있는 owner와 category, amenities는 pk로 나오게 된다. pk로 나오게 되면, 알아야 하는 정보를 한 번에 파악할 수 없다.
#serializers.py
class RoomSerializer(ModelSerializer):
class Meta:
model = Room
fields = "__all__"
depth = 1
serializer에서 'depth = 1'을 사용하면, pk 대신 그 데이터들의 모든 정보를 보이게 해준다. 하지만 필요하지 않은 정보도 보여주기 때문에좋은 방법이 아니다.
이를 해결하기 위해 serializer를 두개 만들 것이다.
① RoomListSerializer: Room을 나열하는 작은 serializer
fields를 이용해 pk, 이름, 국가, 도시(위치), 가격만 보여주게 한다.
② RoomDetailSerializer: Room을 하나만 볼 때, 상세정보를 보여주는 큰 serializer
여기서는 owner의 password 등 개인정보를 감춰야한다.
=>
users 모델 폴더에 serializers.py를 생성 후 원하는 fields를 담는 serializer를 생성한다. 만든 serializer를 rooms 모델 폴더의 serialzers.py에 import하고, RoomDetailSerializer 아래에 적어준다.
Room을 POST로 생성할 때, owner의 이름은 유저가 설정하면 안된다. 그래서 'read_only=True'를 추가해주는 것이다.
amenities와 category는 선택하는 것이므로 유저가 임의대로 적어서 생성하면 안되기 때문에 'read_only=True'를 추가해준다.
Room의 amenities처럼 가져올 것이 리스트이면 'many=True'를 적어주어야 한다.
POST 기능으로 room을 생성했을 때, owner의 정보는 자동으로 django가 채워줘야한다.
request object는 누가 이 url로 요청을 보냈는지 많은 정보를 갖고 있다.
'room = serializer.save()'를 하기 전에, owner가 request.user인 것을 전달해주어야 한다. 그런데 이보다 먼저 user가 인증된 유저인지 확인부터 해야한다.
owner 정보를 제대로 가져오고 있음.
Room Category, Amenity
amenity와 category는 각각 이전에 만들어둔 AmenitySerializer와 CategorySerializer로 인해 형태가 정해져있다. 그리고 ForeignKey와 ManytoMany이기 때문에 유저가 임의로 보낸 데이터는 안된다.
① Category
유저가 POST 기능으로 Category의 pk를 보내면 그 pk에 맞는 Category를 찾아서 보여주는 방식으로 한다. 만약 pk에 맞는 Category가 없을 경우, 오류메세지를 보낸다. 그리고 owner와 같은 방식으로 save()에 추가해준다.
② Amenity
save() 메소드로 Room이 DB에 저장된 후에, 유저가 POST 기능으로 보낸 amenities를 Room 객체에 추가해주는 방식으로 한다.
amenities는 Category와 다르게 ManytoMany방식으로 Room과 관계되어있다. 그래서 amenities는 리스트가 되어야 한다.
그래서 'room.amenities.add(a)'로 리스트에 하나씩 추가시킨다.
#Rooms. views.py
class Rooms(APIView):
def get(self, request):
all_rooms = Room.objects.all()
serializer = RoomListSerializer(all_rooms, many = True)
return Response(serializer.data)
def post(self, request):
if request.user.is_authenticated:
serializer = RoomDetailSerializer(data=request.data)
if serializer.is_valid():
category_pk = request.data.get("category") #유저가 보낸 데이터에서 category의 pk를 가져옴.
if not category_pk: #category pk를 안적었으면 오류
raise ParseError("Category is required.")
try:
category = Category.objects.get(pk=category_pk) #category_pk에 맞는 category가 없으면 오류
if category.kind == Category.CategoryKindChoices.EXPERIENCES: #Room이므로 kind가 experience이면 오류
raise ParseError ("The category kind should be 'rooms'.")
except Category.DoesNotExist:
raise ParseError("Category not found")
room = serializer.save(owner=request.user, category=category) #validated_data에 owner, category 추가
#ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡamenitiesㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
amenities = request.data.get("amenities")
for amenity_pk in amenities: #amenities는 필수 항목이 아니라서 save 뒤에서 받기. 나중에도 추가할 수 있도록.
try:
amenity = Amenity.objects.get(pk=amenity_pk) #amenities 모델에서 amenity_pk에 맞는 amenity 가져오기
room.amenities.add(amenity) #amenity 추가
except Amenity.DoesNotExist: #amenity_pk에 맞는 amenity가 존재하지 않을 경우 오류.
pass #객체를 생성할 때, amenities만 잘못 적었을 경우에는 amenities를 비워두고 객체 생성.
serializer = RoomDetailSerializer(room)
return Response(serializer.data)
else:
return Response(serializer.errors)
else:
raise NotAuthenticated
Transaction: 모두 성공하거나 아무것도 성공하지 않기를 원할 때 사용
만약 유저가 존재하지 않는 amenity를 보내거나 amenity를 리스트가 아닌 형태로 보내면 에러가 발생해야 한다.
이 때, 방을 생성하는 것은 잘 작동하지만, amenity가 존재하지 않게 된다. 이 때, 딱 amenity 쿼리만 실패하게 되어있다. 그래서 생성된 방을 수동으로 삭제해주어야 한다. 수동으로 삭제해주게 되면 pk가 낭비되게 된다.
모두 성공해야만 Room이 생성되는 코드의 세트를 만들면 된다. 만약 성공하지 못했다면, 전체 코드와 변화들이 되돌려진다. 이를 위해서 Transaction을 사용한다.
from django.db import transaction
...
with transaction.atomic():
'with transaction.atomic():' 안에 코드를 두면 django는 이 코드를 즉시 DB에 반영하는 것이 아니라, 오류가 발생하지 않아야 DB로 보낸다. 이 안에 try-exception 구문을 두면 오류가 발생했음을 알지 못하기 때문에 제거해야한다.
try-except문 안에 with transaction.atomic()을 두어 오류가 발생했을 경우, 유저에게 오류가 발생된 이유를 보낸다.
#rooms. views.py
def put(self, request, pk):
room = self.get_object(pk)
if not request.user.is_authenticated:
raise NotAuthenticated
if room.owner != request.user: #방의 주인과 삭제를 요청하는 유저가 다르면
raise PermissionDenied
serializer = RoomDetailSerializer(room, data=request.data, partial=True,)
if serializer.is_valid():
category_pk = request.data.get("category")
if category_pk:
try:
category = Category.objects.get(pk=category_pk)
if category.kind == Category.CategoryKindChoices.EXPERIENCES:
raise ParseError("The category kind should be 'rooms'.")
except Category.DoesNotExist:
raise ParseError("Category not found.")
try:
with transaction.atomic():
if category_pk: #category를 바꾸려할 경우
room = serializer.save(category=category)
else:
room = serializer.save() #category를 바꾸려하는게 아니면 저장
amenities = request.data.get("amenities") #저장 후에 amenity 바꾸기
if amenities:
room.amenities.clear()
for amenity_pk in amenities:
amenity = Amenity.objects.get(pk=amenity_pk)
room.amenities.add(amenity)
return Response(RoomDetailSerializer(room).data)
except Exception:
raise ParseError("Amenity not found")
else:
return Response(serializer.errors)
def delete(self, request, pk):
room = self.get_object(pk)
if not request.user.is_authenticated:
raise NotAuthenticated
if room.owner != request.user: #방의 주인과 삭제를 요청하는 유저가 다르면
raise PermissionDenied
room.delete()
return Response(status=HTTP_204_NO_CONTENT)
PUT메소드와 DELETE 메소드도 만든다.
'노마드 코더 Airbnb 클론 코딩' 카테고리의 다른 글
노마드 코더 에어비앤비 클론 코딩 #12 USERS API (0) | 2022.11.23 |
---|---|
노마드 코더 에어비앤비 클론 코딩 #11 Rest API - 2 (1) | 2022.11.19 |
노마드 코더 에어비앤비 클론 코딩 #10 Django Rest Framework (1) | 2022.11.05 |
노마드 코더 에어비앤비 클론 코딩 #9 Urls and Views (0) | 2022.10.26 |
노마드 코더 에어비앤비 클론 코딩 #8 Power Admin (0) | 2022.10.21 |