티스토리 뷰

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
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday