Created
January 2, 2020 12:52
-
-
Save sojohnnysaid/46a9143472ef0c8eb906da159837b47b to your computer and use it in GitHub Desktop.
CS50 Finance app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
Apology | |
{% endblock %} | |
{% block main %} | |
<img alt="{{ top }}" class="border" src="http://memegen.link/custom/{{ top | urlencode }}/{{ bottom | urlencode }}.jpg?alt=https://i.imgur.com/CsCgN7Ll.png"/> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
from flask import Flask, flash, redirect, render_template, request, session | |
from flask_session import Session | |
from tempfile import mkdtemp | |
from werkzeug.exceptions import default_exceptions | |
from werkzeug.security import check_password_hash, generate_password_hash | |
from models import users_db, transactions_db, portfolio_db, SQL, sqlite3 | |
from helpers import apology, login_required, lookup, usd, get_time | |
# Configure application | |
app = Flask(__name__) | |
# Ensure templates are auto-reloaded | |
app.config["TEMPLATES_AUTO_RELOAD"] = True | |
# Ensure responses aren't cached | |
@app.after_request | |
def after_request(response): | |
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" | |
response.headers["Expires"] = 0 | |
response.headers["Pragma"] = "no-cache" | |
return response | |
# Custom filter | |
app.jinja_env.filters["usd"] = usd | |
# Configure session to use filesystem (instead of signed cookies) | |
app.config["SESSION_FILE_DIR"] = mkdtemp() | |
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.route("/") | |
@login_required | |
def index(): | |
"""Show portfolio of stocks""" | |
with sqlite3.connect('finance.db') as conn: | |
# get user balance | |
u_db = users_db(conn) | |
u_db.set_id(session['user_id']) | |
user_balance = int(u_db.get_balance()[0]) | |
share_total = 0 | |
# get holdings | |
p_db = portfolio_db(session['user_id'], conn) | |
holdings = p_db.get_holdings() | |
for holding in holdings: | |
holding['price'] = lookup(holding['symbol'])['price'] | |
holding['total'] = holding['price'] * int(holding['shares']) | |
share_total += holding['total'] | |
user = {} | |
user['balance'] = user_balance | |
user['grand_total'] = user_balance + share_total | |
return render_template("index.html", holdings=holdings, user=user) | |
@app.route("/buy", methods=["GET", "POST"]) | |
@login_required | |
def buy(): | |
"""Buy shares of stock""" | |
if request.method == "POST": | |
symbol = request.form.get("symbol") | |
shares = request.form.get("shares") | |
with sqlite3.connect('finance.db') as conn: | |
# check if # of shares positive integer | |
if int(shares) <= 0: | |
return apology("shares must be a positive number") | |
# get user balance | |
u_db = users_db(conn) | |
u_db.set_id(session['user_id']) | |
user_balance = u_db.get_balance()[0] | |
# get share price | |
share_price = lookup(symbol)['price'] | |
total_cost = int(share_price) * int(shares) | |
print(user_balance) | |
print(total_cost) | |
#check if user has enough cash to purchase shares | |
if int(user_balance) < int(total_cost): | |
return apology("you do not have enough cash to make this purchase") | |
# update user cash | |
u_db.update_balance(user_balance - total_cost) | |
# update user portfolio | |
p_db = portfolio_db(session['user_id'], conn) | |
current_shares = p_db.get_number_of_shares(symbol) | |
if current_shares != None: | |
p_db.update_shares( str(int(current_shares[0]) + int(shares)), symbol) | |
else: | |
p_db.insert_shares(shares, symbol) | |
#update transaction history | |
t_db = transactions_db(session['user_id'], conn) | |
t_db.insert(('bought', symbol, share_price, shares, get_time())) | |
return redirect('/history') | |
return render_template("buy.html") | |
@app.route("/history") | |
@login_required | |
def history(): | |
"""Show history of transactions""" | |
with sqlite3.connect('finance.db') as conn: | |
t_db = transactions_db(str(session['user_id']), conn) | |
transactions = t_db.get_history() | |
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 = :username", | |
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": | |
# show user a table with the stock price info | |
# stock is a dict (keys are 'name', 'price', and 'symbol') | |
stock = lookup(request.form.get("symbol")) | |
return render_template("quoted.html", stock=stock) | |
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') | |
print(password) | |
print(confirmation) | |
if username is '': | |
return apology('Username can not be left blank') | |
if password != confirmation: | |
return apology('Password and confirmation must match') | |
# store new user in users table | |
with sqlite3.connect('finance.db') as conn: | |
# access users model | |
u_db = users_db(conn) | |
# insert new user | |
# once user is registered this method returns the newly created user_id | |
user_id = u_db.register(username, generate_password_hash(password)) | |
# set session id to new user idp | |
session['user_id'] = user_id | |
# redirect to homepage | |
return redirect('/') | |
return render_template("register.html") | |
@app.route("/sell", methods=["GET", "POST"]) | |
@login_required | |
def sell(): | |
"""Sell shares of stock""" | |
if request.method == "POST": | |
with sqlite3.connect('finance.db') as conn: | |
user_id = session["user_id"] | |
symbol = request.form.get("symbol") | |
shares_to_sell = int(request.form.get("shares")) | |
# portfolio_db model | |
p_db = portfolio_db(user_id, conn) | |
holdings = p_db.get_holdings() | |
# if user inputs shares less than or equal to 0 | |
if shares_to_sell <= 0: | |
return apology("shares must be a positive integer") | |
# if user doesn't own the stock | |
if p_db.get_number_of_shares(symbol) == None: | |
return apology("you don't own any shares of this type") | |
current_shares = int(p_db.get_number_of_shares(symbol)[0]) | |
# if user does not have enough stock to sell | |
if current_shares < shares_to_sell: | |
return apology("you don't have enough shares to sell") | |
## user has enough to sell ## | |
# get current price of symbol | |
share_price = lookup(symbol)['price'] | |
# get users current balance | |
u_db = users_db(conn) | |
u_db.set_id(user_id) | |
current_balance = u_db.get_balance()[0] | |
# get shares_to_sell * price | |
users_new_balance = current_balance + (share_price * shares_to_sell) | |
# update users cash | |
u_db.update_balance(users_new_balance) | |
# subtract shares_sell from current_shares and update_shares_owned | |
p_db.update_shares((current_shares - shares_to_sell), symbol) | |
# update transaction history of this sale | |
t_db = transactions_db(user_id, conn) | |
t_db.insert(('sold', symbol, share_price, str(shares_to_sell), get_time())) | |
return redirect('/history') | |
return render_template("sell.html") | |
@app.route("/preferences", methods=["GET", "POST"]) | |
@login_required | |
def preferences(): | |
"""reset password""" | |
if request.method == "POST": | |
old_password = request.form.get("old_password") | |
new_password = request.form.get("new_password") | |
confirmation = request.form.get("confirmation") | |
with sqlite3.connect('finance.db') as conn: | |
u_db = users_db(conn) | |
u_db.set_id(session['user_id']) | |
u_db.update_password(generate_password_hash(new_password)) | |
return render_template("preferences.html") | |
def errorhandler(e): | |
"""Handle error""" | |
return apology(e.name, e.code) | |
# listen for errors | |
for code in default_exceptions: | |
app.errorhandler(code)(errorhandler) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
Buy | |
{% endblock %} | |
{% block main %} | |
<form action="/buy" method="post"> | |
<div class="form-group"> | |
<input autocomplete="off" autofocus class="form-control" name="symbol" placeholder="symbol" type="text"/> | |
</div> | |
<div class="form-group"> | |
<input autocomplete="off" autofocus class="form-control" name="shares" placeholder="shares" type="text"/> | |
</div> | |
<button class="btn btn-primary" type="submit">Buy</button> | |
</form> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import requests | |
import urllib.parse | |
from datetime import datetime | |
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. | |
http://flask.pocoo.org/docs/0.12/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.""" | |
# Contact API | |
try: | |
response = requests.get(f"https://cloud.iexapis.com/stable/stock/{urllib.parse.quote_plus(symbol)}/quote?token=pk_6d475d970a9f47b7b2b8440acf1d2c7e") | |
response.raise_for_status() | |
except requests.RequestException: | |
return None | |
# Parse response | |
try: | |
quote = response.json() | |
return { | |
"name": quote["companyName"], | |
"price": float(quote["latestPrice"]), | |
"symbol": quote["symbol"] | |
} | |
except (KeyError, TypeError, ValueError): | |
return None | |
def usd(value): | |
"""Format value as USD.""" | |
return f"${value:,.2f}" | |
def get_time(): | |
now = datetime.now() | |
#12-hour format | |
return now.strftime('%m/%d/%Y | %I:%M:%S %p UTC') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
History | |
{% endblock %} | |
{% block main %} | |
<!--Table--> | |
<table id="tablePreview" class="table table-striped table-sm"> | |
<!--Table head--> | |
<thead> | |
<tr> | |
<th>Type</th> | |
<th>Symbol</th> | |
<th>Price</th> | |
<th>Shares</th> | |
<th>Date/Time</th> | |
</tr> | |
</thead> | |
<!--Table head--> | |
<!--Table body--> | |
<tbody> | |
{% for transaction in transactions %} | |
<tr> | |
<td>{{ transaction['type'] }}</td> | |
<td>{{ transaction['symbol'] }}</td> | |
<td>{{ transaction['price'] | int | usd }}</td> | |
<td>{{ transaction['shares'] }}</td> | |
<td>{{ transaction['date_time'] }}</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
<!--Table body--> | |
</table> | |
<!--Table--> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
Home | |
{% endblock %} | |
{% block main %} | |
<div class="jumbotron"> | |
<p class="lead">Cash Balance: {{ user['balance'] | usd }}</p> | |
<p class="lead">Grand Total: {{ user['grand_total'] | usd }}</p> | |
</div> | |
<!--Table--> | |
<table id="tablePreview" class="table table-striped table-sm"> | |
<!--Table head--> | |
<thead> | |
<tr> | |
<th>Symbol</th> | |
<th>Shares</th> | |
<th>Price</th> | |
<th>Total</th> | |
</tr> | |
</thead> | |
<!--Table head--> | |
<!--Table body--> | |
<tbody> | |
{% for holding in holdings %} | |
<tr> | |
<td>{{ holding['symbol'] }}</td> | |
<td>{{ holding['shares'] }}</td> | |
<td>{{ holding['price'] | usd }}</td> | |
<td>{{ holding['total'] | usd }}</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
<!--Table body--> | |
</table> | |
<!--Table--> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<!-- Required meta tags --> | |
<meta charset="utf-8"/> | |
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/> | |
<!-- documentation at http://getbootstrap.com/docs/4.0/, alternative themes at https://bootswatch.com/4-alpha/ --> | |
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" rel="stylesheet"/> | |
<link href="/static/styles.css" rel="stylesheet"/> | |
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script> | |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script> | |
<title>C$50 Finance: {% block title %}{% endblock %}</title> | |
</head> | |
<body> | |
<nav class="navbar navbar-expand-md navbar-light bg-light border"> | |
<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-target="#navbar" data-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 mr-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> | |
<li class="nav-item"><a class="nav-link" href="/preferences">Preferences</a></li> | |
</ul> | |
<ul class="navbar-nav ml-auto mt-2"> | |
<li class="nav-item"><a class="nav-link" href="/logout">Log Out</a></li> | |
</ul> | |
{% else %} | |
<ul class="navbar-nav ml-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> | |
</nav> | |
{% if get_flashed_messages() %} | |
<header> | |
<div class="alert alert-primary border text-center" role="alert"> | |
{{ get_flashed_messages() | join(" ") }} | |
</div> | |
</header> | |
{% endif %} | |
<main class="container p-5"> | |
{% block main %}{% endblock %} | |
</main> | |
<footer class="small text-center text-muted"> | |
Data provided for free by <a href="https://iextrading.com/developer">IEX</a>. View <a href="https://iextrading.com/api-exhibit-a/">IEX’s Terms of Use</a>. | |
</footer> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
Log In | |
{% endblock %} | |
{% block main %} | |
<form action="/login" method="post"> | |
<div class="form-group"> | |
<input autocomplete="off" autofocus class="form-control" name="username" placeholder="Username" type="text"/> | |
</div> | |
<div class="form-group"> | |
<input class="form-control" name="password" placeholder="Password" type="password"/> | |
</div> | |
<button class="btn btn-primary" type="submit">Log In</button> | |
</form> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from cs50 import SQL | |
import sqlite3 | |
class users_db: | |
def __init__(self, conn): | |
self.id = '' | |
self.conn = conn | |
self.c = conn.cursor() | |
def register(self, username, hash): | |
row = self.c.execute('INSERT INTO users (username, hash, cash) VALUES (?, ?, 10000)', (username, hash)) | |
self.conn.commit() | |
return self.c.lastrowid | |
def set_id(self, id): | |
self.id = id | |
def get_balance(self): | |
self.c.execute('SELECT cash FROM users WHERE id=?', (self.id,)) | |
return self.c.fetchone() | |
def update_balance(self, new_balance): | |
self.c.execute('UPDATE users SET cash=? WHERE id=?', (new_balance,self.id)) | |
self.conn.commit() | |
def update_password(self, hash): | |
self.c.execute('UPDATE users SET hash=? WHERE id=?', (hash, self.id)) | |
self.conn.commit() | |
class transactions_db: | |
def __init__(self, user_id, conn): | |
self.id = user_id | |
self.conn = conn | |
self.c = conn.cursor() | |
def get_history(self): | |
self.c.execute('SELECT * FROM transactions where user_id=?', (self.id)) | |
results = [] | |
for data in self.c.fetchall(): | |
col = {} | |
col['type'] = data[1] | |
col['symbol'] = data[2] | |
col['price'] = data[3] | |
col['shares'] = data[4] | |
col['date_time'] = data[5] | |
results.append(col) | |
return results | |
def insert(self, row): | |
self.c.execute('INSERT INTO transactions VALUES (?,?,?,?,?,?)', (self.id,) + row) | |
self.conn.commit() | |
class portfolio_db: | |
def __init__(self, user_id, conn): | |
self.id = user_id | |
self.conn = conn | |
self.c = conn.cursor() | |
def get_holdings(self): | |
self.c.execute('SELECT symbol, shares FROM portfolio WHERE user_id=?', (self.id,)) | |
results = [] | |
for data in self.c.fetchall(): | |
col = {} | |
col['symbol'] = data[0] | |
col['shares'] = data[1] | |
results.append(col) | |
return results | |
def get_number_of_shares(self, symbol): | |
self.c.execute('SELECT shares FROM portfolio WHERE user_id=? AND symbol=?', (self.id, symbol)) | |
return self.c.fetchone() | |
def update_shares(self, shares, symbol): | |
self.c.execute('UPDATE portfolio SET shares=? WHERE symbol=? AND user_id=?', (shares, symbol, self.id)) | |
self.conn.commit() | |
def insert_shares(self, shares, symbol): | |
self.c.execute('INSERT INTO portfolio VALUES (?,?,?)', (self.id, symbol, shares)) | |
self.conn.commit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
Preferences | |
{% endblock %} | |
{% block main %} | |
<form action="/preferences" method="post"> | |
<div class="form-group"> | |
<input autocomplete="off" autofocus class="form-control" name="old_password" placeholder="old password" type="password"/> | |
</div> | |
<div class="form-group"> | |
<input autocomplete="off" autofocus class="form-control" name="new_password" placeholder="new password" type="password"/> | |
</div> | |
<div class="form-group"> | |
<input class="form-control" name="confirmation" placeholder="confirm password" type="password"/> | |
</div> | |
<button class="btn btn-primary" type="submit">Reset Password</button> | |
</form> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% 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" name="symbol" placeholder="symbol" type="text"/> | |
</div> | |
<button class="btn btn-primary" type="submit">Get Quote</button> | |
</form> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
Quoted | |
{% endblock %} | |
{% block main %} | |
<!--Table--> | |
<table id="tablePreview" class="table table-striped table-sm"> | |
<!--Table head--> | |
<thead> | |
<tr> | |
<th>Symbol</th> | |
<th>Company Name</th> | |
<th>Price</th> | |
</tr> | |
</thead> | |
<!--Table head--> | |
<!--Table body--> | |
<tbody> | |
<tr> | |
<td>{{ stock['symbol'] }}</td> | |
<td>{{ stock['name'] }}</td> | |
<td>{{ stock['price'] | usd }}</td> | |
</tr> | |
</tbody> | |
<!--Table body--> | |
</table> | |
<!--Table--> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
Register | |
{% endblock %} | |
{% block main %} | |
<form action="/register" method="post"> | |
<div class="form-group"> | |
<input autocomplete="off" autofocus class="form-control" name="username" placeholder="username" type="text"/> | |
</div> | |
<div class="form-group"> | |
<input autocomplete="off" autofocus class="form-control" name="password" placeholder="new password" type="password"/> | |
</div> | |
<div class="form-group"> | |
<input class="form-control" name="confirmation" placeholder="confirm password" type="password"/> | |
</div> | |
<button class="btn btn-primary" type="submit">Register</button> | |
</form> | |
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% extends "layout.html" %} | |
{% block title %} | |
Sell | |
{% endblock %} | |
{% block main %} | |
<form action="/sell" method="post"> | |
<div class="form-group"> | |
<input autocomplete="off" autofocus class="form-control" name="symbol" placeholder="symbol" type="text"/> | |
</div> | |
<div class="form-group"> | |
<input class="form-control" name="shares" placeholder="shares" type="text"/> | |
</div> | |
<button class="btn btn-primary" type="submit">Sell</button> | |
</form> | |
{% endblock %} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment