Skip to content

Commit 949e369

Browse files
authoredMar 22, 2025
Add files via upload
1 parent 50565ad commit 949e369

17 files changed

+617
-0
lines changed
 

‎app.py

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import os
2+
3+
from cs50 import SQL
4+
from flask import Flask, flash, redirect, render_template, request, session
5+
from flask_session import Session
6+
from werkzeug.security import check_password_hash, generate_password_hash
7+
8+
from helpers import apology, login_required, lookup, usd
9+
10+
# Configure application
11+
app = Flask(__name__)
12+
13+
# Custom filter
14+
app.jinja_env.filters["usd"] = usd
15+
16+
# Configure session to use filesystem (instead of signed cookies)
17+
app.config["SESSION_PERMANENT"] = False
18+
app.config["SESSION_TYPE"] = "filesystem"
19+
Session(app)
20+
21+
# Configure CS50 Library to use SQLite database
22+
db = SQL("sqlite:///finance.db")
23+
24+
25+
@app.after_request
26+
def after_request(response):
27+
"""Ensure responses aren't cached"""
28+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
29+
response.headers["Expires"] = 0
30+
response.headers["Pragma"] = "no-cache"
31+
return response
32+
33+
34+
@app.route("/")
35+
@login_required
36+
def index():
37+
"""Show portfolio of stocks"""
38+
user_id = session["user_id"]
39+
40+
stocks = db.execute("SELECT symbol, price, SUM(shares) as totalShares FROM transactions WHERE user_id = ? GROUP BY symbol", user_id)
41+
cash = db.execute("SELECT cash FROM users WHERE id = ?", user_id)[0]["cash"]
42+
43+
total = cash
44+
45+
for stock in stocks:
46+
total += stock["price"] * stock["totalShares"]
47+
48+
return render_template("index.html", stocks=stocks, cash=cash, usd=usd, total=total)
49+
50+
51+
@app.route("/buy", methods=["GET", "POST"])
52+
@login_required
53+
def buy():
54+
"""Buy shares of stock"""
55+
if request.method == "POST":
56+
symbol = request.form.get("symbol").upper()
57+
item = lookup(symbol)
58+
59+
if not symbol:
60+
return apology("Please enter a symbol!")
61+
elif not item:
62+
return apology("Invalid Symbol!")
63+
64+
try:
65+
shares = int(request.form.get("shares"))
66+
except:
67+
return apology("Shares should be an integer!")
68+
69+
if shares <= 0:
70+
return apology("Shares should be positive integer!")
71+
72+
user_id = session["user_id"]
73+
cash = db.execute("SELECT cash FROM users WHERE id = ?", user_id)[0]["cash"]
74+
75+
item_price = item["price"]
76+
total_price = item_price * shares
77+
78+
if cash < total_price:
79+
return apology("Not Enough Money!")
80+
else:
81+
db.execute("UPDATE users SET cash=? WHERE id=?", cash - total_price, user_id)
82+
db.execute("INSERT INTO transactions (user_id, shares, price, type, symbol) VALUES (?, ?, ?, ?, ?)",
83+
user_id, shares, item_price, 'buy', symbol)
84+
return redirect('/')
85+
86+
else:
87+
return render_template('buy.html')
88+
89+
90+
@app.route("/history")
91+
@login_required
92+
def history():
93+
"""Show history of transactions"""
94+
user_id = session["user_id"]
95+
transactions = db.execute("SELECT type, symbol, price, shares, time FROM transactions WHERE user_id = ?", user_id)
96+
97+
return render_template("history.html", transactions=transactions, usd=usd)
98+
99+
100+
@app.route("/login", methods=["GET", "POST"])
101+
def login():
102+
"""Log user in"""
103+
104+
# Forget any user_id
105+
session.clear()
106+
107+
# User reached route via POST (as by submitting a form via POST)
108+
if request.method == "POST":
109+
# Ensure username was submitted
110+
if not request.form.get("username"):
111+
return apology("must provide username", 403)
112+
113+
# Ensure password was submitted
114+
elif not request.form.get("password"):
115+
return apology("must provide password", 403)
116+
117+
# Query database for username
118+
rows = db.execute(
119+
"SELECT * FROM users WHERE username = ?", request.form.get("username")
120+
)
121+
122+
# Ensure username exists and password is correct
123+
if len(rows) != 1 or not check_password_hash(
124+
rows[0]["hash"], request.form.get("password")
125+
):
126+
return apology("invalid username and/or password", 403)
127+
128+
# Remember which user has logged in
129+
session["user_id"] = rows[0]["id"]
130+
131+
# Redirect user to home page
132+
return redirect("/")
133+
134+
# User reached route via GET (as by clicking a link or via redirect)
135+
else:
136+
return render_template("login.html")
137+
138+
139+
@app.route("/logout")
140+
def logout():
141+
"""Log user out"""
142+
143+
# Forget any user_id
144+
session.clear()
145+
146+
# Redirect user to login form
147+
return redirect("/")
148+
149+
150+
@app.route("/quote", methods=["GET", "POST"])
151+
@login_required
152+
def quote():
153+
"""Get stock quote."""
154+
if (request.method == "POST"):
155+
symbol = request.form.get('symbol')
156+
157+
if not symbol:
158+
return apology("Write a symbol!")
159+
160+
item = lookup(symbol)
161+
162+
if not item:
163+
return apology("Wrong symbol!")
164+
165+
return render_template('quoted.html', item=item, usd=usd)
166+
167+
else:
168+
return render_template('quote.html')
169+
170+
171+
@app.route("/register", methods=["GET", "POST"])
172+
def register():
173+
"""Register user"""
174+
if (request.method == "POST"):
175+
username = request.form.get("username")
176+
password = request.form.get("password")
177+
confirmation = request.form.get("confirmation")
178+
179+
if not username or not password or not confirmation or password != confirmation:
180+
return apology("You have done something wrong! Please try again.")
181+
182+
try:
183+
hash = generate_password_hash(password)
184+
db.execute("INSERT INTO users (username, hash) VALUES (?, ?)", username, hash)
185+
186+
return redirect('/')
187+
188+
except:
189+
return apology("Username has already been exist")
190+
191+
else:
192+
return render_template("register.html")
193+
194+
195+
@app.route("/sell", methods=["GET", "POST"])
196+
@login_required
197+
def sell():
198+
"""Sell shares of stock"""
199+
if request.method == "POST":
200+
user_id = session["user_id"]
201+
symbol = request.form.get("symbol")
202+
shares = int(request.form.get("shares"))
203+
204+
if shares <= 0:
205+
return apology("Shares must be positive!")
206+
207+
item_price = lookup(symbol)["price"]
208+
price = shares * item_price
209+
210+
owned_shares = db.execute("SELECT SUM(shares) FROM transactions WHERE user_id = ? AND symbol = ? ", user_id, symbol)[0]["SUM(shares)"]
211+
212+
if owned_shares < shares:
213+
return apology("You don't have enough shares!")
214+
215+
current_cash = db.execute("SELECT cash FROM users WHERE id = ?", user_id)[0]["cash"]
216+
db.execute("UPDATE users SET cash = ? WHERE id = ?", current_cash + price, user_id)
217+
db.execute("INSERT INTO transactions (user_id, shares, price, type, symbol) VALUES (?, ?, ?, ?, ?)",
218+
user_id, -shares, item_price, "sell", symbol)
219+
return redirect('/')
220+
221+
else:
222+
user_id = session["user_id"]
223+
symbols = db.execute("SELECT symbol FROM transactions WHERE user_id = ? GROUP BY symbol", user_id)
224+
225+
return render_template("sell.html", symbols=symbols)

‎finance.db

20 KB
Binary file not shown.

‎helpers.py

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import csv
2+
import datetime
3+
import pytz
4+
import requests
5+
import urllib
6+
import uuid
7+
8+
from flask import redirect, render_template, request, session
9+
from functools import wraps
10+
11+
12+
def apology(message, code=400):
13+
"""Render message as an apology to user."""
14+
15+
def escape(s):
16+
"""
17+
Escape special characters.
18+
19+
https://github.com/jacebrowning/memegen#special-characters
20+
"""
21+
for old, new in [
22+
("-", "--"),
23+
(" ", "-"),
24+
("_", "__"),
25+
("?", "~q"),
26+
("%", "~p"),
27+
("#", "~h"),
28+
("/", "~s"),
29+
('"', "''"),
30+
]:
31+
s = s.replace(old, new)
32+
return s
33+
34+
return render_template("apology.html", top=code, bottom=escape(message)), code
35+
36+
37+
def login_required(f):
38+
"""
39+
Decorate routes to require login.
40+
41+
https://flask.palletsprojects.com/en/latest/patterns/viewdecorators/
42+
"""
43+
44+
@wraps(f)
45+
def decorated_function(*args, **kwargs):
46+
if session.get("user_id") is None:
47+
return redirect("/login")
48+
return f(*args, **kwargs)
49+
50+
return decorated_function
51+
52+
53+
def lookup(symbol):
54+
"""Look up quote for symbol."""
55+
56+
# Prepare API request
57+
symbol = symbol.upper()
58+
end = datetime.datetime.now(pytz.timezone("US/Eastern"))
59+
start = end - datetime.timedelta(days=7)
60+
61+
# Yahoo Finance API
62+
url = (
63+
f"https://query1.finance.yahoo.com/v7/finance/download/{urllib.parse.quote_plus(symbol)}"
64+
f"?period1={int(start.timestamp())}"
65+
f"&period2={int(end.timestamp())}"
66+
f"&interval=1d&events=history&includeAdjustedClose=true"
67+
)
68+
69+
# Query API
70+
try:
71+
response = requests.get(
72+
url,
73+
cookies={"session": str(uuid.uuid4())},
74+
headers={"Accept": "*/*", "User-Agent": request.headers.get("User-Agent")},
75+
)
76+
response.raise_for_status()
77+
78+
# CSV header: Date,Open,High,Low,Close,Adj Close,Volume
79+
quotes = list(csv.DictReader(response.content.decode("utf-8").splitlines()))
80+
price = round(float(quotes[-1]["Adj Close"]), 2)
81+
return {"price": price, "symbol": symbol}
82+
except (KeyError, IndexError, requests.RequestException, ValueError):
83+
return None
84+
85+
86+
def usd(value):
87+
"""Format value as USD."""
88+
return f"${value:,.2f}"

