반응형
반응형
from pybo import db

class Question(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    subject = db.Column(db.String(200), nullable = False)
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    user = db.relationship('User',backref=db.backref('question_set'))
    modify_date = db.Column(db.DateTime(), nullable=True)

class Answer(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete = 'cascade'))
    question = db.relationship('Question', backref=db.backref('answer_set', ))
    # question = db.relationship('Question', backref=db.backref('answer_set', casecade='all, delete-orphan'))
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    user = db.relationship('User',backref=db.backref('answer_set'))
    modify_date = db.Column(db.DateTime(), nullable=True)

class User(db.Model) :
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

models.py 입니다.

언제 질문, 답변을 수정했는지 알 수 있는 modify_date 필드를 추가하겠습니다.

 

모델이 변경 했으니 migrate, upgrade 명령을 수행하겠습니다.

 

<!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">
                <span class="badge bg-light text-dark text-left">
                    <span class="mb-2"> {{ question.user.username }} </span>
                    <span> {{ question.create_date|datetime }} </span>
                </span>
            </div>
        </div>

        {% if g.user == question.user %}
        <div class="my-3">
            <a href="{{ url_for('question.modify', question_id=question.id) }}" class="btn btn-sm btn-outline-secondary">수정</a>
        </div>
        {% endif %}


        <h5 class="border-bottom my-3">{{ 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>
            <div class="d-flex justify-content-end">
                <span class="badge bg-light text-dark text-left">
                    <span class="mb-2"> {{ answer.user.username }}  </span>
                    <span> {{ answer.create_date|datetime }} </span>
                </span>
            </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 {% if not g.user %} disabled {% endif %} name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>

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

question_detail.html 입니다.

 

HTMl 을 수정해서 로그인한 사용자와 글쓴이가 같은 경우에만 보여야 합니다. 

{% if g.user == question.user %}

 

from datetime import datetime

from flask import Blueprint, render_template, request, url_for, g, flash

from .auth_views import login_required
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():
    page = request.args.get('page', type=int, default=1) # 페이지
    question_list = Question.query.order_by(Question.create_date.desc())
    question_list = question_list.paginate(page, per_page=10)
    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'))
@login_required
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(), user=g.user)
        db.session.add(question)
        db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('question/question_form.html', form=form)

@bp.route('/modify/<int:question_id>', methods=('GET','POST'))
@login_required
def modify(question_id):
    question = Question.query.get_or_404(question_id)
    if g.user != question.user:
        flash('수정권한이 없습니다.')
        return redirect(url_for('question.detail', question_id=question_id))
    if request.method == 'POST':
        form = QuestionForm()
        if form.validate_on_submit():
            form.populate_obj(question)
            question.modify_date = datetime.now()
            db.session.commit()
            return redirect(url_for('question.detail', question_id=question_id))
    else:
        form = QuestionForm(obj=question)
    return render_template('question/question_form.html', form=form)

question_views.py 입니다.

 

질문 수정은 로그인이 필요해서 @login_required을 이용해 로그인이 안 됐으면 로그인페이지로 가게 만들었고

현재 유저와 작성자와 같아야 수정할 수 있기 때문에 같지 않으면 flash('수정권한이 없습니다')

 

현재 페이지에 있는 question 정보 obj에 담아 form 저장합니다.

form = QuestionForm(obj=question)

그리고 form 그에 대한 정보를 담아서 question.detail 라우트로 보냅니다.

@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)

 

만약 같고 POST방식으로 요청되면 (수정저장 버튼 눌렀을 때) form 형식이 맞는지 확인하고

현재 페이지 정보(제목 내용 등...)을 form.popluate_obj(question) 이걸 이용해 가져옵니다.

이건 전달된 객체의 폼과 관련된 속성 화면에 있는 폼 데이터로 채웁니다.

 

그 후 question.modify_date = datetime.now() 수정일시를 저장후 commit을 이용해 저장합니다.

 

 

질문 삭제 기능을 만들겠습니다.

<!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">
                <span class="badge bg-light text-dark text-left">
                    <span class="mb-2"> {{ question.user.username }} </span>
                    <span> {{ question.create_date|datetime }} </span>
                </span>
            </div>
        </div>

        {% if g.user == question.user %}
        <div class="my-3">
            <a href="{{ url_for('question.modify', question_id=question.id) }}" class="btn btn-sm btn-outline-secondary">수정</a>
            <a href="#" class="delete btn btn-sm btn-outline-secondary" data-uri="{{ url_for('question.delete', question_id=question.id) }}">삭제</a>
        </div>
        {% endif %}


        <h5 class="border-bottom my-3">{{ 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>
            <div class="d-flex justify-content-end">
                <span class="badge bg-light text-dark text-left">
                    <span class="mb-2"> {{ answer.user.username }}  </span>
                    <span> {{ answer.create_date|datetime }} </span>
                </span>
            </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 {% if not g.user %} disabled {% endif %} name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>

    {% endblock %}
    {% block script %}
    <script type="text/javascript">
        $(document).ready(function(){
            $(".delete").on("click", function(){
                if(confirm("정말로 삭제하시겠습니까?")) {
                    location.href = $(this).data('uri');
                }
            });
        });
    </script>
    {% endblock %}
</body>
</html>

question_detail.html 입니다.

 

href 속성을 #으로 하고 data-uri 라는 기능을 넣었습니다.

jQuery에서 $(this).data('uri')로 삭제 실행하는 URL을 얻으려고 했습니다.

 

{% block script %}

{% endblock %} 을 이용했는데 jQuery를 템플릿에서 사용할 수 있도록 약간 수정해야하기 때문입니다.

 

자바스크립트문을 설명하자면 화면이 준비 됐을 때 $(document).ready(function() .... function 내용이 수행됩니다.

delete 클래스가 클릭 되면 function 이하에 내용이 수행됩니다.

confirm은 확인창어떤 형식으로 나올지에 대한 것입니다.

 

만약 예를 누를 경우

location.href = $(this).data('uri') 가 실행 되는 것이죠 이것의 의미는

현재 누른 것의 data-uri를 href로 연다는 것입니다.

여기선 question.delete로 되어있습니다. 그에 대한 내용을 추가하겠습니다. (라우트)

<!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>

    {% include "navbar.html" %}

    <!-- 기본 템플릿에 삽입할 내용 Start -->
    {% block content %}
    {% endblock %}
    <!-- 기본 템플릿에 삽입할 내용 End -->

    <!-- jQuery JS -->
    <script src="{{ url_for('static', filename='jquery-3.6.0.min.js') }}"> </script>
    <!-- Bootstarp JS -->
    <script src="{{ url_for('static', filename='bootstrap.min.js') }}"> </script>
    <!-- 자바스크립트 -->
    {% block script %}
    {% endblock %}
</body>
</html>

base.html 입니다.

 

{% block script %}

{% endblock %} 을 추가해서 제이쿼리를 이용할 수 있게 만들었습니다.

 

from datetime import datetime

from flask import Blueprint, render_template, request, url_for, g, flash

from .auth_views import login_required
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():
    page = request.args.get('page', type=int, default=1) # 페이지
    question_list = Question.query.order_by(Question.create_date.desc())
    question_list = question_list.paginate(page, per_page=10)
    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'))
@login_required
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(), user=g.user)
        db.session.add(question)
        db.session.commit()
        return redirect(url_for('main.index'))
    return render_template('question/question_form.html', form=form)

@bp.route('/modify/<int:question_id>', methods=('GET','POST'))
@login_required
def modify(question_id):
    question = Question.query.get_or_404(question_id)
    if g.user != question.user:
        flash('수정권한이 없습니다.')
        return redirect(url_for('question.detail', question_id=question_id))
    if request.method == 'POST':
        form = QuestionForm()
        if form.validate_on_submit():
            form.populate_obj(question)
            question.modify_date = datetime.now()
            db.session.commit()
            return redirect(url_for('question.detail', question_id=question_id))
    else:
        form = QuestionForm(obj=question)
    return render_template('question/question_form.html', form=form)

@bp.route('delete/,<int:question_id>')
@login_required
def delete(question_id):
    question = Question.query.get_or_404(question_id)
    if g.user != question.user:
        flash('삭제권한이 없습니다.')
        return redirect(url_for('question.detail', question_id=question_id))
    db.session.delete(question)
    db.session.commit()
    return redirect(url_for('question._list'))

question_views.py 입니다.

 

question.delete구현한 것입니다.

로그인이 되어야하고 로그인한 아이디와 작성자와 같지 않으면 삭제 권한이 없는 것이고

같다면 db에서 question 객체를 삭제적용시키고 list화면으로 나옵니다.

 

 

 

 

답변을 수정하고 삭제하는 버튼을 추가하겠습니다.

 

<!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">
                <span class="badge bg-light text-dark text-left">
                    <span class="mb-2"> {{ question.user.username }} </span>
                    <span> {{ question.create_date|datetime }} </span>
                </span>
            </div>
        </div>

        {% if g.user == question.user %}
        <div class="my-3">
            <a href="{{ url_for('question.modify', question_id=question.id) }}" class="btn btn-sm btn-outline-secondary">수정</a>
            <a href="#" class="delete btn btn-sm btn-outline-secondary" data-uri="{{ url_for('question.delete', question_id=question.id) }}">삭제</a>
        </div>
        {% endif %}


        <h5 class="border-bottom my-3">{{ 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>
            <div class="d-flex justify-content-end">
                <span class="badge bg-light text-dark text-left">
                    <span class="mb-2"> {{ answer.user.username }}  </span>
                    <span> {{ answer.create_date|datetime }} </span>
                </span>
            </div>
        </div>
        {% if g.user == answer.user %}
        <div class="my-3">
            <a href="{{ url_for('answer.modify', answer_id=answer.id) }}" class="btn btn-sm btn-outline-secondary">수정</a>
            <a href="#" class="delete btn btn-sm btn-outline-secondary" data-uri="{{ url_for('answer.delete', answer_id=answer.id) }}">삭제</a>
        </div>
        {% endif %}

        {% 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 {% if not g.user %} disabled {% endif %} name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>

    {% endblock %}
    {% block script %}
    <script type="text/javascript">
        $(document).ready(function(){
            $(".delete").on("click", function(){
                if(confirm("정말로 삭제하시겠습니까?")) {
                    location.href = $(this).data('uri');
                }
            });
        });
    </script>
    {% endblock %}
</body>
</html>

question_detail.html 입니다.

 

댓글 수정 삭제 버튼을 만들었습니다.

from datetime import datetime

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

from .auth_views import login_required
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', ))
@login_required
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(), user=g.user)  # 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)

@bp.route('/modify/<int:answer_id>', methods=('GET','POST'))
@login_required
def modify(answer_id):
    answer = Answer.query.get_or_404(answer_id)
    if g.user != answer.user:
        flash('수정권한이 없습니다.')
        return redirect(url_for('question.detail', question_id=answer.question.id))
    if request.method == "POST":
        form = AnswerForm()
        if form.validate_on_submit():
            form.populate_obj(answer)
            answer.modify_date = datetime.now()
            db.session.commit()
            return redirect(url_for('question.detail', question_id=answer.question.id))
    else:
        form = AnswerForm(obj=answer)
    return render_template('answer/answer_form.html', answer=answer, form=form)

@bp.route('delete/,<int:answer_id>')
@login_required
def delete(answer_id):
    answer = Answer.query.get_or_404(answer_id)
    question_id = answer.question.id
    if g.user != answer.user:
        flash('삭제권한이 없습니다.')
    db.session.delete(answer)
    db.session.commit()
    return redirect(url_for('question.detail', question_id=question_id))

answer_views.py 입니다

 

위에 내용하고 거의 동일하기 때문에 설명은 넘어가겠습니다. (수정 삭제 기능을 넣었습니다.)

 

<!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">
    <form method="post" class="post-form">
        {{ form.csrf_token }}
        {% include "form_errors.html" %}
        <div class="form-group">
            <label for="content">답변내용</label>
            <textarea class="form-control" name="content" id="content" rows="10">
                {{ form.content.data or '' }}
            </textarea>
        </div>
        <button type="sumbmit" class="btn btn-primary">저장하기</button>
    </form>
  </div>
  {% endblock %}
</body>
</html>

templates/answer/answer_form.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">
                {% if question.modify_date %}
                    <span class="badge bg-light text-dark p-2 text-start mx-3">
                        <div class="mb-2">modified at</div>
                        <div> {{ question.modify_date|datetime }}</div>
                    </span>
                {% endif %}
                <span class="badge bg-light text-dark text-start">
                    <div class="mb-2"> {{ question.user.username }} </div>
                    <div> {{ question.create_date|datetime }} </div>
                </span>
            </div>
        </div>

        {% if g.user == question.user %}
        <div class="my-3">
            <a href="{{ url_for('question.modify', question_id=question.id) }}" class="btn btn-sm btn-outline-secondary">수정</a>
            <a href="#" class="delete btn btn-sm btn-outline-secondary" data-uri="{{ url_for('question.delete', question_id=question.id) }}">삭제</a>
        </div>
        {% endif %}


        <h5 class="border-bottom my-3">{{ 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>
            <div class="d-flex justify-content-end">
                {% if answer.modify_date %}
                <span class="badge bg-light text-dark text-start p-2 mx-3">
                    <div class="mb-2"> modified at  </div>
                    <div> {{ answer.modify_date|datetime }} </div>
                </span>
                {% endif %}
                <span class="badge bg-light text-dark text-start">
                    <div class="mb-2"> {{ answer.user.username }}  </div>
                    <div> {{ answer.create_date|datetime }} </div>
                </span>
            </div>
        </div>
        {% if g.user == answer.user %}
        <div class="my-3">
            <a href="{{ url_for('answer.modify', answer_id=answer.id) }}" class="btn btn-sm btn-outline-secondary">수정</a>
            <a href="#" class="delete btn btn-sm btn-outline-secondary" data-uri="{{ url_for('answer.delete', answer_id=answer.id) }}">삭제</a>
        </div>
        {% endif %}

        {% 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 {% if not g.user %} disabled {% endif %} name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>

    {% endblock %}
    {% block script %}
    <script type="text/javascript">
        $(document).ready(function(){
            $(".delete").on("click", function(){
                if(confirm("정말로 삭제하시겠습니까?")) {
                    location.href = $(this).data('uri');
                }
            });
        });
    </script>
    {% endblock %}
</body>
</html>

quesiton_detail.html 입니다.

 

수정해서 글이나 댓글 수정시각을 추가시켰습니다.

 

 

반응형
반응형

로그인, 로그아웃 기능을 만들었으니 누가 작성했는지 표시할 수 있게 됩니다.

그래서 Quesiton, Answer 모델을 수정하여 글쓴이에 해당하는 user 필드를 추가할 것입니다.

 

ORM을 사용할 때 몇 가지 문제점이 있습니다. SQLite 에서만 해당하고 다른 DB는 상관이 없습니다.

SQLite가 발생시킬 수 있는 오류를 먼저 해결하고 넘어가겠습니다.

(이럴 거면 그냥 다른 DB 쓰면 안 되나?) 

 

몇몇 데이터베이스의 제약 조건 이름이 변경되므로

flask db migrate, flask db upgrade로 데이터베이스를 변경해야 합니다..

 

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

import config

naming_convention ={
        "ix" : "ix_%(column_0_label)s",
        "uq" : "uq_%(table_name)s_%(column_0_name)s",
        "ck" : "ck_%(table_name)s_%(column_0_name)s",
        "fk" : "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
        "pk" : "pk_%(table_name)s"
}
db = SQLAlchemy(metadata=MetaData(naming_convention=naming_convention))
migrate = Migrate()

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

        # ORM
        db.init_app(app) # 우리가 적은 환경으로 DB 초기화
        if app.config['SQLALCHEMY_DATABASE_URI'].startswith("sqlite"):
                migrate.init_app(app, db, render_as_batch=True)  # DB와 우리가 적은 환경을 다른 환경으로 옮김
        else:
                migrate.init_app(app, db)
        from . import models

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

        # 필터
        from .filter import format_datetime
        app.jinja_env.filters['datetime'] = format_datetime

        return app

SQLite 데이터베이스에서 사용하는 인덱스 등의 제약 조건 이름MetaData 클래스를 사용하여

규칙을 정의해야 합니다. 정의 하지 않으면 제약 조건에 이름이 없다는 오류 발생시킵니다.

 

render_as_batchTrue로 지정해야 합니다. False면 제약 조건 변경을 지원하지 않는다는 오류를 만듭니다.

 

이러한 과정들은 SQLite DB플라스크 ORM에서 정상 사용하기 위합니다.

자세한 과정은 몰라도 된다고 적혀있네요...

 

(제약조건 : Primary key 이런 걸 의미하는 것 같습니다.)

 

from pybo import db

class Question(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    subject = db.Column(db.String(200), nullable = False)
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    user = db.relationship('User',backref=db.backref('question_set'))

class Answer(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete = 'cascade'))
    question = db.relationship('Question', backref=db.backref('answer_set', ))
    # question = db.relationship('Question', backref=db.backref('answer_set', casecade='all, delete-orphan'))
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)

class User(db.Model) :
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

일단 Question를 먼저 수정하겠습니다.

 

user_id User의 id값하고 값을 공유합니다.

userUser라는 테이블과 연결해 User테이블에 접근할 수 있게 만듭니다.

 

모델을 수정했으므로 flask db migrate 명령으로 리비전 파일을 생성합시다.

 

리비전 파일을 flask db upgrade 명령으로 적용하겠습니다.

오류가 발생합니다. 이유는 user_id 필드가 Null을 허용하지 않기 때문입니다.

앞서 실습진행하면서 데이터 여러 건 저장했습니다. 그 데이터에는 user_id 필드의 값이 없죠

(왜냐하면 지금 추가했으니깐요 예전 데이터의 값이 없겠죠 그래서 값이 들어가있지 않음)

근데 null 허용을 안 하니까 오류가 발생한 것입니다.

 

 

해결 하기 위한 5단계를 거쳐야 합니다.

 

1. user_id nullable 설정을 False 대신 True로 바꾸기

2. user_id를 임의 값으로 설정하기 (여기서는 1로 설정)

from pybo import db

class Question(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    subject = db.Column(db.String(200), nullable = False)
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=True, server_default='1')
    user = db.relationship('User',backref=db.backref('question_set'))

class Answer(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete = 'cascade'))
    question = db.relationship('Question', backref=db.backref('answer_set', ))
    # question = db.relationship('Question', backref=db.backref('answer_set', casecade='all, delete-orphan'))
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)

class User(db.Model) :
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

Question에 있는 user_id 부분 nullable을 바꾸고 server_defualt을 설정했습니다.

 

 

3. flask db migrate 명령, flask db upgrade 명령 다시 실행하기

이전 migrate 명령은 제대로 수행 되었지만 upgrade를 실패하여 정상 종료가 되었지 않기 때문입니다.

 

flask db heads로 최종 리비전을 확인해봅시다. (방금 적용시킨 거)

 

flask db current현재 작업 리비전(저번에 적용시킨 거)을 확인해 봅시다.

서로 같지 않아현재 리비전을 최종 리비전으로 변경해야 합니다.

 

flask db stamp heads이용해 바꿨고 flasdk db current로 제대로 바뀌었는지 확인 했습니다.

 

그러면 정상 종료된 것이기 때문에 다시 flask db migrate와 flask db upgrade를 해줍시다.

Question 모델 데이터 모든 user_id 필드에 1이 저장되었습니다.

 

4. user_id의 nullable 설정을 다시 False로 변경 (다시 원래 False로 변경)

5. flask db migrate 명령, flask db upgrade 명령 다시 실행

 

from pybo import db

class Question(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    subject = db.Column(db.String(200), nullable = False)
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullabe =False)
    user = db.relationship('User',backref=db.backref('question_set'))

class Answer(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete = 'cascade'))
    question = db.relationship('Question', backref=db.backref('answer_set', ))
    # question = db.relationship('Question', backref=db.backref('answer_set', casecade='all, delete-orphan'))
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)

class User(db.Model) :
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

Question 클래스를 수정flask db migrate, flask db upgrade 명령을 수행하겠습니다.

 

 

 

 

Answer 모델도 같은 방법으로 user_id 필드를 추가하겠습니다.

Question이랑 같은 방법으로 하시면 됩니다. 

 

from pybo import db

class Question(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    subject = db.Column(db.String(200), nullable = False)
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    user = db.relationship('User',backref=db.backref('question_set'))

class Answer(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete = 'cascade'))
    question = db.relationship('Question', backref=db.backref('answer_set', ))
    # question = db.relationship('Question', backref=db.backref('answer_set', casecade='all, delete-orphan'))
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=True, server_default='1')
    user = db.relationship('User',backref=db.backref('answer_set'))

class User(db.Model) :
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

nullable = True 와 server_default = '1'을 수정하고 적용했습니다.

from pybo import db

class Question(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    subject = db.Column(db.String(200), nullable = False)
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    user = db.relationship('User',backref=db.backref('question_set'))

class Answer(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete = 'cascade'))
    question = db.relationship('Question', backref=db.backref('answer_set', ))
    # question = db.relationship('Question', backref=db.backref('answer_set', casecade='all, delete-orphan'))
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    user = db.relationship('User',backref=db.backref('answer_set'))

class User(db.Model) :
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

nullable = False로 수정하고 적용했습니다.

 

from datetime import datetime

from flask import Blueprint, url_for, request, render_template, g
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(), user=g.user)  # 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 입니다.

 

댓글 작성자가 필요하기 때문에 로그인 되어 있으면 g.user에 id가 저장되어 있으므로 그걸 user에다 저장합니다.

 

from datetime import datetime

from flask import Blueprint, render_template, request, url_for, g
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():
    page = request.args.get('page', type=int, default=1) # 페이지
    question_list = Question.query.order_by(Question.create_date.desc())
    question_list = question_list.paginate(page, per_page=10)
    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(), user=g.user)
        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 입니다.

 

글 작성자가 필요하기 때문에 로그인 되어 있으면 g.user에 id가 저장되어 있으므로 user에다 저장합니다.

 

 

로그아웃 상태에서 질문 또는 답변을 등록이 불가능하게 오류를 발생시켜보도록 하겠습니다.

 

@login_required 어노테이션을 지정하면 login_required 데코레이션 함수가 먼저 실행 됩니다.

(즉 def login_required(view)라는 것이 @login_required 과 똑같은 의미입니다.)

login_required 함수는 g.user가 있는지 조사하고 없으면 로그인 URL로 리다이렉트합니다.

g.user가 있으면 원래 함수를 그대로 실행합니다.

 

이런걸 데코레이터 함수라고 하는데 나중에 설명해서 올리도록 하겠습니다.

 

from datetime import datetime

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

from .auth_views import login_required
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', ))
@login_required
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(), user=g.user)  # 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 입니다

 

@login_required를 넣어서 로그인 라우트가 실행하기 전에 로그인 되어있는지 확인하는 것입니다.

 

from datetime import datetime

from flask import Blueprint, render_template, request, url_for, g

from .auth_views import login_required
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():
    page = request.args.get('page', type=int, default=1) # 페이지
    question_list = Question.query.order_by(Question.create_date.desc())
    question_list = question_list.paginate(page, per_page=10)
    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'))
@login_required
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(), user=g.user)
        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 입니다.

 

위와 동일합니다.

 

<!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">
                <span class="badge bg-light text-dark">
                    {{ question.create_date|datetime }}
                </span>
            </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">
                    <span class="badge bg-light text-dark">
                        {{ answer.create_date|datetime }}
                    </span>
                </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 {% if not g.user %} disabled {% endif %} name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>

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

 

question_detail.html 입니다.

 

