반응형
반응형

답변 등록 폼을 추가해 Flask-WTF을 이용하도록 수정하겠습니다.

 

from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField
from wtforms.validators import DataRequired


class QuestionForm(FlaskForm):
    subject = StringField('제목', validators=[DataRequired('제목은 필수 입력 항목입니다.')])
    content = TextAreaField('내용', validators=[DataRequired('내용은 필수 입력 항목입니다.')])

class AnswerForm(FlaskForm):
    content = TextAreaField('내용', validators=[DataRequired('내용은 필수 입력 항목입니다.')])

forms.py 입니다.

 

AnswerForm을 추가시켜서 댓글 작성란에 무조건 입력을 해야하는 검증을 넣었습니다.

from datetime import datetime

from flask import Blueprint, url_for, request, render_template
from werkzeug.utils import redirect

from .. import db
from ..models import Question, Answer
from ..forms import AnswerForm

bp = Blueprint('answer', __name__, url_prefix='/answer')

@bp.route('/create/<int:question_id>', methods=('POST', ))
def create(question_id):
    form = AnswerForm()
    question = Question.query.get_or_404(question_id) # 그 질문에 해당하는 번호를 가져온다.
    if form.validate_on_submit():
        content = request.form['content']  # name 속성이 content인 값
        answer = Answer(content=content, create_date=datetime.now())  # textArea 내용, 작성시각
        question.answer_set.append(answer)  # question.answer_set 은 Answer과 동일 즉, 거기에다가 값을 추가하는 것임
        db.session.commit()  # db에 저장
        return redirect(url_for('question.detail', question_id=question_id))  # 상세 질문과 답변이 있는 페이지로 전달
    return render_template('question/question_detail.html', question=question, form=form)

 

answer_views.py 입니다.

댓글 작성에 만약 form.validate_on_submit()으로 form 검증이 올바른 경우 댓글 등록하는 기능을 넣고

만약 올바르지 않던가 아무런 submit이 없으면 question_detail.html화면을 보여주게 했습니다.

 

from datetime import datetime

from flask import Blueprint, render_template, request, url_for
from ..models import Question
from werkzeug.utils import redirect
from .. import db

from ..forms import QuestionForm, AnswerForm

bp = Blueprint('question', __name__, url_prefix='/question')

@bp.route('/list')
def _list():
    question_list = Question.query.order_by(Question.create_date.desc())
    return render_template('question/question_list.html', question_list=question_list)

@bp.route('/detail/<int:question_id>')
def detail(question_id):
    form = AnswerForm()
    question = Question.query.get_or_404(question_id)
    return render_template('question/question_detail.html', question=question, form=form)

@bp.route('/create/', methods=('GET', 'POST'))
def create():
    form = QuestionForm()
    if request.method == 'POST' and form.validate_on_submit():
        question = Question(subject=form.subject.data, content=form.content.data, create_date=datetime.now())
        db.session.add(question)
        db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('question/question_form.html', form=form)

question_views.py 입니다.

def detail 함수도 수정해줘야 합니다. form값을 보내야지 밑에 새로 정의할 내용

즉 error값에 대한 form값을 못 가져와서 에러를 내보내기 때문입니다.

 

여기에서 form은 댓글 내용이 잘 들어갔는지 확인하는 기능입니다.

<!DOCTYPE html>
<html lang="en">


<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}

    <div class="container my-3">
        <h2 class="border-bottom py-2">{{ question.subject }}</h2>
        <div class="card my-3">
            <div class="card-body">
                <div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
            </div>
            <div class="d-flex justify-content-end">
                <div class="badge badge-light p-2">
                    {{ question.create_date }}
                </div>
            </div>
        </div>


        <h5 class="border-bottom my-3 py-2">{{ question.answer_set|length }}개의 답변이 있습니다.</h5>
        {% for answer in question.answer_set %}
        <div class="card my-3">
            <div class="card-body">
                <div class="card-text" style="white-space : pre-line;">
                    {{ answer.content }}
                </div>
                <div class="d-flex justify-content-end">
                    <div class="badge badge-light p-2">
                        {{ answer.create_date }}
                    </div>
                </div>
            </div>
        </div>
        {% endfor %}

        <h5> {{ question.answer_set|length }}개의 답변이 있습니다. </h5>

        <div>
            <ul>
                {% for answer in question.answer_set %}
                    <li> {{ answer.content }}</li>
                {% endfor %}
            </ul>
        </div>

        <form class="my-3" action="{{ url_for('answer.create', question_id=question.id) }}" method="post">
          {{ form.csrf_token }}
          <!-- 오류표시 Start -->
          {% for field, errors in form.errors.items() %}
              <div class="alert alert-danger" role="alert">
                  <strong>{{ form[field].label }}</strong>: {{ ','.join(errors) }}
              </div>
          {% endfor %}
          <!-- 오류표시 End -->
            <div class="form-group">
                <textarea name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>

    {% endblock %}
</body>
</html>

question_detail.html 입니다.

 

반응형
반응형

사용자에게 입력 양식을 편리하게 제공하기 위해 사용합니다.

플라스크에서 폼을 사용하려면 Flask-WTF라는 라이브러리를 설치해야 합니다.

 

Flask-WTF를 사용하려면 플라스크 환경 변수 SECRET_KEY가 필요합니다.
SECRET_KEY는 CSRF라는 웹 사이트 취약 점 공격을 방지하는데 사용합니다.

(사용자의 요청을 위조하는 웹 사이트 공격 기법입니다. )
CSRF 토큰은 폼으로 전송된 데이터가 실제 웹 페이지에서 작성된 데이터인지를 판단해 주는 역할을 합니다.

 

먼저 cmd를 이용해 설치하겠습니다.

 

import os

BASE_DIR = os.path.dirname(__file__)

SQLALCHEMY_DATABASE_URI = 'sqlite:///{}'.format(os.path.join(BASE_DIR, 'pybo.db'))
SQLALCHEMY_TRACK_MODIFICATIONS = False

SECRET_KEY = 'dev'

# SQLALCHEMY_DATABASE_URI에는 DB접속 주소가 들어간다.
# BASE_DIR 은 루트디렉터리 경로를 의미 (C:/projects/myproject/)'
# pybo.db를 루트디렉터리에 저장하는 것
# SQLALCHEMY_TRACK_MODIFICATIONS는 이벤트 처리 옵션 지금은 필요 없어서 False로 비활성화


config.py을 열어서 SECRET_KEY 환경 변수를 추가하겠습니다.

SECRET_KEY = "dev"는 위험한 설정입니다. 실제 서비스에서는 유추하기 쉬운 문자열을 입력하면 안 됩니다.

 

질문 등록 기능을 넣겠습니다. 먼저 질문 등록 버튼을 만들겠습니다.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}

    <div class="container my-3 text-center">
        <table calss="table">
            <thead>
                <tr class="bg-dark text-white">
                        <th> 번호 </th>
                        <th> 제목 </th>
                        <th> 작성일시 </th>
                </tr>
            </thead>

            <tbody>
                {% if question_list %}
                {% for question in question_list %}
                <tr>
                        <td>{{ loop.index }}</td>
                        <td><a href="{{ url_for('question.detail', question_id=question.id) }}">{{question.subject}}</a></td>
                        <td>{{ question.create_date  }}</td>
                </tr>
                {% endfor %}
                {% else %}
                <tr>
                    <td colspan="3"> 질문이 없습니다. </td>
                </tr>
                {% endif %}
            </tbody>
        </table>
        <a href="{{ url_for('question.create') }}" class="btn btn-primary"> 질문 등록하기 </a>
    </div>

    {% endblock %}

</body>
</html>

 

question_list.html 입니다.

 

from flask import Blueprint, render_template
from pybo.models import Question
from ..forms import QuestionForm

bp = Blueprint('question', __name__, url_prefix='/question')

@bp.route('/list')
def _list():
    question_list = Question.query.order_by(Question.create_date.desc())
    return render_template('question/question_list.html', question_list=question_list)

@bp.route('/detail/<int:question_id>')
def detail(question_id):
    question = Question.query.get_or_404(question_id)
    return render_template('question/question_detail.html', question=question)

@bp.route('/create/', methods=('GET', 'POST'))
def create():
    form = QuestionForm()
    return render_template('question/question_form.html', form=form)

question_views.py 입니다.

 

질문 등록 라우트 함수를 추가했습니다. 여기서 methods 방식을 2개로 적어주세요 (GET, POST)

 

from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField
from wtforms.validators import DataRequired


class QuestionForm(FlaskForm):
    subject = StringField('제목', validators=[DataRequired()])
    content = TextAreaField('내용', validators=[DataRequired()])

pybo/forms.py 입니다.

 

FlaskForm을 상속받아 검증을 해주는 역할을 해줍니다.

 

subject 에는 폼 라벨 '제목'이고 validators=[DataRequired()] 반드시 작성하라는 검증입니다.

content도 위와 동일합니다.

 

(그 외에도 여러한 검증이 있습니다.)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}
    <div class="container">
      <h5 class="my-3 border-bottom pb-2">질문등록</h5>
      <form method="post" class="post-form my-3">

        {{ form.subject.label }}
        {{ form.subject() }}

        {{ form.content.label }}
        {{ form.content() }}

        <button type="submit" class="btn btn-primary">저장하기</button>

      </form>
    </div>
    {% endblock %}
</body>
</html>

question_form.html 입니다.

 

form.subject.label'제목'을 가져오고 form.subject()를 이용해 그러한 검증을 하는 기능이 있는 박스가 생성됩니다.

form.content도 또한 동일합니다.

 

 

 

 

저장해도 저장이 안 될 것입니다. 왜냐하면 db에 저장하는 과정을 추가를 안 했습니다.

추가해보도록 하겠습니다.

 

from flask import Blueprint, render_template, request, url_for
from ..models import Question
from werkzeug.utils import redirect
from .. import db

from ..forms import QuestionForm

bp = Blueprint('question', __name__, url_prefix='/question')

@bp.route('/list')
def _list():
    question_list = Question.query.order_by(Question.create_date.desc())
    return render_template('question/question_list.html', question_list=question_list)

@bp.route('/detail/<int:question_id>')
def detail(question_id):
    question = Question.query.get_or_404(question_id)
    return render_template('question/question_detail.html', question=question)

@bp.route('/create/', methods=('GET', 'POST'))
def create():
    form = QuestionForm()
    if request.method == 'POST' and form.validate_on_submit():
        question = Question(subject=form.subject.data, content=form.content.data, create_date=datetime.now())
        db.session.add(question)
        db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('question/question_form.html', form=form)

question_views.py 입니다.

 

request.methodPOST방식인지 GET방식인지 알 수 있습니다.

form.validate_on_submit()의 경우는 폼 데이터의 정합성을 검사합니다. 

즉 DataRequired() 같은 점검 항목에 이상 없는지 확인해줍니다.

 

질문등록은 GET 방식이기 때문에 render_template을 보여주는 것이고

저장하기의 경우에는 POST방식으로 저장해야해서 methods 방식을 저렇게 한 것입니다.

 

<form method="post" class="post-form my-3">

또한 post-form 이기 때문에 post방식이라서 위에 post를 추가해준 것입니다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}

    <div class="container">
      <h5 class="my-3 border-bottom pb-2">질문등록</h5>
      <form method="post" class="post-form my-3">

        {{ form.subject.label }}
        {{ form.subject(class="form-control") }}

        {{ form.content.label }}
        {{ form.content(class="form-control") }}

        <button type="submit" class="btn btn-primary">저장하기</button>

      </form>
    </div>

    {% endblock %}

