mirror of https://github.com/me50/kukemuna.git
This commit is contained in:
commit
7490381924
|
|
@ -0,0 +1,273 @@
|
|||
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 = request.form.get("shares")
|
||||
|
||||
if not symbol:
|
||||
return apology("Not Symbol")
|
||||
|
||||
stock = lookup(symbol.upper())
|
||||
|
||||
if stock == None:
|
||||
return apology("Symbol not found")
|
||||
|
||||
if not shares or shares.isalpha() or not float(shares).is_integer():
|
||||
return apology("invalid shares")
|
||||
|
||||
else:
|
||||
if float(shares) > 0:
|
||||
transaction_value = float(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!")
|
||||
else:
|
||||
return apology("Missing shares", 400)
|
||||
|
||||
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", 400)
|
||||
|
||||
# Ensure password was submitted
|
||||
elif not request.form.get("password"):
|
||||
return apology("must provide password", 400)
|
||||
|
||||
# Ensure password repeat matches
|
||||
if not request.form.get("password") == request.form.get("confirmation"):
|
||||
return apology("passwords don't match", 400)
|
||||
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
|
||||
username_exists = db.execute("SELECT * FROM users WHERE username = ?", username)
|
||||
|
||||
if username_exists:
|
||||
return apology("username already exists!", 400)
|
||||
|
||||
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")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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}"
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
cs50
|
||||
Flask
|
||||
Flask-Session
|
||||
pytz
|
||||
requests
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 345 B |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Apology
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<!-- https://memegen.link/ -->
|
||||
<!-- https://knowyourmeme.com/memes/grumpy-cat -->
|
||||
<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 }}">
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Buy
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<form action="/buy" method="POST">
|
||||
<div class="mb-3">
|
||||
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus="" name="symbol" placeholder="Symbol" type="text">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus min="1" name="shares" placeholder="Shares" type="text">
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Buy</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
History
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<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 stock in stocks%}
|
||||
<tr>
|
||||
<td class="text-start">{{ stock.symbol }}</td>
|
||||
<td class="text-end">{{ stock.shares }}</td>
|
||||
<td class="text-end">{{ stock.price }}</td>
|
||||
<td class="text-end">{{ stock.date }}</td>
|
||||
</tr>
|
||||
{% endfor%}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Home
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start">Symbol</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-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="3">Cash</td>
|
||||
<td class="border-0 text-end" colspan="3">{{ cash }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border-0 fw-bold text-end" colspan="3">TOTAL</td>
|
||||
<td class="border-0 fw- bold text-end" colspan="3">{{ total }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width">
|
||||
|
||||
<!-- http://getbootstrap.com/docs/5.3/ -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
|
||||
<!-- https://favicon.io/emoji-favicons/money-bag/ -->
|
||||
<link href="/static/favicon.ico" rel="icon">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet">
|
||||
|
||||
<title>C$50 Finance: {% block title %}{% endblock %}</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="bg-light border navbar navbar-expand-md navbar-light">
|
||||
<div class="container-fluid">
|
||||
<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>
|
||||
<button aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler" data-bs-target="#navbar" data-bs-toggle="collapse" type="button">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbar">
|
||||
{% if session["user_id"] %}
|
||||
<ul class="navbar-nav me-auto mt-2">
|
||||
<li class="nav-item"><a class="nav-link" href="/quote">Quote</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/buy">Buy</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/sell">Sell</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/history">History</a></li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto mt-2">
|
||||
<li class="nav-item"><a class="nav-link" href="/logout">Log Out</a></li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<ul class="navbar-nav ms-auto mt-2">
|
||||
<li class="nav-item"><a class="nav-link" href="/register">Register</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/login">Log In</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% if get_flashed_messages() %}
|
||||
<header>
|
||||
<div class="alert alert-primary mb-0 text-center" role="alert">
|
||||
{{ get_flashed_messages() | join(" ") }}
|
||||
</div>
|
||||
</header>
|
||||
{% endif %}
|
||||
|
||||
<main class="container py-5 text-center">
|
||||
{% block main %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="mb-5">
|
||||
|
||||
<p class="mb-3 small text-center text-muted">
|
||||
Data provided by <a href="https://finance.yahoo.com/">Yahoo</a>
|
||||
</p>
|
||||
|
||||
<form action="https://validator.w3.org/check" class="text-center" enctype="multipart/form-data" method="post" target="_blank">
|
||||
<input name="doctype" type="hidden" value="HTML5">
|
||||
<input name="fragment" type="hidden">
|
||||
<input alt="Validate" src="/static/I_heart_validator.png" type="image"> <!-- https://validator.w3.org/ -->
|
||||
</form>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Adapted from https://stackoverflow.com/a/10162353
|
||||
const html = '<!DOCTYPE ' +
|
||||
document.doctype.name +
|
||||
(document.doctype.publicId ? ' PUBLIC "' + document.doctype.publicId + '"' : '') +
|
||||
(!document.doctype.publicId && document.doctype.systemId ? ' SYSTEM' : '') +
|
||||
(document.doctype.systemId ? ' "' + document.doctype.systemId + '"' : '') +
|
||||
'>\n' + document.documentElement.outerHTML;
|
||||
document.querySelector('form[action="https://validator.w3.org/check"] > input[name="fragment"]').value = html;
|
||||
});
|
||||
</script>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Log In
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<form action="/login" method="post">
|
||||
<div class="mb-3">
|
||||
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus 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>
|
||||
<button class="btn btn-primary" type="submit">Log In</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Quote
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% if stock %}
|
||||
<p>A share of {{ stock.symbol }} costs {{ stock.price }}</p>
|
||||
|
||||
{% else%}
|
||||
<form action="/quote" method="POST">
|
||||
<div class="mb-3">
|
||||
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus="" name="symbol" placeholder="Symbol" type="text">
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Quote</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Register
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<form action="/register" method="post">
|
||||
<div class="mb-3">
|
||||
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus 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 %}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{% 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">
|
||||
<option disables="" selected="">Symbol</option>
|
||||
{% for stock in stocks %}
|
||||
<option value="{{ stock.symbol }}">{{ stock.symbol }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input class="form-control mx-auto w-auto" autocomplete="off" autofocus min="1" name="shares" placeholder="Shares" type="number">
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Sell</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue