위 글은 점프 투 장고를 참고해 작성하였습니다.
질문을 등록하려면 먼저 "질문 등록하기" 버튼을 만들어야 한다. 다음처럼 질문 목록 하단(question_list.html
)에 "질문 등록하기" 버튼을 생성하자
(... 생략 ...)
</table>
<!-- a태그 부분이 새로 추가한 부분-->
<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>
</div>
{% endblock %}
<a href="...">
과 같은 링크이지만 부트스트랩의 btn btn-primary
클래스를 적용하면 버튼으로 보임. 버튼을 클릭하면 pybo:question_create
별칭에 해당되는 URL이 호출이제 pybo:question_create
별칭에 해당되는 URL 매핑 규칙(pybo/urls.py
에)을 추가하자
...
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
path('answer/create/<int:question_id>/', views.answer_create, name='answer_create'),
path('question/create/', views.question_create, name='question_create'),
]
views.question_create
함수를 호출하도록 매핑흐름에 따라 이제 views.py
에 question_create
함수를 작성해야 함(views.question_create
)
폼(form)
- 폼은 쉽게 말해 페이지 요청시 전달되는 파라미터들을 쉽게 관리하기 위해 사용하는 클래스
- 폼은 필수 파라미터의 값이 누락되지 않았는지, 파라미터의 형식은 적절한지 검증할 목적으로 사용
- HTML을 자동으로 생성하거나 폼에 연결된 모델을 이용하여 데이터를 저장하는 기능도 있음
질문 등록시 사용할 QuestionForm을 pybo/forms.py
파일에 다음처럼 작성(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)과 연결된 폼으로 폼을 저장하면 연결된 모델의 데이터를 저장할수 있는 폼
즉, QuestionForm은 Question 모델과 연결된 폼이고 속성으로 Question 모델의 subject와 content를 사용한다고 정의한 것
views.question_create
함수를 다음과 같이 작성하자.
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}은 템플릿에서 질문 등록시 사용할 폼 엘리먼트를 생성할 때 쓰임
form에 맞는 템플릿 생성 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 태그에는 항상 action 속성을 지정해 submit 실행시 action에 정의된 URL로 폼을 전송하지만, 여기서는 <form method = "post">
이 form 태그 부분에 action 속성을 지정하지 않음. -> form 태그에 action 속성을 지정하지 않으면 현재 페이지의 URL이 기본 action으로 지정
<form method="post" action="{% url 'pybo:question_create' %}">
이렇게 action 속성을 명확하게 지정해도 됨. 하지만 이 경우 question_form.html 템플릿은 질문등록에서만 사용 가능 ! 따라서 동일한 템플릿을 여러 기능에서 함께 사용할 경우에는 위처럼 form의 action 속성을 비워둠 !!forms.py 같은 신규 파일 작성시에는 로컬 서버 재시작이 필요함
재시작 후 화면을 확인해보면,
새로운 질문을 등록하고 저장하기 버튼을 누르면 아무 일도 일어나지 않는다. 이유는 question_create 함수에 데이터를 저장하는 코드를 아직 작성하지 않았기 때문 !
views.py
의 question_create 함수를 다음과 같이 수정
def question_create(request):
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')
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으로 설정되기 때문 !
GET 방식에서는 form = QuestionForm()
처럼 QuestionForm을 인수 없이 생성했지만 POST 방식에서는 form = QuestionForm(request.POST)
처럼 request.POST
를 인수로 생성. request.POST
를 인수로 QuestionForm을 생성할 경우에는 request.POST
에 담긴 subject, content 값이 QuestionForm의 subject, content 속성에 자동으로 저장되어 객체가 생성됨 !
form.is_valid()
는 form이 유효한지를 검사. 만약 form에 저장된 subject, content의 값이 올바르지 않다면 form에는 오류 메시지가 저장되고 form.is_valid()
가 실패하여 다시 질문 등록 화면을 렌더링 할 것 !
반대로 form이 유효하다면 if form.is_vaild():
이후의 문장이 수행되어 질문 데이터가 생성됨. question = form.save(commit=False)
는 form에 저장된 데이터를 활용해서 Question 데이터를 저장하기 위한 코드
=> 여기서 commit=False
는 임시저장을 의미. 실제 데이터는 아직 데이터베이스에 저장되지 않은 상태로, form.save(commit=False)
대신에 form.save()
를 수행하면 QuestionForm에는 현재 subject, content 속성만 정의되어 있고 create_date 속성은 없기 때문에 Question 모델의 create_date에 값이 없다는 오류가 발생할 것 ! 따라서 임시 저장을 하여 question 객체를 리턴 받고 create_date에 값을 설정한 후 question_save()
로 실제 데이터를 저장 ~
위와 같은 내용을 모두 수행한 후 다시 pybo 페이지에 새로운 질문을 등록해보면,
이제 위와 같이 새 질문이 잘 등록된 것을 확인할 수 있다 !
앞서 포스팅에서 화면을 예쁘게 만들기 위해 부트스트랩을 준비함. 하지만 위에서 question_forms.html
안 {{ form.as_p }}
태그는 HTML 코드를 자동으로 생성하기 때문에 부트스트랩을 적용할 수가 없음 !!
완벽하지는 않지만 아래처럼 QuestionForm을 수정하면 어느정도 디자인을 수정할 수 있다.
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}),
}
다시 질문 등록 화면을 들어가보면,
다음과 같이 부트스트랩이 적용된 화면을 볼 수 있음 ~
질문 등록 화면에 표시되는 'Subject', 'Content'를 영문이 아니라 한글로 표시하고 싶다면 다음처럼 labels 속성을 지정하면 된다.
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 속성
labels = {
'subject': '제목',
'content': '내용',
}
다시 질문 등록 화면을 들어가보면,
'subject'는 '제목'으로, 'Content'는 '내용'으로 변경돼있음 !!
장고 폼(form)에 대한 보다 자세한 내용은 아래 URL 참고 !
{{ form.as_p }}
를 사용하면 빠르게 템플릿을 만들 수 있지만 HTML 코드가 자동으로 생성되므로 디자인 측면에서 많은 제한 존재
아래는 폼을 이용해 자동으로 HTML 코드를 생성하는 대신에 직접 HTML 코드를 작성하는 방법을 사용 !
1) 우선 forms.py
에서 수작업 시 필요없는 widget 속성을 제거
2) 질문 등록 템플릿 (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코드를 직접 작성(아래 html 코드)<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>
form.is_valid()
가 실패할 경우 발생하는 오류의 내용을 표시하기 위해 오류를 표시하는 영역을 추가(아래 html 코드){% 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 %}
{{ form.subject.value|default_if_none:'' }}
처럼 값을 대입해 주었는데 이것은 오류가 발생했을 경우 기존에 입력했던 값을 유지하기 위함. |default_if_none:''
의 의미는 폼 데이터(form.subject.value
)에 값이 없을 경우 None 이라는 문자열이 표시되는데 None 대신 공백으로 표시하라는 의미의 템플릿 필터 !(장고의 템플릿 필터는 위처럼 |
기호와 함께 사용됨이렇게 템플릿(question_form.html
)을 수정하고 다시 질문 등록 페이지를 들어가보자
제목에만 "테스트"라고 입력하고 내용을 비워둔 채 저장하기 버튼을 클릭해보면 "이 입력란을 작성하세요." 라는 오류메시지를 볼 수 있다. 그리고 제목에 입력했던 "테스트"는 사라지지 않고 계속 유지되는 것도 확인할 수 있다.
질문 등록에 장고 폼을 적용한 것처럼 답변 등록에도 장고 폼을 적용해보자!
forms.py
파일에 답변 등록 시 사용할 AnswerForm을 작성
from django import forms
from pybo.models import Question, Answer
(... 생략 ...)
class AnswerForm(forms.ModelForm):
class Meta:
model = Answer
fields = ['content']
labels = {
'content': '답변내용',
}
그리고 views.py
에 answer_create 함수를 다음과 같이 작성
(... 생략 ...)
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 오류가 발생하도록 함
그리고 질문 상세 템플릿(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 %}
위처럼 템플릿을 수정하고 답변 내용 없이 답변을 등록하려고 하면 다음과 같이 오류 메시지가 표시된다 !