‎requirements.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cs50
2+
Flask
3+
Flask-Session
4+
pytz
5+
requests

‎static/I_heart_validator.png

345 Bytes
Loading

‎static/favicon.ico

15 KB
Binary file not shown.

‎static/styles.css

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* Size for brand */
2+
nav .navbar-brand
3+
{
4+
font-size: xx-large;
5+
}
6+
7+
/* Colors for brand */
8+
nav .navbar-brand .blue
9+
{
10+
color: #537fbe;
11+
}
12+
nav .navbar-brand .red
13+
{
14+
color: #ea433b;
15+
}
16+
nav .navbar-brand .yellow
17+
{
18+
color: #f5b82e;
19+
}
20+
nav .navbar-brand .green
21+
{
22+
color: #2e944b;
23+
}

‎templates/apology.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
Apology
5+
{% endblock %}
6+
7+
{% block main %}
8+
<!-- https://memegen.link/ -->
9+
<!-- https://knowyourmeme.com/memes/grumpy-cat -->
10+
<img alt="{{ top }}" class="border img-fluid" src="https://api.memegen.link/images/custom/{{ top | urlencode }}/{{ bottom | urlencode }}.jpg?background=https://i.imgur.com/CsCgN7Ll.png&width=400" title="{{ top }}">
11+
{% endblock %}

‎templates/buy.html

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
Buy
5+
{% endblock %}
6+
7+
{% block main %}
8+
<form action="/buy" method="post">
9+
<div class="mb-3">
10+
<input autocomplete="off" autofocus class="form-control mx-auto w-auto" name="symbol" placeholder="Symbol" type="text">
11+
</div>
12+
<div class="mb-3">
13+
<input class="form-control mx-auto w-auto" name="shares" placeholder="Shares" type="text">
14+
</div>
15+
<button class="btn btn-primary" type="submit">Buy</button>
16+
</form>
17+
{% endblock %}

‎templates/history.html

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
My Transactions
5+
{% endblock %}
6+
7+
{% block main %}
8+
<table class="table table-striped">
9+
<thead>
10+
<tr>
11+
<th>Symbol</th>
12+
<th>Type</th>
13+
<th>Shares</th>
14+
<th>Price</th>
15+
<th>Time</th>
16+
17+
</tr>
18+
</thead>
19+
<tbody>
20+
{% for transaction in transactions %}
21+
<tr>
22+
<td>{{ transaction["symbol"].upper() }}</td>
23+
<td>{{ transaction["type"] }}</td>
24+
<td>{{ transaction["shares"] }}</td>
25+
<td>{{ usd(transaction["price"]) }}</td>
26+
<td>{{ transaction["time"] }}</td>
27+
</tr>
28+
{% endfor %}
29+
</tbody>
30+
</table>
31+
{% endblock %}

‎templates/index.html

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
Home
5+
{% endblock %}
6+
7+
{% block main %}
8+
<table class="table table-striped">
9+
<thead>
10+
<tr>
11+
<th>Symbol</th>
12+
<th>Shares</th>
13+
<th>Price</th>
14+
<th>Total</th>
15+
</tr>
16+
</thead>
17+
<tbody>
18+
{% for stock in stocks %}
19+
<tr>
20+
<td>{{ stock["symbol"] }}</td>
21+
<td>{{ stock["totalShares"] }}</td>
22+
<td>{{ usd(stock["price"]) }}</td>
23+
<td>{{ usd(stock["totalShares"] * stock["price"]) }}</td>
24+
</tr>
25+
{% endfor %}
26+
</tbody>
27+
<tfoot>
28+
<tr>
29+
<td></td>
30+
<td colspan="2"> <strong> CASH </strong></td>
31+
<td> <strong> {{ usd(cash) }} </strong> </td>
32+
</tr>
33+
<tr>
34+
<td></td>
35+
<td colspan="2"> <strong> TOTAL </strong></td>
36+
<td> <strong> {{ usd(total) }} </strong></td>
37+
</tr>
38+
</tfoot>
39+
</table>
40+
{% endblock %}

‎templates/layout.html

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<!DOCTYPE html>
2+
3+
<html lang="en">
4+
5+
<head>
6+
7+
<meta charset="utf-8">
8+
<meta name="viewport" content="initial-scale=1, width=device-width">
9+
10+
<!-- http://getbootstrap.com/docs/5.3/ -->
11+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
12+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
13+
14+
<!-- https://favicon.io/emoji-favicons/money-bag/ -->
15+
<link href="/static/favicon.ico" rel="icon">
16+
17+
<link href="/static/styles.css" rel="stylesheet">
18+
19+
<title>C$50 Finance: {% block title %}{% endblock %}</title>
20+
21+
</head>
22+
23+
<body>
24+
25+
<nav class="bg-light border navbar navbar-expand-md navbar-light">
26+
<div class="container-fluid">
27+
<a class="navbar-brand" href="/"><span class="blue">C</span><span class="red">$</span><span class="yellow">5</span><span class="green">0</span> <span class="red">Finance</span></a>
28+
<button aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler" data-bs-target="#navbar" data-bs-toggle="collapse" type="button">
29+
<span class="navbar-toggler-icon"></span>
30+
</button>
31+
<div class="collapse navbar-collapse" id="navbar">
32+
{% if session["user_id"] %}
33+
<ul class="navbar-nav me-auto mt-2">
34+
<li class="nav-item"><a class="nav-link" href="/quote">Quote</a></li>
35+
<li class="nav-item"><a class="nav-link" href="/buy">Buy</a></li>
36+
<li class="nav-item"><a class="nav-link" href="/sell">Sell</a></li>
37+
<li class="nav-item"><a class="nav-link" href="/history">History</a></li>
38+
</ul>
39+
<ul class="navbar-nav ms-auto mt-2">
40+
<li class="nav-item"><a class="nav-link" href="/logout">Log Out</a></li>
41+
</ul>
42+
{% else %}
43+
<ul class="navbar-nav ms-auto mt-2">
44+
<li class="nav-item"><a class="nav-link" href="/register">Register</a></li>
45+
<li class="nav-item"><a class="nav-link" href="/login">Log In</a></li>
46+
</ul>
47+
{% endif %}
48+
</div>
49+
</div>
50+
</nav>
51+
52+
{% if get_flashed_messages() %}
53+
<header>
54+
<div class="alert alert-primary mb-0 text-center" role="alert">
55+
{{ get_flashed_messages() | join(" ") }}
56+
</div>
57+
</header>
58+
{% endif %}
59+
60+
<main class="container py-5 text-center">
61+
{% block main %}{% endblock %}
62+
</main>
63+
64+
<footer class="mb-5">
65+
66+
<p class="mb-3 small text-center text-muted">
67+
Data provided by <a href="https://finance.yahoo.com/">Yahoo</a>
68+
</p>
69+
70+
<form action="https://validator.w3.org/check" class="text-center" enctype="multipart/form-data" method="post" target="_blank">
71+
<input name="doctype" type="hidden" value="HTML5">
72+
<input name="fragment" type="hidden">
73+
<input alt="Validate" src="/static/I_heart_validator.png" type="image"> <!-- https://validator.w3.org/ -->
74+
</form>
75+
<script>
76+
document.addEventListener('DOMContentLoaded', function() {
77+
// Adapted from https://stackoverflow.com/a/10162353
78+
const html = '<!DOCTYPE ' +
79+
document.doctype.name +
80+
(document.doctype.publicId ? ' PUBLIC "' + document.doctype.publicId + '"' : '') +
81+
(!document.doctype.publicId && document.doctype.systemId ? ' SYSTEM' : '') +
82+
(document.doctype.systemId ? ' "' + document.doctype.systemId + '"' : '') +
83+
'>\n' + document.documentElement.outerHTML;
84+
document.querySelector('form[action="https://validator.w3.org/check"] > input[name="fragment"]').value = html;
85+
});
86+
</script>
87+
</footer>
88+
89+
</body>
90+
91+
</html>

‎templates/login.html

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
Log In
5+
{% endblock %}
6+
7+
{% block main %}
8+
<form action="/login" method="post">
9+
<div class="mb-3">
10+
<input autocomplete="off" autofocus class="form-control mx-auto w-auto" name="username" placeholder="Username" type="text">
11+
</div>
12+
<div class="mb-3">
13+
<input class="form-control mx-auto w-auto" name="password" placeholder="Password" type="password">
14+
</div>
15+
<button class="btn btn-primary" type="submit">Log In</button>
16+
</form>
17+
{% endblock %}

‎templates/quote.html

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
Quote
5+
{% endblock %}
6+
7+
{% block main %}
8+
<form action="/quote" method="post">
9+
<div class="mb-3">
10+
<input autocomplete="off" autofocus class="form-control mx-auto w-auto" name="symbol" placeholder="Symbol" type="text">
11+
</div>
12+
<button class="btn btn-primary" type="submit">Quote</button>
13+
</form>
14+
{% endblock %}

‎templates/quoted.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
Quoted
5+
{% endblock %}
6+
7+
{% block main %}
8+
<p>
9+
A share of {{item.name}} costs {{usd(item.price)}}.
10+
</p>
11+
{% endblock %}

‎templates/register.html

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
Register
5+
{% endblock %}
6+
7+
{% block main %}
8+
<form action="/register" method="post">
9+
<div class="mb-3">
10+
<input autocomplete="off" autofocus class="form-control mx-auto w-auto" name="username" placeholder="Username" type="text">
11+
</div>
12+
13+
<div class="mb-3">
14+
<input class="form-control mx-auto w-auto" name="password" placeholder="Password" type="password">
15+
</div>
16+
17+
<div class="mb-3">
18+
<input class="form-control mx-auto w-auto" name="confirmation" placeholder="Again Password" type="password">
19+
</div>
20+
<button class="btn btn-primary" type="submit">Register</button>
21+
</form>
22+
{% endblock %}

‎templates/sell.html

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}
4+
Sell
5+
{% endblock %}
6+
7+
{% block main %}
8+
<form action="/sell" method="post">
9+
<div class="mb-3">
10+
<select class="form-select mx-auto w-auto" name="symbol">
11+
<option disabled selected>Symbol</option>
12+
{% for symbol in symbols %}
13+
<option value="{{symbol['symbol']}}">{{ symbol["symbol"] }}</option>
14+
{% endfor %}
15+
</select>
16+
</div>
17+
<div class="mb-3">
18+
<input class="form-control mx-auto w-auto" name="shares" placeholder="Shares" type="number">
19+
</div>
20+
<button class="btn btn-primary" type="submit">Sell</button>
21+
</form>
22+
{% endblock %}

0 commit comments

Comments
 (0)
Please sign in to comment.