├── application.py ├── finance.db ├── helpers.py ├── requirements.txt ├── static ├── favicon.ico └── styles.css └── templates ├── apology.html ├── layout.html └── login.html /application.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from cs50 import SQL 4 | from flask import Flask, flash, jsonify, redirect, render_template, request, session 5 | from flask_session import Session 6 | from tempfile import mkdtemp 7 | from werkzeug.exceptions import default_exceptions, HTTPException, InternalServerError 8 | from werkzeug.security import check_password_hash, generate_password_hash 9 | 10 | from helpers import apology, login_required, lookup, usd 11 | 12 | # Configure application 13 | app = Flask(__name__) 14 | 15 | # Ensure templates are auto-reloaded 16 | app.config["TEMPLATES_AUTO_RELOAD"] = True 17 | 18 | # Ensure responses aren't cached 19 | @app.after_request 20 | def after_request(response): 21 | response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" 22 | response.headers["Expires"] = 0 23 | response.headers["Pragma"] = "no-cache" 24 | return response 25 | 26 | # Custom filter 27 | app.jinja_env.filters["usd"] = usd 28 | 29 | # Configure session to use filesystem (instead of signed cookies) 30 | app.config["SESSION_FILE_DIR"] = mkdtemp() 31 | app.config["SESSION_PERMANENT"] = False 32 | app.config["SESSION_TYPE"] = "filesystem" 33 | Session(app) 34 | 35 | # Configure CS50 Library to use SQLite database 36 | db = SQL("sqlite:///finance.db") 37 | 38 | # Make sure API key is set 39 | if not os.environ.get("API_KEY"): 40 | raise RuntimeError("API_KEY not set") 41 | 42 | 43 | @app.route("/") 44 | @login_required 45 | def index(): 46 | """Show portfolio of stocks""" 47 | return apology("TODO") 48 | 49 | 50 | @app.route("/buy", methods=["GET", "POST"]) 51 | @login_required 52 | def buy(): 53 | """Buy shares of stock""" 54 | return apology("TODO") 55 | 56 | 57 | @app.route("/history") 58 | @login_required 59 | def history(): 60 | """Show history of transactions""" 61 | return apology("TODO") 62 | 63 | 64 | @app.route("/login", methods=["GET", "POST"]) 65 | def login(): 66 | """Log user in""" 67 | 68 | # Forget any user_id 69 | session.clear() 70 | 71 | # User reached route via POST (as by submitting a form via POST) 72 | if request.method == "POST": 73 | 74 | # Ensure username was submitted 75 | if not request.form.get("username"): 76 | return apology("must provide username", 403) 77 | 78 | # Ensure password was submitted 79 | elif not request.form.get("password"): 80 | return apology("must provide password", 403) 81 | 82 | # Query database for username 83 | rows = db.execute("SELECT * FROM users WHERE username = :username", 84 | username=request.form.get("username")) 85 | 86 | # Ensure username exists and password is correct 87 | if len(rows) != 1 or not check_password_hash(rows[0]["hash"], request.form.get("password")): 88 | return apology("invalid username and/or password", 403) 89 | 90 | # Remember which user has logged in 91 | session["user_id"] = rows[0]["id"] 92 | 93 | # Redirect user to home page 94 | return redirect("/") 95 | 96 | # User reached route via GET (as by clicking a link or via redirect) 97 | else: 98 | return render_template("login.html") 99 | 100 | 101 | @app.route("/logout") 102 | def logout(): 103 | """Log user out""" 104 | 105 | # Forget any user_id 106 | session.clear() 107 | 108 | # Redirect user to login form 109 | return redirect("/") 110 | 111 | 112 | @app.route("/quote", methods=["GET", "POST"]) 113 | @login_required 114 | def quote(): 115 | """Get stock quote.""" 116 | return apology("TODO") 117 | 118 | 119 | @app.route("/register", methods=["GET", "POST"]) 120 | def register(): 121 | """Register user""" 122 | return apology("TODO") 123 | 124 | 125 | @app.route("/sell", methods=["GET", "POST"]) 126 | @login_required 127 | def sell(): 128 | """Sell shares of stock""" 129 | return apology("TODO") 130 | 131 | 132 | def errorhandler(e): 133 | """Handle error""" 134 | if not isinstance(e, HTTPException): 135 | e = InternalServerError() 136 | return apology(e.name, e.code) 137 | 138 | 139 | # Listen for errors 140 | for code in default_exceptions: 141 | app.errorhandler(code)(errorhandler) 142 | -------------------------------------------------------------------------------- /finance.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmalan/demo/9749642318662cc90e7ef8127ff7e9891374a518/finance.db -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import urllib.parse 4 | 5 | from flask import redirect, render_template, request, session 6 | from functools import wraps 7 | 8 | 9 | def apology(message, code=400): 10 | """Render message as an apology to user.""" 11 | def escape(s): 12 | """ 13 | Escape special characters. 14 | 15 | https://github.com/jacebrowning/memegen#special-characters 16 | """ 17 | for old, new in [("-", "--"), (" ", "-"), ("_", "__"), ("?", "~q"), 18 | ("%", "~p"), ("#", "~h"), ("/", "~s"), ("\"", "''")]: 19 | s = s.replace(old, new) 20 | return s 21 | return render_template("apology.html", top=code, bottom=escape(message)), code 22 | 23 | 24 | def login_required(f): 25 | """ 26 | Decorate routes to require login. 27 | 28 | http://flask.pocoo.org/docs/1.0/patterns/viewdecorators/ 29 | """ 30 | @wraps(f) 31 | def decorated_function(*args, **kwargs): 32 | if session.get("user_id") is None: 33 | return redirect("/login") 34 | return f(*args, **kwargs) 35 | return decorated_function 36 | 37 | 38 | def lookup(symbol): 39 | """Look up quote for symbol.""" 40 | 41 | # Contact API 42 | try: 43 | api_key = os.environ.get("API_KEY") 44 | response = requests.get(f"https://cloud-sse.iexapis.com/stable/stock/{urllib.parse.quote_plus(symbol)}/quote?token={api_key}") 45 | response.raise_for_status() 46 | except requests.RequestException: 47 | return None 48 | 49 | # Parse response 50 | try: 51 | quote = response.json() 52 | return { 53 | "name": quote["companyName"], 54 | "price": float(quote["latestPrice"]), 55 | "symbol": quote["symbol"] 56 | } 57 | except (KeyError, TypeError, ValueError): 58 | return None 59 | 60 | 61 | def usd(value): 62 | """Format value as USD.""" 63 | return f"${value:,.2f}" 64 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cs50 2 | Flask 3 | Flask-Session 4 | requests 5 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmalan/demo/9749642318662cc90e7ef8127ff7e9891374a518/static/favicon.ico -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | nav .navbar-brand 2 | { 3 | /* size for brand */ 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 | } 24 | 25 | main .form-control 26 | { 27 | /* center form controls */ 28 | display: inline-block; 29 | 30 | /* override Bootstrap's 100% width for form controls */ 31 | width: auto; 32 | } 33 | 34 | main 35 | { 36 | /* scroll horizontally as needed */ 37 | overflow-x: auto; 38 | 39 | /* center contents */ 40 | text-align: center; 41 | } 42 | 43 | main img 44 | { 45 | /* constrain images on small screens */ 46 | max-width: 100%; 47 | } 48 | -------------------------------------------------------------------------------- /templates/apology.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Apology 5 | {% endblock %} 6 | 7 | {% block main %} 8 | {{ top }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | C$50 Finance: {% block title %}{% endblock %} 24 | 25 | 26 | 27 | 28 | 29 | 53 | 54 | {% if get_flashed_messages() %} 55 |
56 | 59 |
60 | {% endif %} 61 | 62 |
63 | {% block main %}{% endblock %} 64 |
65 | 66 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 | Log In 5 | {% endblock %} 6 | 7 | {% block main %} 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | {% endblock %} 18 | --------------------------------------------------------------------------------