Flask/WTForms/database

JOOYEUN SEOยท2024๋…„ 10์›” 25์ผ

100 Days of Python

๋ชฉ๋ก ๋ณด๊ธฐ
64/76
post-thumbnail

๐Ÿ—‚๏ธ Day64 ํ”„๋กœ์ ํŠธ: ๋ฒ ์ŠคํŠธ 10 ์˜ํ™” ์›น์‚ฌ์ดํŠธ

์—ญ๋Œ€ ์ตœ๊ณ ์˜ ์˜ํ™” 10ํŽธ์„ ๊ผฝ๋Š” ์›น์‚ฌ์ดํŠธ

1. ์˜ํ™” ๋ชฉ๋ก ํ•ญ๋ชฉ ๋งŒ๋“ค๊ธฐ

๐Ÿ” ์œ ์˜ ์‚ฌํ•ญ

  • ํ™ˆํŽ˜์ด์ง€์—์„œ ํƒ‘ 10 ์˜ํ™” ๋ชฉ๋ก์„ ์นด๋“œ ํ˜•์‹์œผ๋กœ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ค๊ธฐ
  • SQL ์•Œ์ผ€๋ฏธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ SQ ๋ผ์ดํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ (ํ‘œ ์ด๋ฆ„: Movie)
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ํ•˜๋“œ์ฝ”๋”ฉ์œผ๋กœ ์ƒˆ ํ•ญ๋ชฉ ์ถ”๊ฐ€ (์ฝ”๋“œ๋ฅผ ํ•œ ๋ฒˆ ์‹คํ–‰ ํ›„์—๋Š” ์‚ญ์ œํ•ด์•ผ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์•ˆ ํ•จ)
  • ์นด๋“œ ์•ž๋ฉด์€ ์˜ํ™” ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€, ๋’ท๋ฉด์€ ์˜ํ™” ์ œ๋ชฉ(์—ฐ๋„), ๋ณ„์ , ๋ฆฌ๋ทฐ, ์„ค๋ช…

2. ์˜ํ™” ๋“ฑ๊ธ‰ ๋ฐ ๋ฆฌ๋ทฐ ์ˆ˜์ • ๊ธฐ๋Šฅ ๋„ฃ๊ธฐ

๐Ÿ” ์œ ์˜ ์‚ฌํ•ญ

  • ์˜ํ™” ์นด๋“œ ๋’ท๋ฉด์˜ ์—…๋ฐ์ดํŠธ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ํ‰์ ๊ณผ ๋ฆฌ๋ทฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋„๋ก ํ•˜๊ธฐ
  • WTForms๋กœ edit.html์—์„œ ๋ Œ๋”๋งํ•  Quick Form์„ ์ƒ์„ฑ
  • ํผ์„ ์ œ์ถœํ•˜๊ณ  ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•œ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ•ด๋‹น ์˜ํ™” ํ•ญ๋ชฉ์— ์—…๋ฐ์ดํŠธ ์‚ฌํ•ญ์„ ์ถ”๊ฐ€

3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์˜ํ™” ์‚ญ์ œ

๐Ÿ” ์œ ์˜ ์‚ฌํ•ญ

  • ์˜ํ™” ์นด๋“œ ๋’ท๋ฉด์˜ ์‚ญ์ œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์˜ํ™” ํ•ญ๋ชฉ์„ ์‚ญ์ œ

4. ์ƒˆ ์˜ํ™”๋ฅผ ์ƒˆ ํŽ˜์ด์ง€์— ์ถ”๊ฐ€

๐Ÿ” ์œ ์˜ ์‚ฌํ•ญ

  • Add Movie ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ์ถ”๊ฐ€ ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง๋˜๋„๋ก ์ˆ˜์ •
    (์˜ํ™” ์ œ๋ชฉ ํ•„๋“œ๋งŒ ํฌํ•จ๋œ WTF quick form์ด ํ‘œ์‹œ๋˜์–ด์•ผ ํ•œ๋‹ค)
  • ์˜ํ™” ์ œ๋ชฉ ์ž…๋ ฅ ํ›„ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ”Œ๋ผํฌ์Šค ์„œ๋ฒ„๊ฐ€ ํ•ด๋‹น ์˜ํ™” ์ œ๋ชฉ์„ ์ˆ˜์‹ ํ•˜๋„๋ก ๋ณ€๊ฒฝ
  • requests๋กœ ํ•ด๋‹น ์ œ๋ชฉ๊ณผ ์ผ์น˜ํ•˜๋Š” ๋ชจ๋“  ์˜ํ™”์— ๋Œ€ํ•ด The Movie Database API๋ฅผ ์š”์ฒญ ๋ฐ ๊ฒ€์ƒ‰
    • ๋ฌด๋ฃŒ ๊ฐ€์ž… ํ›„ ์„ค์ • โ†’ API ํ‚ค ์š”์ฒญ โ†’ Developer โ†’ ์–‘์‹ ๊ธฐ์ž… ํ›„ ์ƒ์„ฑ
    • ์ฐธ๊ณ : ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค๋ช…๋ž€์— ๋„ˆ๋ฌด ์งง๊ฒŒ ์ ์œผ๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ ๊ฐ™์Œ
    • ์ฐธ๊ณ : API_KEY ๋Œ€์‹  ACCESS_TOKEN ๋งŒ ๋˜๋Š” ๊ฒƒ ๊ฐ™์Œ
  • ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด API๋กœ ์˜ํ™” ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญ
  • ๊ฒ€์ƒ‰๋œ ์˜ํ™” ๋ฆฌ์ŠคํŠธ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•˜๋ฉด
    • ํ•ด๋‹น ์˜ํ™” ID๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜ํ™” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค API์˜ ๋‹ค๋ฅธ ๊ฒฝ๋กœ๊ฐ€ ์š”์ฒญ๋˜์–ด์•ผ ํ•œ๋‹ค
    • ํ•ด๋‹น ์˜ํ™”์— ๋Œ€ํ•œ ์ œ๋ชฉ, ์ด๋ฏธ์ง€url, ๊ฐœ๋ด‰์—ฐ๋„, ์„ค๋ช… ๊ฐ€์ ธ์˜ค๊ธฐ
    • edit.html ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜ํ•ด์„œ ํ‰์ ๊ณผ ํ•œ ์ค„ ๋ฆฌ๋ทฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ

5. ์˜ํ™”๋ฅผ ๋“ฑ๊ธ‰๋ณ„๋กœ ์ •๋ฆฌํ•˜๊ณ  ์ˆœ์œ„ ๋งค๊ธฐ๊ธฐ

๐Ÿ” ์œ ์˜ ์‚ฌํ•ญ

  • ์ถ”๊ฐ€ํ•œ ์˜ํ™”๋“ค์˜ ํ‰์ ์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ๋“ฑ๊ธ‰์„ ๋งค๊ธฐ๋„๋ก ๋ณ€๊ฒฝํ•˜๊ธฐ
  • ๋ณ„์ ์ด ๊ฐ€์žฅ ๋‚ฎ์€ ์ˆœ์œผ๋กœ ์œ„์—์„œ๋ถ€ํ„ฐ ์ •๋ ฌ

โŒจ๏ธ main.py

from flask import Flask, render_template, redirect, url_for, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
import requests

TMDB_SEARCH_URL = "https://api.themoviedb.org/3/search/movie"
TMDB_DETAILS_URL = "https://api.themoviedb.org/3/movie/"
TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/original"
TMDB_API_ACCESS_TOKEN = "๊ฐœ์ธ ํ† ํฐ"
headers = {
    "accept": "application/json",
    "Authorization": f"Bearer {TMDB_API_ACCESS_TOKEN}"
}

app = Flask(__name__)
app.config['SECRET_KEY'] = '8BYkEfBA6O6donzWlSihBXox7C0sKR6b'
Bootstrap(app)

class RateMovieForm(FlaskForm):
    rating = StringField("Your Rating Out of 10", validators=[DataRequired()])
    review = StringField("Your Review", validators=[DataRequired()])
    submit = SubmitField("Done")

class FindMovieForm(FlaskForm):
    title = StringField("Movie Title", validators=[DataRequired()])
    submit = SubmitField("Add Movie")

# CREATE DB
class Base(DeclarativeBase):
  pass
db = SQLAlchemy(model_class=Base)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///movie.db"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)

# CREATE TABLE
class Movie(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
    year: Mapped[int] = mapped_column(nullable=False)
    description: Mapped[str] = mapped_column(String(500), nullable=False)
    rating: Mapped[float] = mapped_column(nullable=True)
    review: Mapped[str] = mapped_column(String(250), nullable=True)
    ranking: Mapped[int] = mapped_column(nullable=True)
    img_url: Mapped[str] = mapped_column(nullable=False)

with app.app_context():
    db.create_all()
    # # ํ•˜๋“œ์ฝ”๋”ฉ์œผ๋กœ ์˜ํ™” ์ถ”๊ฐ€ (ํ•œ ๋ฒˆ ์ถ”๊ฐ€ํ•œ ๋ฐ์ดํ„ฐ๋Š” ์ฝ”๋“œ์—์„œ ์‚ญ์ œํ•˜๊ธฐ)
    # new_movie = Movie(
    #     title="Phone Booth",
    #     year=2002,
    #     description="Publicist Stuart Shepard finds himself trapped in a phone booth, pinned down by an extortionist's sniper rifle. Unable to leave or receive outside help, Stuart's negotiation with the caller leads to a jaw-dropping climax.",
    #     rating=7.3,
    #     ranking=10,
    #     review="My favourite character was the caller.",
    #     img_url="https://image.tmdb.org/t/p/w500/tjrX2oWRCM3Tvarz38zlZM7Uc10.jpg"
    # )
    # db.session.add(new_movie)
    # db.session.commit()

@app.route("/")
def home():
    # Movie ํ…Œ์ด๋ธ”์˜ ๋ฐ์ดํ„ฐ๋ฅผ rating ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌํ•œ ํ›„ db.session.execute()๋กœ ์ฟผ๋ฆฌ ์‹คํ–‰
    result = db.session.execute(db.select(Movie).order_by(Movie.rating))
    # result.scalars()๋กœ ScalarResult ๊ฐ์ฒด์—์„œ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”์ถœํ•œ ํ›„ ํŒŒ์ด์ฌ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜
    all_movies = result.scalars().all()

    for i in range(len(all_movies)):
        all_movies[i].ranking = len(all_movies) - i
    db.session.commit()
    return render_template("index.html", movies=all_movies)


@app.route("/edit", methods=["GET", "POST"])
def rate_movie():
    form = RateMovieForm()
    movie_id = request.args.get("id")
    movie = Movie.query.get(movie_id)
    if form.validate_on_submit():
        # .data ์†์„ฑ์œผ๋กœ ์ž…๋ ฅ๊ฐ’์„ ์ถ”์ถœ
        movie.rating = float(form.rating.data)
        movie.review = form.review.data
        db.session.commit()
        return redirect(url_for('home'))
    return render_template("edit.html", movie=movie, form=form)


@app.route("/delete")
def delete():
    movie_id = request.args.get("id")
    movie_to_delete = db.get_or_404(Movie, movie_id)
    db.session.delete(movie_to_delete)
    db.session.commit()
    return redirect(url_for('home'))


@app.route("/add", methods=["GET", "POST"])
def add():
    form = FindMovieForm()
    if form.validate_on_submit():
        movie_title = form.title.data
        params = {
            "query": movie_title,
            "include_adult": "false",
            "language": "en-US",
        }
        response = requests.get(TMDB_SEARCH_URL, params=params, headers=headers)
        data = response.json()["results"]
        return render_template("select.html", options=data)
    return render_template("add.html", form=form)


@app.route("/find")
def find():
    movie_api_id = request.args.get("id")
    if movie_api_id:
        params = {"language": "en-US"}
        response = requests.get(TMDB_DETAILS_URL + f"{movie_api_id}",  params=params, headers=headers)
        data = response.json()
        new_movie = Movie(
            title= data["title"],
            year=data["release_date"].split("-")[0],
            img_url=f"{TMDB_IMAGE_BASE_URL}{data['poster_path']}",
            description=data["overview"]
        )
        db.session.add(new_movie)
        db.session.commit()
        return redirect(url_for('rate_movie', id=new_movie.id))


if __name__ == '__main__':
    app.run(debug=True)

๐Ÿ—๏ธ base.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1, shrink-to-fit=no"
  />
  {% block styles %}
    <!-- Load Bootstrap-Flask CSS here -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
    <!-- Link to the styles.css here to apply styling to all the child templates.-->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
    <script src="https://kit.fontawesome.com/cfaaaf681d.js" crossorigin="anonymous"></script>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}"/>
  {% endblock %}
  <title>{% block title %}{% endblock %}</title>
