見出し画像

CS50 2023 - Week9 Flask


概要

Week9では、Flaskについて学びます。
講義の主な内容は、Flaskの基礎、ルート、デコレータ、リクエストとレスポンスの処理、セッション、クッキーです。

なお、FlaskはPythonのWebアプリケーションプラットフォームです。


Lab 9

Birthday

ユーザーが誕生日を登録し、確認できるアプリケーションを作成します。

以下は実際に私が提出したコードです。

app.py

import os

from cs50 import SQL
from flask import Flask, flash, jsonify, redirect, render_template, request, session

# Configure application
app = Flask(__name__)

# Ensure templates are auto-reloaded
app.config["TEMPLATES_AUTO_RELOAD"] = True

# Configure CS50 Library to use SQLite database
db = SQL("sqlite:///birthdays.db")


@app.after_request
def after_request(response):
    """Ensure responses aren't cached"""
    response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    response.headers["Expires"] = 0
    response.headers["Pragma"] = "no-cache"
    return response


@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        # TODO: Add the user's entry into the database
        name = request.form.get("name")
        month = request.form.get("month")
        day = request.form.get("day")

        db.execute(
            "INSERT INTO birthdays (name, month, day) VALUES (?, ?, ?)",
            name,
            month,
            day,
        )

        return redirect("/")

    else:
        birthdays = db.execute("SELECT * FROM birthdays")

        # TODO: Display the entries in the database on index.html
        return render_template("index.html", birthdays=birthdays)

index.html

<!DOCTYPE html>

<html lang="en">

    <head>
        <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500&display=swap" rel="stylesheet">
        <link href="/static/styles.css" rel="stylesheet">
        <title>Birthdays</title>
    </head>

    <body>
        <div class="header">
            <h1>Birthdays</h1>
        </div>
        <div class="container">
            <div class="section">

                <h2>Add a Birthday</h2>
                <!-- TODO: Create a form for users to submit a name, a month, and a day -->
                <form action="/" method="post">
                    <input name="name" placeholder="Name" type="text">
                    <input name="month" placeholder="Month" type="number" min="1" max="12">
                    <input name="day" placeholder="Day" type="number" min="1" max="31">
                    <input type="submit" value="Add Birthday">
                </form>
            </div>

            <div class="section">

                <h2>All Birthdays</h2>
                <table>
                    <thead>
                        <tr>
                            <th>Name</th>
                            <th>Birthday</th>
                        </tr>
                    </thead>
                    <tbody>
                        <!-- TODO: Loop through the database entries to display them in this table -->
                        {% for birthday in birthdays %}
                        <tr>
                            <td>{{ birthday.name }}</td>
                            <td>{{ birthday.month }}/{{ birthday.day }}</td>
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
            </div>
        </div>
    </body>

</html>

Problem Set 9

Finance

ユーザーが株の売買をできるアプリケーションを作成します。
自分的には、この課題が最難関でした。

課題の詳細ページに、CS50のスタッフが作成したデモサイトへのリンクがあります。このソースコードを表示して、HTMLCSSの構造を参考にすることをおすすめします。

アプリケーションでは、以下の機能を実装する必要があります。

  • register:ユーザーがアカウントを登録できるようにする。パスワードはハッシュ化して保存する。

  • quote:ユーザーが現在の株価を検索できるようにする。

  • buy:ユーザーが株の購入をできるようにする。ユーザーの所持金以上の株を購入できないようにする。

  • index:ログインしているユーザーが所有している株情報をテーブルで表示する。

  • sell:ユーザーが株の売却をできるようにする。ユーザーが所有していない株は売却できないようにする。

  • history:ユーザーの売買履歴をリスト化して表示する。

  • personal touch:下記から最低1つを選んで実装する。

    • ユーザーがパスワードを変更できるようにする

    • ユーザーがアカウントに所持金を追加できるようにする

    • 株式の銘柄を手動入力しなくても、ユーザーが所有している株式をindexを介して売買できるようにする

    • ユーザーが設定するパスワードに、最低文字数、数字や記号を含めるなどの条件を追加する