</body>
</html>

question_form.html 입니다.

 

기능은 잘 작동 안 하지만 먼저 폼에 부트스트랩을 적용해보도록 하겠습니다.

저런식으로 class를 넣어서 부트스트랩을 적용시킬 수 있습니다.

 

위와 같은 방식으로하면 HTML 코드를 자동적으로 생성해 빠르게 만드는데 도움이 되지만

내가 원하는 디자인 적용하기나 속성 추가하기도 어렵습니다. 직접 다시 수정해서 만들어보도록 하겠습니다.

 

어떤 방식으로 하든 정답이 없지만 여기에서는 직접 HTML을 작성하는 방식으로 구성할 것입니다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}

    <div class="container">
      <h5 class="my-3 border-bottom pb-2">질문등록</h5>
      <form method="post" class="post-form my-3">
          <!-- 오류표시 Start -->
          {% for field, errors in form.errors.items() %}
              <div class="alert alert-danger" role="alert">
                  <strong>{{ form[field].label }}</strong>: {{ ','.join(errors) }}
              </div>
          {% endfor %}
          <!-- 오류표시 End -->
          <div class="form-group">
              <label for="subject">제목</label>
              <input type="text" class="form-control" name="subject" id="subject">
          </div>
          <div class="form-group">
              <label for="content">내용</label>
              <textarea class="form-control" name="content" id="content" rows="5"></textarea>
          </div>
          <button type="submit" class="btn btn-primary">저장하기</button>
      </form>
    </div>

    {% endblock %}

</body>
</html>

question_form.html 입니다.

 

이제 기능이 왜 작동 안 하는지 알아보도록 하겠습니다.

form.errors.items() 을 이용하면 에러를 여러개를 잡아줍니다. (제목 : field , 에러내용 : errors)

form[field].label오류 제목 errors라는 곳에는 오류 내용이 들어갑니다.

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}

    <div class="container">
      <h5 class="my-3 border-bottom pb-2">질문등록</h5>
      <form method="post" class="post-form my-3">
          {{ form.csrf_token }}
          <!-- 오류표시 Start -->
          {% for field, errors in form.errors.items() %}
              <div class="alert alert-danger" role="alert">
                  <strong>{{ form[field].label }}</strong>: {{ ','.join(errors) }}
              </div>
          {% endfor %}
          <!-- 오류표시 End -->
          <div class="form-group">
              <label for="subject">제목</label>
              <input type="text" class="form-control" name="subject" id="subject">
          </div>
          <div class="form-group">
              <label for="content">내용</label>
              <textarea class="form-control" name="content" id="content" rows="5"></textarea>
          </div>
          <button type="submit" class="btn btn-primary">저장하기</button>
      </form>
    </div>

    {% endblock %}

</body>
</html>

검증에 필요한 CSRF 토큰이 빠졌다라는 오류가 나오게 됩니다. 이걸 해결해 보겠습니다.

그냥 간단하게 {{ form.csrf_token }}를 추가시키면 됩니다.

 

 

 

잘 작동하게 됩니다. 하지만 오류메세지가 발생했을 때 입력한 값이 다 초기화되어서

다시 쳐야하는 불상사가 찾아옵니다. (티스토리에서 날려먹은 자료가 몇갠지.. 하..)

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}

    <div class="container">
      <h5 class="my-3 border-bottom pb-2">질문등록</h5>
      <form method="post" class="post-form my-3">
          {{ form.csrf_token }}
          <!-- 오류표시 Start -->
          {% for field, errors in form.errors.items() %}
              <div class="alert alert-danger" role="alert">
                  <strong>{{ form[field].label }}</strong>: {{ ','.join(errors) }}
              </div>
          {% endfor %}
          <!-- 오류표시 End -->
          <div class="form-group">
              <label for="subject">제목</label>
              <input type="text" class="form-control" name="subject" id="subject" value="{{ form.subject.data or '' }}">
          </div>
          <div class="form-group">
              <label for="content">내용</label>
              <textarea class="form-control" name="content" id="content" rows="5" value="{{ form.content.data or '' }}"></textarea>
          </div>
          <button type="submit" class="btn btn-primary">저장하기</button>
      </form>
    </div>

    {% endblock %}

</body>
</html>

request_form.html 입니다.

 

value="{{ form.subject.data or '' }}" 이거만 추가시켜주면 됩니다.

 

form.forms.py에서 작성한 변수.data 그 변수의 데이터를 보관해준다는 의미입니다. 

or ''의 경우는 데이터가 아무 것도 안 적혀있을 때 None 값이 나오게되는데 기본값을 ''으로 해준다는 의미입니다.

 

 

from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField
from wtforms.validators import DataRequired


class QuestionForm(FlaskForm):
    subject = StringField('제목', validators=[DataRequired('제목은 필수 입력 항목입니다.')])
    content = TextAreaField('내용', validators=[DataRequired('내용은 필수 입력 항목입니다.')])

forms.py 입니다.

 

나오는 오류 메세지를 한글로 바꿔보도록 하겠습니다. DataRequired 안에 추가만 시키면 됩니다.

 

 

반응형
반응형

HTML 파일을 구성할 때 중복되는 부분이 있습니다.

예를 들어서 부트스트랩의 link가 대표적이고
어떤 부분은 어느 한 부분만 똑같은 부분이 있을 수도 있습니다.



그래서 중복되는 것들을 하나의 html에 저장해서 불러오면 되는 것입니다.

(여기에서는 위에 부분이 바뀌지 않음)

