ABOUT ME

일상, 학업 일지 기록, 경제적 자유를 위한 나의 기록들

  • Flask Project - MyBlog contactForm, mypage 추가하기
    프로젝트 2024. 4. 23. 23:01
    반응형

    각 카테고리별 게시글, 특정 사용자의 게시글 등을 확인할 수 있도록 posts_list.html을 적절하게 활용해주겠습니다. 그 뒤에 블로그와 블로그 소유주에 대한 소개를 하는 about_me 페이지와, 문제사항 제보할 수 있는 contact 페이지를 수정해보겠습니다. 그리고 로그인한 후 자신의 정보를 확인해볼 수 있는 mypage를 추가해보겠습니다.


    1. posts_list 활용 엔드포인트 추가

    home에서는 모든 post, posts_list 에서는 해당 카테고리 post, 사용자 post(user_posts)에서는 사용자가 작성한 모든 post를 보여주도록 하겠습니다.

     

    posts_list.html에서 post_category와 post_owner_name 부분을 클릭하면 해당 카테고리, 사용자의 게시글을 볼 수 있도록 연결해줍니다.

    <!-- flask_app/blog/templates/views/posts_list.html -->
    {% extends "base.html" %}
    
    {% block title %} 
        {% if type == 'home' %}
            MyBlog - home
        {% elif type == 'category_posts' %}
            MyBlog - {{category_name}} Category
        {% else %}
            MyBlog - {{selected_user.username}} Posts
        {% endif %}        
    {% endblock %}
    
    {% block header %}
    <header class="masthead" style="background-image: url('../static/assets/img/post-bg.jpg')">
        <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">
                        {% if type == 'user_posts' %}
                            <h2 id="user_wrapper">User: {{selected_user.username}}</h2>
                        {% else %}
                            <h2 id="category_wrapper">Category : {{category_name}}</h2>
                        {% endif %} 
                        <span class="subheading" id="posts_count">총 {{ posts | length }}개의 포스트가 있습니다.</span>
                    </div>
                </div>
            </div>
        </div>
    </header>
    {% endblock %}
    
    {% block 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">
                {% if posts | length %}
                    {% for post in posts %}
                        <div class="post-preview">
                            <!-- 제목 -->
                            <a href="{{url_for('views.post', post_id=post.id)}}">
                                <h6 class="post-title" id="post_title">{{post.title}}</h6>
                            </a>
                            <!-- 작성자, 날짜 -->
                            <p class="post-meta">
                                Category: <a href="{{url_for('views.posts_list', category_id=post.category_id)}}" id="post_category">{{post.category.name}}</a>
                                Posted by
                                <a href="{{url_for('views.user_posts', user_id=post.author_id)}}" id="post_owner_name">{{post.user.username}}</a>
                                <br>
                                {{post.date_created | datetime}}
                            </p>
                        </div>
                        <!-- Divider-->
                        <hr class="my-4" />
                    {% endfor %}
                {% else %}
                    <h2 style="text-align: center;">Post가 존재하지 않습니다.</h2>
                    <br><br>
                {% endif %}
            </div>
        </div>
    </div>
    {% endblock %}

     

     

    그리고 home과 posts_list, user_posts(새로운 엔드포인트)를 아래처럼 수정해줍니다.

    # flask_app/blog/views.py
    
    @views.route("/")
    @views.route("/home")
    def home():
        posts = db.session.query(get_model('post')).options(
        	selectinload(get_model('post').user), selectinload(get_model('post').category)).all() 
        return render_template(BASE_VIEWS_DIR + "posts_list.html", 
            user=current_user, 
            posts=posts, 
            type='home',
            category_name='all',
        )
    
    @views.route('/category')
    def category():
        categories = db.session.query(get_model('category')).all()
        return render_template(BASE_VIEWS_DIR + "category.html", user=current_user, categories=categories)
    
    @views.route("/posts-list/<int:category_id>")
    def posts_list(category_id):
        selected_category = db.session.get(get_model('category'), category_id)
        category_posts = db.session.query(get_model('post')).filter_by(category_id=category_id).options(
        	selectinload(get_model('post').user)).all() 
        if category_posts is None:
            flash('해당 카테고리는 존재하지 않습니다.', category="error")
            return redirect(url_for('views.category'))
        
        return render_template(BASE_VIEWS_DIR + "posts_list.html", 
            user=current_user, 
            posts=category_posts, 
            type='category_posts',
            category_name=selected_category.name,
        )
    
    @views.route("/user_posts/<int:user_id>")
    def user_posts(user_id):
        selected_user = db.session.get(get_model('user'), user_id)
        user_posts = db.session.query(get_model('post')).filter_by(author_id=user_id).options(
        	selectinload(get_model('post').category)).all() 
        if user_posts is None:
            flash('해당 유저가 작성한 글이 존재하지 않습니다.', category="error")
            return redirect(url_for('views.home'))
        
        return render_template(BASE_VIEWS_DIR + "posts_list.html", 
            user=current_user, 
            posts=user_posts, 
            type='user_posts',
            selected_user=selected_user,
        )

     

    이제 특정 카테고리, 특정 사용자의 게시글을 확인할 수 있습니다.

     

    2. about_me 페이지 수정 

    <!-- flask_app/blog/templates/views/about_me.html -->
    {% extends "base.html" %}
    
    {% block title %} MyBlog - About Me {% endblock %}
    
    {% block header %}
    <header class="masthead" style="background-image: url('../static/assets/img/about-bg.jpg')">
        <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">
                        <h2>About Me</h2>
                        <span class="subheading">Let me introduce myself...</span>
                    </div>
                </div>
            </div>
        </div>
    </header>
    {% endblock %}
    
    {% block content %}
    <section class="about-section text-center" style="margin: 24px;">
        <div class="row">
            <div class="col mx-auto">
                <p class="text">
                    <b>Name:</b> 이름 <br>
                    <b>Email</b>: 이메일 <br>
                    <b>github:</b> <a href="github 링크">github 링크</a><br><br>
                </p>
            </div>
        </div>
    </section>
    {% endblock %}

     

     

    3. contact 기능(model, form 생성)

    먼저 사용자가 문제 사항을 제보할 수 있도록 contact 페이지에서 입력을 받을 예정입니다. 사용자 id와 정보를 저장하면 되므로 아래처럼 Message 모델을 생성해줍니다.

    class Message(db.Model):
        id = db.Column(db.Integer, primary_key=True)
        content = db.Column(db.Text, nullable=False)
        
        user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    
        user = db.relationship('User', back_populates='user_messages')
        
    def get_model(arg):
        models = {
            'user': User,
            'post': Post,
            'category': Category,
            'comment': Comment,
            'message': Message
        }
        return models[arg]

     

     

    모델 생성 후 사용자 입력을 위한 ContactForm을 생성해줍니다. 이 때 현재 사용자의 이름과 이메일은 자동으로 채워지고, 변경 불가능하도록 아래처럼 설정해줍니다.

    class ContactForm(FlaskForm):
        name = StringField('name', render_kw={'readonly': True})
        email = EmailField('email', render_kw={'readonly': True})
        content = TextAreaField('content', validators=[DataRequired('내용을 입력해주세요.')])
    
        def __init__(self, *args, **kwargs):
            super(ContactForm, self).__init__(*args, **kwargs)
            self.name.data = current_user.username
            self.email.data = current_user.email

     

    이제 해당 폼을 사용하기 위해 contact.html을 아래처럼 수정해줍니다.

    <!-- flask_app/blog/templates/views/contact.html -->
    {% extends "base.html" %}
    
    {% block title %} MyBlog - Contact Me {% endblock %}
    
    {% block header %}
    <header class="masthead" style="background-image: url('../static/assets/img/about-bg.jpg')">
        <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">
                        <h2>Contact Me</h2>
                        <span class="subheading">Feel free to use the contact form below to send me a message</span>
                    </div>
                </div>
            </div>
        </div>
    </header>
    {% endblock %}
    
    {% block content %}
    <section class="about-section text-center" style="margin: 24px;">
        <div class="row justify-content-center">
            <div class="row" style="border: 1px solid; padding: 8px; max-width: 600px; min-width: 200px;">
                <div class="col mx-auto">
                    <form id="contactForm" method="POST">
                        {{form.csrf_token}}
                        {% for field in form if field.name != 'csrf_token'%}
                            <div class="form-floating">
                                {{ field(id=field.id, class="form-control" ~ (' is-invalid' if field.errors else ''), required=True,
                                style=("font-size: 16px;" ~ ("height:200px; resize: none;" if field.name == 'content' else ""))) }}
                                <label for="{{ field.id }}" {% if field.errors %} class="invalid-feedback" {% endif %}>{{ field.label.text }}</label>
                                {% for error in field.errors %}
                                    <span class="invalid-feedback" style="font-size: 16px; color:red;">{{ error }}</span>
                                {% endfor %}
                            </div>
                        {% endfor %}
                        <br />
                        <button class="btn btn-primary text-uppercase" id="submitButton" type="submit">send</button>
                    </form>
                </div>
            </div>
        </div>
    </section>
    {% endblock %}

     

    4. contact 엔드포인트 수정

    아래처럼 사용자로부터 입력받고 db에 저장한 뒤 content를 초기화한 뒤 다시 돌려줍니다.

    @views.route("/contact", methods=['GET', 'POST'])
    def contact():
        form = ContactForm()
    
        if request.method == 'POST' and form.validate_on_submit():
            message = get_model('message')(
                content=form.content.data,
                user_id=current_user.id,
            )
            db.session.add(message)
            db.session.commit()
            flash('메세지 전송이 완료되었습니다.', category="success")
            form.content.data = '' # 폼 초기화
            
        return render_template(BASE_VIEWS_DIR + "contact.html", 
            user=current_user,
            form=form,
        )

     

    5. mypage 생성(User 모델 수정)

    기본적으로 작성한 게시글, 댓글 개수만 출력하도록 했습니다. 이 때 User 모델에 posts_count, comments_count 필드를 추가해줍니다. 단지 개수만 출력할 때 posts, comments 정보를 가져오는 것은 낭비이기 때문입니다.

     

    sqlalchemy의 event listener를 활용해서, post, comment 추가시 posts_count, comments_count를 변경할 수 있도록 구현했습니다.

     

    아래 수정사항을 확인해주세요.

    # flask_app/blog/models.py
    
    class User(db.Model, UserMixin):
        __tablename__ = 'user'                                                          # 테이블 이름 명시적 선언
        id = db.Column(db.Integer, primary_key=True)                                    # primary key 설정
        username = db.Column(db.String(150), unique=True)                               # username unique
        email = db.Column(db.String(150), unique=True)                                  # email unique
        password = db.Column(db.String(150))                                            # password 
        date_created = db.Column(db.DateTime, default=datetime.now(KST_offset))         # 회원가입 날짜, 시간 기록
        post_create_permission = db.Column(db.Boolean, default=False)                   # 글 작성 권한 여부
        admin_check = db.Column(db.Boolean, default=False)                              # 관리자 권한 여부
    
        posts_count = db.Column(db.Integer, default=0)
        comments_count = db.Column(db.Integer, default=0)
        user_posts = db.relationship('Post', back_populates='user', cascade='delete, delete-orphan', lazy='dynamic')             
        user_comments = db.relationship('Comment', back_populates="user", cascade='delete, delete-orphan', lazy='dynamic') 
        user_messages = db.relationship('Message', back_populates='user', cascade='delete, delete-orphan', lazy='dynamic')     
    
        def __init__(self, username, email, password, post_create_permission=False, admin_check=False, posts_count=0, comments_count=0):
            self.username = username
            self.email = email
            self.password = generate_password_hash(password)
            self.date_created = datetime.now(KST_offset)                                      
            self.post_create_permission = post_create_permission 
            self.admin_check = admin_check
            self.posts_count = posts_count
            self.comments_count = comments_count
    
        def update_myinfo(self):
            self.posts_count = self.user_posts.count()
            self.comments_count = self.user_comments.count()
            db.session.commit()
    
        def __repr__(self):
            return f'{self.__class__.__name__} {self.id}: {self.username}'
            
    from sqlalchemy import event, func
    from sqlalchemy.orm iimport object_session
    @event.listens_for(db.session, 'before_flush')
    def after_insert_and_delete(session, flush_context, instances):
        for obj in session.new | session.deleted:
            if isinstance(obj, get_model('comment')):
                post = db.session.get(get_model('post'), obj.post_id)
                post.comments_count = object_session(obj).query(func.count(get_model('comment').id)).filter_by(post_id=obj.post_id).scalar()
    
                user = db.session.get(get_model('user'), obj.author_id)
                user.comments_count = object_session(obj).query(func.count(get_model('comment').id)).filter_by(author_id=obj.author_id).scalar()
                if obj in session.new:      # 추가
                    post.comments_count += 1
                    user.comments_count += 1
                else:                       # 삭제
                    post.comments_count -= 1
                    user.comments_count -= 1
    
            elif isinstance(obj, get_model('post')):
                user = db.session.get(get_model('user'), obj.author_id)
                user.posts_count = object_session(obj).query(func.count(get_model('post').id)).filter_by(author_id=obj.author_id).scalar()
                if obj in session.new:      # 추가
                    user.posts_count += 1
                else:                       # 삭제
                    user.posts_count -= 1
    # flask_app/blog/views.py
    
    @views.route('/mypage')
    def mypage():
        current_user.update_myinfo()
        return render_template(BASE_VIEWS_DIR + "mypage.html", user=current_user)
    <!-- flask_app/blog/templates/views/mypage.html -->
    {% extends "base.html" %}
    
    {% block title %} MyBlog - MyPage {% endblock %}
    
    {% block header %}
    <header class="masthead" style="background-image: url('../static/assets/img/about-bg.jpg')">
        <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">
                        <h2>Current User: {{user.username}}</h2>
                    </div>
                </div>
            </div>
        </div>
    </header>
    {% endblock %}
    
    {% block content %}
    <section class="about-section text-center" style="margin: 24px;">
        <div class="row">
            <div class="col mx-auto">
                <p class="text">
                    <b>Name:</b> {{user.username}} <br>
                    <b>Email:</b> {{user.email}} <br>
                    <b>작성한 게시글 개수:</b> {{user.posts_count}}<br>
                    <b>작성한 댓글 개수:</b> {{user.comments_count}}<br>
                </p>
            </div>
        </div>
    </section>
    {% endblock %}
    반응형
Designed by Tistory.