티스토리 뷰
1. 모델 작성하기
카테고리 안에 여러 게시물이 있고, 한 유저는 여러 게시물을 작성할 수 있다. [카테고리/유저 - 게시물]은 일대다 관계이다.
post_user -> user_id
post_category -> category_id
# models.py
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text(), nullable=False)
created_at = db.Column(db.DateTime(timezone=True), default=func.now())
author_id = db.Column(db.Integer, db.ForeignKey(
'user.id', ondelete='CASCADE'), nullable=False)
user = db.relationship(
'User', backref=db.backref('posts', cascade='delete'))
category_id = db.Column(db.Integer, db.ForeignKey(
'category.id', ondelete='CASCADE'), nullable=False)
category = db.relationship('Category',
backref=db.backref('category', cascade='delete'))
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(150), unique=True)
def __repr__(self):
return f'<{self.__class__.__name__}(name={self.name})>'
게시물 생성하는 페이지
<!-- create_post.html 생성 -->
{% extends 'base.html' %}
{% block title %}Create a Post{% endblock %}
{% block header %}
<header class="masthead"
style="background-image: url('{{ url_for('static', filename='assets/img/create-bg.jpeg') }}'); height: 130px;">
<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="site-heading">
<h1>Create a Post</h1>
<h2>Post whatever you want!</h2>
</div>
</div>
</div>
</div>
</header>
{% endblock %}
{% block content %}
<!-- Main Content-->
<main class="mb-4">
<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-12">
<div class="my-5">
<form id="createForm" method="POST">
{# {{ form.csrf_token }}#}
<div style="margin: 20px;">
<input class="form-control" id="title" type="text" placeholder="Post Title"
name="title" style="font-size: 40px;"/>
</div>
<div style="margin: 20px;">
<textarea class="form-control" id="content" type="text" placeholder="Content"
name="content" style="height: 500px;"></textarea>
</div>
<div style="margin: 20px;" id="category">
<select class="form-control" name="category" required>
<option value="" disabled selected>select a category</option>
{% for category in categories %}
<option value="{{category.id}}">{{category.name}}</option>
{% endfor %}
</select>
</div>
<br/>
<div style="text-align: center">
<button class="btn btn-primary text-uppercase" id="submitButton" type="submit">
Create
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
# views.py
from flask import Blueprint, render_template, login_required
from .models import get_category_model
@views.route("/categories-list")
def categories_list():
categories = get_category_model().query.all()
return render_template("categories_list.html", user=current_user, categories=categories)
모든 게시물 페이지
<!-- category_list.html -->
{% extends 'base.html' %}
{% block title %}All Categories{% endblock %}
{% block header %}
<!-- Page Header-->
<header class="masthead"
style="background-image: url({{ url_for('static', filename='assets/img/categories-bg.jpeg') }})">
<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="site-heading">
<h1>All Categories</h1>
<span class="subheading">Blog categories...</span>
</div>
</div>
</div>
</div>
</header>
{% endblock %}
{% block content %}
<!-- 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-10">
{% for category in categories %}
<!-- Category item-->
<div class="post-preview">
<a href="post_detail.html">
<h2 class="post-title">{{ category.name }}</h2>
</a>
</div>
<!-- Divider-->
<hr class="my-4"/>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
# views.py
@views.route("/create-post", methods=['GET', 'POST'])
@login_required
def create_post():
categories = get_category_model().query.all()
return render_template("post_create_form.html", user=current_user)
import unittest
from os import path
from flask_login import current_user
from bs4 import BeautifulSoup
from blog import create_app
from blog import db
import os
from blog.models import get_user_model, get_post_model, get_category_model
basedir = os.path.abspath(os.path.dirname(__file__))
app = create_app()
app.testing = True
'''
회원가입, 로그인, 로그아웃 부분을 테스트
1. 2명의 유저를 데이터베이스에 넣어 본 후, 데이터베이스에 들어간 유저의 수가 총 2명이 맞는지를 확인한다.
2. auth/sign-up 에서 폼을 통해서 회원가입 요청을 보낸 후, 데이터베이스에 값이 잘 들어갔는지를 확인한다.
3. 로그인 전에는 네비게이션 바에 "login", "sign up" 이 보여야 하고, 로그인한 유저 이름과 "logout" 이 표시되면 안 된다.
'''
class TestAuth(unittest.TestCase):
# 테스트를 위한 사전 준비
def setUp(self):
self.ctx = app.app_context()
self.ctx.push()
self.client = app.test_client()
# 테스트를 위한 db 설정
self.db_uri = 'sqlite:///' + os.path.join(basedir, 'test.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = self.db_uri
if not path.exists("tests/" + "test_db"): # DB 경로가 존재하지 않는다면,
db.create_all(app=app) # DB를 하나 만들어낸다.
# 테스트가 끝나고 나서 수행할 것, 테스트를 위한 데이터베이스의 내용들을 모두 삭제한다.
def tearDown(self):
os.remove('test.db')
self.ctx.pop()
# 1. 2명의 유저를 데이터베이스에 넣어 본 후, 데이터베이스에 들어간 유저의 수가 총 2명이 맞는지를 확인한다.
def test_signup_by_database(self):
self.user_test_1 = get_user_model()(
email="hello@example.com",
username="testuserex1",
password="12345",
is_staff=True
)
db.session.add(self.user_test_1)
db.session.commit()
self.user_test_2 = get_user_model()(
email="hello2@example.com",
username="testuserex2",
password="12345",
)
db.session.add(self.user_test_2)
db.session.commit()
# 데이터베이스에 있는 유저의 수가 총 2명인가?
self.assertEqual(get_user_model().query.count(), 2)
# 2. auth/sign-up 에서 폼을 통해서 회원가입 요청을 보낸 후, 데이터베이스에 값이 잘 들어갔는지를 확인한다.
def test_signup_by_form(self):
response = self.client.post('/auth/sign-up',
data=dict(email="helloworld@naver.com", username="hello", password1="dkdldpvmvl",
password2="dkdldpvmvl"))
self.assertEqual(get_user_model().query.count(), 1)
# 3. 로그인 전에는 네비게이션 바에 "login", "sig up" 이 보여야 하고, 로그인한 유저 이름과 "logout" 이 표시되면 안 된다.
def test_before_login(self):
# 로그인 전이므로, 네비게이션 바에는 "login", "sign up" 이 보여야 한다.
response = self.client.get('/')
soup = BeautifulSoup(response.data, 'html.parser')
navbar_before_login = soup.nav # nav 태그 선택
# navbar 안에 "Login" 이 들어있는지 테스트
self.assertIn("Login", navbar_before_login.text)
# navbar 안에 "Sign Up" 이 들어있는지 테스트
self.assertIn("Sign Up", navbar_before_login.text, )
self.assertNotIn("Logout", navbar_before_login.text,
) # navbar 안에 "Logout" 이 없는지 테스트
# 로그인을 하기 위해서는 회원가입이 선행되어야 하므로, 폼에서 회원가입을 진행해 준다.
response = self.client.post('/auth/sign-up',
data=dict(email="helloworld@naver.com", username="hello", password1="dkdldpvmvl",
password2="dkdldpvmvl"))
# 이후, auth/login 에서 로그인을 진행해 준다.
with self.client:
response = self.client.post('/auth/login',
data=dict(email="helloworld@naver.com", username="hello",
password="dkdldpvmvl"),
follow_redirects=True)
soup = BeautifulSoup(response.data, 'html.parser')
navbar_after_login = soup.nav
# 로그인이 완료된 후, 네비게이션 바에는 로그인한 유저 이름과 "Logout" 이 표시되어야 한다.
self.assertIn(current_user.username, navbar_after_login.text)
self.assertIn("Logout", navbar_after_login.text)
# 로그인이 완료된 후, 네비게이션 바에는 "Login" 과 "Sign Up" 이 표시되면 안 된다.
self.assertNotIn("Login", navbar_after_login.text)
self.assertNotIn("Sign up", navbar_after_login.text)
class TestPostwithCategory(unittest.TestCase):
# 테스트를 위한 사전 준비
def setUp(self):
self.ctx = app.app_context()
self.ctx.push()
self.client = app.test_client()
# 테스트를 위한 db 설정
self.db_uri = 'sqlite:///' + os.path.join(basedir, 'test.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = self.db_uri
if not path.exists("tests/" + "test_db"): # DB 경로가 존재하지 않는다면,
db.create_all(app=app) # DB를 하나 만들어낸다.
# 테스트가 끝나고 나서 수행할 것, 테스트를 위한 데이터베이스의 내용들을 모두 삭제한다.
def tearDown(self):
os.remove('test.db')
self.ctx.pop()
'''
1. 임의의 카테고리를 넣어본 후, 데이터베이스에 카테고리가 잘 추가되어 있는지 확인한다.
2. 카테고리를 넣은 후, /categories-list 에 접속했을 때, 넣었던 카테고리들이 잘 추가되어 있는지 확인한다.
3. 게시물을 작성할 때에, 로그인하지 않았다면 접근이 불가능해야 한다.
4. 임의의 카테고리를 넣어본 후,
웹 페이지에서 폼으로 게시물을 추가할 때에 option 태그에 값이 잘 추가되는지,
게시물을 추가한 후 게시물은 잘 추가되어 있는지
저자는 로그인한 사람으로 추가되어 있는지 확인한다.
'''
def test_add_category_and_post(self):
# 이름 = "python" 인 카테고리를 하나 추가하고,
self.python_category = get_category_model()(
name="python"
)
db.session.add(self.python_category)
db.session.commit()
# 추가한 카테고리의 이름이 "python" 인지 확인한다.
self.assertEqual(get_category_model().query.first().name, "python")
# id는 1로 잘 추가되어있는지 확인한다.
self.assertEqual(get_category_model().query.first().id, 1)
# 이름 = "rust" 인 카테고리를 하나 추가하고,
self.rust_category = get_category_model()(
name="rust"
)
db.session.add(self.rust_category)
db.session.commit()
self.assertEqual(get_category_model().query.filter_by(id=2).first().name,
"rust") # id가 2인 카테고리의 이름이 "rust" 인지 확인한다.
# 이름 = "javascript" 인 카테고리를 하나 더 추가해 주자.
self.rust_category = get_category_model()(
name="javascript"
)
db.session.add(self.rust_category)
db.session.commit()
# 카테고리 리스트 페이지에 접속했을 때에, 추가했던 3개의 카테고리가 잘 추가되어 있는지?
response = self.client.get('/categories-list')
soup = BeautifulSoup(response.data, 'html.parser')
self.assertIn('python', soup.text)
self.assertIn('rust', soup.text)
self.assertIn('javascript', soup.text)
# 로그인 전에는, 포스트 작성 페이지에 접근한다면 로그인 페이지로 이동해야 한다. 리디렉션을 나타내는 상태 코드는 302이다.
response = self.client.get('/create-post', follow_redirects=False)
self.assertEqual(302, response.status_code)
# 게시물의 작성자 생성
response = self.client.post('/auth/sign-up',
data=dict(email="helloworld@naver.com", username="hello", password1="dkdldpvmvl",
password2="dkdldpvmvl"))
# 위에서 만든 유저로 로그인
with self.client:
response = self.client.post('/auth/login',
data=dict(email="helloworld@naver.com", username="hello",
password="dkdldpvmvl"),
follow_redirects=True)
# 로그인한 상태로, 게시물 작성 페이지에 갔을 때에 폼이 잘 떠야 한다.
response = self.client.get('/create-post')
# 서버에 get 요청을 보냈을 때에, 정상적으로 응답한다는 상태 코드인 200을 돌려주는가?
self.assertEqual(response.status_code, 200)
# 미리 작성한 카테고리 3개가 셀렉트 박스의 옵션으로 잘 뜨고 있는가?
soup = BeautifulSoup(response.data, 'html.parser')
select_tags = soup.find(id='category')
self.assertIn("python", select_tags.text)
self.assertIn("rust", select_tags.text)
self.assertIn("javascript", select_tags.text)
response_post = self.client.post('/create-post',
data=dict(title="안녕하세요, 첫 번째 게시물입니다.",
content="만나서 반갑습니다!",
category="1"),
follow_redirects=True)
# 게시물을 폼에서 작성한 후, 데이터베이스에 남아 있는 게시물의 수가 1개가 맞는가?
self.assertEqual(1, get_post_model().query.count())
# 게시물은 잘 추가되어 있는지?
response = self.client.get(f'/posts/1')
soup = BeautifulSoup(response.data, 'html.parser')
# 게시물의 페이지에서 우리가 폼에서 입력했던 제목이 잘 나타나는지?
title_wrapper = soup.find(id='title-wrapper')
self.assertIn("안녕하세요, 첫 번째 게시물입니다.", title_wrapper.text)
# 게시물 페이지에서, 로그인했던 유저의 이름이 저자로 잘 표시되는지?
author_wrapper = soup.find(id='author-wrapper')
self.assertIn("hello", author_wrapper.text)
if __name__ == "__main__":
unittest.main()
'Python > 파이썬 플라스크' 카테고리의 다른 글
[5주차] 관리자 페이지 (0) | 2022.08.07 |
---|---|
[4주차] 로그인/회원가입 처리하기 (0) | 2022.07.18 |
[3주차-2] render_template (0) | 2022.07.11 |
[3주차] __init__ / blueprint (0) | 2022.07.11 |
[2주차-2] Python DB API/SQLite3 (0) | 2022.07.08 |
댓글