私は、ユーザーがパスワードを変更できるようにする機能を選びました。

以下は実際に私が提出したコードです。

app.py

import os

from cs50 import SQL
from flask import Flask, flash, redirect, render_template, request, session
from flask_session import Session
from tempfile import mkdtemp
from werkzeug.security import check_password_hash, generate_password_hash

from helpers import apology, login_required, lookup, usd

# Configure application
app = Flask(__name__)

# Custom filter
app.jinja_env.filters["usd"] = usd

# Configure session to use filesystem (instead of signed cookies)
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)

# Configure CS50 Library to use SQLite database
db = SQL("sqlite:///finance.db")


@app.after_request
def after_request(response):
    """Ensure responses aren't cached"""
    response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    response.headers["Expires"] = 0
    response.headers["Pragma"] = "no-cache"
    return response


@app.route("/")
@login_required
def index():
    """Show portfolio of stocks"""
    portfolio = db.execute(
        "SELECT * FROM portfolio WHERE user_id = ?", session["user_id"]
    )

    portfolio_summary = []
    total = 0

    for stock in portfolio:
        current_stock = lookup(stock["symbol"])
        total_stock_value = current_stock["price"] * stock["shares"]
        total += total_stock_value

        portfolio_summary.append(
            {
                "symbol": stock["symbol"],
                "name": current_stock["name"],
                "shares": stock["shares"],
                "price": usd(current_stock["price"]),
                "total": usd(total_stock_value),
            }
        )

    cash = db.execute("SELECT cash FROM users WHERE id = ?", session["user_id"])[0][
        "cash"
    ]
    total += cash

    return render_template(
        "index.html", stocks=portfolio_summary, cash=usd(cash), total=usd(total)
    )


@app.route("/buy", methods=["GET", "POST"])
@login_required
def buy():
    """Buy shares of stock"""

    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":
        # Ensure symbol was submitted
        if not request.form.get("symbol"):
            return apology("must provide symbol", 400)

        # Ensure shares was submitted
        elif not request.form.get("shares"):
            return apology("must provide shares", 400)

        # Check if shares is a positive integer
        elif not request.form.get("shares").isdigit():
            return apology("shares must be a positive integer", 400)

        shares = int(request.form.get("shares"))

        # Lookup the current price of the stock
        quote = lookup(request.form.get("symbol"))

        # Ensure symbol exists
        if quote is None:
            return apology("invalid symbol", 400)

        # Calculate total price of the purchase
        total_price = quote["price"] * shares

        # Query database for username
        rows = db.execute(
            "SELECT cash FROM users WHERE id = :user_id", user_id=session["user_id"]
        )

        # Ensure user can afford
        if rows[0]["cash"] < total_price:
            return apology("can't afford", 400)

        # Update user cash
        db.execute(
            "UPDATE users SET cash = cash - :total_price WHERE id = :user_id",
            user_id=session["user_id"],
            total_price=total_price,
        )

        # Add purchase to the transactions table
        db.execute(
            "INSERT INTO transactions (user_id, symbol, shares, price, transacted, transaction_type) VALUES (:user_id, :symbol, :shares, :price, datetime('now'), 'buy')",
            user_id=session["user_id"],
            symbol=quote["symbol"],
            shares=shares,
            price=quote["price"],
        )

        # Add or update purchase in the portfolio table
        db.execute(
            "INSERT INTO portfolio (user_id, symbol, shares) VALUES (:user_id, :symbol, :shares) ON CONFLICT(user_id, symbol) DO UPDATE SET shares = shares + :shares WHERE user_id = :user_id",
            user_id=session["user_id"],
            symbol=quote["symbol"],
            shares=shares,
        )

        # After successfully executing a BUY, flash a success message
        flash("Bought")

        # Redirect user to home page
        return redirect("/")
    # User reached route via GET (as by clicking a link or via redirect)
    else:
        return render_template("buy.html")


@app.route("/history")
@login_required
def history():
    """Show history of transactions"""
    transactions = db.execute(
        "SELECT * FROM transactions WHERE user_id = ? ORDER BY transacted DESC",
        session["user_id"],
    )

    return render_template("history.html", transactions=transactions)


@app.route("/login", methods=["GET", "POST"])
def login():
    """Log user in"""

    # Forget any user_id
    session.clear()

    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":
        # Ensure username was submitted
        if not request.form.get("username"):
            return apology("must provide username", 400)

        # Ensure password was submitted
        elif not request.form.get("password"):
            return apology("must provide password", 400)

        # Query database for username
        rows = db.execute(
            "SELECT * FROM users WHERE username = ?", request.form.get("username")
        )

        # Ensure username exists and password is correct
        if len(rows) != 1 or not check_password_hash(
            rows[0]["hash"], request.form.get("password")
        ):
            return apology("invalid username and/or password", 400)

        # Remember which user has logged in
        session["user_id"] = rows[0]["id"]

        # Redirect user to home page
        return redirect("/")

    # User reached route via GET (as by clicking a link or via redirect)
    else:
        return render_template("login.html")


@app.route("/logout")
def logout():
    """Log user out"""

    # Forget any user_id
    session.clear()

    # Redirect user to login form
    return redirect("/")


@app.route("/quote", methods=["GET", "POST"])
@login_required
def quote():
    """Get stock quote."""
    # If form is submitted
    if request.method == "POST":
        # Check if symbol is provided
        symbol = request.form.get("symbol")
        if not symbol:
            return apology("must provide symbol", 400)

        # Use lookup function to get the stock's current price
        stock = lookup(symbol)
        if stock is None:
            return apology("invalid symbol", 400)

        # Render quoted page with stock information
        return render_template("quoted.html", stock=stock)

    # User reached route via GET (as by clicking a link or via redirect)
    else:
        # Render quote form page
        return render_template("quote.html")


@app.route("/register", methods=["GET", "POST"])
def register():
    """Register user"""
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        confirmation = request.form.get("confirmation")

        # Ensure username was submitted
        if not username:
            return apology("must provide username", 400)

        # Ensure password was submitted
        elif not password:
            return apology("must provide password", 400)

        # Ensure password confirmation was submitted and matches password
        elif not confirmation or password != confirmation:
            return apology("passwords must match", 400)

        # Query database for username
        rows = db.execute("SELECT * FROM users WHERE username = ?", username)

        # Check if username already exists
        if len(rows) == 1:
            return apology("username already exists", 400)

        # Insert new user into database, storing a hash of the user's password
        db.execute(
            "INSERT INTO users (username, hash) VALUES (?, ?)",
            username,
            generate_password_hash(password),
        )

        # Redirect user to login page
        return redirect("/login")

    else:
        # User reached route via GET (as by clicking a link or via redirect)
        return render_template("register.html")


@app.route("/sell", methods=["GET", "POST"])
@login_required
def sell():
    """Sell shares of stock"""

    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":
        # Ensure symbol was submitted
        if not request.form.get("symbol"):
            return apology("must provide symbol", 400)

        # Ensure number of shares was submitted
        shares = int(request.form.get("shares"))
        if not shares:
            return apology("must provide number of shares", 400)

        # Check if enough shares are owned
        owned_shares = db.execute(
            "SELECT shares FROM portfolio WHERE user_id = ? AND symbol = ?",
            session["user_id"],
            request.form.get("symbol"),
        )[0]["shares"]
        if shares > owned_shares:
            return apology("not enough shares", 400)

        # Get current price
        current_price = lookup(request.form.get("symbol"))["price"]

        # Update user cash (add the revenue from selling stocks)
        db.execute(
            "UPDATE users SET cash = cash + ? WHERE id = ?",
            shares * current_price,
            session["user_id"],
        )

        # Record the sell transaction
        db.execute(
            "INSERT INTO transactions (user_id, symbol, shares, price, transacted, transaction_type) VALUES (?, ?, ?, ?, datetime('now'), 'sell')",
            session["user_id"],
            request.form.get("symbol"),
            shares,
            current_price,
        )

        # Update shares count or delete from portfolio
        if shares == owned_shares:
            db.execute(
                "DELETE FROM portfolio WHERE user_id = ? AND symbol = ?",
                session["user_id"],
                request.form.get("symbol"),
            )
        else:
            db.execute(
                "UPDATE portfolio SET shares = shares - ? WHERE user_id = ? AND symbol = ?",
                shares,
                session["user_id"],
                request.form.get("symbol"),
            )

        # After successfully executing a SELL, flash a success message
        flash("Sold")

        # Redirect user to home page
        return redirect("/")

    else:
        # User reached route via GET (as by clicking a link or via redirect)

        # Query database for symbols
        rows = db.execute(
            "SELECT symbol FROM portfolio WHERE user_id = ?", session["user_id"]
        )
        return render_template("sell.html", symbols=[row["symbol"] for row in rows])


@app.route("/change_password", methods=["GET", "POST"])
@login_required
def change_password():
    """Change user password."""

    # User reached route via POST (as by submitting a form via POST)
    if request.method == "POST":
        # Ensure old password was submitted
        if not request.form.get("old_password"):
            return apology("must provide old password", 400)

        # Ensure new password was submitted
        if not request.form.get("new_password"):
            return apology("must provide new password", 400)

        # Ensure confirmation password is the same
        if request.form.get("new_password") != request.form.get("confirm_new_password"):
            return apology("new passwords must match", 400)

        # Query database for username
        rows = db.execute("SELECT * FROM users WHERE id = :id", id=session["user_id"])

        # Ensure old password is correct
        if len(rows) != 1 or not check_password_hash(
            rows[0]["hash"], request.form.get("old_password")
        ):
            return apology("invalid old password", 400)

        # Hash the new password
        hash = generate_password_hash(request.form.get("new_password"))

        # Update the database with the new password hash
        db.execute(
            "UPDATE users SET hash = :hash WHERE id = :id",
            hash=hash,
            id=session["user_id"],
        )

        # Flash a message to let the user know their password was updated
        flash("Password Changed")

        # Redirect user to home page
        return redirect("/")

    # User reached route via GET (as by clicking a link or via redirect)
    else:
        return render_template("change_password.html")

register.html

{% extends "layout.html" %}

{% block title %}
Register
{% endblock %}

{% block main %}
<form action="/register" method="post">
    <div class="mb-3">
        <input autocomplete="off" autofocus class="form-control mx-auto w-auto" name="username" placeholder="Username" type="text">
    </div>
    <div class="mb-3">
        <input class="form-control mx-auto w-auto" name="password" placeholder="Password" type="password">
    </div>
    <div class="mb-3">
        <input class="form-control mx-auto w-auto" name="confirmation" placeholder="Password (again)" type="password">
    </div>
    <button class="btn btn-primary" type="submit">Register</button>
</form>
{% endblock %}

quote.html

{% extends "layout.html" %}

{% block title %}
Quote
{% endblock %}

{% block main %}
<form action="/quote" method="post">
    <div class="form-group">
        <input autocomplete="off" autofocus class="form-control mx-auto w-auto mb-3" name="symbol" placeholder="Stock Symbol" type="text">
    </div>
    <button class="btn btn-primary" type="submit">Quote</button>
</form>
{% endblock %}

buy.html

{% extends "layout.html" %}

{% block title %}
Buy
{% endblock %}

{% block main %}
<form action="/buy" method="post">
    <div class="mb-3">
        <input autocomplete="off" autofocus class="form-control mx-auto w-auto" id="symbol" name="symbol" placeholder="Symbol" type="text">
    </div>
    <div class="mb-3">
        <input class="form-control mx-auto w-auto" id="shares" name="shares" placeholder="Shares" type="number" min="1">
    </div>
    <button class="btn btn-primary" type="submit">Buy</button>
