해쟈스토리

클론코딩 - 에어비앤비 2주차 본문

파이썬

클론코딩 - 에어비앤비 2주차

해쟈 2024. 10. 7. 04:20
728x90
반응형

ORM

python manage.py shell

 

python console을 제공하는데 일반적인 python과 다르다. django가 구성된 콘솔임.

우리가 settings에 적었던 설정들이 적용되어 있고 model에 접근도 가능한 터미널인 셈.

 

 

그래서 모든 room을 가져오려면 어떻게 해야할까?

>>> from rooms.models import Room
>>> Room.objects.all()
<QuerySet [<Room: room1>]>

>>> Room.objects.get(name="room1")
<Room: room1>

>>> room = Room.objects.get(name="room1")
>>> room.pk
1

>>> room.owner
<User: haeji>
>>> room.owner.email
'haeji1124@naver.com'

 

위과 같이 model의 정보를 가져올 수 있다. 

그러면 모델의 데이터를 수정하려면 어떻게 할 수 있을까?

>>> room.price
100
>>> room.price = 20
>>> room.save()
>>> room.price
20

 

filter, get, create, delete를 알아보자

app을 만들면 자동으로 manager가 생긴다. 모든 model은 objects라는게 생김.

>>> for room in Room.objects.all():
...     print(room.name)
...
room1

 

위와 같은게 가능해짐.

 

>>> Room.objects.all()
<QuerySet [<Room: room1>, <Room: room2>]>
>>> Room.objects.get(pet_friendly=True)
<Room: room1>

 

get을 이용해서 특정 속성의 값이 일치할때 가져올수 있다.

