개발 여행/Django

[Django Admin] 2. 모델, 렌더링

찰나의꿈 2023. 2. 19. 22:18

Tutorial Chapter 2, 3을 다룬다. 

모델 및 어드민 도입

djangotutorial/
    manage.py
    djangotutorial/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py

settings.py: DB 연결, 디렉토리, 앱, 미들웨어 등

  • Databases: 엔진과 그에 따른 옵션 설정 가능. 필요한 설정은 엔진마다 다르며, ‘read_default_file’에 cnf 파일 경로를 지정하여 password, user, host 등의 정보 저장 가능.
# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/path/to/my.cnf',
        },
    }
}

# my.cnf
[client]
database = NAME
user = USER
password = PASSWORD
default-character-set = utf8

manage.py migrate 를 통해, settings.py 내 INSTALLED_APPS에 필요한 DB를 미리 만들 수 있다.

models.py: 데이터베이스 레이아웃 정의. 이것만 보고 데이터가 다 보여야 한다.

 

기본적으로 orm. models.Model을 상속받는 클래스로 table을 정의한다.

#polls/models.py
from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

 

 

CharField, DateTimeField와 같이 들어갈 필드의 형태를 지정 가능함.

첫 번째 argument로 들어가는 String은 주석과 같은 역할.

ForeignKey 연결도 가능하며, 1-n, n-n 다 가능하고 삭제 시 지정도 가능.

 

 

앱 활성화

장고 기본 철학은 ‘앱을 모듈처럼 다루자’. 한 곳에서 사용한 앱을 다른 프로젝트에 못쓸 이유가 없음.

아무튼 모듈을 활성화시켜야 사용 가능. root/settings.py 내 installed_apps 세팅에 ‘Configuration Class’를 연결해줘야 한다. 이건 각 앱의 apps.py에 있다.

#djangotutorial/settings.py
INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
#djangotutorial/polls/apps.py
from django.apps import AppConfig

class PollsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'polls'

그 뒤 polls 앱의 모델을 반영하기 위해 makemigrations polls를 실행한다. 이러면 변화점이 polls/migrations 폴더 내에 저장된다. 버전컨트롤이라기에는 전체 요소가 아닌 변화가 발생할 때마다 그 변화를 하나하나 기록하는 것이기 때문에, 이전 버전을 수정 시 이후 버전도 영향을 받으므로 신중히 다뤄야 한다.

 

cf. sqlmigrate 명령어를 통해 이 과정으로 sql로도 볼 수 있다.

 

 

요약: models.py 수정 —> manage.py makemigrations를 통해 각 app 내의 변경점을 migration으로 변경 —> manage.py migrate를 통해 각 app의 migration을 전부 통합하여 db에 반영

쿼리 치기

  • 여느 ORM과 같이, ClassName.methodName 등을 통해 쿼리를 치거나, q = ClassName(kwargs) / q.save() 등을 통해 개별 row를 저장할 수 있다.
  • Question.objects.all()을 쳐보면 representation이 단순히 object 설명과 동일하다. 이후 쿼리 진행 등에서도 전혀 도움이 되지 않으므로, 모든 model에는 반드시 str 를 붙여야 한다.
  • DateTimeField 등에 대해, django.utils.timezone 혹은 datetime.timedelta 등을 활용할 수 있다.
  • 엄연히 클래스이므로 커스텀 메서드 등을 붙일 수 있음. 이를 통해 각 row(= class instance)별로 메서드 실행을 통한 로직 구현이 가능하다.
  • FK로 연결된 다른 entity에 대해, 직접 추가하여 db에 더할 수도 있다. q.choice_set.create(…)인데, choice_set은 이름이 자동으로 붙었다는 점을 확인.

 

어드민 기능

어드민은 superuser를 생성하면 이를 통해 접속이 가능하다. createsuperuser를 통해 만들 수 있다.

python manage.py createsuperuser

그 뒤 서버를 열고 localhost:8000/admin 에 접속하여 로그인하면 어드민 페이지에 접속할 수 있다.

 

 

어드민에서 수정 가능하게 하기

polls/admin.py 내에 모델을 등록해야 한다. Low-code같은 감성이다. 모델을 등록하면 알아서 읽기 쉬운 형태로 바꿔준다.

#polls/admin.py
from django.contrib import admin
from .models import Question

admin.site.register(Question)

 

 

렌더링 및 템플릿