</head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

๐Ÿ—๏ธ index.html

{% extends 'base.html' %}
{% block title %}My Top 10 Movies{% endblock %}

{% block content %}
  <div class="container">
    <h1 class="heading">My Top 10 Movies</h1>
    <p class="description">These are my all-time favourite movies.</p>

    {% for movie in movies %}
      <div class="card" >
        <div class="front" style="background-image: url('{{ movie.img_url }}');">
          <p class="large">{{ movie.ranking }}</p>
        </div>

        <div class="back">
          <div>
            <div class="title">{{ movie.title }} <span class="release_date">({{ movie.year }})</span></div>
              <div class="rating">
                <label>{{ movie.rating }}</label>
                <i class="fas fa-star star"></i>
              </div>
                <p class="review">"{{ movie.review }}"</p>
              <p class="overview">{{ movie.description }}</p>
              <a href="{{ url_for('rate_movie', id=movie.id) }}" class="button">Update</a>
              <a href="{{ url_for('delete', id=movie.id) }}" class="button delete-button">Delete</a>
          </div>
        </div>
      </div>
    {% endfor %}
  </div>
  <div class="container text-center add">
  <a href="{{ url_for('add') }}" class="button">Add Movie</a>
  </div>
{% endblock %}

๐Ÿ—๏ธ edit.html

{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Edit Movies{% endblock %}

{% block content %}
<div class="content">
  <h1 class="heading">{{ movie.title }}</h1>
  <p class="description">Edit Movie Rating</p>
  {{ wtf.quick_form(form, novalidate=True) }}
  </div>
{% endblock %}

๐Ÿ—๏ธ add.html

{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Add Movie{% endblock %}

{% block content %}
<div class="content">
    <h1 class="heading">Add a Movie</h1>
    {{ wtf.quick_form(form, novalidate=True) }}
</div>
{% endblock %}

๐Ÿ—๏ธ select.html

{% extends 'base.html' %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Select Movie{% endblock %}

{% block content %}
<div class="container">
    <h1 class="heading">Select Movie</h1>

    {% for movie in options %}
      <p>
        <a href="{{ url_for('find', id=movie.id) }}">
            {{ movie.title }} - {{ movie.release_date }}
        </a>
      </p>
    {% endfor %}
</div>
{% endblock %}




โ–ท Angela Yu, [Python ๋ถ€ํŠธ์บ ํ”„ : 100๊ฐœ์˜ ํ”„๋กœ์ ํŠธ๋กœ Python ๊ฐœ๋ฐœ ์™„์ „ ์ •๋ณต], Udemy, https://www.udemy.com/course/best-100-days-python/?couponCode=ST3MT72524

0๊ฐœ์˜ ๋Œ“๊ธ€