만약 room1, room2모두 pet_friendly하면 어떨까?

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "C:\Users\sungj\AppData\Local\pypoetry\Cache\virtualenvs\airbnb-clone-backend-yX6RVG4g-py3.10\lib\site-packages\django\db\models\manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\Users\sungj\AppData\Local\pypoetry\Cache\virtualenvs\airbnb-clone-backend-yX6RVG4g-py3.10\lib\site-packages\django\db\models\query.py", line 652, in get
    raise self.model.MultipleObjectsReturned(
rooms.models.Room.MultipleObjectsReturned: get() returned more than one Room -- it returned 2!

 

위와 같이 return이 2개라며 에러가 발생한다. get은 하나만 return 할 수 있다.

 

그러면 2개이상인 애들 가져오려면 어떻게 해야할까? 이때 filter를 사용한다.

>>> Room.objects.filter(pet_friendly=True)
<QuerySet [<Room: room1>, <Room: room2>]>

 

get은 없는 애를 가져오려고 해도 error가 발생한다.

>>> Room.objects.get(pet_friendly=False)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "C:\Users\sungj\AppData\Local\pypoetry\Cache\virtualenvs\airbnb-clone-backend-yX6RVG4g-py3.10\lib\site-packages\django\db\models\manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\Users\sungj\AppData\Local\pypoetry\Cache\virtualenvs\airbnb-clone-backend-yX6RVG4g-py3.10\lib\site-packages\django\db\models\query.py", line 649, in get
    raise self.model.DoesNotExist(
rooms.models.Room.DoesNotExist: Room matching query does not exist.

 

하지만 filter는 빈리스트를 반환한다.

>>> Room.objects.filter(pet_friendly=False)
<QuerySet []>

 

filter는 많은게 가능하다 일치하는 것 뿐만아니라 name에 특정 문자열이 있는것만 가져온다던지, price가 15이상인 애를 가져온다던지.

>>> Room.objects.filter(price__gt=15)
<QuerySet [<Room: room1>, <Room: room2>]>
>>> Room.objects.filter(price__gt=150)
<QuerySet [<Room: room2>]>
>>> Room.objects.filter(name__contains='2')
<QuerySet [<Room: room2>]>

 

이제 create를 해보자

>>> from rooms.models import Amenity
>>> Amenity.objects.all()
<QuerySet [<Amenity: 샴푸>]>
>>> Amenity.objects.create(name="린스")
<Amenity: 린스>
>>> Amenity.objects.all()
<QuerySet [<Amenity: 샴푸>, <Amenity: 린스>]>

 

지우는 것도 해보자 delete

>>> to_delete = Amenity.objects.get(pk=2)
>>> to_delete.delete()
(1, {'rooms.Amenity': 1})
>>> Amenity.objects.all()
<QuerySet [<Amenity: 샴푸>]>

 

QuerySets

그런데 우리는 이렇게 하나씩? 처리하는 거는 많지 않다. 무슨말이냐면 필터를 여러개 중첩할수도 있고 어떤 특정 list가져오고 그다음 뭔가 할수도 있다. 이럴때 어떻게 하나?

>>> Room.objects.filter(pet_friendly=True).exclude(price__lt=150).filter(name__contains="2")
<QuerySet [<Room: room2>]>

 

위와 같이 pet_friendly는 true이면서 price가 150보다 낮은걸 제외하고 거기거 이름에2가 들어가는 거를 필터할 수 있다.

근데 이건 좀 비효율적이고 좀더 간단하게는 아래와 같이 가능하다.

 

Room.objects.filter(
    pet_friendly=True,
    price__gte=150,
    name__contains="2"
)

 

Admin Methods

__contains이런 것들을 lookup이라고 한다. 

__exact는 동일한거

__iexact는 동일한데 대소문자 구분없이

__icontains도 포함된거 찾는데 대소문자 구분없이

__startswith, __endswith, __istatswith, __iendswith도 있다.

 

이런 룩업은 아래에서 확인 가능하다

https://docs.djangoproject.com/en/4.1/ref/models/querysets/#field-lookups

 

 

자, 이제 admin페이지에서 rooms에서 각 room이 가진 amenity 수를 나타내는 컬럼이 하나를 만들어보자.

admin 수정

 

@admin.register(Room)
class RoomAdmin(admin.ModelAdmin):
    list_display = (
        "name",
        "price",
        "kind",
        "owner",
        "total_amenities",
        "created_at",
        "updated_at",
    )
    list_filter = (
        "country",
        "city",
        "pet_friendly",
        "kind",
        "amenities",
        "created_at",
        "updated_at",
    )

 

위와 같이 total_amenities를 넣으면 서버에서 에러가 난다

Exception in thread django-main-thread:
Traceback (most recent call last):
  File "C:\Users\sungj\AppData\Local\Programs\Python\Python310\lib\threading.py", line 1016, in _bootstrap_inner
    self.run()
  File "C:\Users\sungj\AppData\Local\Programs\Python\Python310\lib\threading.py", line 953, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Users\sungj\AppData\Local\pypoetry\Cache\virtualenvs\airbnb-clone-backend-yX6RVG4g-py3.10\lib\site-packages\django\utils\autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
  File "C:\Users\sungj\AppData\Local\pypoetry\Cache\virtualenvs\airbnb-clone-backend-yX6RVG4g-py3.10\lib\site-packages\django\core\management\commands\runserver.py", line 134, in inner_run
    self.check(display_num_errors=True)
  File "C:\Users\sungj\AppData\Local\pypoetry\Cache\virtualenvs\airbnb-clone-backend-yX6RVG4g-py3.10\lib\site-packages\django\core\management\base.py", line 563, in check
    raise SystemCheckError(msg)
django.core.management.base.SystemCheckError: SystemCheckError: System check identified some issues:

ERRORS:
<class 'rooms.admin.RoomAdmin'>: (admin.E108) The value of 'list_display[4]' refers to 'total_amenities', which is not a callable or attribute of 'RoomAdmin', or an attribute, method, or field on 'rooms.Room'.

System check identified 1 issue (0 silenced).

 

model를 수정해보자. room 클래스 안에 total_amenities라는 메소드를 생성한다.

def total_amenities(room):
    return room.amenities.count()

 

그러면 에러가 사라진다.

혹은 admin 에 적용할 수도 있다. roomAdmin 클래스 내에 메소드를 만든다. 

 

def total_amenities(self, room):
    return room.amenities.count()

 

이런 방법들은 해단 컬럼이 admin에서만 쓰이는 컬럼인지 추후 API가 컨트롤해줘야하는 컬럼인지에 따라 위치를 정할 수 있다.

 

reverse accessors

관계를 뒤집어서 접근할 수 있게 해준다.

foreign key를 통해 room과 user를 연결해줬는데 room에서 owner가 누구인지는 알수 있었음.

그런데 반대로 user가 어떤 room을 소유했는지 알기 위해서 필요한데 reverse accessors다.

물론 필터링으로 가능하긴 하다. 모든 room에서 owner가 누구인지 찾으면 되니까

 

근데 매번 필터링하기 싫은거야. 이런식으로 알아내야하는게 room - user도 있고 review도 있 고 엄청 많아.

그냥 user.rooms, user.reviews, user.wishlists 이런식으로 접근하고 싶어.

이걸 위해서 reverce accessor를 하는 거다.

 

>>> from users.models import User
>>> me = User.objects.get(pk=1)
>>> me
<User: haeji>
>>> me.rooms
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'User' object has no attribute 'rooms'
>>> dir(me)
['CurrencyChoices', 'DoesNotExist', 'EMAIL_FIELD', 'GenderChoices', 'LanguageChoices', 'Meta', 'MultipleObjectsReturned', 'REQUIRED_FIELDS', 'USERNAME_FIELD', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__',
'__weakref__', '_check_column_name_clashes', '_check_constraints', '_check_db_table_comment', '_check_default_pk', '_check_field_name_clashes', '_check_fields', '_check_id_field', '_check_indexes', '_check_local_fields', '_check_long_column_names', '_check_m2m_through_same_relationship', '_check_managers', '_check_model', '_check_model_name_db_lookup_clashes', '_check_ordering', '_check_property_name_related_field_accessor_clashes', '_check_single_primary_key', '_check_swappable', '_check_unique_together', '_do_insert', '_do_update', '_get_FIELD_display', '_get_expr_references', '_get_field_expression_map', '_get_next_or_previous_by_FIELD', '_get_next_or_previous_in_order', '_get_pk_val', '_get_session_auth_hash', '_get_unique_checks', '_meta', '_parse_params', '_password', '_perform_date_checks', '_perform_unique_checks', '_prepare_related_fields_for_save', '_save_parents', '_save_table', '_set_pk_val', '_state', '_validate_force_insert', 'acheck_password', 'adelete', 'arefresh_from_db', 'asave', 'avatar', 'booking_set', 'chattingroom_set', 'check', 'check_password', 'clean', 'clean_fields', 'currency', 'date_error_message', 'date_joined', 'delete', 'email', 'email_user', 'experience_set', 'first_name', 'from_db', 'full_clean', 'gender', 'get_all_permissions', 'get_constraints', 'get_currency_display', 'get_deferred_fields', 'get_email_field_name', 'get_full_name', 'get_gender_display', 'get_group_permissions', 'get_language_display', 'get_next_by_date_joined', 'get_previous_by_date_joined', 'get_session_auth_fallback_hash', 'get_session_auth_hash', 'get_short_name', 'get_user_permissions', 'get_username', 'groups', 'has_module_perms', 'has_perm', 'has_perms',
'has_usable_password', 'house_set', 'id', 'is_active', 'is_anonymous', 'is_authenticated', 'is_host', 'is_staff', 'is_superuser',
'language', 'last_login', 'last_name', 'likes', 'logentry_set', 'message_set', 'name', 'natural_key', 'normalize_username', 'objects', 'password', 'pk', 'prepare_database_save', 'refresh_from_db', 'review_set', 'room_set', 'save', 'save_base', 'serializable_value', 'set_password', 'set_unusable_password', 'tweets', 'unique_error_message', 'user_permissions', 'username', 'username_validator', 'validate_constraints', 'validate_unique', 'wishlist_set']

 

dir을 해보면 많은 _set이라는게 있다. rooms_set, revies_set 등등

이게 바로 reverse accesor다.

 

>>> me.room_set.all()
<QuerySet [<Room: room1>, <Room: room3>]>

 

이제 여기서 room_set.all()이 아니라 .rooms로 접근하게 해보자

rooms model에서 owner부분에 related_name='rooms'를 하면 된다.

그 다음 makemigration후 migrate한다.

 

모든 model들의 foreign key 부분에 related_name을 넣어주자

참고 : 7.6 related_name · nomadcoders/airbnb-clone-backend@64163fc (github.com)

 

7.6 related_name · nomadcoders/airbnb-clone-backend@64163fc

serranoarevalo committed Aug 30, 2022

github.com

 

Power Admin

액션, 커스텀 필터 등을 해보자.

먼저 평균 review 점수를 admin 페이지에 나타내보자.

def rating(room):
        count = room.reviews.count()
        total_rating = 0

        if count:
            for review in room.reviews.all():
                total_rating += review.rating
               
            return round(total_rating / count, 2)
        else:
            return "No Reviews"

 

위와 같이 진행할 수 있다. 근데 for문을 보면 review객체를 가져오게 되는데 사실상 지금 필요한건 rating 뿐이다. 그래서 효율적으로 가져오기 위해 아래와 같이 진행한다.

def rating(room):
        count = room.reviews.count()
        total_rating = 0

        if count:
            for review in room.reviews.all().values("rating"):  # [{rating: 5}, {rating: 4}]
                total_rating += review["rating"]
            return round(total_rating / count, 2)
        else:
            return "No Reviews"

 

values를 쓰면 딕셔너리형태로 가져오게 된다. 그래서 review['rating'] 으로 딕셔너리 값을 꺼내는 방식으로 가져와야한다.

 

검색

room에서 검색창을 넣어보자. admin에 아래와 같이 roomAdmin 클래스에 추가한다. 

search_fields = (
        "name",
        "price",
    )

 

기본적으로 검색은 __contains로 찾게 된다. 근데 검색방식을 바꾸고 싶을수도 있잖아. 만약 startswith로 하고 싶다던지.

그럴때는 ^필드명으로 하면 된다.

 

2로 검색하면 name에 2거나 price에 2들어간 애들이 나온다

 

^를 적용해보자

 

현재 rooms는 위와 같다. 서울로 검색해보자

 

다른 방식은 어떻게 할 수 있을까?

=는 정확히 같은거를 나타낸다.

이런식으로도 된다.

owner__contains, owner__username

 

owner_username 에 앞에 ^, =를 추가할 수 있다.

 

Admin Actions

action을 등록하기 위해서는 @admin.action을 사용하면 된다. class밖에...

그리고나서 roomAdmin 클래스 안에 actions = (방금만든 데코레이터,) 를 넣어준다

@admin.action(description="set all prices to zero")
def reset_prices(model_admin, request, rooms):
    for room in rooms.all():
        room.price = 0
        room.save()

@admin.register(Room)
class RoomAdmin(admin.ModelAdmin):
    actions = (reset_prices,)

 

Custom Filter

reviews에서 user모델의 필드중 is_host의 값에 따라 필터링을 하고 싶으면 어떻게 해야할까?

is_host는 reviews model에 없기 때문에 list_filter에 추가하면 에러가 난다. 그래서 필터링에 넣을때는 아래와 같이 추가해야한다.

list_filter = ("user__is_host",)

 

foriegnkey를 계속 타고 들어갈수도 있다. room_category_something  이런식으로

필터나 action을 위한 코드는 따로 떼고 import해서 써도 된다.

 

어쨌든 커스텀 필터를 위한 클래스 만들고 그 클래스를 list_filter 에 넣어주면 된다.

class WordFilter(admin.SimpleListFilter):
    title = "Filter by words!"
    parameter_name = "word"
    def lookups(self, request, model_admin):
        return [
            ("good", "Good"),
            ("great", "Great"),
            ("awesome", "Awesome"),
        ]
    def queryset(self, request, reviews):
        word = self.value()
        if word:
            return reviews.filter(payload__contains=word)
        else:
            reviews

 

SimpleListFilter를 상속받으면 lookups라는 메소드를 반드시 구현해야된다.

parameter_name은 필터를 적용하면 url에 표시되는 값이다. ex. ?word=good 이런식

원래 url에서 word가 뭔지 뽑아내야는데, django는 self.value로 받아올 수 있다.

 

Urls and Views

config/urls.py를 수정하자. 이 파일은 유저가 특정 url로 접근시 장고가 해야할 행동들이 적혀있다.

url은 config에 모든 url을 적어도 되고, apps마다 url을 만들어도 된다.

views.py는 어차피 url에서 import를 해서 쓰기 때문에 꼭 views라는 이름을 가질 필요는 없다. 다른 model이나 admin은 이름을 지켜줘야한다.

 

urls.py

from django.contrib import admin
from django.urls import path
from rooms import views

urlpatterns = [
    path("admin/", admin.site.urls),
    path("rooms", views.say_hello),
]

 

views.py

from django.http import HttpResponse

# Create your views here.
def say_hello(request):
    return HttpResponse("hello!")

 

include

config/url을 쪼개서 apps/url로 옮겨주자

먼저 config에 있는 urls.py를 아래와 같이 바꿔준다. include를 사용

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("rooms/", include("rooms.urls")),
]

 

그리고나서 rooms/urls.py를 바꿔준다

from django.urls import path
from . import views

urlpatterns = [
    path("", views.say_hello),
]

 

 

url arguments

urls의 argument를 가져오기 위해서 views에 아래와 같이 적어준다

from django.http import HttpResponse


# Create your views here.
def see_all_rooms(request):
    return HttpResponse("see all rooms")


def see_one_room(request, room_id):
    return HttpResponse(f"see room with id: {room_id}")

 

urls를 수정한다.

from django.urls import path
from . import views

urlpatterns = [
    path("", views.see_all_rooms),
    path("<int:room_id>", views.see_one_room),
]

 

path를 써줄때, <타입:argment name>을 적어준다.

 

render

flask template render와 유사하다.

from django.shortcuts import render
from django.http import HttpResponse
from .models import Room


# Create your views here.
def see_all_rooms(request):
    rooms = Room.objects.all()
    return render(
        request,
        "all_rooms.html",
        {
            "rooms": rooms,
            "title": "Hello! this title comes from django!",
        },
    )

 

render라는 메소드로 rendering할수 있는데 request, 템플릿명, 넘겨줄 데이터

템플릿은 apps 폴더에서 찾게 되어 있다. 그래서 templates디렉토리 생성 후 html파일 만들자.

<h1>{{title}}</h1>

 

Templates

<h3>{{title}}</h3>
<ul>
  {% for room in rooms %}
  <li>
    <a href="/rooms/{{room.pk}}">
      {{room.name}}<br />
      {% for amenity in room.amenities.all %}
      <span>-{{amenity.name}}<br /></span>
      {% endfor%}</a
    >
  </li>
  {% endfor %}
</ul>

 

 

이제 위와 같이 rooms url에 가면 rooms 목록을 볼 수 있다.

 

하나의 Room 상세보기 및 NorExists

이제 하나의 room에 대해 상세보기 페이지를 만들자.

def see_one_room(request, room_pk):
    try:
        room = Room.objects.get(pk=room_pk)
        return render(
            request,
            "room_detail.html",
            {
                "room": room,
            },
        )
    except Room.DoesNotExist:
        return render(
            request,
            "room_detail.html",
            {
                "not_found": True,
            },
        )

 

템플릿을 만들자

{% if not not_found %}
    <h1>{{room.name}}</h1>
    <h3>{{room.country}}/{{room.city}}</h3>
    <h4>{{room.price}}</h4>
    <p>{{room.description}}</p>
    <h5>{{room.category.name}}</h5>
{% else %}
	<h1>404 not found</h1>
{% endif %}

 

url에서 정의한 room_pk를 view가 받아서 DB에서 꺼내고 그걸 토대로 템플릿한테 보내준다.

urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("", views.see_all_rooms),
    path("<int:room_pk>", views.see_one_room),
]

 

없는 id를 넣으면(rooms/404) view.py에 의해서 동작하는데 try문에 의해 excep되고 없으면 template을 render할때 not_found라는 속성을 넘겨준다. template에서는 이 속성의 존재여부에 따라 404 not found를 return하거나 data를 return한다.

 

Django VS React

근데 template은 잘 안써. 왜냐면 이 방법으로는 다이나믹한 웹을 만들기 힘들어.

예를 들어 로그인창을 만드는데, 유저가 로그인하면 로딩을 보여주고 환영한다는 알림을 보여주고 싶어. 그리고 나서 url을 빠르게 바꿔주고 싶어. 이 모든 것은 템플릿으로는 불가능해.

그래서 템플릿대신 리액트를 쓸거야.

 

Django REST Framework

poetry add djangorestframework

 

설치하고 나서 config/setting.py 파일을 수정한다.

THIRD_PARTY_APPS = [
    "rest_framework",
]

INSTALLED_APPS = SYSTEM_APPS + CUSTOM_APPS + THIRD_PARTY_APPS

 

리액트와 통신하기 위해 rest api를 만들어야한다. json형식으로 보내야하니까.

 

카테고리 rest api를 만들어보자.

일단 category app에서 urls.py을 만들고 config의 urls를 수정해주자

from django.urls import path
from . import views

urlpatterns = [
    path("", views.categories),
]

 

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("rooms/", include("rooms.urls")),
    path("tweets/", include("tweets.urls")),
    path("categories/", include("categories.urls")),
]

 

그리고 view를 만들어주자

from django.http import JsonResponse
from .models import Category

def categories(request):
    all_categories = Category.objects.all()
    return JsonResponse(
        {
            "ok": True,
            "categories": all_categories,
        }
    )

 

그런데 all_categories는 querySet인데 이걸 json으로 바꿀수가 없어. serialize가 안돼.

파이썬 객체를 JSON으로 변환해야함.

 

api_view

위에서 serialize가 필요하다고 했다. 이를 위해서는 그냥 serializer를 import하면 된다.

from django.http import JsonResponse
from django.core import serializers
from .models import Category

def categories(request):
    all_categories = Category.objects.all()
    return JsonResponse(
        {
            "ok": True,
            "categories": serializers.serialize("json", all_categories), 
        }
    )

 

이렇게 계속 해도 되지만, categories/1 을 할때도 serializer써여되고 rooms에서도 써야된다. 

또한 내가 특정 컬럼은 안보여주고 싶은데 그런데 안돼.

그래서 django rest framework를 쓰는거야.

 

rest_framework.decorators의 api_view를 import 하자

그리고 데코레이터를 함수에 적용시켜주자, 또한 JsonResponse 대신 django rest framework의 Response를 사용하자

# from django.http import JsonResponse
# from django.core import serializers
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Category

@api_view()
def categories(request):
    # all_categories = Category.objects.all()
    # return JsonResponse(
    return Response(
        {
            "ok": True,
            # "categories": serializers.serialize("json", all_categories),
            "categories": Category.objects.all(),
        }
    )

 

 

"categories" 부분을 빼면 아래와 같은 화면이 나온다

 

categories 부분을 넣으면 아래와 같이 에러가 발생한다.

 

Serializer

자동으로 category가 JSON으로 변환되지 않아서 위와 같은 에러가 발생한다.

categories/serializers.py 파일을 생성한다.

이전에는 django core serializer를 사용했었는데 이것도 좋지만, 커스터마이징이 안된다.

from rest_framework import serializers

class CategorySerializer(serializers.Serializer):
    pk = serializers.IntegerField()
    name = serializers.CharField(required=True)
    kind = serializers.CharField()
    created_at = serializers.DateTimeField()

 

위와 같이 어떤 필드를 보여줄지 커스터마이징 한다.

그리고 나서 view.py 파일에 import 해준다. 같은 종류의 여러개의 객체를 serialize하려면 many=True해야한다.

from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Category
from .serializers import CategorySerializer


@api_view()
def categories(request):
    all_categories = Category.objects.all()
    serializer = CategorySerializer(all_categories, many=True)
    return Response(
        {
            "ok": True,
            "categories": serializer.data,
        }
    )

 

이제 특정 cate의 id를 받는 url을 만들어보자

urls.py를 수정하자

from django.urls import path
from . import views

urlpatterns = [
    path("", views.categories),
    path("<int:pk>", views.category),
]

 

현재는 views.category함수는 없다. views.py에서 만들어주자

@api_view()
def category(request, pk):
    category = Category.objects.get(pk=pk)
    serializer = CategorySerializer(category)
    return Response(serializer.data)

 

 

이제 POST API도 만들어보자

api_view 데코레이터에 POST를 추가하자

@api_view(["GET", "POST"])
def categories(request):
    if request.method == "GET":
        all_categories = Category.objects.all()
        serializer = CategorySerializer(all_categories, many=True)
        return Response(serializer.data)
    elif request.method == "POST":
        Category.objects.create(
            name=request.data["name"],
            kind=request.data["kind"],
        )
        return Response({"created": True})

 

그러나 위와 같이 하는 것은 좋은것이 아니다. 왜냐면 user가 준 값에 대해 검증이 없기 때문. 이러면 데이터베이스에서 에러가 날 수 있다. 

 

is_valid()

모델에 작성한 것과 같이 name은 50자라는 글자 제한을 가지지만 api로 post할때 이를 검증하는 절차가 없다.

검증을 위해서는 serializer에 형식을 적어주고 views에서 가져다 쓸때 .is_valid()만 쓰면 된다.

class CategorySerializer(serializers.Serializer):

    pk = serializers.IntegerField(read_only=True)
    name = serializers.CharField(
        required=True,
        max_length=50,
    )
    kind = serializers.CharField(
        max_length=15,
    )
    created_at = serializers.DateTimeField(read_only=True)
elif request.method == "POST":
        serializer = CategorySerializer(data=request.data)
        if serializer.is_valid():
            return Response({"created": True})
        else:
            return Response(serializer.errors)

 

그런데 kind는 categories 모델을 보면 choice class를 만들었고 그중 하나를 선택하게 되어 있다.

class Category(CommonModel):
    """Room or Experience Category"""
    class CategoryKindChoices(models.TextChoices):
        ROOMS = "rooms", "Rooms"
        EXPERIENCES = "experiences", "Experiences"
    
    name = models.CharField(max_length=50)
    kind = models.CharField(
        max_length=15,
        choices=CategoryKindChoices.choices,
    )
    def __str__(self) -> str:
        return f"{self.kind.title()}: {self.name}"
    
    class Meta:
        verbose_name_plural = "Categories"

 

그러면 kind와 같이 choices로 된 애들을 검증은 어떻게 할까?

model import하고 choices class를 넣어주면 된다.

from rest_framework import serializers
from .models import Category

class CategorySerializer(serializers.Serializer):
    pk = serializers.IntegerField(read_only=True)
    name = serializers.CharField(
        required=True,
        max_length=50,
    )
    kind = serializers.ChoiceField(
        choices=Category.CategoryKindChoices.choices,
    )
    created_at = serializers.DateTimeField(read_only=True)

 

Save()

 

우리는 POST에서 실제로 DB를 Create하는 부분이 없었다. 그러면 Category.objects.create하면 될까? 안된다.

serializer는 save라는 메소드를 가지고 있다. 그러나 save를 한다고 데이터베이스가 달라지진 않는다. save는 어떤 메소드를 수행할 뿐이다. serializer의 save은 create라는 메소드를 자동으로 수행하게 되어 있다. 그러나 현재 우리는 create라는 메소드가 없다.

즉, views에서 serializer.save()를 하면 serializer의 create메소드를 실행하게 된다. 

from rest_framework import serializers
from .models import Category


