Yesterday (around 10 hours prior to this post) I successfully completed the Problem Set 9 Finance as part of the CS50 Introduction to Computer Science.
I have passed all the tests multiple times, implemented some additional features as per the spec, ran the tests again multiple times (successfully) and finally submitted the application with submit50 cs50/problems/2023/x/finance
My test logs are as follows: https://submit.cs50.io/check50/4cfbef0a2307b38b3be96461761f25bc1fb7094d
What could've caused the issue:
When I first unzipped the starting boilerplate into /finance
directory I tried to split the app.py file into smaller files with blueprints. The idea failed because apparently the test suite doesn't like app.py being split into standalone files, so I abandoned it altogether and created a new /finance2
directory to which I copypasted the code from /finance
. After the code was fully pasted into /finance2
, I deleted the original directory /finance
and renamed /finance2
to /finance
.
I have no idea why it would create an issue, but this is the first time I have encountered it and the only difference from submitting other problem sets is me changing the directory names.
Oddly enough, my style50
command fails the style check:
Can't check your style just yet! Try running your code, fix any errors, then check its style again!
I use prettier for styling the code. Running style50
had never been an problem up until yesterday. Perhaps it is somehow connected.
Upd: added code for those experiencing the same problem as me
import os
from cs50 import SQL
from flask import Flask, flash, redirect, render_template, request, session
from flask_session import Session
from werkzeug.security import check_password_hash, generate_password_hash
from helpers import apology, login_required, lookup, usd, validate_password
# 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"""
user_id = session["user_id"]
user = db.execute("SELECT username FROM users WHERE id = ?", user_id)
# Query for user's stocks
stocks = db.execute(
"SELECT symbol, SUM(shares) as shares_sum FROM transactions WHERE user_id = ? GROUP BY symbol HAVING shares_sum > 0",
user_id,
)
# Calculate total for every stock and get prices
holdings = []
total_stocks = 0
for stock in stocks:
stock_data = lookup(stock["symbol"])
stock_val = stock_data["price"] * stock["shares_sum"]
total_stocks += stock_val
holdings.append(
{
"symbol": stock["symbol"],
"name": stock_data["name"],
"shares": stock["shares_sum"],
"price": stock_data["price"],
"total": stock_val,
}
)
cash_result = db.execute("SELECT cash FROM users WHERE id = ?", user_id)
cash = cash_result[0]["cash"] if cash_result else 0
return render_template(
"index.html", holdings=holdings, cash=cash, grand_total=total_stocks + cash
)
@app.route("/buy", methods=["GET", "POST"])
@login_required
def buy():
"""Buy shares of stock"""
if request.method == "POST":
symbol = request.form.get("symbol").upper()
shares_to_buy = request.form.get("shares")
try:
shares_to_buy = int(request.form.get("shares"))
except ValueError:
return apology("shares quantity must be a positive number", 400)
result = execute_buy(session["user_id"], symbol, shares_to_buy)
if "error" in result:
return apology(result["error"], result["code"])
else:
flash(result)
return redirect("/")
return render_template("buy.html")
def execute_buy(user_id: str, symbol: str, shares_to_buy: int) -> {"error": str, "code": str} or str:
if not symbol:
return {"error": "must provide symbol", "code": "400"}
stock = lookup(symbol)
if not stock:
return {"error": "ticker symbol not found", "code": "400"}
if shares_to_buy <= 0:
return {"error": "shares quantity must be a positive number", "code": "400"}
rows = db.execute("SELECT cash FROM users WHERE id = ?", user_id)
available: int = rows[0]["cash"]
total = stock["price"] * shares_to_buy # total purchase price
# check cash balance
if available < total:
return {"error": "Not enough cash to buy", "code": "400"}
# record the purchase
db.execute(
"INSERT INTO transactions (user_id, symbol, shares, price_per_share, timestamp) VALUES (?,?,?,?, CURRENT_TIMESTAMP)",
user_id,
symbol,
shares_to_buy,
stock["price"],
)
# update user's balance
db.execute("UPDATE users SET cash = cash - ? WHERE id = ?", total, user_id)
return f"Purchased {shares_to_buy} shares of {symbol}"
@app.route("/history")
@login_required
def history():
"""Show history of transactions"""
user_id = session["user_id"]
# Query for transactions
transactions = db.execute(
"SELECT symbol, shares, price_per_share, timestamp FROM transactions WHERE user_id = ?",
user_id,
)
for tr in transactions:
tr["type"] = "Buy" if tr["shares"] > 0 else "Sell"
tr["shares"] = abs(tr["shares"]) # quantity should be positive
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", 403)
# Ensure password was submitted
elif not request.form.get("password"):
return apology("must provide password", 403)
# 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", 403)
# Remember which user has logged in
session["user_id"] = rows[0]["id"]
session["username"] = rows[0]["username"]
# Redirect user to home page
flash(f"Welcome back, {session["username"]}")
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."""
stock_data = None
if request.method == "POST":
symbol = request.form.get("symbol")
if not symbol:
return apology("must provide symbol", 400)
stock_data = lookup(symbol)
if not stock_data:
return apology("ticker symbol not found", 400)
return render_template("quote.html", stock=stock_data)
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "GET":
return render_template("register.html")
elif request.method == "POST":
# Ensure username was submitted
if not request.form.get("username"):
return apology("must provide username")
# Ensure password was submitted
elif not request.form.get("password"):
return apology("must provide password")
# Ensure password was confirmed
elif not request.form.get("confirmation"):
return apology("must confirm password")
pwd_is_valid, msg = validate_password(request.form.get("password"))
if not pwd_is_valid:
return apology(msg, 400)
# Check if passwords match
elif request.form.get("password") != request.form.get("confirmation"):
return apology("passwords do not match")
# Query database for a matching username
db_username = db.execute(
"SELECT username FROM users WHERE username = ?",
request.form.get("username"),
)
if db_username:
return apology("user already exists", 400)
db.execute(
"INSERT INTO users (username, hash) VALUES (?, ?)",
request.form.get("username"),
generate_password_hash(request.form.get("password")),
)
user_id = db.execute(
"SELECT id FROM users WHERE username = ?", request.form.get("username")
)[0]["id"]
session["user_id"] = user_id
session["username"] = request.form.get("username")
flash(
"Success! You are now registered and logged in, using your new credentials"
)
return redirect("/")
@app.route("/sell", methods=["GET", "POST"])
@login_required
def sell():
"""Sell shares of stock"""
if request.method == "POST":
user_id = session["user_id"]
symbol = request.form.get("symbol")
shares_to_sell = (
int(request.form.get("shares")) if request.form.get("shares") else 0
)
if shares_to_sell <= 0 or not shares_to_sell:
return apology("Expected a positive integer", 400)
result = execute_sell(user_id, symbol, shares_to_sell)
if "error" in result:
return apology(result["error"], result["code"])
flash(result)
return redirect("/")
user_id = session["user_id"]
stocks = db.execute(
"SELECT symbol FROM transactions WHERE user_id = ? GROUP BY symbol HAVING SUM(shares) > 0",
user_id,
)
return render_template("sell.html", stocks=stocks)
def execute_sell(user_id: str, symbol: str, shares_to_sell: int):
if not symbol:
return {"error": "Select a stock symbol to sell", "code": "400"}
if shares_to_sell <= 0:
return {"error": "Expected a positive integer", "code": "400"}
rows = db.execute(
"SELECT SUM(shares) as total_shares FROM transactions WHERE user_id = ? AND symbol = ? GROUP BY symbol",
user_id,
symbol,
)
if len(rows) != 1:
return {"error": "Rows are not equal to 1", "code": "400"}
if shares_to_sell > rows[0]["total_shares"]:
return {"error": "Too many shares to sell", "code": "400"}
stock_data = lookup(symbol)
if not (stock_data):
return {"error": "Could not find the stock price", "code": "400"}
current_price = stock_data["price"]
total_to_sell = current_price * shares_to_sell
# Reflect the result of sale in user's balance
db.execute("UPDATE users SET cash = cash + ? WHERE id = ?", total_to_sell, user_id)
# Record the loss of user's shares in transaction
db.execute(
"INSERT INTO transactions (user_id, symbol, shares, price_per_share) VALUES (?,?,?,?)",
user_id,
symbol,
-shares_to_sell,
current_price,
)
return f"Sold {shares_to_sell} shares of {symbol}"
@app.route("/change_password", methods=["GET", "POST"])
@login_required
def change_password():
if request.method == "POST":
user_id = session["user_id"]
rows = db.execute("SELECT hash FROM users WHERE id = ?", user_id)
old_pwd_hash = rows[0]["hash"]
old_pwd = request.form.get("old_password")
new_pwd = request.form.get("new_password")
confirmation = request.form.get("confirm_password")
if not check_password_hash(old_pwd_hash, old_pwd):
return apology("Old password is incorrect", 403)
if new_pwd != confirmation:
return apology("Old and new passwords don't match", 400)
new_pwd_hash = generate_password_hash(new_pwd)
db.execute("UPDATE users SET hash = ? WHERE id = ?", new_pwd_hash, user_id)
flash("Password changed successfully")
return redirect("/")
return render_template("change_password.html")
@app.route("/trade", methods=["GET", "POST"])
@login_required
def trade():
if request.method == "POST":
user_id = session["user_id"]
symbol = request.form.get("symbol").upper()
shares_qty = request.form.get("shares")
action = request.form.get("action").lower()
try:
shares_qty = int(shares_qty)
if shares_qty <= 0:
return apology("Expected be a positive number", 400)
except ValueError:
return apology("Expected a positive number", 400)
if action == "sell":
result = execute_sell(user_id, symbol, shares_qty)
if "error" in result:
return apology(result["error"], result["code"])
flash(result)
return redirect("/")
elif action == "buy":
result = execute_buy(user_id, symbol, shares_qty)
if "error" in result:
return apology(result["error"], result["code"])
flash(result)
return redirect("/")
else:
return apology("Invalid action", 400)
return redirect("/")
style50
was the culprit hours after I posted a solution, I assume my problem desc was not so bad after all.