</form>
{% endblock %}

index.html

{% extends "layout.html" %}

{% block title %}
Portfolio
{% endblock %}

{% block main %}
<main class="container py-5 text-center">
    <table class="table table-striped">
        <thead>
            <tr>
                <th class="text-start">Symbol</th>
                <th class="text-start">Name</th>
                <th class="text-end">Shares</th>
                <th class="text-end">Price</th>
                <th class="text-end">TOTAL</th>
            </tr>
        </thead>
        <tbody>
            {% for stock in stocks %}
            <tr>
                <td class="text-start">{{ stock.symbol }}</td>
                <td class="text-start">{{ stock.name }}</td>
                <td class="text-end">{{ stock.shares }}</td>
                <td class="text-end">{{ stock.price }}</td>
                <td class="text-end">{{ stock.total }}</td>
            </tr>
            {% endfor %}
        </tbody>
        <tfoot>
            <tr>
                <td class="border-0 fw-bold text-end" colspan="4">Cash</td>
                <td class="border-0 text-end">{{ cash }}</td>
            </tr>
            <tr>
                <td class="border-0 fw-bold text-end" colspan="4">TOTAL</td>
                <td class="border-0 w-bold text-end">{{ total }}</td>
            </tr>
        </tfoot>
    </table>
</main>
{% endblock %}

sell.html

{% extends "layout.html" %}

{% block title %}
Sell
{% endblock %}

{% block main %}
<form action="/sell" method="post">
    <div class="mb-3">
        <select class="form-select mx-auto w-auto" name="symbol" id="symbol">
            <option disabled selected value="">Symbol</option>
            {% for symbol in symbols %}
            <option value="{{ symbol }}">{{ symbol }}</option>
            {% endfor %}
        </select>
    </div>
    <div class="mb-3">
        <input class="form-control mx-auto w-auto" name="shares" id="shares" placeholder="Shares" type="number" min="1">
    </div>
    <button class="btn btn-primary" type="submit">Sell</button>
</form>
{% endblock %}

history.html

{% extends "layout.html" %}

{% block title %}
Transaction History
{% endblock %}

{% block main %}
<main class="container py-5 text-center">
    <table class="table">
        <thead>
            <tr>
                <th class="text-start">Symbol</th>
                <th class="text-end">Shares</th>
                <th class="text-end">Price</th>
                <th class="text-end">Transacted</th>
            </tr>
        </thead>
        <tbody>
            {% for transaction in transactions %}
            <tr>
                <td class="text-start">{{ transaction.symbol }}</td>
                <td class="text-end">
                    {% if transaction.transaction_type == "sell" %}
                    -{{ transaction.shares }}
                    {% else %}
                    {{ transaction.shares }}
                    {% endif %}
                </td>
                <td class="text-end">${{ transaction.price }}</td>
                <td class="text-end">{{ transaction.transacted }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</main>
{% endblock %}

change_password.html

{% extends "layout.html" %}

{% block title %}
Change Password
{% endblock %}

{% block main %}
<form action="/change_password" method="post">
    <div class="mb-3">
        <input autocomplete="off" autofocus class="form-control mx-auto w-auto" name="old_password" placeholder="Current Password" type="password">
    </div>
    <div class="mb-3">
        <input class="form-control mx-auto w-auto" name="new_password" placeholder="New Password" type="password">
    </div>
    <div class="mb-3">
        <input class="form-control mx-auto w-auto" name="confirm_new_password" placeholder="New Password (again)" type="password">
    </div>
    <button class="btn btn-primary" type="submit">Change Password</button>
</form>
{% endblock %}

さいごに

Flask自体が取り組みづらかったうえに、Financeがかなり難しかったです。着手から提出までに2週間半ほどかかってしまいました。

なお、Financeの詳細ページのみ、ページタイトルが「C$50」になっています。こういった遊び心があるところが本当に好きです。

次はいよいよ最終週、Final Projectです。

この記事が気に入ったらサポートをしてみませんか?