이번 포스팅에서는 장고의 기본 요소 중 하나인 폼(Form)에 대하여 공부를 해보고자 한다.
폼(Form)
Django의 폼은, 사용자에게 정보를 수정하는 화면을 직접 보여줄 때 사용한다.
쉽게 말해서 폼은 페이지 요청 시 전달되는 파라미터들을 쉽게 관리하기 위해 사용하는 클래스이다.
폼은 필수 파라미터의 값이 누락되지 않았는지, 파라미터의 형식은 적절한지 등을 검증할 목적으로 사용한다.
이 외에도 HTML을 자동으로 생성하거나 폼에 연결된 모델을 이용하여 데이터를 저장하는 기능도 있다.
질문 등록
질문을 등록하려면 먼저 "질문 등록하기" 버튼을 만들어야 한다.
[파일이름: projects\mysite\templates\pybo\question_list.html]
(... 생략 ...)
</table>
<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>
</div>
{% endblock %}
<a href="..."> 과 같은 링크이지만 부트스트랩의 btn btn-primary 클래스를 적용하면 버튼으로 보인다.
버튼을 클릭하면 pybo:question_create 별칭에 해당되는 URL이 호출된다.
URL 매핑
pybo:question_create 별칭에 해당되는 URL 매핑 규칙을 추가한다.
[파일명: projects\mysite\pybo\urls.py]
(... 생략 ...)
urlpatterns = [
(... 생략 ...)
path('question/create/', views.question_create, name='question_create'),
]
views.question_create 함수를 호출하도록 매핑한다.
폼(Form)
views.question_create 함수를 작성한다.
질문 등록 시 사용할 QuestionForm을 만들어야 하므로 QuestionForm을 forms.py 파일에 작성한다.
forms.py 파일은 신규로 작성해야 한다.
[파일명: projects\mysite\pybo\forms.py]
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question # 사용할 모델
fields = ['subject', 'content'] # QuestionForm에서 사용할 Question 모델의 속성
QuestionForm은 모델 폼(forms.ModelForm)을 상속한다.
장고의 폼은 일반 폼(forms.Form)과 모델 폼(forms.ModelForm)이 있는데 모델 폼은 모델(Model)과 연결된 폼으로 폼을 저장하면 연결된 모델의 데이터를 저장할 수 있는 폼이다.
모델 폼은 이너 클래스인 Meta 클래스가 반드시 필요하다.
Meta 클래스에는 사용할 모델과 모델의 속성을 적어야 한다.
즉, QuestionForm은 Question 모델과 연결된 폼이고 속성으로 Question 모델의 subject와 content를 사용한다고 정의한 것이다.
View 함수
views.question_create 함수를 작성한다.
[파일명: projects\mysite\pybo\views.py]
from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from .models import Question
from .forms import QuestionForm
(... 생략 ...)
def question_create(request):
form = QuestionForm()
return render(request, 'pybo/question_form.html', {'form': form})
question_create 함수는 위에서 작성한 QuestionForm을 사용한다.
render 함수에 전달한 {'form': form}은 템플릿에서 질문 등록 시 사용할 폼 엘리먼트를 생성할 때 쓰인다.
템플릿(Template)
pybo/question_form.html 템플릿을 작성한다.
[파일명: projects\mysite\templates\pybo\question_form.html]
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
템플릿에서 사용한 {{ form.as_p }}의 form은 question_create 함수에서 전달한 QuestionForm의 객체이다.
{{ form.as_p }}는 폼에 정의한 subject, content 속성에 해당하는 HTML 코드를 자동으로 생성한다.
우선 <form method="post">처럼 form 태그에 action 속성을 지정하지 않았다는 점을 눈여겨봐야 한다.
보통 form 태그에는 항상 action 속성을 지정하여 submit 실행 시 action에 정의된 URL로 폼을 전송해야 한다.
하지만 여기서는 특별하게 action 속성을 지정하지 않았다.
form 태그에 action 속성을 지정하지 않으면 현재 페이지의 URL이 디폴트 action으로 설정된다.
물론 action 속성을 다음처럼 명확하게 지정해도 된다.
<form method="post" action="{% url 'pybo:question_create' %}">
하지만 이렇게 하면 question_form.html 템플릿은 "질문 등록" 에서만 사용 가능하다.
이후에 진행할 "질문 수정" 에서는 이 템플릿을 활용할 수가 없다.
질문 수정일 경우에는 action 값을 달리해야 하기 때문이다.
동일한 템플릿을 여러 기능에서 함께 사용할 경우에는 이처럼 form의 action 속성을 비워두는 트릭을 종종 사용한다.
이후에 "질문 수정" 기능을 구현할 때도 question_form.html 템플릿을 사용할 것이므로 action 속성은 비워두는 것이 좋다.
GET과 POST
질문 목록 페이지를 요청할 경우
질문 목록 화면 하단에 "질문 등록하기" 버튼이 추가되었다.
"질문 등록하기" 버튼을 클릭하면 다음과 같이 "질문 등록" 화면이 나타난다.
subject와 content 입력 창에 아무 값이나 입력하고 "저장하기" 버튼을 클릭하면 아무런 반응도 일어나지 않는다.
question_create 함수에 데이터를 저장하는 코드를 아직 작성하지 않았기 때문이다.
question_create 함수를 수정해야 한다.
[파일명: projects\mysite\pybo\views.py]
def question_create(request):
if request.method == 'POST':
form = QuestionForm(request.POST)
if form.is_valid():
question = form.save(commit=False)
question.create_date = timezone.now()
question.save()
return redirect('pybo:index')
else:
form = QuestionForm()
context = {'form': form}
return render(request, 'pybo/question_form.html', context)
동일한 URL 요청을 POST, GET 요청 방식에 따라 다르게 처리한다.
질문 목록 화면에서 "질문 등록하기" 버튼을 클릭한 경우에는 /pybo/question/create/ 페이지가 GET 방식으로 요청되어 question_create 함수가 실행된다.
<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>와 같이 링크를 통해 페이지를 요청할 경우에는 무조건 GET 방식이 사용되기 때문이다.
따라서 이 경우에는 request.method 값이 GET이 되어 if .. else .. 구문에서 else 구문을 타게 되어 질문을 등록하는 화면을 렌더링 한다.
질문 등록 화면에서 subject, content 항목에 값을 기입하고 "저장하기" 버튼을 누르면 이번에는 /pybo/question/create/ 페이지를 POST 방식으로 요청한다.
앞서 설명했듯이 form 태그에 action 속성이 지정되지 않으면 현재 페이지가 디폴트 action으로 설정되기 때문이다.
따라서 질문 등록 화면에서 "저장하기" 버튼을 클릭하면 question_create 함수가 실행되고 request.method 값은 POST가 되어 다음 코드 블록이 실행된다.
if request.method == 'POST':
form = QuestionForm(request.POST)
if form.is_valid(): # 폼이 유효하다면
question = form.save(commit=False) # 임시 저장하여 question 객체를 리턴받는다.
question.create_date = timezone.now() # 실제 저장을 위해 작성일시를 설정한다.
question.save() # 데이터를 실제로 저장한다.
return redirect('pybo:index')
GET 방식에서는 form = QuestionForm( ) 처럼 QuestionForm을 인수 없이 생성했지만 POST 방식에서는 form = QuestionForm(request.POST) 처럼 request.POST를 인수로 생성한다.
request.POST를 인수로 QuestionForm을 생성할 경우에는 request.POST에 담긴 subject, content 값이 QuestionForm의 subject, content 속성에 자동으로 저장되어 객체가 생성된다.
request.POST에는 화면에서 사용자가 입력한 내용들이 담겨있다.
form.is_valid( )는 form이 유효한지를 검사한다.
만약 form에 저장된 subject, content의 값이 올바르지 않다면 form에는 오류 메시지가 저장되고 form.is_valid( )가 실패하여 다시 질문 등록 화면을 렌더링 한다.
이때 form에는 오류 메시지가 저장되므로 화면에 오류를 표시할 수 있다.
form이 유효하다면 if form.is_valid( ): 이후의 문장이 수행되어 질문 데이터가 생성된다.
question = form.save(commit=False)는 form에 저장된 데이터로 Question 데이터를 저장하기 위한 코드이다.
QuestionForm이 Question 모델과 연결된 모델 폼이기 때문에 이와 같이 사용할 수 있다.
여기서 commit=False는 임시 저장을 의미한다.
즉, 실제 데이터는 아직 데이터베이스에 저장되지 않은 상태를 말한다.
여기서 form.save(commit=False) 대신 form.save( )를 수행하면 Question 모델의 create_date에 값이 없다는 오류가 발생한다.
QuestionForm에는 현재 subject, content 속성만 정의되어 있고 create_date 속성은 없기 때문이다.
이러한 이유로 임시 저장을 하여 question 객체를 리턴 받고 create_date에 값을 설정한 후 question.save( )로 실제 데이터를 저장한다.
create_date 속성은 데이터 저장 시점에 생성해야 하는 값이므로 QuestionForm에 등록하여 사용하지 않는다.
앞으로 만들 기능들이 모두 이와 비슷한 패턴을 따르기 때문에 꼭 숙지해야 하는 내용이라는데.. 굉장히 어렵다ㅠ
브라우저에서 질문 등록이 잘 되는지 확인하기 위해 질문 목록에서 <질문 등록하기> 버튼을 눌러 질문 등록 화면으로 이동한다.
질문 등록 화면에서 제목과 내용을 입력하고 "저장하기" 버튼을 클릭한다.
다음과 같이 질문이 잘 등록된 것을 확인할 수 있다.
폼 위젯(Form Widgets)
질문 등록은 잘 되지만 한 가지 아쉬운 점이 남아있다.
{{ form.as_p }} 태그는 HTML 코드를 자동으로 생성하기 때문에 부트스트랩을 적용할 수가 없다.
완벽하지는 않지만 다음처럼 QuestionForm을 조금 수정하면 어느 정도 해결이 가능하다.
[파일명: projects\mysite\pybo\forms.py]
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question
fields = ['subject', 'content']
widgets = {
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
}
위와 같이 widgets 속성을 지정하면 subject, content 입력 필드에 form-control과 같은 부트스트랩 클래스를 추가할 수 있다.
다시 질문 등록 화면을 요청해 보면 다음과 같이 부트스트랩이 적용된 화면을 볼 수 있다.
폼 레이블(Form Labels)
질문 등록 화면에 표시되는 'Subject', 'Content'를 영문이 아니라 한글로 표시하고 싶다면 다음처럼 labels 속성을 지정하면 된다.
[파일명: projects\mysite\pybo\forms.py]
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question
fields = ['subject', 'content']
widgets = {
'subject': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
}
labels = {
'subject': '제목',
'content': '내용',
}
'Subject'는 '제목'으로 'Content'는 '내용'으로 변경된다.
수동 폼 작성
{{ form.as_p }}를 사용하면 빠르게 템플릿을 만들 수 있지만 HTML 코드가 자동으로 생성되므로 디자인 측면에서 많은 제한이 생긴다.
예를 들어 폼 엘리먼트 내에 특정 태그를 추가하거나 필요한 클래스를 추가하는 작업에 제한이 생긴다.
또 디자인 영역과 서버 프로그램 영역이 혼재되어 웹 디자이너와 개발자의 역할을 분리하기도 모호해진다.
이번에는 폼을 이용하여 자동으로 HTML 코드를 생성하지 말고 직접 HTML 코드를 작성하는 방법을 사용한다.
우선 수작업 시 필요 없는 widget 속성을 제거한다.
[파일명: projects\mysite\pybo\forms.py]
from django import forms
from pybo.models import Question
class QuestionForm(forms.ModelForm):
class Meta:
model = Question # 사용할 모델
fields = ['subject', 'content'] # QuestionForm에서 사용할 Question 모델의 속성
labels = {
'subject': '제목',
'content': '내용',
}
질문 등록 템플릿을 다음과 같이 수정한다.
[파일명: projects\mysite\templates\pybo\question_form.html]
{% extends 'base.html' %}
{% block content %}
<div class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form method="post">
{% csrf_token %}
<!-- 오류표시 Start -->
{% if form.errors %}
<div class="alert alert-danger" role="alert">
{% for field in form %}
{% if field.errors %}
<div>
<strong>{{ field.label }}</strong>
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- 오류표시 End -->
<div class="mb-3">
<label for="subject" class="form-label">제목</label>
<input type="text" class="form-control" name="subject" id="subject"
value="{{ form.subject.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea class="form-control" name="content"
id="content" rows="10">{{ form.content.value|default_if_none:'' }}</textarea>
</div>
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
{{ form.as_p }}로 자동으로 생성되는 HTML 대신 제목과 내용에 해당되는 HTML 코드를 직접 작성한다
그리고 question_create 함수에서 form.is_valid( ) 가 실패할 경우 발생하는 오류의 내용을 표시하기 위해 오류를 표시하는 영역을 추가한다.
제목(subject) 항목의 value에는 {{ form.subject.value|default_if_none:'' }} 처럼 값을 대입해 주었는데 이것은 오류가 발생했을 경우 기존에 입력했던 값을 유지하기 위함이다.
|default_if_none:''의 의미는 폼 데이터(form.subject.value)에 값이 없을 경우 None 이라는 문자열이 표시되는데 None 대신 공백으로 표시하라는 의미의 템플릿 필터이다.
장고의 템플릿 필터는 |default_if_none:'' 처럼 | 기호와 함께 사용된다.
이렇게 수정한 후에 "질문 등록" 화면에서 제목에만 "TEST"라고 입력하고 "내용"은 비워둔 채 "저장하기" 버튼을 클릭한다.
"내용"에 아무런 값도 입력하지 않았기 때문에 "내용"을 입력하라는 오류 메시지를 볼 수 있다.
그리고 "제목"에 입력했던 "TEST"는 사라지지 않고 계속 유지되는 것도 확인할 수 있다.
답변 등록
답변 등록에도 장고 폼을 적용한다.
답변을 등록할 때 사용할 AnswerForm을 pybo/forms.py 파일에 다음과 같이 작성한다.
[파일명: projects\mysite\pybo\forms.py]
from django import forms
from pybo.models import Question, Answer
(... 생략 ...)
class AnswerForm(forms.ModelForm):
class Meta:
model = Answer
fields = ['content']
labels = {
'content': '답변내용',
}
answer_create 함수를 다음과 같이 수정한다.
[파일명: projects\mysite\pybo\views.py]
(... 생략 ...)
from django.http import HttpResponseNotAllowed
from .forms import QuestionForm, AnswerForm
(... 생략 ...)
def answer_create(request, question_id):
"""
pybo 답변등록
"""
question = get_object_or_404(Question, pk=question_id)
if request.method == "POST":
form = AnswerForm(request.POST)
if form.is_valid():
answer = form.save(commit=False)
answer.create_date = timezone.now()
answer.question = question
answer.save()
return redirect('pybo:detail', question_id=question.id)
else:
return HttpResponseNotAllowed('Only POST is possible.')
context = {'question': question, 'form': form}
return render(request, 'pybo/question_detail.html', context)
question_create와 같은 방법으로 AnswerForm을 이용하도록 변경한다.
하지만 답변 등록은 POST 방식만 사용되기 때문에 GET 방식으로 요청할 경우에는 HttpResponseNotAllowed 오류가 발생하도록 한다.
질문 상세 템플릿도 오류를 표시하기 위한 영역을 다음처럼 추가한다.
[파일명: projects\mysite\templates\pybo\question_detail.html]
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
(... 생략 ...)
<form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
{% csrf_token %}
<!-- 오류표시 Start -->
{% if form.errors %}
<div class="alert alert-danger" role="alert">
{% for field in form %}
{% if field.errors %}
<div>
<strong>{{ field.label }}</strong>
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- 오류표시 End -->
<div class="form-group">
<textarea name="content" id="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="답변등록" class="btn btn-primary">
</form>
</div>
{% endblock %}
이렇게 수정한 후에 테스트를 했을 때, 만약 답변 내용 없이 답변을 등록하려고 하면 다음과 같은 오류 메시지가 표시된다.
'Framework > Django' 카테고리의 다른 글
[Django] 장고(Django) 서비스 개발 - 페이징(Paging) (0) | 2022.12.06 |
---|---|
[Django] 장고(Django) 서비스 개발 - 내비게이션 바(navigation bar) (0) | 2022.12.05 |
[Django] 장고(Django) 기본요소 - 템플릿 상속(extend) (0) | 2022.12.02 |
[Django] 장고(Django) 기본요소 - 부트스트랩(Bootstrap) (2) | 2022.12.01 |
[Django] 장고(Django) 기본요소 - 스태틱(static) (0) | 2022.11.29 |