
🗂️ Day67 프로젝트: 블로그 RESTful 라우팅에서 사용자 인증을 업데이트하여 블로그 프로젝트 완성
🔍 유의 사항
- 사용자가 /register 경로로 이동해서 블로그 웹 사이트에 등록할 수 있도록 만들기
- forms.py에 RegisterForm이라는 WTForm 생성 후,
플라스크 부트스트랩을 사용하여 wtf quick_form을 렌더링- 사용자가 입력한 데이터가 User 테이블의 blog.db에 새 테이블로 추가되어야 한다
(이메일, 이름, 해싱된 비밀번호)
🔍 유의 사항
- 등록을 마친 사용자는 로그인 페이지에서 로그인 가능하도록 설정하기
- 등록을 성공적으로 완료된 사용자를 홈페이지로 리디렉션
- 등록/로그인 전과 후의 네비게이션 바의 항목이 다르게 표시되어야 함
- 플래시 메시지로 피드백
- 데이터베이스에 이미 있는 이메일로 등록을 시도하는 경우
- 데이터베이스에 없는 이메일로 로그인을 시도하거나 비밀번호가 일치하지 않는 경우
- 로그아웃 버튼을 클릭하면 로그아웃 후 홈페이지로 이동하도록 /logout 경로 수정
🔍 유의 사항
- 최초 등록한 사용자가 관리자가 되어 새 게시물을 생성하거나 게시물 수정 및 삭제 가능
- 최초 사용자의 id는 1
- 관리자에게만 새 게시물 생성, 게시물 수정, 게시물 삭제 버튼이 보이도록 변경하기
- 다른 사용자가 수동으로 경로에 접근하지 못하도록 @admin_only로 해당 라우트 보호
- @login_required 데코레이터 예시
- flask.abort( code, *args, **kwargs ) 함수로 HTTP 상태 코드 반환 가능
🔍 유의 사항
- 사용자가 생성한 게시물을 해당 사용자와 데이터베이스에서 연결시키기
- user 테이블과 blog_post 테이블 간의 관계 생성
- 한 사용자(부모)가 여러 게시물 객체(자식)를 생성할 수 있는 일대다 관계
- ForeignKey, relationship() 메서드 사용
- 테이블 연결 후 BlogPost에 새 열이 추가됐기 때문에 기존 blog.db 파일을 삭제해야 함
- 블로그 포스트에 사라진 작성자 이름이 나타나도록 수정
🔍 유의 사항
- 사용자들이 게시물에 댓글을 작성할 수 있도록 CommentForm 생성
- 어느 사용자든 게시물에 댓글을 달 수 있도록 새로운 테이블 생성
- 이름이 comments인 Comment 테이블
- id와 text 속성으로 CKEditor에 기본 키와 텍스트가 들어가게 하기
- 관계 설정
- User 객체(부모)와 Comment 객체(자식) 간에 일대다 관계 설정
- BlogPost 객체(부모)와 Comment 객체(자식) 간에 일대다 관계 설정
- 로그인된 사용자만 댓글을 남길 수 있도록 /post/<int:post_id> 라우트 업데이트
- post.html 파일을 수정해서 게시물에 달린 모든 댓글을 출력하기
- 댓글을 단 사용자의 프로필 이미지를 Gravatar로 추가
(Flask 구버전에서만 동작하므로 2.x 버전으로 다운그레이드 해야 함)
⌨️ main.py
from datetime import date
from flask import Flask, abort, render_template, redirect, url_for, flash
from flask_bootstrap import Bootstrap5
from flask_ckeditor import CKEditor
from flask_gravatar import Gravatar
from flask_login import UserMixin, login_user, LoginManager, current_user, logout_user
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import relationship, DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, Text
from functools import wraps
from werkzeug.security import generate_password_hash, check_password_hash
# forms.py에서 만든 form을 임포트
from forms import CreatePostForm, RegisterForm, LoginForm, CommentForm
app = Flask(__name__)
app.config['SECRET_KEY'] = '8BYkEfBA6O6donzWlSihBXox7C0sKR6b'
ckeditor = CKEditor(app)
Bootstrap5(app)
# Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# 관리자만 접근 가능한 데코레이터 생성
def admin_only(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# id가 1이 아니면 403 에러 반환
if current_user.id != 1:
return abort(403)
# 관리자가 맞을 경우 라우트 계속 사용 가능
return f(*args, **kwargs)
return decorated_function
# CREATE DATABASE
class Base(DeclarativeBase):
pass
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
db = SQLAlchemy(model_class=Base)
db.init_app(app)
# CONFIGURE TABLES
class User(UserMixin, db.Model):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
name: Mapped[str] = mapped_column(String(100), nullable=False)
password: Mapped[str] = mapped_column(String(100), nullable=False)
# User 객체와 BlogPost 객체에서 부모 관계
posts: Mapped[list["BlogPost"]] = relationship(back_populates="author")
# User 객체와 Comment 객체에서 부모 관계
comments: Mapped[list["Comment"]] = relationship(back_populates="comment_author")
class BlogPost(db.Model):
__tablename__ = "blog_posts"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# User 객체와 BlogPost 객체에서 자식 관계
author_id: Mapped[int] = mapped_column(db.ForeignKey("users.id"))
author: Mapped["User"] = relationship(back_populates="posts")
title: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
subtitle: Mapped[str] = mapped_column(String(250), nullable=False)
date: Mapped[str] = mapped_column(String(250), nullable=False)
body: Mapped[str] = mapped_column(Text, nullable=False)
img_url: Mapped[str] = mapped_column(String(250), nullable=False)
# BlogPost 객체와 Comment 객체에서 부모 관계
comments: Mapped[list["Comment"]] = relationship(back_populates="parent_post")
class Comment(db.Model):
__tablename__ = "comments"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# User 객체와 Comment 객체에서 자식 관계
author_id: Mapped[int] = mapped_column(db.ForeignKey("users.id"))
comment_author: Mapped["User"] = relationship(back_populates="comments")
# BlogPost 객체와 Comment 객체에서 자식 관계
post_id: Mapped[int] = mapped_column(db.ForeignKey("blog_posts.id"))
parent_post: Mapped["BlogPost"] = relationship(back_populates="comments")
text: Mapped[str] = mapped_column(Text, nullable=False)
with app.app_context():
db.create_all()
# 댓글 사용자의 프로필 이미지 추가
gravatar = Gravatar(
app, size=100, rating='g', default='retro', force_default=False, force_lower=False, use_ssl=False, base_url=None
)
# Use Werkzeug to hash the user's password when creating a new user
@app.route('/register', methods=["GET", "POST"])
def register():
form = RegisterForm()
if form.validate_on_submit():
if User.query.filter_by(email=form.email.data).first():
flash("You've already signed up with that email, log in instead!")
return redirect(url_for('login'))
hash_and_salted_password = generate_password_hash(
form.password.data,
method='pbkdf2:sha256',
salt_length=8
)
new_user = User(
email=form.email.data,
name=form.name.data,
password=hash_and_salted_password
)
db.session.add(new_user)
db.session.commit()
# 해당 유저를 Flask-Login으로 인증
login_user(new_user)
return redirect(url_for("get_all_posts"))
return render_template("register.html", form=form)
# Retrieve a user from the database based on their email.
@app.route('/login', methods=["GET", "POST"])
def login():
form = LoginForm()
if form.validate_on_submit():
email = form.email.data
password = form.password.data
user = User.query.filter_by(email=email).first()
# 입력한 이메일이 존재하지 않음
if not user:
flash("That email does not exist, please try again.")
return redirect(url_for('login'))
# 비밀번호가 틀림
elif not check_password_hash(user.password, password):
flash('Password incorrect, please try again.')
return redirect(url_for('login'))
else:
login_user(user)
return redirect(url_for('get_all_posts'))
return render_template("login.html", form=form)
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('get_all_posts'))
@app.route('/')
def get_all_posts():
result = db.session.execute(db.select(BlogPost))
posts = result.scalars().all()
return render_template("index.html", all_posts=posts)
@app.route("/post/<int:post_id>", methods=["GET", "POST"])
def show_post(post_id):
comment_form = CommentForm()
requested_post = db.get_or_404(BlogPost, post_id)
if comment_form.validate_on_submit():
if not current_user.is_authenticated:
flash("You need to login or register to comment.")
return redirect(url_for("login"))
new_comment = Comment(
text=comment_form.comment_text.data,
comment_author=current_user,
parent_post=requested_post
)
db.session.add(new_comment)
db.session.commit()
return render_template("post.html", post=requested_post, form=comment_form)
@app.route("/new-post", methods=["GET", "POST"])
@admin_only
def add_new_post():
form = CreatePostForm()
if form.validate_on_submit():
new_post = BlogPost(
title=form.title.data,
subtitle=form.subtitle.data,
body=form.body.data,
img_url=form.img_url.data,
author=current_user,
date=date.today().strftime("%B %d, %Y")
)
db.session.add(new_post)
db.session.commit()
return redirect(url_for("get_all_posts"))
return render_template("make-post.html", form=form)
@app.route("/edit-post/<int:post_id>", methods=["GET", "POST"])
@admin_only
def edit_post(post_id):
post = db.get_or_404(BlogPost, post_id)
edit_form = CreatePostForm(
title=post.title,
subtitle=post.subtitle,
img_url=post.img_url,
author=post.author,
body=post.body
)
if edit_form.validate_on_submit():
post.title = edit_form.title.data
post.subtitle = edit_form.subtitle.data
post.img_url = edit_form.img_url.data
post.author = current_user
post.body = edit_form.body.data
db.session.commit()
return redirect(url_for("show_post", post_id=post.id))
return render_template("make-post.html", form=edit_form, is_edit=True)
@app.route("/delete/<int:post_id>")
@admin_only
def delete_post(post_id):
post_to_delete = db.get_or_404(BlogPost, post_id)
db.session.delete(post_to_delete)
db.session.commit()
return redirect(url_for('get_all_posts'))
@app.route("/about")
def about():
return render_template("about.html")
@app.route("/contact")
def contact():
return render_template("contact.html")
if __name__ == "__main__":
app.run(debug=True)
🏗️ header.html
<!DOCTYPE html>
<html lang="en">
<head 💬 >
<body>
<!-- Navigation-->
<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
<div class="container px-4 px-lg-5">
<a class="navbar-brand" href="/">Start Bootstrap</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive"
aria-expanded="false"
aria-label="Toggle navigation"
>
Menu
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto py-4 py-lg-0">
<li class="nav-item">
<a
class="nav-link px-lg-3 py-3 py-lg-4"
href="{{ url_for('get_all_posts') }}"
>Home</a
>
</li>
{% if not current_user.is_authenticated: %}
<li class="nav-item">
<a
class="nav-link px-lg-3 py-3 py-lg-4"
href="{{ url_for('login') }}"
>Login</a
>
</li>
<li class="nav-item">
<a
class="nav-link px-lg-3 py-3 py-lg-4"
href="{{ url_for('register') }}"
>Register</a
>
</li>
{% else %}
<li class="nav-item">
<a
class="nav-link px-lg-3 py-3 py-lg-4"
href="{{ url_for('logout') }}"
>Log Out</a
>
</li>
{% endif %}
<li class="nav-item">
<a
class="nav-link px-lg-3 py-3 py-lg-4"
href="{{ url_for('about') }}"
>About</a
>
</li>
<li class="nav-item">
<a
class="nav-link px-lg-3 py-3 py-lg-4"
href="{{ url_for('contact') }}"
>Contact</a
>
</li>
</ul>
</div>
</div>
</nav>
</body>
</html>
🏗️ footer.html
<!-- Footer-->
<footer class="border-top" 💬 >
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
</body>
</html>
🏗️ index.html
{% include "header.html" %}
<!-- Page Header-->
<header 💬 >
<!-- Main Content-->
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<!-- Post preview-->
{% for post in all_posts %}
<div class="post-preview">
<a href="{{ url_for('show_post', post_id=post.id) }}">
<h2 class="post-title">{{ post.title }}</h2>
<h3 class="post-subtitle">{{ post.subtitle }}</h3>
</a>
<p class="post-meta">
Posted by
<!-- BlogPost의 author는 User 객체가 됩 -->
{{post.author.name}}
on {{post.date}}
<!-- 관리자만 게시물 삭제 버튼에 접근 가능 -->
{% if current_user.id == 1: %}
<a href="{{url_for('delete_post', post_id=post.id) }}">✘</a>
{% endif %}
</p>
</div>
<!-- Divider-->
<hr class="my-4" />
{% endfor %}
<!-- New Post -->
<!-- 관리자만 새 게시물 생성 버튼에 접근 가능 -->
{% if current_user.id == 1: %}
<div class="d-flex justify-content-end mb-4">
<a
class="btn btn-primary float-right"
href="{{url_for('add_new_post')}}"
>Create New Post</a
>
</div>
{% endif %}
<!-- Pager-->
<div class="d-flex justify-content-end mb-4" 💬 >
</div>
</div>
</div>
{% include "footer.html" %}
🏗️ register.html
{% from "bootstrap5/form.html" import render_form %}
{% block content %} {% include "header.html" %}
<!-- Page Header -->
<header 💬 >
<main class="mb-4">
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
<!--Rendering the registration form here-->
{{ render_form(form, novalidate=True, button_map={"submit": "primary"}) }}
</div>
</div>
</div>
</main>
{% include "footer.html" %} {% endblock %}
🏗️ login.html
{% from "bootstrap5/form.html" import render_form %} {% block content %} {%
include "header.html" %}
<!-- Page Header -->
<header 💬 >
<main class="mb-4">
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
<div style="color:red">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<p>{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!--Rendering login form here-->
{{ render_form(form, novalidate=True, button_map={"submit": "primary"}) }}
</div>
</div>
</div>
</main>
{% include "footer.html" %} {% endblock %}
🏗️ make-post.html
{% from "bootstrap5/form.html" import render_form %}
{% block content %} {% include "header.html" %}
<!-- Page Header -->
<header 💬 >
<main class="mb-4">
<div class="container">
<div class="row">
<div class="col-lg-8 col-md-10 mx-auto">
{{ ckeditor.load() }}
{{ ckeditor.config(name='body') }}
{{ render_form(form, novalidate=True, button_map={"submit": "primary"}) }}
</div>
</div>
</div>
</main>
{% include "footer.html" %} {% endblock %}
🏗️ post.html
{% from "bootstrap5/form.html" import render_form %}
{% include "header.html" %}
<!-- Page Header-->
<header class="masthead" style="background-image: url('{{post.img_url}}')">
<div class="container position-relative px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
<div class="post-heading">
<h1>{{ post.title }}</h1>
<h2 class="subheading">{{ post.subtitle }}</h2>
<span class="meta"
>Posted by
{{ post.author.name }}
on {{ post.date }}
</span>
</div>
</div>
</div>
</div>
</header>
<!-- Post Content -->
<article>
<div class="container px-4 px-lg-5">
<div class="row gx-4 gx-lg-5 justify-content-center">
<div class="col-md-10 col-lg-8 col-xl-7">
{{ post.body|safe }}
<!-- 관리자만 게시물 수정 버튼에 접근 가능 -->
{% if current_user.id == 1 %}
<div class="d-flex justify-content-end mb-4">
<a
class="btn btn-primary float-right"
href="{{url_for('edit_post', post_id=post.id)}}"
>Edit Post</a
>
</div>
{% endif %}
<!-- Comments Area -->
<!-- CKEditor -->
{{ ckeditor.load() }}
{{ ckeditor.config(name='comment_text') }}
{{ render_form(form, novalidate=True, button_map={"submit": "primary"}) }}
<div class="comment">
{% for comment in post.comments: %}
<ul class="commentList">
<li>
<div class="commenterImage">
<img src="{{ comment.comment_author.email | gravatar }}" />
</div>
<div class="commentText">
{{ comment.text|safe }}
<span class="date sub-text">{{ comment.comment_author.name }}</span>
</div>
</li>
</ul>
{% endfor %}
</div>
</div>
</div>
</div>
</article>
{% include "footer.html" %}