commit 17247a56bba7bd92dbc923836e39be42d483bc5e Author: bot50 Date: Thu Apr 25 20:23:56 2024 +0000 kukemuna-cs50/problems/2024/x/finance@20240425T202356.252107234Z diff --git a/app.py b/app.py new file mode 100644 index 0000000..353c36c --- /dev/null +++ b/app.py @@ -0,0 +1,261 @@ +import os +import datetime + +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 + +# 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"] + + cash = db.execute("SELECT * FROM users where id = ?", user_id) + + stocks = db.execute( + "SELECT symbol, sum(shares) FROM transactions WHERE user_id = ? GROUP BY symbol HAVING sum(shares) > 0", user_id) + + stocks_total = 0 + + for stock in stocks: + + symbol = stock["symbol"] + stock_price = lookup(symbol.upper()) + stock["shares"] = stock["sum(shares)"] + stock["price"] = stock_price["price"] + stock["total"] = usd(stock_price["price"] * stock["sum(shares)"]) + stocks_total = stocks_total + (stock_price["price"] * stock["sum(shares)"]) + + total = usd(cash[0]["cash"] + stocks_total) + + return render_template("home.html", cash=usd(cash[0]["cash"]), stocks=stocks, total=total) + # return apology("MOFO") + + +@app.route("/buy", methods=["GET", "POST"]) +@login_required +def buy(): + """Buy shares of stock""" + if request.method == "GET": + return render_template("buy.html") + else: + symbol = request.form.get("symbol") + shares = int(request.form.get("shares")) + + if not symbol: + return apology("Not Symbol") + + stock = lookup(symbol.upper()) + + if stock == None: + return apology("Symbol not found") + + transaction_value = shares * stock["price"] + + user_id = session["user_id"] + user_cash_db = db.execute("SELECT cash FROM users WHERE id = ?", user_id) + user_cash = user_cash_db[0]["cash"] + + if user_cash < transaction_value: + return apology("U broke, m8!") + + free_cash = user_cash - transaction_value + + db.execute("UPDATE users SET cash = ? WHERE id = ?", free_cash, user_id) + + date = datetime.datetime.now() + + db.execute("INSERT INTO transactions (user_id, symbol, shares, price, date) VALUES (?, ?, ?, ?, ?)", + user_id, stock["symbol"], shares, stock["price"], date) + + flash("Bought!") + + return redirect("/") + # return apology("TODO") + + +@app.route("/history") +@login_required +def history(): + """Show history of transactions""" + stocks = db.execute("SELECT * FROM transactions") + + return render_template("history.html", stocks=stocks) + # return apology("TODO") + + +@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"] + + # 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 request.method == "POST": + + symbol = request.form.get("symbol") + if not symbol: + return apology("Not Symbol") + + stock = lookup(symbol.upper()) + + if stock == None: + return apology("Symbol not found") + + stock["price"] = usd(stock["price"]) + + return render_template("quote.html", stock=stock) + else: + return render_template("quote.html") + # return apology("TODO") + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + """Register user""" + 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) + + # Ensure password repeat matches + if not request.form.get("password") == request.form.get("password_again"): + return apology("passwords don't match", 403) + + username = request.form.get("username") + password = request.form.get("password") + + db.execute("INSERT INTO users(username, hash) VALUES(?, ?)", + username, generate_password_hash(password)) + return redirect("/") + else: + return render_template("register.html") + # return apology("MOFO") + + +@app.route("/sell", methods=["GET", "POST"]) +@login_required +def sell(): + """Sell shares of stock""" + user_id = session["user_id"] + + if request.method == "POST": + symbol = request.form.get("symbol") + shares = int(request.form.get("shares")) + + if not symbol: + return apology("Not Symbol") + + stock = lookup(symbol.upper()) + + if stock == None: + return apology("Symbol not found") + + stocks = db.execute( + "SELECT symbol, sum(shares) FROM transactions WHERE user_id = ? AND symbol = ?", user_id, symbol) + + if stocks[0]["sum(shares)"] < shares: + return apology("Not enough shares") + + transaction_value = shares * stock["price"] + + user_cash_db = db.execute("SELECT cash FROM users WHERE id = ?", user_id) + user_cash = user_cash_db[0]["cash"] + + new_cash = transaction_value + user_cash + + db.execute("UPDATE users SET cash = ? WHERE id = ?", new_cash, user_id) + + date = datetime.datetime.now() + + db.execute("INSERT INTO transactions (user_id, symbol, shares, price, date) VALUES (?, ?, ?, ?, ?)", + user_id, stock["symbol"], -abs(shares), stock["price"], date) + + flash("Sold!") + + return redirect("/") + + else: + 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) + # return apology("TODO") diff --git a/finance.db b/finance.db new file mode 100644 index 0000000..77daec9 Binary files /dev/null and b/finance.db differ diff --git a/flask_session/2029240f6d1128be89ddc32729463129 b/flask_session/2029240f6d1128be89ddc32729463129 new file mode 100644 index 0000000..8b04914 Binary files /dev/null and b/flask_session/2029240f6d1128be89ddc32729463129 differ diff --git a/flask_session/4c5bec882c6cb133810c5c1724f348ac b/flask_session/4c5bec882c6cb133810c5c1724f348ac new file mode 100644 index 0000000..2c4d704 Binary files /dev/null and b/flask_session/4c5bec882c6cb133810c5c1724f348ac differ diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..43349ca --- /dev/null +++ b/helpers.py @@ -0,0 +1,88 @@ +import csv +import datetime +import pytz +import requests +import urllib +import uuid + +from flask import redirect, render_template, request, session +from functools import wraps + + +def apology(message, code=400): + """Render message as an apology to user.""" + + def escape(s): + """ + Escape special characters. + + https://github.com/jacebrowning/memegen#special-characters + """ + for old, new in [ + ("-", "--"), + (" ", "-"), + ("_", "__"), + ("?", "~q"), + ("%", "~p"), + ("#", "~h"), + ("/", "~s"), + ('"', "''"), + ]: + s = s.replace(old, new) + return s + + return render_template("apology.html", top=code, bottom=escape(message)), code + + +def login_required(f): + """ + Decorate routes to require login. + + https://flask.palletsprojects.com/en/latest/patterns/viewdecorators/ + """ + + @wraps(f) + def decorated_function(*args, **kwargs): + if session.get("user_id") is None: + return redirect("/login") + return f(*args, **kwargs) + + return decorated_function + + +def lookup(symbol): + """Look up quote for symbol.""" + + # Prepare API request + symbol = symbol.upper() + end = datetime.datetime.now(pytz.timezone("US/Eastern")) + start = end - datetime.timedelta(days=7) + + # Yahoo Finance API + url = ( + f"https://query1.finance.yahoo.com/v7/finance/download/{urllib.parse.quote_plus(symbol)}" + f"?period1={int(start.timestamp())}" + f"&period2={int(end.timestamp())}" + f"&interval=1d&events=history&includeAdjustedClose=true" + ) + + # Query API + try: + response = requests.get( + url, + cookies={"session": str(uuid.uuid4())}, + headers={"Accept": "*/*", "User-Agent": request.headers.get("User-Agent")}, + ) + response.raise_for_status() + + # CSV header: Date,Open,High,Low,Close,Adj Close,Volume + quotes = list(csv.DictReader(response.content.decode("utf-8").splitlines())) + price = round(float(quotes[-1]["Adj Close"]), 2) + return {"price": price, "symbol": symbol} + except (KeyError, IndexError, requests.RequestException, ValueError): + return None + + +def usd(value): + """Format value as USD.""" + return f"${value:,.2f}" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..743fadd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +cs50 +Flask +Flask-Session +pytz +requests diff --git a/static/I_heart_validator.png b/static/I_heart_validator.png new file mode 100644 index 0000000..c7db3d2 Binary files /dev/null and b/static/I_heart_validator.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..ea184db Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..e30cfff --- /dev/null +++ b/static/styles.css @@ -0,0 +1,23 @@ +/* Size for brand */ +nav .navbar-brand +{ + font-size: xx-large; +} + +/* Colors for brand */ +nav .navbar-brand .blue +{ + color: #537fbe; +} +nav .navbar-brand .red +{ + color: #ea433b; +} +nav .navbar-brand .yellow +{ + color: #f5b82e; +} +nav .navbar-brand .green +{ + color: #2e944b; +} diff --git a/templates/apology.html b/templates/apology.html new file mode 100644 index 0000000..78679eb --- /dev/null +++ b/templates/apology.html @@ -0,0 +1,11 @@ +{% extends "layout.html" %} + +{% block title %} + Apology +{% endblock %} + +{% block main %} + + + {{ top }} +{% endblock %} diff --git a/templates/buy.html b/templates/buy.html new file mode 100644 index 0000000..2659c6e --- /dev/null +++ b/templates/buy.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} + +{% block title %} + Buy +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+ +
+{% endblock %} diff --git a/templates/history.html b/templates/history.html new file mode 100644 index 0000000..7384232 --- /dev/null +++ b/templates/history.html @@ -0,0 +1,30 @@ +{% extends "layout.html" %} + +{% block title %} + History +{% endblock %} + +{% block main %} + + + + + + + + + + + + {% for stock in stocks%} + + + + + + + {% endfor%} + +
SymbolSharesPriceTransacted
{{ stock.symbol }}{{ stock.shares }}{{ stock.price }}{{ stock.date }}
+ +{% endblock %} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..e7fe1bb --- /dev/null +++ b/templates/home.html @@ -0,0 +1,40 @@ +{% extends "layout.html" %} + +{% block title %} + Home +{% endblock %} + +{% block main %} + + + + + + + + + + + + {% for stock in stocks%} + + + + + + + {% endfor%} + + + + + + + + + + + + +
SymbolSharesPriceTOTAL
{{ stock.symbol }}{{ stock.shares }}{{ stock.price }}{{ stock.total }}
Cash{{ cash }}
TOTAL{{ total }}
+{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..14d3053 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + C$50 Finance: {% block title %}{% endblock %} + + + + + + + + {% if get_flashed_messages() %} +
+ +
+ {% endif %} + +
+ {% block main %}{% endblock %} +
+ + + + + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..0fe2d8e --- /dev/null +++ b/templates/login.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} + +{% block title %} + Log In +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+ +
+{% endblock %} diff --git a/templates/quote.html b/templates/quote.html new file mode 100644 index 0000000..aebde0e --- /dev/null +++ b/templates/quote.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} + +{% block title %} + Quote +{% endblock %} + +{% block main %} + + {% if stock %} +

A share of {{ stock.symbol }} costs {{ stock.price }}

+ + {% else%} +
+
+ +
+ +
+ {% endif %} + + + +{% endblock %} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..afc2896 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,20 @@ +{% extends "layout.html" %} + +{% block title %} + Register +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+
+ +
+ +
+{% endblock %} diff --git a/templates/sell.html b/templates/sell.html new file mode 100644 index 0000000..68f5186 --- /dev/null +++ b/templates/sell.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} + +{% block title %} + Sell +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+ +
+{% endblock %}