{% extends 'base.html' %}
이걸로 불러 올 수 있습니다. 하지만 이것만 쓰면 전체가 다 덮어씌워지게 됩니다.
즉 question_list에 적으면 base.html로 다 덮어씌워지게 되는 것이죠
그래서 {% block content %} 와 {% endblock %} 을 사용해서 question_list가 쓰일 수 있는 영역을 만들어주는 것입니다.

 

<!DOCTYPE html>
<html lang="en">
<head>

    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Title</title>
</head>
<body>

    {% block content %}
    {% endblock %}

</body>
</html>

base.html 입니다.

 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}

    <div class="container my-3 text-center">
        <table calss="table">
            <thead>
                <tr class="bg-dark text-white">
                        <th> 번호 </th>
                        <th> 제목 </th>
                        <th> 작성일시 </th>
                </tr>
            </thead>

            <tbody>
                {% if question_list %}
                {% for question in question_list %}
                <tr>
                        <td>{{ loop.index }}</td>
                        <td><a href="{{ url_for('question.detail', question_id=question.id) }}">{{question.subject}}</a></td>
                        <td>{{ question.create_date  }}</td>
                </tr>
                {% endfor %}
                {% else %}
                <tr>
                    <td colspan="3"> 질문이 없습니다. </td>
                </tr>
                {% endif %}
            </tbody>
        </table>
    </div>

    {% endblock %}

</body>
</html>

question.list.html 입니다.

<!DOCTYPE html>
<html lang="en">


<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    {% extends 'base.html' %}
    {% block content %}

    <div class="container my-3">
        <h2 class="border-bottom py-2">{{ question.subject }}</h2>
        <div class="card my-3">
            <div class="card-body">
                <div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
            </div>
            <div class="d-flex justify-content-end">
                <div class="badge badge-light p-2">
                    {{ question.create_date }}
                </div>
            </div>
        </div>


        <h5 class="border-bottom my-3 py-2">{{ question.answer_set|length }}개의 답변이 있습니다.</h5>
        {% for answer in question.answer_set %}
        <div class="card my-3">
            <div class="card-body">
                <div class="card-text" style="white-space : pre-line;">
                    {{ answer.content }}
                </div>
                <div class="d-flex justify-content-end">
                    <div class="badge badge-light p-2">
                        {{ answer.create_date }}
                    </div>
                </div>
            </div>
        </div>
        {% endfor %}

        <h5> {{ question.answer_set|length }}개의 답변이 있습니다. </h5>

        <div>
            <ul>
                {% for answer in question.answer_set %}
                    <li> {{ answer.content }}</li>
                {% endfor %}
            </ul>
        </div>

        <form class="my-3" action="{{ url_for('answer.create', question_id=question.id) }}" method="post">
            <div class="form-group">
                <textarea name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>

    {% endblock %}
</body>
</html>

question.detail.html 입니다.

반응형
반응형

웹 페이지에 디자인을 적용하려면 CSS를 사용해야 합니다.

 

CSS 파일이 pybo/static 디렉터리에 있어야 합니다.

 

cmd를 이용해 static폴더를 만들어 주겠습니다.

 

우리가 적용할 CSS텍스트 창 너비 100% 답변등록 버튼 위에 마진 10px을 추가하겠습니다.

 

style.css라는 이름으로 만들어주세요 이제 link 태그로 가져와야겠죠?

 

<!DOCTYPE html>
<html lang="en">

<link rel="stylesheet" href=" {{ url_for('static', filename='style.css') }} ">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1> {{ question.subject }} </h1>

    <div>
        {{ question.content }}
    </div>

    <h5> {{ question.answer_set|length }}개의 답변이 있습니다. </h5>

    <div>
        <ul>
            {% for answer in question.answer_set %}
                <li> {{ answer.content }}</li>
            {% endfor %}
        </ul>
    </div>

    <form action="{{ url_for('answer.create', question_id=question.id) }}" method="post">
        <textarea name="content" id="content" row="15"></textarea>
        <input type="submit" value="답변등록">
    </form>

</body>
</html>

link 태그를 question_detail.html에 추가해주세요

 

부트스트랩을 적용해보겠습니다.

 

2가지 방법이 있죠?

 

1. cdn쓰기

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

2. 다운로드 받아서 사용하기

<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">

 

부트스트랩에 관한 내용은 포스팅 했으니 그걸 참고해주세요

 

<!DOCTYPE html>
<html lang="en">

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div class="container my-3 text-center">
    <table calss="table">
        <thead>
            <tr class="bg-dark text-white">
                    <th> 번호 </th>
                    <th> 제목 </th>
                    <th> 작성일시 </th>
            </tr>
        </thead>

        <tbody>
            {% if question_list %}
            {% for question in question_list %}
            <tr>
                    <td>{{ loop.index }}</td>
                    <td><a href="{{ url_for('question.detail', question_id=question.id) }}">{{question.subject}}</a></td>
                    <td>{{ question.create_date  }}</td>
            </tr>
            {% endfor %}
            {% else %}
            <tr>
                <td colspan="3"> 질문이 없습니다. </td>
            </tr>
            {% endif %}
        </tbody>
    </table>
</div>

</body>
</html>

question_list.html 입니다. 여기에서 cdn을 사용했습니다.

<!DOCTYPE html>
<html lang="en">