<textarea {% if not g.user %} disabled {% endif %} name="content" ....

이 부분을 이용해 아예 로그인이 되어있지 않을 때 입력조차 불가능하게 만들었습니다.

 

 

<!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">
        <table calss="table" width="100%">
            <thead >
                <tr class="bg-dark text-white text-center">
                        <th> 번호 </th>
                        <th style="width:50%"> 제목 </th>
                        <th> 글쓴이 </th>
                        <th> 작성일시 </th>
                </tr>
            </thead>

            <tbody>
                {% if question_list %}
                {% for question in question_list.items %}
                <tr class="text-center">
                        <td>{{ question_list.total - ((question_list.page-1) * question_list.per_page) - loop.index0 }}</td>
                        <td class="text-start">
                            <a href="{{ url_for('question.detail', question_id=question.id) }}">{{question.subject}}</a>
                            {% if question.answer_set|length > 0 %}
                                <span class="text-danger small">{{ question.answer_set|length }}</span>
                            {% endif %}
                        </td>
                        <td>{{ question.user.username }}</td>
                        <td>{{ question.create_date|datetime  }}</td>
                </tr>
                {% endfor %}
                {% else %}
                <tr>
                    <td colspan="3"> 질문이 없습니다. </td>
                </tr>
                {% endif %}
            </tbody>
        </table>
        <!-- 페이징 -->
        <nav aria-label="Page navigation example">
          <ul class="pagination pagination-sm my-5 justify-content-center">
              <!-- 이전 페이징 구현 (이전 페이지 있을시)-->
              {% if question_list.has_prev %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ question_list.prev_num }}">이전</a>
                </li>
              <!-- 이전 페이지가 없을 시 -->
              {% else %}
                <li class="page-item disabled">
                    <a class="page-link" href="">이전</a>
                </li>
              {% endif %}
              <!-- 중간 페이징 -->
              {% for page_num in question_list.iter_pages() %}
                {% if page_num %}
                    {% if page_num != question_list.page %}
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
                        </li>
                    {% else %}
                        <li class="page-item active" aria-current="page">
                            <a class="page-link" href="#">{{ page_num }}</a>
                        </li>
                    {% endif %}
                {% else %}
                    <li class="disabled">
                        <a class="page-link" href="#">...</a>
                    </li>
                {% endif %}
              {% endfor %}
              <!-- 다음 페이징 구현 (다음 페이지가 있는 경우_ -->
              {% if question_list.has_next %}
                  <li class="page-item">
                      <a class="page-link" href="?page={{ question_list.next_num }}">다음</a>
                  </li>
              <!-- 다음 페이지 없는 경우 -->
              {% else %}
                  <li class="page-item disabled">
                      <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
                  </li>
              {% endif %}
          </ul>
        </nav>

        </ul>
        <a href="{{ url_for('question.create') }}" class="btn btn-primary"> 질문 등록하기 </a>
    </div>

    {% endblock %}

</body>
</html>

question_list.html 입니다.

 

이제 유저값이 들어갔으므로 글쓴이와 댓글 작성자를 표시하겠습니다.

중요한 부분은 {{ question.user.username }} 입니다.

 

 

DB객체에 접근하는 법에 대해서 다시 정리해보겠습니다.

question.answer_set 으로 Answer 객체에 접근 가능합니다. (<Answer[1]> <Answer[2]>....)

Answer에서 선언해야 접근가능합니다 (question = db.relationship('Question', backref=db.backref('answer_set', )))

하지만 내부는 불가능 합니다. (question.answer_set이 Answer[1]이지만 question.answer_set.id 불가능)

 

question.클래스속성으로 Question 객체 클래스 속성 접근 가능합니다. ( {{ question.id }} )

 

user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)

User.id값하고 Question의 user_id하고 똑같은 값을 가지게 됩니다.

 

relationship(테이블명, back? 이건 모르겠음) 조사해야합니다....

user = db.relationship('User',backref=db.backref('question_set'))

이걸로는 해당 user_id에 해당하는 모든 user 속성에 접근 가능하게 만듭니다.
question.user.username 이렇게 접근할 수 있게 하는 것이죠

<!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">
                <span class="badge bg-light text-dark text-left">
                    <span class="mb-2"> {{ question.user.username }} </span>
                    <span> {{ question.create_date|datetime }} </span>
                </span>
            </div>
        </div>


        <h5 class="border-bottom my-3">{{ 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>
            <div class="d-flex justify-content-end">
                <span class="badge bg-light text-dark text-left">
                    <span class="mb-2"> {{ answer.user.username }}  </span>
                    <span> {{ answer.create_date|datetime }} </span>
                </span>
            </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 {% if not g.user %} disabled {% endif %} name="content" id="content" row="15"></textarea>
                <input class="btn btn-primary" type="submit" value="답변등록">
            </div>
        </form>
    </div>

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

question_detail.html 입니다.

 

게시글 작성 시각과 댓글 작성 시각을 추가시켰습니다.

 

반응형
반응형

로그인과 로그아웃을 구현해보도록 하겠습니다.

 

from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, PasswordField
from wtforms.fields.html5 import EmailField
from wtforms.validators import DataRequired, Email, EqualTo, Length


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

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

class UserCreateForm(FlaskForm):
    username = StringField('사용자이름', validators=[
        DataRequired(), Length(min=3, max=25)])
    password1 = PasswordField('비밀번호', validators=[
        DataRequired(), EqualTo('password2', '비밀번호가 일치하지 않습니다.')])
    password2 = PasswordField('비밀번호확인', validators=[DataRequired()])
    email = EmailField('이메일', [DataRequired(), Email()])

class UserLoginForm(FlaskForm):
    username = StringField('사용자이름', validators=[DataRequired(), Length(min=3, max=25)])
    password = PasswordField('비밀번호', validators=[DataRequired()])

forms.py 입니다.

 

로그인할 때 검증에 필요한 userLoginForm을 만들었습니다. (이미 했던 거라 설명은 생략하겠습니다.)

from flask import Blueprint, request, render_template, flash, url_for, session
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import redirect

from .. import db
from pybo.forms import UserCreateForm, UserLoginForm
from pybo.models import User

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

@bp.route('/signup/', methods=('GET','POST'))
def signup():
    form = UserCreateForm()
    if request.method == 'POST' and form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if not user:
            user = User(username=form.username.data,
                        password=generate_password_hash(form.password1.data),
                        email=form.email.data)
            db.session.add(user)
            db.session.commit()
            return redirect(url_for('main.index'))
        else:
            flash('이미 존재하는 사용자입니다.')
    return render_template('auth/signup.html', form=form)

@bp.route('/login/', methods=('GET','POST'))
def login():
    form = UserLoginForm()
    if request.method == 'POST' and form.validate_on_submit():
        error = None
        user = User.query.filter_by(username=form.username.data).first()
        if not user:
            error = "존재하지 않는 사용자입니다."
        elif not check_password_hash(user.password, form.password.data):
            error = "비밀번호가 올바르지 않습니다."
        if error is None:
            session.clear()
            session['user_id'] = user.id
            return redirect(url_for('main.index'))
        flash(error)
    return render_template('auth/login.html', form=form)

auth_views.py 입니다.

 

POST방식이라면 로그인을 수행하고 그렇지 않으면 GET방식으로 auth/login.html 템플릿을 렌더링합니다.

 

POST 방식으로 들어오고 올바른 폼이라면 error에 None을 넣습니다.

 

username.data값DB에 있는 username이 일치하는 경우 비밀번호를 체크합니다.

물론 없는 경우(if not user) '존재하지 않는 사용자입니다'를 넣습니다.

 

비밀번호 암호화되어서 저장했으므로 바로 비교할 수 없습니다.

입력된 비밀번호는 반드시 check_pass_word_hash 함수로 똑같이 암호화하여 비교해야합니다.

틀린 경우는 '비밀번호가 올바르지 않습니다.'를 넣습니다.

 

만약 error값이 그대로 None이면 (다 일치했으니까 그대로이다.) session에 키와 키값을 저장합니다

에는 user_id라는 문자열을 넣고 키값은 DB에서 조회된 사용자 id값을 넣습니다.

그 후 main.index로 리다이렉트합니다.

 

세션은 플라스크 서버를 구하는 동안 영구히 참조할 수 있는 값입니다.

세션은 시간제한이 있어서 일정 시간 접속하지 않으면 자동으로 삭제됩니다.(?)

[뭔 소린지 모르겠습니다. 책에 있는 내용이라...]

<!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">
    <form method="post" class="post-form">
      {{ form.csrf_token }}
      {% include "form_errors.html" %}
      <div class="form-group">
        <label for="username">사용자 이름</label>
        <input type="text" class="form-control" name="username" id="username" value="{{ form.username.data or '' }}">
      </div>
      <div class="form-group">
        <label for="password">비밀번호</label>
        <input type="password" class="form-control" name="password" id="password" value="{{ form.password.data or '' }}">
      </div>
      <button type="submit" class="btn btn-primary">로그인</button>
    </form>
  </div>
  {% endblock %}
</body>
</html>

templates/auth/login.html 입니다.

 

로그인 템플릿을 만들어주세요

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <div class="container-fluid">
        <a class="navbar-brand" href="{{ url_for('main.index') }}">Pybo</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
          <ul class="navbar-nav">
            <li class="nav-item">
              <a class="nav-link" href="{{ url_for('auth.signup') }}">계정생성</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="{{ url_for('auth.login') }}">로그인</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
</body>
</html>

navbar.html 입니다.

 

네비게이션 바에 로그인 링크를 또한 추가하겠습니다.

 

로그인 여부 session에 저장된 값을 조사하면 할 수 있습니다.

단순히 session에 저장된 user_id값 여부로 로그인을 확인할 수 있지만

여기서는 좀 더 널리 사용할 수 있는 방법을 선택하겠습니다.

 

from flask import Blueprint, request, render_template, flash, url_for, session, g
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import redirect

from .. import db
from pybo.forms import UserCreateForm, UserLoginForm
from pybo.models import User

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

@bp.route('/signup/', methods=('GET','POST'))
def signup():
    form = UserCreateForm()
    if request.method == 'POST' and form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if not user:
            user = User(username=form.username.data,
                        password=generate_password_hash(form.password1.data),
                        email=form.email.data)
            db.session.add(user)
            db.session.commit()
            return redirect(url_for('main.index'))
        else:
            flash('이미 존재하는 사용자입니다.')
    return render_template('auth/signup.html', form=form)

@bp.route('/login/', methods=('GET','POST'))
def login():
    form = UserLoginForm()
    if request.method == 'POST' and form.validate_on_submit():
        error = None
        user = User.query.filter_by(username=form.username.data).first()
        if not user:
            error = "존재하지 않는 사용자입니다."
        elif not check_password_hash(user.password, form.password.data):
            error = "비밀번호가 올바르지 않습니다."
        if error is None:
            session.clear()
            session['user_id'] = user.id
            return redirect(url_for('main.index'))
        flash(error)
    return render_template('auth/login.html', form=form)

@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')
    if user_id is None:
        g.user = None
    else:
        g.user = User.query.get(user_id)

auth_view.py 입니다.

 

load_logged_in_user 함수를 구현하겠습니다.

 

@bp.before_app_request 어노테이션을 사용하겠습니다. 

이 어노테이션은 라우트 함수보다 먼저 실행 됩니다. 즉 모든 라우트 함수보다 먼저 실행이 되는 것이죠

 

g는 플라스크가 제공하는 변수입니다.(누군 글로벌변수, 누구는 로컬변수라고하네요...)

JSP에서 Application 영역입니다. 즉 서버가 꺼질 때까지 남아있다는 말입니다.

 

일단 로그인을 했을때 session에 user_id를 저장했기 때문에 session에 user_id를 가져옵니다.

그게 있으면 이제 g.user에 user_id값을 쿼리문으로 조회해서 넣습니다.

(그냥 user_id를 바로 넣어도 되는 거 아닌가? 잘 모르겠네요..)

 

from flask import Blueprint, request, render_template, flash, url_for, session, g
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import redirect

from .. import db
from pybo.forms import UserCreateForm, UserLoginForm
from pybo.models import User

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


@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')
    if user_id is None:
        g.user = None
    else:
        g.user = User.query.get(user_id)

@bp.route('/signup/', methods=('GET','POST'))
def signup():
    form = UserCreateForm()
    if request.method == 'POST' and form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if not user:
            user = User(username=form.username.data,
                        password=generate_password_hash(form.password1.data),
                        email=form.email.data)
            db.session.add(user)
            db.session.commit()
            return redirect(url_for('main.index'))
        else:
            flash('이미 존재하는 사용자입니다.')
    return render_template('auth/signup.html', form=form)

@bp.route('/login/', methods=('GET','POST'))
def login():
    form = UserLoginForm()
    if request.method == 'POST' and form.validate_on_submit():
        error = None
        user = User.query.filter_by(username=form.username.data).first()
        if not user:
            error = "존재하지 않는 사용자입니다."
        elif not check_password_hash(user.password, form.password.data):
            error = "비밀번호가 올바르지 않습니다."
        if error is None:
            session.clear()
            session['user_id'] = user.id
            return redirect(url_for('main.index'))
        flash(error)
    return render_template('auth/login.html', form=form)

@bp.route('/logout/')
def logout():
    session.clear()
    return redirect(url_for('main.index'))

auth_views.py 입니다.

 

logout 라우트를 만들어줬습니다.

user_id = session.get('user_id') 이거를 통해서 session에 값을 가져와서 g.user에 저장하는 형식이였기 때문에

session이 비게 되면 이제 더이상 로그인상태를 유지할 수 없습니다.

 

그리고 main.index로 리다이렉트 했습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container-fluid">
            <a class="navbar-brand" href="{{ url_for('main.index') }}">Pybo</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            {% if g.user %}
            <ul class="navbar-nav">
                <li class="nav-item">
                    <a class="nav-link" href="{{ url_for('auth.logout') }}">{{ g.user.username }} (로그아웃)</a>
                </li>
            </ul>
            {% else %}
            <div class="collapse navbar-collapse" id="navbar-nav">
                <ul class="navbar-nav">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.signup') }}">계정생성</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('auth.login') }}">로그인</a>
                    </li>
                </ul>
            </div>
            {% endif %}
        </div>
    </nav>
</body>
</html>

navbar.html 입니다.

 

if문을 이용해 g.user에 값이 들어가면 로그아웃이 나오게 하고 아니면 계정생성과 로그인이 나오게 했습니다.

반응형
반응형

회원가입 기능을 만들어 보았다면 웹 프로그래밍은 거의 마스터 했다고 할 수 있습니다.

 

from pybo import db

class Question(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    subject = db.Column(db.String(200), nullable = False)
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)

class Answer(db.Model) :
    id = db.Column(db.Integer, primary_key = True)
    question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete = 'cascade'))
    question = db.relationship('Question', backref=db.backref('answer_set', ))
    # question = db.relationship('Question', backref=db.backref('answer_set', casecade='all, delete-orphan'))
    content = db.Column(db.Text(), nullable = False)
    create_date = db.Column(db.DateTime(), nullable = False)

class User(db.Model) :
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

models.py 입니다.

 

User 모델을 작성해주세요

 

flask db migrate 명령으로 리비전 파일을 생성하겠습니다.

 

이제 리비전 파일로 데이터베이스를 변경하겠습니다.

 

from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, PasswordField
from wtforms.fields.html5 import EmailField
from wtforms.validators import DataRequired, Email, EqualTo, Length


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

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

class UserCreateForm(FlaskForm):
    username = StringField('사용자이름', validators=[
        DataRequired(), Length(min=3, max=25)])
    passowrd1 = PasswordField('비밀번호', validators=[
        DataRequired(), EqualTo('password2', '비밀번호가 일치하지 않습니다.')])
    password2 = PasswordField('비밀번호확인', validators=[DataRequired()])
    email = EmailField('이메일', [DataRequired(), Email()])

