├── 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 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |