1. Django Tutorial(Airbnb) - Custom Admin Site

🌈 Custom Admin Site

🔥 Admin Site에 Model 등록 방법

🔥 Django의 Admin Panel 확장시키기

🔥 Admin Panel Option

🔥 Model의 매서드를 admin에서 활용하기

🔥 Timezone Utils 사용하기

🔥 Save Method 오버라이딩


1. Admin Site에 Model 등록 방법

  • Admin Site에 Model을 나타내는 방법은 아래 2가지가 모두 가능해요. 두 방법 모두 Model을 import 하는건 동일합니다.

1) 데코레이터(@) 사용

  • 데코레이터를 통해 Model을 등록하면 바로 아래 class를 자동으로 참조하기 때문에 어떤 admin class인지 명시할 필요가 없어요.
    • 🔎 @admin.register(Model Class)
from django.contrib import admin
from . import models
@admin.register(models.User)
class CustomUserAdmin(admin.ModelAdmin):
    pass

2) admin.site.register 사용

  • admin.site.register를 이용하면 어떤 admin class를 등록하는지 모델과 함께 명시해야해요.
    • 🔎 admin.site.register(Model Class, Admin Class)
from django.contrib import admin
from . import models
class CustomUserAdmin(admin.ModelAdmin):
    pass
admin.site.register(models.User, CustomUserAdmin)

2. Django의 Admin Panel 확장시키기

  • Django의 User 모델을 AbstractUser를 통해 확장시켰다면 Admin Panel 또한 기존 Django가 가지고 있던 admin을 상속 받아 사용할 수 있어요!

1) Django의 User Admin Panel 상속 받기

  • Django의 Admin 가져오기
    • 🔎 from django.contrib.auth.admin import UserAdmin
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin # 👈 UserAdmin이 위치한 곳
from . import models
@admin.register(models.User)  
class CustomUserAdmin(UserAdmin): # 👈 UserAdmin 상속받음
    """Custom User Admin"""
    pass

  • Django의 Admin을 상속해오면, list_disply 및 list_filter가 자동으로 세팅되어 있어 나타나고, 검색이 가능한 Search bar도 기본 세팅되어 있어요. 물론 Custom도 가능합니다!

2) fieldsets 설정

  • Admin Panel 내부로 들어가면, 확장시킨 필드(avatar, gender, bio, birthday 등)들이 모두 나타나지 않는데 fieldsets을 통해 Admin Panel 내부를 Custom 할 수 있어요!
  • fieldsets 포맷 형태는 아래와 같습니다. 또한 기존 Django Admin이 갖고 있던 field까지 모두 표시하기 위해 UserAdmin.fieldsets과 우리가 생성한 fieldsets을 합쳐줄께요:)
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from . import models
# Register your models here.
@admin.register(models.User)
class CustomUserAdmin(UserAdmin):
    """Custom User Admin"""
    fieldsets = UserAdmin.fieldsets + (
        (
            "Custom Profile", # 👈 분류 title
            {
                "fields": (
                    "avatar",
                    "gender",
                    "bio",
                    "birthdate",
                    "language",
                    "currency",
                    "superhost",
                )
            },
        ),
    )

3. Admin Panel Option

1) list_diplsy 옵션

  • list_diplsy에 튜플로 필드명을 입력하면 Admin Panel에 나타낼 필드값을 지정할 수 있어요.
    • 🔎 list_display = ("필드명1", "필드명2", "필드명3", )
  • 단, ManyToManyField는 list_display에 필드값으로 사용할 수 없기 때문에 Admin Panel에 나타내고자한다면 매서드를 생성하여 list_display에 넣어줍니다.
    • 여기서 self와 obj를 전달하는데요,, self는 Admin Class인 RoomAdmin 가르키고, obj는 admin의 현재 row를 의미합니다.
    • [매서드명].short_description로 Admin Panel에 출력될 필드 이름을 수정할 수 있어요!
@admin.register(models.Room)
class RoomAdmin(admin.ModelAdmin):
    """Room Admin Dfinition"""
    list_display = (
        "name",
        "country",
        "city",
        "price",
        "guests",
        "beds",
        "bedrooms",
        "baths",
        "check_out",
        "check_in",
        "instant_book",
        "count_amenities",
    )
    def count_amenities(self, obj):
        return obj.amenities.count() # 👈 현재 row의 amenities를 갯수를 반환
    count_amenities.short_description = "amenities" # 👈 count_amenities을 amenities로 변경 

2) list_filter 옵션

  • list_filter은 우측에 필터 기능을 추가시켜 줍니다.
    • 🔎 list_filter = ("필드명1", "필드명2", "필드명3", )
@admin.register(models.Room)
class RoomAdmin(admin.ModelAdmin):
    """Room Admin Dfinition"""
    list_filter = (
        "instant_book",
        "host__superhost",
        "room_type",
        "amenities",
        "facilities",
        "house_rule",
        "city",
        "country",
    )  

3) search_filter 옵션

  • search_filter를 사용하면 search bar를 간편하게 만들 수 있어요.
    • 🔎 search_filter = ("필드명1", "필드명2", "필드명3", )
  • search_filter는 default로 icontains방식으로 데이터를 찾아주며, Prefix를 사용하여 검색어를 어떤 기준으로 찾을지 결정할 수 있어요.
    • None(icontains) : 둔감한 기준(대소문자 구별 안함)으로 입력한 검색어가 포함된 object를 반환
    • ^(startswith) : ^옵션을 같이 넣어주면, 요청한 검색어로 시작되는 object를 반환
    • =(iexact) : 요청한 검색어와 정확히 일치하는 object를 반환
  • 생성된 필드 중 ForeignKey가 있다면, 더블 언더스코어(__)를 통해 참조하는 대상의 모델로 접근이 가능합니다. 이런 문법 스타일은 list_disply, list_filter 등에서도 사용 가능해요!
    • 🔎 search_fields = ("^host__username", )
@admin.register(models.Room)
class RoomAdmin(admin.ModelAdmin):
    """Room Admin Dfinition"""
    list_display = (
        "name",
        "country",
        "city",
        "price",
    )
    search_fields = (
        "city",
        "^host__username",
        "=price",
    )

4) filter_horizontal 옵션

  • filter_horizontal 옵션은 ManyToManyField의 Admin Panel 내부에 적용되는 기능이에요.
  • filter_horizontal 옵션을 사용하면, 필드의 스타일을 편리하게 변경해줘요.
@admin.register(models.Room)
class RoomAdmin(admin.ModelAdmin):
    """Room Admin Dfinition"""
    filter_horizontal = (
        "amenities",
        "facilities",
        "house_rule",
    )

👉 default

👉 filter_horizontal

5) ordering 옵션

  • ordering 옵션에 필드값을 지정하면, Admin Panel에 정렬 버튼(삼각형 버튼)을 생성해줍니다.
@admin.register(models.Room)
class RoomAdmin(admin.ModelAdmin):
    """Room Admin Dfinition"""
    ordering = (
        "name",
        "price",
    )    

6) raw_id_fields 옵션

  • Admin Panel 내부로 들어가볼 까요? 사용자가 늘어난다면, 필드값이 많아져 스크롤의 압박으로 불편을 유발합니다:)
  • 이럴 경우, raw_id_fields로 해당 필드를 지정해주면 팝업창을 만들어줘요. 원하는 값을 찾기 쉬워집니다!
@admin.register(models.Room)
class RoomAdmin(admin.ModelAdmin):
    """Room Admin Dfinition"""
    raw_id_fields = ("host",)    


4. Model의 매서드를 admin에서 활용하기

  • Admin Panel에는 Model에서 선언한 필드값만 나타낼 수 있는 것은 아니에요. 필요에 의해서 admin.py 또는 models.py에 매서드를 만들고 이를 자유롭게 list_display에 추가할 수 있답니다. 각 Review에 대한 "평점 정보"를 Model에서 매서드 만들고 list_display에 추가해 볼께요:)
  • "평점 정보"를 Admin Panel만이 아닌 일반 사용자도 접근 가능한 Templates 영역에서도 사용할 예정이라면 Model 영역(models.py)에서 매서드를 생성해주는 것이 좋아요. Model에서 생성된 매서드는 Admin Panel 뿐만아니라 Templates 영역에서도 사용 가능하기 때문이죠!
  • 만약 Admin Panel에서만 해당 매서드를 사용할 계획이라면 admin.py에 매서드를 생성해주면 됩니다.

1) reviews/models.py

  • 각 Review Object의 평점의 평균을 구해서 반환하는 매서드 작성했어요.
    • 각 객실의 점수 : accuracy, communication, cleanliness, location, check_in, value
  • 해당 매서드명을 list_display에 추가만하면 Admin Panel에서 각 Reivew에 대한 평정 정보를 볼 수 있답니다.
  • short_description은 admin.py 및 models.py 모두 사용 가능합니다.
    • 🔎 rating_average.short_description = "Avg."
from django.db import models
from core import models as core_models
# Create your models here.
class Review(core_models.TimeStampedModel):
    """Review Model Definition"""
    review = models.TextField()
    accuracy = models.IntegerField()
    communication = models.IntegerField()
    cleanliness = models.IntegerField()
    location = models.IntegerField()
    check_in = models.IntegerField()
    value = models.IntegerField()
    user = models.ForeignKey(
        "users.User", related_name="reviews", on_delete=models.CASCADE
    )
    room = models.ForeignKey(
        "rooms.Room", related_name="reviews", on_delete=models.CASCADE
    )
    def __str__(self):
        return f"{self.review} - {self.room}"
    def rating_average(self):
        avg = (
            self.accuracy
            + self.communication
            + self.cleanliness
            + self.location
            + self.check_in
            + self.value
        ) / 6
        return round(avg, 2) # 👈 소수점 2째자리까지 나타낼거에요:)
    rating_average.short_description = "Avg."

2) reviews.admin.py

  • __str__ 매직매서가 list_display에 덮어씌어져 Admin Panel에 보이지 않을 때는 당황하지 말고,, "list_display"에 __str__ 를 추가해주면 되요!
from django.contrib import admin
from . import models
# Register your models here.
@admin.register(models.Review)
class ReviewAdmin(admin.ModelAdmin):
    """Review Admin Definition"""
    list_display = (
        "__str__",
        "rating_average",
    )

  • 각 객실이 여러 사람에 의해 이미 이용했었을 수 있고, 그렇다면 평점도 여러개가 존재할꺼에요. 이번에는 각 객실에 대한 여러 평점 정보의 평균을 Admin Panel과 Front 영역에서 나타내고자 해요.
  • Review 모델은 ForeignKey로 Room을 참조하고 있기 때문에 Room에서는 related_name의 값을 통해서 각 객실을 가르키는 모든 Review들의 정보에 역으로 접근할 수 있어요:)

3) rooms/models.py

  • 각 객실을 참조하는 모든 Review 정보를 all_reviews에 담았어요. "self.reviews.all()"에서 reviews는 어디서 왓을까요? room필드(reviews/models.py)에 related_name 값입니다.
    • 🔎 all_reviews = self.reviews.all()
  • 이제 all_reviews에 각 객실을 가르키는 모든 Review를 담아두었어요. rating_average() 매서드를 통해 평점 값을 모두 합한 뒤, all_reviews의 길이만큼 나눠 반환하면 각 객실에 대한 평점 정보 제공해줘요.
    • 🔎 all_rating += review.rating_average()
class Room(core_models.TimeStampedModel):
    """Room Model Definifion"""
    def total_rating(self):
        all_reviews = self.reviews.all()
        all_rating = 0
        if len(all_reviews) != 0:
            for review in all_reviews:
                all_rating += review.rating_average() # 👈 매서드도 접근 가능
            return all_rating / len(all_reviews)
        else: # 👈 Review가 1개도 작성되지 않은 객실이 존재할 경우를 대비한 방어코드에요:)
            return 0
    total_rating.short_description = "rating"


5. Timezone Utils 사용하기

1) Timezone Utils

  • Python의 Time 라이브러리를 사용하지 않고, TimeZone을 사용하는 이유는 settgins.py에서 설정된 TimeZone이 Django의 서버의 시간을 관장하고 있기 때문이에요.
    • 🔎 from django.utils import timezone
  • 반환값을 Boolean 아이콘으로 표시하는 방법
    • 🔎 in_progress.boolean = True
from django.db import models
from django.utils import timezone # 👈 timezone util을 가져오기
from core import models as core_models
# Create your models here.
class Reservation(core_models.TimeStampedModel):
    """Reservation Model Definition"""
    STATUS_PENDING = "pending"
    STATUS_CONFIRMED = "confirmed"
    STATUS_CANCELED = "canceled"
    STATUS_CHOICES = (
        (STATUS_PENDING, "Pending"),
        (STATUS_CONFIRMED, "Confirmed"),
        (STATUS_CANCELED, "Canceled"),
    )
    status = models.CharField(
        max_length=12, choices=STATUS_CHOICES, default=STATUS_PENDING
    )
    check_in = models.DateField()
    check_out = models.DateField()
    guest = models.ForeignKey(
        "users.User", related_name="reservations", on_delete=models.CASCADE
    )
    room = models.ForeignKey(
        "rooms.Room", related_name="reservations", on_delete=models.CASCADE
    )
    def __str__(self):
        return f"{self.room} - {self.check_in}"
    def in_progress(self):
        now = timezone.now().date() # 👈 현재 서버 날짜
        return now >= self.check_in and now <= self.check_out
    in_progress.boolean = True  # 👈 return값을 True, False가 아닌 아이콘으로 나타내줘요.
    def is_finished(self):
        now = timezone.now().date() # 👈 현재 서버 날짜
        return now > self.check_out
    is_finished.boolean = True # 👈 return값을 True, False가 아닌 아이콘으로 나타내줘요.


6. Save Method 오버라이딩

  • Save Method를 가로채야하는 상황이 발생하는데요,, 그 이유에 대해서 알아볼께요.
  • "users/models.py"의 city 필드는 host가 입력한 값이 저장되기 때문에 같은 장소임에 불구하고 대소문자 등의 차이가 발생할 수 있어요. 어떤 host는 Seoul로 입력하고, 또 다른 host는 seoul로 입력할 수 있겟죠.
  • country처럼 정해진 선택 목록을 제공할 수도 있으나, city는 양이 방대하고 모든 city를 제공하면 실제 사용되지 않는 불필요한 값이 존재할거에요.
  • 이에 Model 값의 등록은 사용자에게 맡기지만, 동일한 값인 경우 최대한 값들을 통일하여 저장될 수 있게할 필요가 있어요. 이럴 때, save() 매서드가 실행되서 값을 저장하기 전에 Save Envent를 가로채서 해당 값을 수정한 뒤, 저장시킬 수 있어요!

1) rooms.models.py

  • form에 입력한 정보가 DB에 해당 필드로 저장되는 것이 save() 매서드의 역할이에요. 즉, Save Event를 가로챈다는 것은 기존에 save() 매서드를 오버라이딩하여 값을 다룬 뒤, super()를 통해 save()의 본래 기능을 실행시키는 것 입니다. 저장되기 전에 잠시 값을 가로채서 조치를 취하고 저장시키는 거죠!
from django.db import models
from django_countries.fields import CountryField
from core import models as core_models
# Create your models here.
class Room(core_models.TimeStampedModel):
    """Room Model Definifion"""
    ...
    ...
    city = models.CharField(max_length=80)
    ...
    ...
    def save(self, *args, **kwargs):
        self.city = str.capitalize(self.city) # 👈 앞글자를 대분자로 바꾸고 
        super().save(*args, **kwargs) # 👈 원래 save 매서드를 실행

좋은 웹페이지 즐겨찾기