Flask

Flask 이용한 웹사이트 제작기 (7) - blog blueprint 추가

sseram 2023. 7. 23. 16:53
반응형

2023.07.22 - [Flask] - Flask 이용한 웹사이트 제작기 (6) - static file 추가

 

Flask 이용한 웹사이트 제작기 (6) - static file 추가

2023.07.21 - [Flask] - Flask 이용한 웹사이트 제작기 (5) - 기본 template 구현 Flask 이용한 웹사이트 제작기 (5) - 기본 template 구현 2023.07.18 - [Flask] - Flask 이용한 웹사이트 제작기 (4) - auth view code 작성 Flask

donot-simsim.tistory.com

 

 

이전 글까지 잘 따라옸으면, 계정 페이지가 정상적으로 만들어졌을 것이다. 그 페이지를 만들었던 경험을 살려, 이번에는 글을 작성하고 / 수정 / 삭제 가능한 blog app을 만들어 보자.

 

app이라고 표현하는 게 맞나?

 

 


- The Blueprint

 

 

auth blueprint를 만들었던 것 처럼 blog blueprint도 먼저 만들어야 한다. auth.py를 만들었던 것 처럼, blog.py도 만들어 준 후, blog blueprint를 만들어 준다.

 

// flaskr/blog.py

from flask import (
    Blueprint, flash, g, redirect, render_template, request, url_for
)
from werkzeug.exceptions import abort

from flaskr.auth import login_required
from flaskr.db import get_db

bp = Blueprint('blog', __name__)

 

그 후, __init__.py 에 가서 auth때 했던 거와 똑같이 blueprint를 등록해 준다.

 

// flaskr/__init__.py

def create_app():
    app = ...
    # existing code omitted

    from . import blog
    app.register_blueprint(blog.bp)
    app.add_url_rule('/', endpoint='index')

    return app

 

auth를 등록할 때엔 url rule은 지정해 주지 않았었는데, blog를 등록할 때엔 url rule도 같이 지정해 주엇다.

url rule을 지정해 줄 때, url_prefix를 지정해 주지 않았다. '/' 에 들어오면 바로 index로 들어온다. 우리가 맨 처음 웹사이트 접속하였을 때 아무런 url없이 들어오니.. 일단 들어오면 저 index page가 보이겠구나.

앞으로 기본 뷰가 blog.index라고 생각하고, 거기서 무언가 새로운 뷰들이 추가 될 때마다 저 / 뒤에 하나씩 붙게 된다.

 

 

- Index

 

그럼 실제로 index를 만들어 보자.

 

// flaskr/blog.py

@bp.route('/')
def index():
    db = get_db()
    posts = db.execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' ORDER BY created DESC'
    ).fetchall()
    return render_template('blog/index.html', posts=posts)
// flaskr/templates/blog/index.html

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Posts{% endblock %}</h1>
  {% if g.user %}
    <a class="action" href="{{ url_for('blog.create') }}">New</a>
  {% endif %}
{% endblock %}

{% block content %}
  {% for post in posts %}
    <article class="post">
      <header>
        <div>
          <h1>{{ post['title'] }}</h1>
          <div class="about">by {{ post['username'] }} on {{ post['created'].strftime('%Y-%m-%d') }}</div>
        </div>
        {% if g.user['id'] == post['author_id'] %}
          <a class="action" href="{{ url_for('blog.update', id=post['id']) }}">Edit</a>
        {% endif %}
      </header>
      <p class="body">{{ post['body'] }}</p>
    </article>
    {% if not loop.last %}
      <hr>
    {% endif %}
  {% endfor %}
{% endblock %}

 

여기서 신기한 게 두 개 있었는데.. 위의 header 쪽에서 if를 통해 g.user가 있다면, 즉 로그인 되어 있다면 create를 하는 것과 밑에서 g.user['id'] 와 post['author_id'] 가 같다면 edit 버튼을 보여주는 것이었다. 이런 프론트엔드 비스무리한 걸 제대로 처음 봤는데... 이렇게 간편하게 보여줄 수도 있구나.

 

 

 

- Create

 

그리고 create view.

auth register와 비슷하게, user에게 form을 보여주고, 데이터를 입력받은 후 전해받는다.

이미 login_required를 추가 해 주었기 때문에 로그인이 실제로 되어있는지 코드에서 신경 쓸 필요는 없다고 한다. 좋구만..

 

// flaskr/blog.py

@bp.route('/create', methods=('GET', 'POST'))
@login_required
def create():
    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'INSERT INTO post (title, body, author_id)'
                ' VALUES (?, ?, ?)',
                (title, body, g.user['id'])
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/create.html')

 

