2023.07.22 - [Flask] - Flask 이용한 웹사이트 제작기 (6) - static file 추가
이전 글까지 잘 따라옸으면, 계정 페이지가 정상적으로 만들어졌을 것이다. 그 페이지를 만들었던 경험을 살려, 이번에는 글을 작성하고 / 수정 / 삭제 가능한 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' 카테고리의 다른 글
Flask 이용한 웹사이트 제작기 (9) - 배포 (2) | 2023.07.25 |
---|---|
Flask 이용한 웹사이트 제작기 (8) - 환경 세팅 / 테스트 (0) | 2023.07.24 |
Flask 이용한 웹사이트 제작기 (6) - static file 추가 (0) | 2023.07.22 |
Flask 이용한 웹사이트 제작기 (5) - 기본 template 구현 (0) | 2023.07.21 |
Flask 이용한 웹사이트 제작기 (4) - auth view code 작성 (0) | 2023.07.18 |