class CategorySerializer(serializers.Serializer):
    pk = serializers.IntegerField(read_only=True)
    name = serializers.CharField(
        required=True,
        max_length=50,
    )
    kind = serializers.ChoiceField(
        choices=Category.CategoryKindChoices.choices,
    )
    created_at = serializers.DateTimeField(read_only=True)

    def create(self, validated_data):
        return Category.objects.create(**validated_data)

 

Category.objects.create시 아래와 같이 할 수 있는데

 

이렇게 하면 필드가 늘어날때마다 다 넣어줘야한다. 불편하지. 파이썬은 dict를 **를 써서 풀어서 알아서 해준다. 그래서 **validated_data를 해주는 것이다.

 

update()

PUT은 어떻게 할 수 있을까? POST랑 비슷하다. 일단 수정을 위해서는 PK로 찾아야하는데 없는 PKK를 찾으려고 하면 DoesNotExist error가 발생한다. 이를 위해 try-catch로 잡아준다. error가 발생하면 NotFound를 raise하도록 아래와 같이 import해준다.

from rest_framework.exceptions import NotFound

 

 

@api_view(["GET", "PUT"])
def category(request, pk):
    try:
        category = Category.objects.get(pk=pk)
    except Category.DoesNotExist:
        raise NotFound
    if request.method == "GET":
        serializer = CategorySerializer(category)
        return Response(serializer.data)
    elif request.method == "PUT":

 

이제 update 부분을 해보자. post와 같이 serializer를 쓸건데 유저가 준 데이터와 DB에서 가져온 데이터를 기반으로 만든다. 

 

@api_view(["GET", "PUT"])
def category(request, pk):
    try:
        category = Category.objects.get(pk=pk)
    except Category.DoesNotExist:
        raise NotFound
    if request.method == "GET":
        serializer = CategorySerializer(category)
        return Response(serializer.data)
    elif request.method == "PUT":
        serializer = CategorySerializer(
            category,
            data=request.data,
            partial=True,
        )
        if serializer.is_valid():
            updated_category = serializer.save()
            return Response(CategorySerializer(updated_category).data)
        else:
            return Response(serializer.errors)

 

POST랑 비교해보면 거의 같다. serializer  부분만 다르다. 우리가 업데이트할때는 kind만 바꾸고 싶을 수도 있고 name만 바꾸고 싶을 수도 있어. 두 필드 모구 필수사항이라서 없을수가 없는데 업데이트할때 하나만 업데이트하고 싶어서 값이 없을 수 있어. 그래서 serializer에게 우리가 보내는 데이터가 일부만 보낼 수 있다고 말을해줘야한다. 그게 partial=True이다.

 

POST일때 serializer.save가 create메소드를 부르듯이 PUT일때는 save가 update메소드를 부른다. 그래서 serializer.py를 수정해야한다.

def update(self, instance, validated_data):
        instance.name = validated_data.get("name", instance.name)
        instance.kind = validated_data.get("kind", instance.kind)
        instance.save()
        return instance

 

instance는 update의 경우 DB에 이미 있는 data(instance) + 유저가 입력한 데이터(request.data)를 합쳐서 실행해야되기 때문에 create보다 매개변수가 하나 더 있다.

딕셔너리에서 get은 첫번째 매개변수를 찾는데 못찾으면 None을 리턴한다. 그러나 두번째 매개변수가 존재하면 첫번째 매개변수가 없다면 두번째 매개변수로 대체해준다.

 

Delete

delete는 데코레이터에 DELETE만 넣어주고 elif로 DELETE일때 처리를 해주면 된다.

@api_view(["GET", "PUT", "DELETE"])
def category(request, pk):
    try:
        category = Category.objects.get(pk=pk)
    except Category.DoesNotExist:
        raise NotFound

    if request.method == "GET":
        serializer = CategorySerializer(category)
        return Response(serializer.data)
    elif request.method == "PUT":
        serializer = CategorySerializer(
            category,
            data=request.data,
            partial=True,
        )
        if serializer.is_valid():
            updated_category = serializer.save()
            return Response(CategorySerializer(updated_category).data)
        else:
            return Response(serializer.errors)
    elif request.method == "DELETE":
        category.delete()
        return Response(status=HTTP_204_NO_CONTENT)

 

우리는 delete되었다는 것을 알리기 위해 HTTP status 코드를 보내자. 이것을 위해서는 from rest_framework.status import HTTP_204_NO_CONTENT 를 해야한다.

 

delete 버튼이 생기고 delete가 가능해진다.


이제 대대적인 코드 수정을 할 차례다. 여태까지 작성한 코드들은 중복도 있고 비효율적인 부분이 있다. 내부적으로 이해를 위해 해본것이다. 그래서 리펙토링을 시작하자

 

먼저 APIView를 상속받는 클래스를 작성해보자 views.py

from rest_framework.views import APIView 를 하고나서 아래와 같이 작성하자