URL 내에서 파라미터를 가져올 수 있다. Spring @PathVariable 과 동일한 기능.

#polls/views.py

...
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

기존의 views 내에 새로운 뷰들을 넣었다. 이제 이 뷰들은 파라미터를 받는다. 이 파라미터를 URL 패턴 내에서 받을 수 있다. 거의 서비스 패턴이다.

 

Django는 view에게 반드시 HttpResponse, 혹은 에러 response code를 받기를 원한다.

 

#polls/urls.py
urlpatterns = [
    # /polls/
    path('', views.index, name='index'),

    # /polls/5
    path('<int:question_id>/', views.detail, name='detail'),

    # /polls/5/results
    path('<int:question_id>/results/', views.results, name='results'),

    # /polls/5/vote
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

int:question_id 는 int converter / variable name question_id 를 < >를 통해 매칭한다.

이제 본격적으로 view template과 매칭을 해준다. template 파일 경로 등은 프로젝트 setting의 TEMPLATES 옵션으로 조정 가능하다. 기본적으로 루트 폴더의 templates 폴더와 각 앱 내의 templates 폴더를 찾는다.

 

 

참고.

이때 주의 사항으로, 각 app 내의 root template 폴더 내에 직접 html을 넣지 마라. Django는 모든 templates 폴더를 뒤지게 될텐데, 예를 들어 polls/templates/index.html 과 루트 폴더의 templates/index.html은 절대 구분할 수 없다(둘 다 templates 대비 상대 경로가 같다). app 내에 있는 templates 폴더라도, 별도의 폴더(app 이름과 동일하면 편할 것이다)를 파서 그 안에 html을 넣는 것이 경로 지정을 하기 쉽다.

 

 

<!-- polls/templates/polls/index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index of Polls</title>
</head>
<body>
{% if latest_question_list %}
    <ul>
        {% for question in latest_question_list %}
            <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
        {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}
</body>
</html>

이제 이 index.html에 스프링으로 치면 ‘모델’을 넘겨줄 때이다.

# polls/views.py

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))

앞서 말했듯 장고는 views에게 HttpResponse를 원한다. render 메서드는 template가 담당하고 이를 HttpResponse에 넣는다. context(모델)는 render 시에 전한다.

그런데 이 패턴은 정말 많이 쓰므로 shortcut이 있다.

 

...
		return render(request, 'polls/index.html', context)

 

 

Error Raise

404를 반환해본다.

question_id에 해당하는 question이 없을 경우, 모델은 ObjectDoesNotExist 예외를 발생시킨다. Question은 이 예외를 받아서 DoesNotExist flag를 켠다. 이를 받아서 Http404로 변환해야 한다.

 

 

def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question of given id does not exist")
    return render(request, 'polls/detail.html', {'question': question})

해당하는 entity가 없는 경우 404를 발생시키는 패턴은 매우 흔하다.

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

get_object_or_404 혹은 get_list_or_404 의 경우 object/list를 반환한다는 차이만 있다.

이제 detail로 넘긴 question을 좀 더 상세히 다룬다.

<!-- /polls/templates/polls/detail.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Question Detail: {{ question.id }}</title>
</head>
<body>
<h1>{{ question.question_text }}</h1>
<ul>
    {% for choice in question.choice_set.all %}
        <li>{{ choice.choice_text }}</li>
    {% endfor %}
</ul>
</body>
</html>

여러 template 관련 기능이 쓰였다.

  • {{ 변수명 }}(띄어쓰기 포함)을 통해 원하는 변수를 렌더링할 수 있다.
  • {% for … %} / {% endfor %}를 통해 반복문을 쓸 수 있다.

다만 .all의 경우 분명 메서드인데 메서드 콜링이 변수 호출처럼 되어 있다. 이런 식으로 template은 좀 헷갈리는 부분이 있어서, template 가이드 참고가 그때그때 필요해보인다.

  • {% url %} 태그를 통해 urls.py 내에 정의한 ‘이름’ 값을 그대로 가져오고, 이를 붙일 수 있다.
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
path('<int:question_id>/', views.detail, name='detail'),

 

name 파라미터를 기억해보자. 이러면 URL에 대한 책임은 온전히 urls.py에 있게 되며, URL을 수정하더라도 즉시 반영되어 유연한 유지보수가 가능해진다.

detail이라는 이름이 app마다 있는게 부담스럽다면 app name을 붙일 수도 있다.

 

# polls/urls.py
app_name = 'polls'
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>