// flaskr/templates/blog/create.html

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}New Post{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title" value="{{ request.form['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
{% endblock %}

 

 

- Update

 

다음은 update.

update와 delete view 둘 다 post 의 id를 가져와, 작성자의 id가 맞는지 매치하는 과정이 필요하다. 코드 중복을 피하기 위해, blog.py에 해당 valid code를 미리 작성해 두고 시작하자.

 

// flaskr/blog.py

def get_post(id, check_author=True):
    post = get_db().execute(
        'SELECT p.id, title, body, created, author_id, username'
        ' FROM post p JOIN user u ON p.author_id = u.id'
        ' WHERE p.id = ?',
        (id,)
    ).fetchone()

    if post is None:
        abort(404, f"Post id {id} doesn't exist.")

    if check_author and post['author_id'] != g.user['id']:
        abort(403)

    return post

 

위에서 abort는 exception throw같은 느낌이다. 그리고 옆에 써진 숫자는, 우리가 자주 보던 그 '404 not found' 할 때 그 404가 맞다. http의 status error code라고 하는데, 이거는 따로 찾아가며 쓰면 될 것 같다.

 

valid code까지 작성하였으니, update code를 마저 작성해 보자.

 

// flaskr/blog.py

@bp.route('/<int:id>/update', methods=('GET', 'POST'))
@login_required
def update(id):
    post = get_post(id)

    if request.method == 'POST':
        title = request.form['title']
        body = request.form['body']
        error = None

        if not title:
            error = 'Title is required.'

        if error is not None:
            flash(error)
        else:
            db = get_db()
            db.execute(
                'UPDATE post SET title = ?, body = ?'
                ' WHERE id = ?',
                (title, body, id)
            )
            db.commit()
            return redirect(url_for('blog.index'))

    return render_template('blog/update.html', post=post)

 

지금까지 구현했던 코드와 다른 점이 있다면, route에 /<int:id>/ update , 이렇게 route에 변수가 추가되었다는 점이다. flask에서는 저런 형식으로 route가 들어온다면, 예를 들어서 '/3/update ' 처럼 들어왔다면 자동으로 해당 값을 캡쳐하여 id로 넘겨준다고 한다. 이렇게 하여, 원하는 post를 가져올 수 있다.

 

그리고 듀토리얼에서 설명하길.. create 와 update의 코드가 거의 비슷하다고 하며, 직접 리팩토링을 해 보라고 권유한다.

클린 코드를 만드는 것도 중요하지만, 처음부터 완벽하게 깨끗한 코드를 만들겠다는 건 허상일 뿐이다. 일단 작동되는 것을 보고, 그 상태를 기반으로 조금씩 리팩토링 해 나가야 하지 않을까? 맞다. 지금 안 하겠다는 소리다.

 

 

대신 update.html을 만들자.

 

// flaskr/templates/blog/update.html

{% extends 'base.html' %}

{% block header %}
  <h1>{% block title %}Edit "{{ post['title'] }}"{% endblock %}</h1>
{% endblock %}

{% block content %}
  <form method="post">
    <label for="title">Title</label>
    <input name="title" id="title"
      value="{{ request.form['title'] or post['title'] }}" required>
    <label for="body">Body</label>
    <textarea name="body" id="body">{{ request.form['body'] or post['body'] }}</textarea>
    <input type="submit" value="Save">
  </form>
  <hr>
  <form action="{{ url_for('blog.delete', id=post['id']) }}" method="post">
    <input class="danger" type="submit" value="Delete" onclick="return confirm('Are you sure?');">
  </form>
{% endblock %}

 

첫 번째 폼은 /<id>/update 를 통하여 post를 edit하는 form. 두 번째 form은 delete 하는 form이다. delete 시에 어떤 post를 delete할 건지 필요하니 id를 argument로 같이 넘겨주는 걸 볼 수 있고...

저 위에 'request.form['title'] or post['title']' 이런 식으로 둘 중 하나의 값을 표현하는 식도 볼 수 있다.

 

 

- Delete

 

delete는 update.html에 같이 구현해 두었으니, 따로 template 이 필요는 없다. 다만 내가 이 코드를 정리를 한다면 따로 빼 둘 것 같긴 하다만..

 

코드는 엄청 간단하다.

 

@bp.route('/<int:id>/delete', methods=('POST',))
@login_required
def delete(id):
    get_post(id)
    db = get_db()
    db.execute('DELETE FROM post WHERE id = ?', (id,))
    db.commit()
    return redirect(url_for('blog.index'))

 

이렇게, delete 함수까지 완료.

 

 

이렇게 작성한 후, 다시 flask app을 구동시켜 보면..!

 

 

 

 

쟈잔

 

나름 형태가 갖춰진 blog가 완성되었다!

 

 

이렇게 코드 작성은 완료되었고, 이제는 배포 / 테스트 등등 코드 작성 외의 것들만 남아있다.

 

 

2023.07.24 - [Flask] - Flask 이용한 웹사이트 제작기 (8) - 환경 세팅 / 테스트

 

Flask 이용한 웹사이트 제작기 (8) - 환경 세팅 / 테스트

2023.07.23 - [Flask] - Flask 이용한 웹사이트 제작기 (7) - blog blueprint 추가 Flask 이용한 웹사이트 제작기 (7) - blog blueprint 추가 2023.07.22 - [Flask] - Flask 이용한 웹사이트 제작기 (6) - static file 추가 Flask 이용

donot-simsim.tistory.com

 

반응형