class Categories(APIView):
    def get(self, request):
        all_categories = Category.objects.all()
        serializer = CategorySerializer(all_categories, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = CategorySerializer(data=request.data)
        if serializer.is_valid():
            new_category = serializer.save()
            return Response(
                CategorySerializer(new_category).data,
            )
        else:
            return Response(serializer.errors)

 

APIView는 method에 따라 분기하는 코드, @api_view(["GET", "POST"]) 와 같은 게 들어 있다.

그래서 원래 catefgories 함수에 있는 내용을 위와 같이 class로 옮겨준다

get을 정의하고 싶다면 get이라는 메소드 구현을 한다. post도 마찬가지.

 

이렇게 바꿔주면 urls에서 문제가 되는데 지워준 categories 함수를 바라보기 때문이다. 그래서 함수에서 클래스로 바꿔주고 as_view() 함수를 매칭해 준다.

urlpatterns = [
    path("", views.Categories.as_view()),
    path("<int:pk>", views.category),
]

 

이제 catogory에도 적용해보자.

class CategoryDetail(APIView):
    def get_object(self, pk):
        try:
            return Category.objects.get(pk=pk)
        except Category.DoesNotExist:
            raise NotFound

    def get(self, request, pk):
        serializer = CategorySerializer(self.get_object(pk))
        return Response(serializer.data)

    def put(self, request, pk):
        serializer = CategorySerializer(
            self.get_object(pk),
            data=request.data,
            partial=True,
        )
        if serializer.is_valid():
            updated_category = serializer.save()
            return Response(CategorySerializer(updated_category).data)
        else:
            return Response(serializer.errors)

    def delete(self, request, pk):
        self.get_object(pk).delete()
        return Response(status=HTTP_204_NO_CONTENT)

 

원래는 category함수가 실행될때 category id를 검사하기 위해 try-catch문으로 썼었는데 그걸 get_object함수를 만들고 그걸 쓰도록 했다.

이제 url를 바꿔보자

from django.urls import path
from . import views

urlpatterns = [
    path("", views.Categories.as_view()),
    path("<int:pk>", views.CategoryDetail.as_view()),
]

 

ModelSerializer

model에 정의한 내용으로 serializer에 정의하고 있었다. 이거는 굉장히 비슷하다. 이 중복되는 내용을 고치려고 한다.

model로 부터 모든 정보를 받아서 serializer를 만들기 위해서는 serializers.ModelSerializer를 상속하면 된다.

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = "__all__"

 

위와 같이 serializer를 만들면 필드 속성을 알아서 정하고 create, update 메소드를 자동으로 생성해준다.

보여주고 싶은 필드를 정하고 싶으면 fields = ("name", "kind") 할 수 있다. 또한 exclude로 제외하는 것도 가능하다.

__all__을 하면 모든 필드를 가져온다.

 

exclude = ['amenities']

 

ModelViewSet

ModelSerializer로 함축된 코드를 작성할 수 있다는 것을 알았다. view는 어떨까?

from rest_framework.viewsets import ModelViewSet 하고 코드를 작성해보자

 

from rest_framework.decorators import api_view
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
from rest_framework.status import HTTP_204_NO_CONTENT
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from .models import Category
from .serializers import CategorySerializer


class CategoryViewSet(ModelViewSet):

    serializer_class = CategorySerializer
    queryset = Category.objects.all()

 

ModelViewSet를 상속받자. 이 클래스는 두개의 property가 필요하다 serializer가 뭔지 알려줘야하고 VeiwSet의 object가 뭔지 알려줘야한다.

APIView도 놀라웠는데 이건 뭔가 싶다.

urls도 수정하자

urlpatterns = [
    path(
        "",
        views.CategoryViewSet.as_view(
            {
                "get": "list",
                "post": "create",
            }
        ),
    ),
    path(
        "<int:pk>",
        views.CategoryViewSet.as_view(
            {
                "get": "retrieve",
                "put": "partial_update",
                "delete": "destroy",
            }
        ),
    ),
]

 

get 메소드가 들어오면 list메소드가 실행되고 retrieve는 한개를 검색해서 주는 메소드다. 

post 메소드가 들어오면 create메소드가 실행된다. 이런식으로 매칭만 시켜주면 된다.

전체를 업데이트 하고 싶다면 update를 쓰면 된다. 여기서는 partial_update로 일부만 업데이트 할 수 있도록 한다.

 

이렇게 쓰면 content 부분에 json로 보낼 필요없이 아래와 같이 입력이 더 쉬워진다

 

728x90
반응형

'파이썬' 카테고리의 다른 글

FastAPI로 정적파일 mount하기  (1) 2024.11.06
클론코딩 - 에어비앤비 1주차  (2) 2024.09.30