<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div class="container my-3">
        <h2 class="border-bottom py-2">{{ question.subject }}</h2>
        <div class="card my-3">
            <div class="card-body">
                <div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
            </div>
            <div class="d-flex justify-content-end">
                <div class="badge badge-light p-2">
                    {{ question.create_date }}
                </div>
            </div>
        </div>


        <h5 class="border-bottom my-3 py-2">{{ question.answer_set|length }}개의 답변이 있습니다.</h5>
        {% for answer in question.answer_set %}
        <div class="card my-3">
            <div class="card-body">
                <div class="card-text" style="white-space : pre-line;">
                    {{ answer.content }}
                </div>
                <div class="d-flex justify-content-end">
                    <div class="badge badge-light p-2">
                        {{ answer.create_date }}
                    </div>
                </div>
            </div>
        </div>
        {% endfor %}

        <h5> {{ question.answer_set|length }}개의 답변이 있습니다. </h5>

        <div>
            <ul>
                {% for answer in question.answer_set %}
                    <li> {{ answer.content }}</li>
                {% endfor %}
            </ul>
        </div>

        <form class="my-3" action="{{ url_for('answer.create', question_id=question.id) }}" method="post">
            <div class="form-group">
                <textarea name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>
</body>
</html>

question_detail.html입니다. 여기에서 다운로드 받은 걸 사용했습니다.

 

 

반응형
반응형
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1> {{ question.subject }} </h1>

    <div>
    {{ question.content }}
    </div>

    <form action="{{ url_for('answer.create', question_id=question.id}}" method="post">
        <textarea name="content" id="content" row="15"></textarea>
        <input type="submit" value="답변등록">
    </form>

</body>
</html>
from datetime import datetime

from flask import Blueprint, url_for, request
from werkzeug.utils import redirect

from pybo import db
from pybo.models import Question, Answer

bp = Blueprint('answer', __name__, url_prefix='/answer')

@bp.route('/create/<int:question_id>', methods=('Post', ))
def create(question_id):
    question = Question.query.get_or_404(question_id) # 그 질문에 해당하는 번호를 가져온다.
    content = request.form['content'] # name 속성이 content인 값
    answer = Answer(content=content, create_date=datetime.now()) # textArea 내용, 작성시각
    question.answer_set.append(answer)  # question.answer_set 은 Answer과 동일 즉, 거기에다가 값을 추가하는 것임
    db.session.commit() # db에 저장
    return redirect(url_for('question.detail', question_id=question_id)) # 상세 질문과 답변이 있는 페이지로 전달

 

이번에는 답변 등록 기능을 만들겠습니다.

 

먼저 question_detail.html에 태그를 작성해주세요

그 후 기능을 넣을 부분을 answer_views.py를 작성해서 넣어주세요

 

@bp.route('/create/<int:question_id>', methods=('Post', ))

methods 방식은 form에 적은 거랑 동일해야합니다.

 

