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__)
app.register_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
블로그 블루프린트는 사용자인증 블루프린트와는 달리 url_prefix
를 사용하지 않습니다. 따라서 index(디폴트) 뷰가 / 아래 바로 위치합니다. 마찬가지로 create
뷰는 /create
에 위치합니다. 블로그는 Flaskr 프로젝트의 메인 기능이므로 블로그 전체의 인덱스를 포스트 리스트 화면으로 세팅하는 것입니다.
그럼에도 불구하고, 아래 코드는 디폴트 index뷰의 엔드포인트를 blog.index 로 설정합니다. 사용자인증 뷰 중 일부도 여기서와 같이 간단히 엔드포인트를 지정했었죠. app.add_url_rule()
함수에서는 엔드포인트 이름 'index'
를 /
만으로 간단히 지정할 수 있습니다. 또한 url_for('index')
이나 url_for('blog.index')
역시 마찬가지 기능을 수행합니다.
다른 앱에서는 블로그 블루프린트에 url_prefix를 주거나 어플리캐이션 팩토리에서 별도로 인덱스를 지정하기도 합니다. hello 뷰를 참고하세요. 그렇게 하면 index
와 blog.index
는 서로 다른 엔드포인트를 가리키게 됩니다.
인덱스 (디폴트 뷰)
인덱스는 전체 포스트 목록을 최신글부터 보여줍니다. 포스트 목록과 작성자를 함께 표시하기 위해 SQL문을 이용해 DB에서 목록을 불러올 때 JOIN을 이용합니다.
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 블록 내에 있는 New 버튼이 활성화되며 create
뷰로의 링크가 생성됩니다. 사용자가 포스트 작성자라면 update 뷰로 연결되는 Edit 버튼이 활성화됩니다. loop.last
는 Jinja for loops에 포함된 특별한 기능입니다. 이 기능을 이용해 마지막 글 까지 루프를 돌며 제목 사이에 줄을 삽입합니다.
글 쓰기 Create
create 뷰는 사용자 등록을 위한 register 뷰와 동일하게 작동합니다. 새 글을 입력할 폼을 표시하고, 입력된 정보의 유효성 체크를 하며, DB에 저장하는 기능 입니다.
@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
와 delete
뷰는 id 정보를 함께 표출합니다. 코드를 중복해서 작성하지 않도록 get_post 함수로 작성해서 각각의 뷰에서 불러와 사용합니다.
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, "Post id {0} doesn't exist.".format(id))
if check_author and post['author_id'] != g.user['id']:
abort(403)
return post
abort()
는 미리 정의된 예외상황에 따른 HTTP 코드값을 반환합니다. 이 코드에서 사용된 404
는 “Not Found”, 403
은 “Forbidden” 을 의미합니다.
check_author 인자는 익명글을 허용할지에 대한 부분입니다.
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)
지금까지 만들어온 뷰들과는 달리 update
함수는 id라는 인자값을 받아옵니다. 이 인자는 경로값에 있는 <int:id>
값을 사용합니다. 실제 URL을 보면, /1/update 와 같이 되어 있는것을 확인할 수 있습니다. Flask는 경로에서 1 값을 가져와 이 값이 int 인지 확인하고 id 인자로 넘겨줍니다. 만일 int: 를 표시하지 않고 <id> 처럼 쓰게되면, 이 값은 문자로 처리됩니다. 수정화면에 대한 URL을 만들기 위해 url_for()
를 사용하고, 여기에 id값을 함께 포함해서 보냅니다 : url_for('blog.update', id=post['id']). 이 방법은 index.html 에서도 사용되었습니다.
create
와 update
뷰는 꽤 비슷한데요, 차이점은 update 뷰에서는 내용을 DB에 전달할 때 INSERT 가 아닌 UPDATE 를 사용한다는 것 입니다. 잘만 만들면 템플릿 하나로 양쪽 뷰 모두에 사용할 수 있습니다.
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
)에 수정된 데이터를 보여주기 위한 폼이고, 두번째는 삭제를 위한 버튼 하나 입니다. 버튼은 삭제확인을 위한 자바스크립트 코드를 담고 있습니다.
패턴 {{ request.form['title'] or post['title'] }}
는 폼에 어떤 데이터를 넣을 것인지를 고르기 위해 사용됩니다. 폼의 데이터가 아직 전달되지 않았다면, 수정 전의 데이터가 표출됩니다. 규칙에 맞지 않는 데이터가 입력됐다면, 사용자가 수정할 수 있도록 request.form
를 이용해 수정전 데이터 대신 표출합니다.
삭제 Delete
삭제 뷰는 별도의 템플릿이 없고, 버튼도 update.html
의 일부분으로 포함되어 /<id>/delete
URL로 연결됩니다. 별도의 템플릿이 없기 때문에 데이터를 넘겨주는 부분과 인덱스로 화면을 전환하는 부분만 처리합니다.
flaskr/blog.py
@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'))
축하합니다. 이제 앱 개발이 완료되었습다! 브라우저에서 이것저것 해보세요.
개발은 완성됐지만, 조금 더 알아볼게 남아있어요.
다음 글 : Make the Project Installable.
역자 : 웹사이트 제작은 여기까지 입니다. 이 다음 글 부터는 배포파일 작성, 테스트 등과 관련한 내용이라, 관심 없는 분은 여기까지만 읽어도 충분분합니다. ^^