![見出し画像](https://assets.st-note.com/production/uploads/images/116192456/rectangle_large_type_2_bc915d849be74ab50489f3f839676569.png?width=1200)
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のスタッフが作成したデモサイトへのリンクがあります。このソースコードを表示して、HTMLとCSSの構造を参考にすることをおすすめします。
アプリケーションでは、以下の機能を実装する必要があります。
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です。
この記事が気に入ったらサポートをしてみませんか?