content = request.form['content'

from안에 있는 name이 content라는 내용을 가져오는 것입니다.

<textarea name="content" id="content" row="15"></textarea>

 

answer = Answer(content=content, create_date=datetime.now())

textArea 내용, 작성시각을 저장해서 answer이라는 객체에 넣습니다.

 

question.answer_set.append(answer)

question.answer_setAnswer과 동일합니다. 즉, 거기에다가 값을 추가하는 것입니다.


db.session.commit() 

답변 내용을 db에 저장한 걸 적용시킵니다.


return redirect(url_for('question.detail', question_id=question_id)) 

답변을 작성 했으면 다시 상세 질문과 답변이 있는 페이지로 이동해야하니 redirect했습니다.

 

from flask import Flask
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

import config

db = SQLAlchemy()
migrate = Migrate()

def create_app():
        app = Flask(__name__)
        app.config.from_object(config) # config.py에 작성 내용을 환경 변수로 부르기 위한 작업

        # ORM
        db.init_app(app) # 우리가 적은 환경으로 DB 초기화
        migrate.init_app(app,db) # DB와 우리가 적은 환경을 다른 환경으로 옮김
        from . import models

        from .views import main_views, question_views, answer_views # .view에서 .은 현재위치의 views 폴더로부터 import한다는 말
        app.register_blueprint(main_views.bp) # 블루프린트 객체 bp 등록
        app.register_blueprint(question_views.bp) # 블루프린트 객체 bp 등록
        app.register_blueprint(answer_views.bp) 

        return app

블루프린터 적용을 __init__.py에 추가해주세요

 

지금까지 등록 답변을 저장하는 기능만 했고 이제 보여주는 기능을 만들겠습니다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1> {{ question.subject }} </h1>

    <div>
        {{ question.content }}
    </div>

    <h5> {{ question.answer_set|length }}개의 답변이 있습니다. </h5>

    <div>
        <ul>
            {% for answer in question.answer_set %}
                <li> {{ answer.content }}</li>
            {% endfor %}
        </ul>
    </div>

    <form action="{{ url_for('answer.create', question_id=question.id) }}" method="post">
        <textarea name="content" id="content" row="15"></textarea>
        <input type="submit" value="답변등록">
    </form>

</body>
</html>

question_detail.html을 수정 해주세요

|length탬플릿 필터인데 객체의 길이를 반환합니다.

반응형
반응형

 

from flask import Blueprint, render_template
from pybo.models import Question


bp = Blueprint('main', __name__, url_prefix='/')

@bp.route('/hello')
def hello_pybo():
        return 'Hello, Pybo!'

@bp.route('/')
def index():
        question_list = Question.query.order_by(Question.create_date.desc())
        return render_template('question/question_list.html', question_list=question_list)

main_views.py 파일을 수정 해보겠습니다.

 

질문 목록Question.query.order_by로 얻을 수 있습니다. (order_by는 조회 결과를 정렬해줍니다.)
Question.create_date.desc() 코드는 작성일시 기준 역순으로 정렬하라는 의미입니다.

 

즉, 가장 최신 내용이 첫번째로 나오게 되고 가장 오래된 게 뒤에 나오게 되는 것이죠

하루전 글

3일전  글 .... 이런식이여야지

 

3일전   글

하루전  글... 이렇진 않잖아요?

 

question_list에 대해선 좀 이따 이야기하겠습니다.

 


render_template 함수는 템플릿 파일을 화면에 그려줍니다.
사용할 question/quesiton_list.html 템플릿 파일을 작성해야 합니다.
이 파일은 template라는 디렉터리에 저장하면 별 다른 설정 하지 않아도 

template 디렉터리를 템플릿 디렉터리로 인식합니다.

 

 

cmd로 디렉터리를 만들어줬고 파이참에서 html파일을 그 안에 만들어 줬습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

{% if question_list %}
    <ul>
        {% for question in question_list %}
            <li>
                <a href="/detail/{{question.id}}/">{{question.subject}}</a>
            </li>
        {% endfor %}
    </ul>
{% else %}
    <p> 질문이 없습니다. </p>
{% endif %}

</body>
</html>

{% %} 으로 둘러싸인 문장탬플릿 태그라고 합니다. JSP같은 느낌입니다. 이 태그가 파이썬 코드와 연결 됩니다.

 

{% if question_list %}

question_list 가 있는지 확인 합니다. 있으면 True값을 반환해서 참값인 부분에 들어가겠죠?

 

{% for question in question_list %} 

for 문 입니다. question_list값이 하나씩 question에 들어가게 되는 것이죠

 

{% else %}

말 그대로 else문 입니다.

 

{% endif %}

if문이 끝나는 곳을 알려줍니다.

 

@bp.route('/')
def index():
        question_list = Question.query.order_by(Question.create_date.desc())
        return render_template('question/question_list.html', question_list=question_list)

여기에서 그럼 question_list는 뭐냐고 물으신다면 바로 여기입니다. question_list = question_list

question_list는 넘겨 빈 값을 의미하고 question_listQuestion.query로 조회한 질문 DB 내용입니다.

 

 

자주 사용하는 템플릿 태그에 대해서 알아보도록 하겠습니다.

 

1. if, elif, else문

 

{% if 조건문1 %}
	<p> 조건문1에 해당하면 실행 </p>
{% elif 조건문2 %}
	<p> 조건문2에 해당하면 실행 </p>
{% else %}
	<p> 모두 해당하지 않으면 실행 </p>
{% endif %}

그냥 보시면 공부하셨으니까 알 거라고 생각합니다. 꼭 endif로 닫아줘야 합니다.

 

2. 반복문 태그

 

{% for item in list %}
	<p> 순서 : {{ loop.index }} </p>
    <p> {{ item }} </p>
{% endfor %}

반복문도 공부하셨으면 구조가 아주 유사해서 잘 아실 겁니다.

 

loop.index는 반복 순서를 나타냅니다. 1부터 1씩 증가

loop.index()는 반복 순서를 나타냅니다. 0부터 1씩 증가

 

이 외에도 여러 가지가 있지만 여기까지만 하겠습니다.

 

3. 객체 태그 

 

{{ 객체 }}
{{ 객체.속성 }}

이렇게 객체나 객체 속성을 출력하거나 그 안에서 계산도 가능합니다.

변수는 어떤가요? 이러는데 파이썬은 객체로만 이루어진 프로그래밍 언어라서 변수 취급하는 것도 다 가능합니다.

 

나머지는 하면서 더 알아보도록 하겠습니다.

 

<a href="/detail/{{question.id}}/">{{question.subject}}</a>

이제 여기에 연결될 부분을 구현하겠습니다. 

 

먼저 라우트 함수를 구현해야 하죠

from flask import Blueprint, render_template
from pybo.models import Question


bp = Blueprint('main', __name__, url_prefix='/')

@bp.route('/hello')
def hello_pybo():
        return 'Hello, Pybo!'

@bp.route('/')
def index():
        question_list = Question.query.order_by(Question.create_date.desc())
        return render_template('question/question_list.html', question_list=question_list)

@bp.route('/detail/<int:question_id>/')
def detail(question_id) :
        question = Question.query.get_or_404(question_id)
        return render_template('question/question_detail.html', question=question)

main_views.py를 수정해줍시다.

 

@bp.route('/detail/<int:question_id>/')

<자료형 : 테이블 속성명>

db에서 가져올 속성 이름과 자료형을 같이 적어줘야 합니다.

 

Question.query.get_or_404get으로 가져오는 방식과 동일한데 만약 값이 없을 때 get은 빈 값을 넣어주지만

get_or_404의 경우는 없으면 404 에러 페이지를 띄웁니다.

get으로 빈 페이지를 보여주는 것보단 404 에러페이지를 띄우는게 더 좋겠죠?

 

render_template을 이용해 templates 폴더에 있는 question_detail.html파일을 가져옵니다. 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <h1> {{ question.subject }} </h1>

  <div>
    {{ question.content }}
  </div>
</body>
</html>

question_detail.html파일을 코딩하겠습니다.

 

1번은 저번에 삭제해서 404가 나옵니다.

 

 

이제 블루프린트로 파일을 분리해서 관리하겠습니다.

 

question_views.py 파일을 새로 만들고 질문 목록 조회와 질문 상세 조회 기능을 이동하겠습니다.

 

from flask import Blueprint, render_template
from pybo.models import Question

bp = Blueprint('question', __name__, url_prefix='/question')

@bp.route('/list')
def _list():
    question_list = Question.query.order_by(Question.create_date.desc())
    return render_template('question/question_list.html', question_list=question_list)

@bp.route('/detail/<int:question_id>')
def detail(question_id):
    question = Question.query.get_or_404(question_id)
    return render_template('question/question_detail.html', question=question)

question_views.py 입니다.

 

from flask import Flask
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

import config

db = SQLAlchemy()
migrate = Migrate()

def create_app():
        app = Flask(__name__)
        app.config.from_object(config) # config.py에 작성 내용을 환경 변수로 부르기 위한 작업

        # ORM
        db.init_app(app) # 우리가 적은 환경으로 DB 초기화
        migrate.init_app(app,db) # DB와 우리가 적은 환경을 다른 환경으로 옮김
        from . import models

        from .views import main_views, question_views # .view에서 .은 현재위치의 views 폴더로부터 import한다는 말
        app.register_blueprint(main_views.bp) # 블루프린트 객체 bp 등록
        app.register_blueprint(question_views.bp) # 블루프린트 객체 bp 등록

        return app

__init__.py입니다.

 

from flask import Blueprint, render_template, url_for
from werkzeug.utils import redirect

bp = Blueprint('main', __name__, url_prefix='/')

@bp.route('/hello')
def hello_pybo():
        return 'Hello, Pybo!'

@bp.route('/')
def index():
        return redirect(url_for('question._list'))

main_views.py입니다. 중복된 거를 빼고 index()를 수정했습니다.

 

question._list에 해당하는 URL로 redirect할 수 있게 수정했습니다. 입력받은 URL로 리다이렉트 해줍니다.

 

url_for에 있는 question._listquestion, _list 순서로 해석되어 함수명을 찾아줍니다.

question은 등록된 블루 프린트 이름을 의미하고

(bp = Blueprint('question', __name__, url_prefix='/question') 이 부분입니다.

_list블루 프린트에 등록된 함수명이라고 생각하면 됩니다.

@bp.route('/list')
def _list():
    question_list = Question.query.order_by(Question.create_date.desc())
    return render_template('question/question_list.html', question_list=question_list)

이 부분입니다.

 

@bp.route('/list/')이므로 bp의 접두어(url_prefix)인 /question//list(라우트값)가 더해진

/question/list/ URL을 반환합니다.

 

즉, url_for('블루프린트 이름 . 등록된 함수명) 입니다. 

 

그러면 저 부분은 render_template가 있기 때문에 저 html로 연결해줍니다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

{% if question_list %}
    <ul>
        {% for question in question_list %}
            <li>
                <a href="{{ url_for('question.detail', question_id=question.id)}}">{{question.subject}}</a>
            </li>
        {% endfor %}
    </ul>
{% else %}
    <p> 질문이 없습니다. </p>
{% endif %}

</body>
</html>

하드코딩된 question_list.html에 부분도 수정하겠습니다

 

<a href="{{ url_for('question.detail', question_id=question.id)}}">{{question.subject}}</a>

이 부분을 수정했습니다. url_for를 템플릿태그로 쓰려면 {{ }} 안에다 적으시면 됩니다.

 

여기에서도 보면 question 블루프린트 이름에 detail이라는 함수로 연결해주는 것이죠

그리고 question_id값을 같이 보내줍니다.

 

여기에서 question은 question_list으로부터 담긴 부분입니다.

@bp.route('/list')
def _list():
    question_list = Question.query.order_by(Question.create_date.desc())
    return render_template('question/question_list.html', question_list=question_list)

question_list는 여기로부터 question_list=question_list로 위에 question_list.html로 보낸 것입니다.

 

이제 그걸 다시 url_for로 /question/detail/<int:question_id>로 보내는 것입니다.

question_id값도 같이 보냈으니 정확한 url값이 나오게 되겠죠?

 

 

 

 

 

url_for를 사용하는 장점은 유지보수가 쉬워집니다.

 

예를 들어 보겠습니다. (밑에 예시는 위에 내용하고 관계 없습니다.)

 

<a href="/question/detail/{{ question.id }}">{{ question.subject }}</a> 이렇게 하드 코딩이 되어 있으면

다음 과 같은 URL 구성방식 자체가 변경되면 이런 코드는 쉽게 대응하기 힘듭니다.

 

localhost:5000/detail/question/2 (기존)

localhost:5000/detail/2/question (바꿀 것)

 

이렇게 href가 여러개로 연결 되어 있는 부분이 있으면 하나씩 href 부분을 바꿔야합니다.

하지만 url_for를 이용하는 경우는 bp.route부분을 바꾸면 다른 부분은 바꿔줄 필요가 없습니다.

 

(bp = Blueprint('question', __name__, url_prefix='/detail') 

 

@bp.route('/question/<int:question_id>') 이거를

@bp.route('/<int:question_id>/question') 이런식으로 바꾸면 되겠죠?

 

 

반응형
반응형

플라스크 셸은 플라스크를 실행하는데 필요한 환경이 자동적으로 설정되는 것입니다.

(즉 cmd에서 플라스크를 구동하는데 필요한 환경이 자동적으로 설정되는 것이죠)

 

 

Question과 Answer 모델을 플라스크 셸에 불러오겠습니다. 그 후 Question 모델 객체를 만들겠습니다.

 

지운 부분은 제가 오타내서 오류난 거라 무시하시면 됩니다.

 

q객체를 만들었다고 해서 데이터베이스에 저장되는 것은 아닙니다.

데이터 베이스에 저장하려면 다음처럼 SQLAlchemy의 객체를 사용해야 합니다.(DB가 이해할 수 있는 걸로 바꾸기)

 

add 함수를 사용하고 commit까지 실행해야 합니다. 

db.session데이터베이스와 연결된 세션 즉, 접속된 상태를 의미합니다.

데이터베이스를 처리하려면 이 세션이 필요합니다. (add 든 delete든 작업 후 무조건 commit은 해줘야 합니다.)

작업을 취소하고 싶으면 db.session.rollback을 실행하시면 commit전으로 돌아가게됩니다.

 

결과를 보시면 id가 1이 들어감을 볼 수 있습니다. 하나 더 작성 후 다시 확인해 보겠습니다.

이번엔 2가 들어간게 보입니다. 잘 들어갔군요

 

Question.query.all 함수로 모든 데이터베이스에 저장된 질문 데이터를 조회할 수 있습니다.

 

filter함수를 이용해 첫번째 질문 데이터만 조회하도록 하겠습니다.

(filter 함수는 조건에 맞는 데이터를 모두 반환해줍니다.) 여기에선 Question.id값이 1인 것만 조회했습니다.

 

다만 get 함수로 조회하면 리스트가 아닌 Question 객체 1개만 반환 합니다. 

 

<Question 1> (Question.query.get으로 조회)

[<Question 1>](Question.query.filter로 조회)

 

Question.query.filter의 경우는 리스트에 담겨 있죠?

 

Question.subject.like('%플라스크%') 코드의 의미는 Question 모델 subject 속성에

플라스크라는 문자열이 포함되었는가? 라는 의미이다.

 

플라스크%   : '플라스크'로 시작하는 문자열

%플라스크   : '플라스크'로 끝나는 문자열

%플라스크% : '플라스크'를 포함하는 문자열

 

 

 

이번엔 데이터를 수정해보겠습니다.

 

위에 조회하는 방법을 이용해 객체에 담은 다음 속성을 접근해서 바꾼다음 commit 해주었습니다.

 

이번엔 데이터를 삭제해보겠습니다.

 

위에 데이터를 조회해서 객체에 담은 후 q에 담긴 <Question 1>을 찾아삭제후 적용시키고 검색해보니

값이 안 나온 걸 보니 잘 삭제 된 걸 알 수 있습니다.

 

답변 데이터를 생성 후 저장해보겠습니다.

 

id가 2인 질문 데이터를 가져온 다음 q에 저장했습니다. q = Question.query.get(2)

a에 그에 해당하는 답변을 넣고 그걸 db에 추가 후 적용 시킵니다.

 

question에 q를 대입하면 question_id에 값을 지정하지 않아도 자동으로 입력됩니다.

왜냐하면 question값은 역참조가 걸려있어서 질문에 대한 id값이 들어오게 되면 그걸로 찾아갈 수 있게 되어있습니다.

 

a는 Answer에 대한 건데 a.question이라고 하면 Answer과 연결된 Question을 가져올 수 있습니다.

또한 q는 Question에 대한 건데 q.answer_set이라고 하면 그에 해당하는 Answer이 뭔지 가져올 수 있습니다.

backref=db.backref('answer_set')으로 적용 했기 때문

 

역참조라는 녀석은 아주 신통방통한 녀석으로서 꼭 기억해 두세요

 

 

반응형
반응형

@app.route 같이 어노테이션으로 맵핑되는 함수라우트 함수라고 합니다.

새로운 URL이 생길 때 라우트 함수를 create_app 함수 안에 계속 추가해야 하는 불편함이 있습니다. 

이 때 사용할 수 있는 클래스가 블루프린트입니다.

블루 프린트를 이용하면 라우트 함수를 구조적으로 관리할 수 있습니다.

@bp.route에서 bp는 Blueprint클래스로 생성한 객체입니다. 
생성시 Blueprint(이름모듈명URL프리픽스)를 전달해야 합니다.

url_prefix에 대해 설명하자면 url_prefix='/' 일 경우 localhost:5000/ 
(localhost:5000은 기본적으로 설정된 아이피와 포트번호입니다. 다를 수도 있어요)

url_prefix='/main' 이라고 입력 되면 localhost:5000/main이 되게 됩니다.

 



from flask import Blueprint

bp = Blueprint('main', __name__, url_prefix='/')

@bp.route('/hello')
def hello_pybo():
        return 'Hello, Pybo!'

@bp.route('/')
def index():
        return 'Pybo, index'

main_views.py 내용을 작성해주세요

from flask import Flask

def create_app():
        app = Flask(__name__)

        from .views import main_views # .view에서 .은 현재위치의 views 폴더로부터 import한다는 말
        app.register_blueprint(main_views.bp) # 블루프린트 객체 bp 등록

        return app

pybo.py를 수정해주세요

 

이렇게 블루프린트 객체app.register_blueprint(파일.bp객체명)으로 등록해주면 main_views.py를 참조하게 됩니다.

 

 

이제 플라스크 실행 후 들어가서 확인 해보겠습니다.

 

반응형
반응형

프로젝트 규모가 커질수록 문제가 발생할 확률이 높아집니다.

app 객체를 전역으로 사용할 때 발생하는 문제가 생기는데
예방하려면 애플리케이션 팩토리를 사용하라고 권합니다.

pybo.py파일을 pybo/__init__.py 파일로 대체하고 플라스크 서버를 실행해보겠습니다.

pybo라는 폴더를 생성해주고 기존의 pybo.py를 pybo/__init__.py로 옮기고 이름을 변경해봤습니다.

그리고 실행 시켰는데 아주 잘 실행됩니다.

 

우리는 저번에 FLASK_APP=pybo로 설정해놨기 때문에 c:\projects\myproject 에서 pybo.py를 찾을 수 있습니다.

만약 pybo.py가 아니라 pybo123로 cmd를 설정하고 pybo123으로 이름 바꾸면 또한 찾을 수 있습니다.

 

그리고 만약 pybo라는 파일이 없는데 폴더가 있는 경우 거기에서

기본적으로 __init__.py라는 파일을 실행(기본적으로 __init__.py가 default값)시켜주는 것입니다.

애플리케이션 팩토리를 사용해보도록 하겠습니다.

 

from flask import Flask

def create_app():
        app = Flask(__name__)
        @app.route('/')
        def heello_pybo():
            return 'Hello, Pybo!'
        return app

만약 app이라는 변수를 전역으로 사용할 경우 문제가 생길 수 있기 때문에 지역변수로 선언해 문제가 없게 합니다.

create_app 대신 다른 이름을 사용하면 정상 동작하지 않습니다. flask 내부 정의된 함수명입니다.

 

반응형