[Django] 장고의 기본 요소 익히기 - 폼

싱숭생숭어·2023년 5월 7일
0

Django

목록 보기
14/19
post-thumbnail

위 글은 점프 투 장고를 참고해 작성하였습니다.

질문 등록

질문을 등록하려면 먼저 "질문 등록하기" 버튼을 만들어야 한다. 다음처럼 질문 목록 하단(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이 호출

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 함수를 호출하도록 매핑

폼(Form)

흐름에 따라 이제 views.pyquestion_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)과 연결된 폼으로 폼을 저장하면 연결된 모델의 데이터를 저장할수 있는 폼

    • 모델 폼은 이너 클래스인 Meta 클래스가 반드시 필요
    • Meta 클래스에는 사용할 모델과 모델의 속성을 적어야 함
  • 즉, 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 속성을 비워둠 !!

GET과 POST

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}),
        }
  • widgets 속성을 지정하면 subject, content 입력 필드에 form-control과 같은 부트스트랩 클래스를 추가 가능 !

다시 질문 등록 화면을 들어가보면,

다음과 같이 부트스트랩이 적용된 화면을 볼 수 있음 ~

폼 레이블

질문 등록 화면에 표시되는 '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>
  • question_create 함수에서 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 %}
  • 제목(subject) 항목의 value에는 {{ 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 %}

위처럼 템플릿을 수정하고 답변 내용 없이 답변을 등록하려고 하면 다음과 같이 오류 메시지가 표시된다 !

profile
공부합시당

0개의 댓글