그리고 forms.py 검증하는 부분을 추가시키도록 하겠습니다.

회원가입에 필요한 것인데

userName에는 반드시 데이터를 입력하게 했고(DataRequired()) 길이를 제한했습니다. (Length(min=3, max=25)

password1에는 반드시 데이터를 입력하게 했고 (DataRequired()) 

             비밀번호확인하고 일치하는지 검증을 했습니다. (EqualTo('password2', '비밀번호가 일치하지 않습니다.'))

password2에는 반드시 데이터를 입력하게 했습니다. (DataRequired()) 

email에는 반드시 데이터를 입력하게 했고 (DataRequired())  이메일이 맞는지 확인하게했습니다. (Email())

 

email 검증을 사용하려면 email-validator가 필요하므로 설치해줍시다.

 

회원가입용 화면을 만들어 보겠습니다. 

메인 뷰 (main_views.py)와 답변 뷰 (answer_views.py)에 어디에도 해당하지 않으므로 새로운 파일을 만들겠습니다.

 

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

from .. import db
from pybo.forms import UserCreateForm
from myproject.pybo.models import User

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

@bp.route('/signup/', methods=('GET','POST'))
def signup():
    form = UserCreateForm()
    if request.method == 'POST' and form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if not user:
            user = User(username=form.username.data,
                        password=generate_password_hash(form.password1.data),
                        email=form.email.data)
            db.session.add(user)
            db.session.commit()
            return redirect(url_for('main.index'))
        else:
            flash('이미 존재하는 사용자입니다.')
    return render_template('auth/signup.html', form=form)

pybo/views/auth_views.py 입니다.

 

/auth/가 URL 접두어이고 그와 연결된 /auth/singup 과 연결된 signup 함수를 만들었습니다.

POST방식 요청에는 계정 등록을 GET방식 요청에는 계정 등록을(??)

 

form = UserCreateForm() 검증 단계를 가진 객체를 가집니다.

 

요청되어진 method 방식이 POST(request.method == 'POST')이고

올바른 방식으로 폼형태가 들어오면 (form.validate_on_submit())

 

username(id) 이미 존재하는지 확인합니다. (User.query.filter_by(username=form.username.data).first())

query.filter_by select문 + where 절이라고 생각하시면 됩니다. 그 안에 적는 내용이 where절입니다.

== 가 아닌 이유는 filter_by의 경우는 ==을 = 취급합니다.

username == form.username.data 폼에 검증을 끝낸 데이터와 DB에 있는 username하고 같은게 있는지

확인하는 과정입니다. 그래서 있으면 무슨 값이 들어가겠죠?

 

if not user 를 통해 똑같은 아이디(username)이 없으면 그 아이디와 비밀번호와 이메일을 저장합니다.

비밀번호를 저장할 때는 hash 함수로 암호화하여 저장합니다.

generate_password_hash 함수는 암호화 데이터는 복호화할 수 없습니다.

 

만약 user에 값이 들어가면 이미 존재하는 사용자입니다.(flash('이미 존재하는 사용자입니다.') 오류를 발생시킵니다.

 

<!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">
    <form method="post" class="post-form">
      {{ form.csrf_token }}
      {% include "form_errors.html" %}
      <div class="form-group">
        <label for="username">사용자 이름</label>
        <input type="text" class="form-control" name="username" id="username" value="{{ form.useranme.data or '' }}">
      </div>
      <div class="form-group">
        <label for="password1">비밀번호</label>
        <input type="password" class="form-control" name="password1" id="password1" value="{{ form.password1.data or '' }}">
      </div>
      <div class="form-group">
        <label for="password2">비밀번호 확인</label>
        <input type="password" class="form-control" name="password2" id="password2" value="{{ form.password2.data or '' }}">
      </div>
      <div class="form-group">
        <label for="email">비밀번호</label>
        <input type="text" class="form-control" name="eamil" id="email" value="{{ form.email.data or '' }}">
      </div>
      <button type="submit" class="btn btn-primary">생성하기</button>
    </form>
  </div>
{% endblock %}
</body>

</html>

templates/auth/signup.html 입니다.

 

회원가입 템플릿을 작성해주세요

그리고 회원 가입할 때 발생하는 오류를 표시하도록 했습니다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <!-- 필드 오류 -->
  {% for field, errors in form.errors.items() %}
      <div class="alert alert-danger" role="alert">
          <strong>{{ form[field].label }}</strong>: {{ ', '.join(errors) }}
      </div>
  {% endfor %}
      <!-- flash 오류 -->
      {% for message in get_flashed_messages() %}
      <div class="alert alert-danger" role="alert">
        {{ message }}
      </div>
  {% endfor %}
</body>
</html>

templates/form_errors.html 입니다.

 

필드 오류의 경우는 검증에서 틀린 부분을 이야기합니다. 비밀번호가 다르다, 덜 입력했다. 등...

 

flash('이미 존재하는 사용자입니다.') 이 부분을 처리해주는 부분이  get_flashed_messages() 입니다.

message 내용을 받아서 출력하면 flash 안에 있는 내용이 나오게 되는 것이죠

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <div class="container-fluid">
        <a class="navbar-brand" href="{{ url_for('main.index') }}">Pybo</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
          <ul class="navbar-nav">
            <li class="nav-item">
              <a class="nav-link" href="{{ url_for('auth.signup') }}">계정생성</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#">로그인</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
</body>
</html>

navbar.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">
        <table calss="table" width="100%">
            <thead >
                <tr class="bg-dark text-white">
                        <th> 번호 </th>
                        <th> 제목 </th>
                        <th> 작성일시 </th>
                </tr>
            </thead>

            <tbody>
                {% if question_list %}
                {% for question in question_list.items %}
                <tr>
                        <td>{{ question_list.total - ((question_list.page-1) * question_list.per_page) - loop.index0 }}</td>
                        <td>
                            <a href="{{ url_for('question.detail', question_id=question.id) }}">{{question.subject}}</a>
                            {% if question.answer_set|length > 0 %}
                                <span class="text-danger small">{{ question.answer_set|length }}</span>
                            {% endif %}
                        </td>

                        <td>{{ question.create_date|datetime  }}</td>
                </tr>
                {% endfor %}
                {% else %}
                <tr>
                    <td colspan="3"> 질문이 없습니다. </td>
                </tr>
                {% endif %}
            </tbody>
        </table>
        <!-- 페이징 -->
        <nav aria-label="Page navigation example">
          <ul class="pagination pagination-sm my-5 justify-content-center">
              <!-- 이전 페이징 구현 (이전 페이지 있을시)-->
              {% if question_list.has_prev %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ question_list.prev_num }}">이전</a>
                </li>
              <!-- 이전 페이지가 없을 시 -->
              {% else %}
                <li class="page-item disabled">
                    <a class="page-link" href="">이전</a>
                </li>
              {% endif %}
              <!-- 중간 페이징 -->
              {% for page_num in question_list.iter_pages() %}
                {% if page_num %}
                    {% if page_num != question_list.page %}
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
                        </li>
                    {% else %}
                        <li class="page-item active" aria-current="page">
                            <a class="page-link" href="#">{{ page_num }}</a>
                        </li>
                    {% endif %}
                {% else %}
                    <li class="disabled">
                        <a class="page-link" href="#">...</a>
                    </li>
                {% endif %}
              {% endfor %}
              <!-- 다음 페이징 구현 (다음 페이지가 있는 경우_ -->
              {% if question_list.has_next %}
                  <li class="page-item">
                      <a class="page-link" href="?page={{ question_list.next_num }}">다음</a>
                  </li>
              <!-- 다음 페이지 없는 경우 -->
              {% else %}
                  <li class="page-item disabled">
                      <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
                  </li>
              {% endif %}
          </ul>
        </nav>

        </ul>
        <a href="{{ url_for('question.create') }}" class="btn btn-primary"> 질문 등록하기 </a>
    </div>

    {% endblock %}

</body>
</html>

question_list.html 입니다.

{% if question.answer_set|length > 0 %}
	<span class="text-danger small">{{ question.answer_set|length }}</span>
{% endif %}

답변의 갯수가 0개 초과라면 그만큼의 댓글 개수를 출력합니다.

 

반응형
반응형

페이지를 이동해봐도 게시물 번호는 1부터 시작합니다. 첫번째 페이지 마지막 게시물이 번호 1번??

정상대로라면 마지막 페이지가 1번을 가져야 할텐데요 그걸 수정해보도록 하겠습니다.

 

게시물 번호 공식을 이용해 만들어보겠습니다.

 

번호 = 전체 게시물 개수 - (현재 페이지 -1) * 페이지당 게시물 개수 - 나열 인덱스

 

18개 게시물현재페이지가 1페이지이고 페이지당 개시물이 10개일시

 

18 - (1 - 1) * 10 = 18 입니다.

 

가장 위에 있는 부분이 먼저 만들어집니다.

 

여기서 예를 들자면 저 빨간색 부분이 먼저 만들어지고 파란색이 그 뒤에 만들어지죠

그러니까 빨간색이 인덱스 0뒤에 인덱스 1이 되는 것입니다.

 

그러면 18 - 0 = 18빨간 부분 게시물 번호가 될테고 18 - 1 = 17 파란 부분 게시물 번호가 되는 것입니다.

 

<!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" width="100%">
            <thead >
                <tr class="bg-dark text-white">
                        <th> 번호 </th>
                        <th> 제목 </th>
                        <th> 작성일시 </th>
                </tr>
            </thead>

            <tbody>
                {% if question_list %}
                {% for question in question_list.items %}
                <tr>
                        <td>{{ question_list.total - ((question_list.page-1) * question_list.per_page) - loop.index0 }}</td>
                        <td><a href="{{ url_for('question.detail', question_id=question.id) }}">{{question.subject}}</a></td>
                        <td>{{ question.create_date|datetime  }}</td>
                </tr>
                {% endfor %}
                {% else %}
                <tr>
                    <td colspan="3"> 질문이 없습니다. </td>
                </tr>
                {% endif %}
            </tbody>
        </table>
        <!-- 페이징 -->
        <nav aria-label="Page navigation example">
          <ul class="pagination pagination-sm my-5 justify-content-center">
              <!-- 이전 페이징 구현 (이전 페이지 있을시)-->
              {% if question_list.has_prev %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ question_list.prev_num }}">이전</a>
                </li>
              <!-- 이전 페이지가 없을 시 -->
              {% else %}
                <li class="page-item disabled">
                    <a class="page-link" href="">이전</a>
                </li>
              {% endif %}
              <!-- 중간 페이징 -->
              {% for page_num in question_list.iter_pages() %}
                {% if page_num %}
                    {% if page_num != question_list.page %}
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
                        </li>
                    {% else %}
                        <li class="page-item active" aria-current="page">
                            <a class="page-link" href="#">{{ page_num }}</a>
                        </li>
                    {% endif %}
                {% else %}
                    <li class="disabled">
                        <a class="page-link" href="#">...</a>
                    </li>
                {% endif %}
              {% endfor %}
              <!-- 다음 페이징 구현 (다음 페이지가 있는 경우_ -->
              {% if question_list.has_next %}
                  <li class="page-item">
                      <a class="page-link" href="?page={{ question_list.next_num }}">다음</a>
                  </li>
              <!-- 다음 페이지 없는 경우 -->
              {% else %}
                  <li class="page-item disabled">
                      <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
                  </li>
              {% endif %}
          </ul>
        </nav>

        </ul>
        <a href="{{ url_for('question.create') }}" class="btn btn-primary"> 질문 등록하기 </a>
    </div>

    {% endblock %}

</body>
</html>

question_list.html 입니다.

 

<td>{{ question_list.total - ((question_list.page-1) * question_list.per_page) - loop.index0 }}</td>

 

반응형
반응형

datetime객체에 작성일시가 너무 상세하게 나오는데 그걸 필터를 적용해서 바꾸겠습니다.

 

pybo/filter.py 파일을 만들어주세요

 

def format_datetime(value, fmt="%Y년 %m월 %d일 %H:%M"):
    return value.strftime(fmt)

value에 값이 들어오면 fmt 규격에 따라 datetime객체strftime을 이용해 규격에 맞게 변환시켜줍니다.

 

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

        # bluePrint
        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)

        # 필터
        from .filter import format_datetime
        app.jinja_env.filters['datetime'] = format_datetime

        return app

__init__.py 입니다.

 

여기에다가 filter를 추가시켜주면 됩니다. jinja_env.filters[필터이름] = 내가 만든 필터함수

app.jinja_env.filters['datetime'] = format_datetime 이런식으로 적용시키면 됩니다.

 

<!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" width="100%">
            <thead >
                <tr class="bg-dark text-white">
                        <th> 번호 </th>
                        <th> 제목 </th>
                        <th> 작성일시 </th>
                </tr>
            </thead>

            <tbody>
                {% if question_list %}
                {% for question in question_list.items %}
                <tr>
                        <td>{{ loop.index }}</td>
                        <td><a href="{{ url_for('question.detail', question_id=question.id) }}">{{question.subject}}</a></td>
                        <td>{{ question.create_date|datetime  }}</td>
                </tr>
                {% endfor %}
                {% else %}
                <tr>
                    <td colspan="3"> 질문이 없습니다. </td>
                </tr>
                {% endif %}
            </tbody>
        </table>
        <!-- 페이징 -->
        <nav aria-label="Page navigation example">
          <ul class="pagination pagination-sm my-5 justify-content-center">
              <!-- 이전 페이징 구현 (이전 페이지 있을시)-->
              {% if question_list.has_prev %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ question_list.prev_num }}">이전</a>
                </li>
              <!-- 이전 페이지가 없을 시 -->
              {% else %}
                <li class="page-item disabled">
                    <a class="page-link" href="">이전</a>
                </li>
              {% endif %}
              <!-- 중간 페이징 -->
              {% for page_num in question_list.iter_pages() %}
                {% if page_num %}
                    {% if page_num != question_list.page %}
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
                        </li>
                    {% else %}
                        <li class="page-item active" aria-current="page">
                            <a class="page-link" href="#">{{ page_num }}</a>
                        </li>
                    {% endif %}
                {% else %}
                    <li class="disabled">
                        <a class="page-link" href="#">...</a>
                    </li>
                {% endif %}
              {% endfor %}
              <!-- 다음 페이징 구현 (다음 페이지가 있는 경우_ -->
              {% if question_list.has_next %}
                  <li class="page-item">
                      <a class="page-link" href="?page={{ question_list.next_num }}">다음</a>
                  </li>
              <!-- 다음 페이지 없는 경우 -->
              {% else %}
                  <li class="page-item disabled">
                      <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
                  </li>
              {% endif %}
          </ul>
        </nav>

        </ul>
        <a href="{{ url_for('question.create') }}" class="btn btn-primary"> 질문 등록하기 </a>
    </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">
                <span class="badge bg-light text-dark">
                    {{ question.create_date|datetime }}
                </span>
            </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">
                    <span class="badge bg-light text-dark">
                        {{ answer.create_date|datetime }}
                    </span>
                </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 입니다.

 

create_date -> create_date|datetime 이런식으로 적용시켜주면 됩니다.

(datetime은 jinja_env.filters['datetime']) 이 부분

 

* badge 부분 수정했습니다. 

<span class="badge bg-light text-dark">
	... 이런식으로 바꿔주세요
</span>

 

반응형
반응형

테스트 데이터를 대량으로 만들어 보겠습니다. 가장 편한 방법은 플라스크 셸을 이용하는 것입니다.

for문 안에 내용은 탭으로 무조건 들여쓰기를 해주세요. 적용 후에 exit(0) 명령으로 쉘 스크립트를 나갈 수 있습니다.

 

그리고 서버를 실행해보겠습니다.

 

데이터가 어마무시하게 들어갔습니다. 

 

그런데 보통 이렇게 안 쓰고 페이지별로 10개씩이라든가 나눠서 보여줍니다.

그걸 페이징이라고 하는데 그걸 구현해 보겠습니다.

 

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():
    page = request.args.get('page', type=int, default=1) # 페이지
    question_list = Question.query.order_by(Question.create_date.desc())
    question_list = question_list.paginate(page, per_page=10)
    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 입니다.

 

reuqest.args.get('page', type=int, default=1)

page값이 없으면 기본값이 1이라는 의미이고 page자료형이 int형이라는 의미입니다.

( localhost:5000/question/list/?page=5 ) 이런식인 것이다. 

( localhost:5000/question/list/ ) 는 page값이 자동으로 1인 것입니다.

 

question_list.paginate(page, per_page=10)

per_page페이지마다 보여줄 게시물을 의미합니다.

paginate함수조회 데이터를 감싸 Pagination객체로 반환합니다.

question_list는 paginate 함수를 사용해 Pagination 객체가 되어서 페이징 처리를 아주 쉽게 해줍니다.

 

즉 여기서는 페이징 설정을 해주는 것입니다.

항목 설명(예시)
items 현재 페이지에 해당하는 게시물 리스트
[<Question 282>,<Question 283>...]
total 게시물 전체 개수
per_page 페이지당 보여줄 게시물 개수
page 현재 페이지 번호
iter_pages 페이지 범위 ([1, 2....31])
prev_num / next_num 이전 페이지, 다음 페이지 번호 (현재  페이지 3인 경우 2 / 4)
has_prev / has_next 이전, 다음 페이지 존재여부 (True, False)

 

페이징은 사실 구현하기 무척 어려운 기술입니다.

 

 

 

<!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" width="100%">
            <thead >
                <tr class="bg-dark text-white">
                        <th> 번호 </th>
                        <th> 제목 </th>
                        <th> 작성일시 </th>
                </tr>
            </thead>

            <tbody>
                {% if question_list %}
                {% for question in question_list.items %}
                <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>
        <!-- 페이징 -->
        <nav aria-label="Page navigation example">
          <ul class="pagination pagination-sm my-5 justify-content-center">
              <!-- 이전 페이징 구현 (이전 페이지 있을시)-->
              {% if question_list.has_prev %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ question_list.prev_num }}">이전</a>
                </li>
              <!-- 이전 페이지가 없을 시 -->
              {% else %}
                <li class="page-item disabled">
                    <a class="page-link" href="">이전</a>
                </li>
              {% endif %}
              <!-- 중간 페이징 -->
              {% for page_num in question_list.iter_pages() %}
                {% if page_num %}
                    {% if page_num != question_list.page %}
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>
                        </li>
                    {% else %}
                        <li class="page-item active" aria-current="page">
                            <a class="page-link" href="#">{{ page_num }}</a>
                        </li>
                    {% endif %}
                {% else %}
                    <li class="disabled">
                        <a class="page-link" href="#">...</a>
                    </li>
                {% endif %}
              {% endfor %}
              <!-- 다음 페이징 구현 (다음 페이지가 있는 경우_ -->
              {% if question_list.has_next %}
                  <li class="page-item">
                      <a class="page-link" href="?page={{ question_list.next_num }}">다음</a>
                  </li>
              <!-- 다음 페이지 없는 경우 -->
              {% else %}
                  <li class="page-item disabled">
                      <a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
                  </li>
              {% endif %}
          </ul>
        </nav>

        </ul>
        <a href="{{ url_for('question.create') }}" class="btn btn-primary"> 질문 등록하기 </a>
    </div>

    {% endblock %}

</body>
</html>

question_list.html 입니다.

 

이전 페이징 구현하는 것인데 만약 이전 페이지가 없다면 disable로 클릭할 수 없게 만듭니다.

{% if question_list.has_prev %}

 

다음 페이징 구현하는 것도 똑같습니다.

{% if question_list.has_next %}

 

문제는 중간페이징인데

 

question_list.iter_pages()이게 기능이 좀 특이합니다. 전체 페이지를 하나씩 도는 반복자는 맞습니다.

하지만 만약 페이지가 7페이지라면 1, 2, None, 5, 6, 7, 8, 9, 10, 11, None, 30, 31 이렇게 나옵니다.

이게 페이지가 너무 많으면 None을 보여줍니다. 

그리고 우리는 None이면 ...을 출력하게 해놔서 저렇게 보이는 것입니다.

(<a class="page-link" href="#">...</a>)

 

그래서 None값이 아니고 현재 페이지와 다르면 그 페이지로 이동하는 url이 있는 페이지를 만들어줍니다.

{% if page_num != question_list.page %} 

<a class="page-link" href="?page={{ page_num }}">{{ page_num }}</a>

 

만약 현재 페이지면 저렇게 파란색으로 active 효과를 주고 url을 안 줍니다.

<li class="page-item active" aria-current="page">
    <a class="page-link" href="#">{{ page_num }}</a>
</li>

 

 

반응형
반응형

이번에는 Navbar를 적용시켜볼 것입니다.

 

https://getbootstrap.com/docs/5.0/components/navbar/

 

Navbar

Documentation and examples for Bootstrap’s powerful, responsive navigation header, the navbar. Includes support for branding, navigation, and more, including support for our collapse plugin.

getbootstrap.com

navbar에서 이 부분을 개조해서 쓸 것입니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <div class="container-fluid">
        <a class="navbar-brand" href="{{ url_for('main.index') }}">Pybo</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
          <ul class="navbar-nav">
            <li class="nav-item">
              <a class="nav-link" href="#">계정생성</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#">로그인</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
</body>
</html>

templates/navbar.html 를 만들어주시고 작성해주세요

 

수정사항은 Home 부분을 없앴고 Pybo, 계정생성, 로그인을 바꿔 적었습니다.

또한 Pybo 링크에 main.index url_for를 적용했습니다.

 

너비를 줄이면 그에 대한 반응형웹처럼 햄버거가 나옵니다. 그걸 누르면 저렇게 나와야 합니다.

근데 작동을 안 할 것입니다. 왜냐하면 저 부분은 자바스크립트로 한 것이기 때문에 부트스트랩의 JS를 추가해야합니다.

 

 

https://getbootstrap.com/docs/5.1/getting-started/download/

 

Download

Download Bootstrap to get the compiled CSS and JavaScript, source code, or include it with your favorite package managers like npm, RubyGems, and more.

getbootstrap.com

여기에서 다운로드 받아서 bootstrap.min.js를 추가해주시면 됩니다.

 

그리고 bootstrap에 있는 기능은 부트스트랩.js로 만들어졌지만

그 안에 제이쿼리로 만들어진 것도 있기 때문에 제이쿼리도 또한 가져와야합니다.

 

둘다 cdn으로 사용가능한데 여기선 일단 다운로드 받아서 넣어보겠습니다.

 

https://jquery.com/

 

jQuery

What is jQuery? jQuery is a fast, small, and feature-rich JavaScript library. It makes things like HTML document traversal and manipulation, event handling, animation, and Ajax much simpler with an easy-to-use API that works across a multitude of browsers.

jquery.com

 

 

 

<!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>

    {% include "navbar.html" %}

    <!-- 기본 템플릿에 삽입할 내용 Start -->
    {% block content %}
    {% endblock %}
    <!-- 기본 템플릿에 삽입할 내용 End -->

    <!-- jQuery JS -->
    <script src="{{ url_for('static', filename='jquery-3.6.0.min.js') }}"> </script>
    <!-- Bootstarp JS -->
    <script src="{{ url_for('static', filename='bootstrap.min.js') }}"> </script>
</body>
</html>

base.html 입니다.

 

우리는 저렇게 block content 처럼 삽입할 내용을 모듈화로서 넣을 수도 있지만 {% include %}를 이용할 수도 있습니다.

 

1번만 사용되지만 따로 파일로 관리해야 이후 유지 보수하는데 유리하므로 분리를 했습니다.

 

어떤 페이지를 가든 위에는 고정되어 있습니다.

반응형