├── .gitattributes ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .pyup.yml ├── Pipfile ├── Pipfile.lock ├── README.md ├── application.py ├── helpers.py ├── img ├── 400.svg ├── C$50 Finance - CS50x.pdf ├── Web - CS50x.pdf ├── demo.gif └── landing.svg ├── static ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── site.webmanifest ├── images │ ├── animated-400.svg │ ├── animated-401.svg │ ├── animated-403.svg │ ├── animated-404.svg │ ├── animated-finance.svg │ ├── animated-landing.svg │ ├── animated-login.svg │ ├── animated-revenue.svg │ ├── animated-stats.svg │ ├── animated-wallet.svg │ ├── hat-with-feather-black.svg │ ├── hat-with-feather-green.svg │ ├── history.svg │ ├── home.svg │ ├── landing.svg │ ├── no-data.svg │ └── sprite.svg ├── index.js └── styles.css ├── templates ├── bought.html ├── buy.html ├── error.html ├── history.html ├── index.html ├── landing.html ├── layout.html ├── login.html ├── quote.html ├── quoted.html ├── register.html ├── sell.html └── sold.html └── tests ├── test_app.py └── test_views.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-generated -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '34 13 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | finances.db 3 | .vscode 4 | .env 5 | # Elastic Beanstalk Files 6 | .elasticbeanstalk/* 7 | !.elasticbeanstalk/*.cfg.yml 8 | !.elasticbeanstalk/*.global.yml 9 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | schedule: '' 5 | update: false 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | requests = "==2.26.0" 10 | flask-marshmallow = "==0.14.0" 11 | marshmallow-sqlalchemy = "==0.26.1" 12 | python-dotenv = "==0.19.0" 13 | pytest = "*" 14 | mysql-connector = "*" 15 | Flask = "==2.0.1" 16 | Flask-Session = "==0.4.0" 17 | Flask-SQLAlchemy = "==2.5.1" 18 | Werkzeug = "==2.0.1" 19 | redis = "==3.5.3" 20 | 21 | [requires] 22 | python_version = "3" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Wall Street Trader logo 3 | 4 | # [Finance: Full Stack Web App using Flask and SQL](http://wallstreettrader.app/) 5 | [![Website shields.io](https://img.shields.io/website-up-down-green-red/http/shields.io.svg)](http://flask-env.eba-z6mwdiua.us-west-2.elasticbeanstalk.com/) 6 | ![Security Headers](https://img.shields.io/security-headers?url=http%3A%2F%2Fflask-env.eba-z6mwdiua.us-west-2.elasticbeanstalk.com%2F) 7 | [![Build Status](https://travis-ci.org/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL.svg?branch=master)](https://travis-ci.org/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL) 8 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/323b83dec4c44b78bde6a4b2aa3477ec)](https://www.codacy.com/gh/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/dashboard?utm_source=github.com&utm_medium=referral&utm_content=JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL&utm_campaign=Badge_Grade) 9 | [![Updates](https://pyup.io/repos/github/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/shield.svg)](https://pyup.io/repos/github/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/) 10 | [![Python 3](https://pyup.io/repos/github/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/python-3-shield.svg)](https://pyup.io/repos/github/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/) 11 |
12 | 13 | ## Homework from [Harvard's Introduction to Computer Science CS50 hosted on eDX](https://www.edx.org/course/cs50s-introduction-to-computer-science) 🎓 [Web Track](https://cs50.harvard.edu/x/2020/tracks/web/) - [Finance](https://cs50.harvard.edu/x/2020/tracks/web/finance/) 14 | 15 | 16 | ## 🚀 Getting Started 17 | ### To run this project on your system: 18 | - Ensure that `python3` and `python3-pip` are installed on your system 19 | - In your terminal, navigate to the root project directory and run the following commands 20 | - Activate the virtual environment 21 | ``` 22 | $ pipenv shell 23 | ``` 24 | - Install the dependencies 25 | ``` 26 | $ pipenv install -r requirements.txt 27 | ``` 28 | - You'll need to register for an API key in order to be able to query IEX’s data 29 | - [Register](iexcloud.io/cloud-login#/register/) for an account 30 | - Enter your email address and a password, and click “Create account” 31 | - On the next page, scroll down to choose the Start (free) plan 32 | - Once you’ve confirmed your account via a confirmation email, sign in to iexcloud.io 33 | - Click API Tokens 34 | - Copy the key that appears under the Token column (it should begin with pk_) into the `` in the next step 35 | - Create a .env file and paste the following into it: `API_KEY=` 36 | - To start the web server, execute (without debugging): 37 | ``` 38 | $ python application.py 39 | ``` 40 | - Alternatively, execute (with debugging): 41 | ``` 42 | $ export FLASK_APP=application.py 43 | $ flask run 44 | ``` 45 | - Lastly, create a SQL database named `finances.db` 46 | - To initialize the SQL database within application.py, add `db.create_all()` below `Initialize Schemas`. Once the code runs and the you've verified the database exists, remove `db.create_all()` 47 | - To initialize the SQL database in the python shell, execute: 48 | ``` 49 | $ python 50 | $ from application import db 51 | $ db.create_all() 52 | ``` 53 | - To initialize the database with SQL command-line arguemnts (using MySQL syntax) run each `CREATE TABLE` command (one at a time): 54 | ``` 55 | CREATE TABLE users ( 56 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 57 | username VARCHAR(50) UNIQUE, 58 | hash VARCHAR(200) NOT NULL, 59 | cash INTEGER 60 | ); 61 | CREATE TABLE portfolio ( 62 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 63 | user_id INTEGER, 64 | symbol VARCHAR(5), 65 | current_shares INTEGER 66 | ); 67 | CREATE TABLE bought ( 68 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 69 | buyer_id INTEGER, 70 | time VARCHAR(100), 71 | symbol VARCHAR(5), 72 | shares_bought INTEGER, 73 | price_bought FLOAT 74 | ); 75 | CREATE TABLE sold ( 76 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 77 | seller_id INTEGER, 78 | time VARCHAR(100), 79 | symbol VARCHAR(5), 80 | shares_sold INTEGER, 81 | price_sold FLOAT 82 | ); 83 | ``` 84 | 85 | ## 📣 Attribution 86 | - Stock prices pulled from [IEX Stock Quote API](https://iexcloud.io/docs/api/#quote) 87 | - Hat icon made by [Alice Noir](https://thenounproject.com/AliceNoir/) from [the Noun Project](https://thenounproject.com/icon/pirate-hat-4121754/) 88 | - Feather icon made by [Jacopo Mencacci](https://thenounproject.com/jacopoPaper/) from [the Noun Project](https://thenounproject.com/icon/feather-10683/) 89 | - Illustrations by [Freepik Storyset](https://storyset.com/people/rafiki) 90 | 91 | ## 🔒 License 92 | Copyright Notice and Statement: currently not offering any license. Permission only to view and download. Refer to [choose a license](https://choosealicense.com/no-permission/) for more info. 93 | -------------------------------------------------------------------------------- /application.py: -------------------------------------------------------------------------------- 1 | import os 2 | import redis 3 | from datetime import datetime 4 | from flask import Flask, flash, jsonify, redirect, render_template, request, session 5 | from flask_session import Session 6 | from flask_sqlalchemy import SQLAlchemy 7 | from flask_marshmallow import Marshmallow 8 | from tempfile import mkdtemp 9 | from werkzeug.exceptions import default_exceptions, HTTPException, InternalServerError 10 | from werkzeug.security import check_password_hash, generate_password_hash 11 | from helpers import errorPage, login_required, lookup, usd 12 | 13 | # Configure application 14 | application = Flask(__name__) 15 | basedir = os.path.abspath(os.path.dirname(__file__)) 16 | 17 | # Ensure templates are auto-reloaded 18 | application.config["TEMPLATES_AUTO_RELOAD"] = True 19 | 20 | # Ensure responses aren't cached 21 | @application.after_request 22 | def after_request(response): 23 | response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" 24 | response.headers["Expires"] = 0 25 | response.headers["Pragma"] = "no-cache" 26 | return response 27 | 28 | # Custom filter 29 | application.jinja_env.filters["usd"] = usd 30 | 31 | # Configure Redis for storing the session data locally on the server-side 32 | application.secret_key = 'BAD_SECRET_KEY' 33 | application.config['SESSION_TYPE'] = 'redis' 34 | application.config['SESSION_PERMANENT'] = False 35 | application.config['SESSION_USE_SIGNER'] = True 36 | application.config['SESSION_REDIS'] = redis.from_url('redis://localhost:6379') 37 | # Create and initialize the Flask-Session object AFTER `app` has been configured 38 | server_session = Session(application) 39 | 40 | # Configure Flask to use local SQLite3 database with SQLAlchemy 41 | application.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'finances.db') 42 | application.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 43 | application.config['SQLALCHEMY_ECHO'] = True 44 | db = SQLAlchemy(application) 45 | 46 | # Configure marshmallow 47 | ma = Marshmallow(application) 48 | 49 | # Create classes/models 50 | class Users(db.Model): 51 | id = db.Column(db.Integer, primary_key=True) 52 | username = db.Column(db.String(length=50)) 53 | hash = db.Column(db.String(length=200)) 54 | cash = db.Column(db.Integer) 55 | # Create initializer/constructor 56 | def __init__(self, username, hash, cash): 57 | self.username = username 58 | self.hash = hash 59 | self.cash = cash 60 | class Portfolio(db.Model): 61 | id = db.Column(db.Integer, primary_key=True) 62 | user_id = db.Column(db.Integer) 63 | symbol = db.Column(db.String(length=5)) 64 | current_shares = db.Column(db.Integer) 65 | # Create initializer/constructor 66 | def __init__(self, user_id, symbol, current_shares): 67 | self.user_id = user_id 68 | self.symbol = symbol 69 | self.current_shares = current_shares 70 | class Bought(db.Model): 71 | id = db.Column(db.Integer, primary_key=True) 72 | buyer_id = db.Column(db.Integer) 73 | time = db.Column(db.String(length=100)) 74 | symbol = db.Column(db.String(length=5)) 75 | shares_bought = db.Column(db.Integer) 76 | price_bought = db.Column(db.Float) 77 | # Create initializer/constructor 78 | def __init__(self, buyer_id, time, symbol, shares_bought, price_bought): 79 | self.buyer_id = buyer_id 80 | self.time = time 81 | self.symbol = symbol 82 | self.shares_bought = shares_bought 83 | self.price_bought = price_bought 84 | class Sold(db.Model): 85 | id = db.Column(db.Integer, primary_key=True) 86 | seller_id = db.Column(db.Integer) 87 | time = db.Column(db.String(length=100)) 88 | symbol = db.Column(db.String(length=5)) 89 | shares_sold = db.Column(db.Integer) 90 | price_sold = db.Column(db.Float) 91 | # Create initializer/constructor 92 | def __init__(self, seller_id, time, symbol, shares_sold, price_sold): 93 | self.seller_id = seller_id 94 | self.time = time 95 | self.symbol = symbol 96 | self.shares_sold = shares_sold 97 | self.price_sold = price_sold 98 | 99 | # Create Schemas (only include data you want to show) 100 | class UsersSchema(ma.Schema): 101 | class Meta: 102 | fields = ('username', 'cash') 103 | class PortfolioSchema(ma.Schema): 104 | class Meta: 105 | fields = ('symbol', 'current_shares') 106 | class BoughtSchema(ma.Schema): 107 | class Meta: 108 | fields = ('time', 'symbol', 'shares_bought', 'price_bought') 109 | class SoldSchema(ma.Schema): 110 | class Meta: 111 | fields = ('time', 'symbol', 'shares_sold', 'price_sold') 112 | 113 | # Initialize Schemas 114 | users_schema = UsersSchema 115 | portfolio_schema = PortfolioSchema(many=True) 116 | bought_schema = BoughtSchema(many=True) 117 | sold_schema = SoldSchema(many=True) 118 | 119 | # Make sure API key is set 120 | os.environ.get("API_KEY") 121 | 122 | if not os.environ.get("API_KEY"): 123 | raise RuntimeError("API_KEY not set") 124 | 125 | @application.route("/") 126 | def landing(): 127 | return render_template("landing.html") 128 | 129 | @application.route("/home") 130 | @login_required 131 | def index(): 132 | # Obtain user id 133 | user = session["user_id"] 134 | print("user: ", user) 135 | 136 | # Obtain available cash 137 | available = (Users.query.filter_by(id = user).first()).cash 138 | print("available: ", available) 139 | 140 | # Obtain at least one stock symbol that the user possesses 141 | symbol_list = Portfolio.query.filter_by(user_id = user).all() 142 | print("symbol list: ", symbol_list) 143 | 144 | # If user has no stocks return minimum information 145 | if symbol_list == []: 146 | return render_template("index.html", available = usd(available), grand_total = usd(available), total = [], shares = [], price = [], symbols = [], symbol_list_length = 0) 147 | # If user owns stocks return the remaining information 148 | else: 149 | # Calculate symbol list length for iteration in index.html 150 | symbol_list_length = len(symbol_list) 151 | print("symbol_list_length: ", symbol_list_length) 152 | 153 | # Create empty arrays to store values 154 | symbols = [] 155 | price = [] 156 | shares = [] 157 | total = [] 158 | # Calculate value of each holding of stock in portfolio 159 | for i in range(len(symbol_list)): 160 | symbol_index = symbol_list[i].symbol 161 | print("symbol_index:", symbol_index) 162 | symbols.append(symbol_index) 163 | # Obtain price of stock using iex API 164 | price_index = float(lookup(symbol_index).get('price')) 165 | print("price_index:", price_index) 166 | price.append(price_index) 167 | # Obtain number of shares that the user possesses to calculate total value 168 | shares_list = Portfolio.query.filter_by(user_id = user, symbol = symbol_index).all() 169 | print("shares_list:", shares_list) 170 | #("SELECT current_shares FROM portfolio WHERE user_id = :id AND symbol = :symbol", id = user, symbol = symbol_index) 171 | # Iterate over list of dicts 172 | for i in range(len(shares_list)): 173 | share_index = shares_list[i].current_shares 174 | print("share_index:", share_index) 175 | shares.append(share_index) 176 | # Calculate total value of stocks 177 | calc = share_index * price_index 178 | print("calc:", calc) 179 | total.append(calc) 180 | print("symbols:", symbols) 181 | print("price:", price) 182 | print("shares:", shares) 183 | print("total:", total) 184 | # Calculate grand total value of all assets 185 | grand_total = sum(total) + available 186 | 187 | # Render page with information 188 | return render_template("index.html", symbol_list = symbol_list, symbol_list_length = symbol_list_length, shares = shares, price = price, total = total, available = usd(available), grand_total = usd(grand_total)) 189 | 190 | 191 | @application.route("/buy", methods=["GET", "POST"]) 192 | @login_required 193 | def buy(): 194 | if request.method == "GET": 195 | return render_template("buy.html") 196 | else: 197 | symbol = request.form.get("symbol").upper() 198 | 199 | # User error handling: stop empty symbol and shares fields, stop invalid symbols, and negative share numbers 200 | if not symbol: 201 | return errorPage(title="No Data", info = "Please enter a stock symbol, i.e. AMZN", file = "no-data.svg") 202 | result = lookup(symbol) 203 | if result == None: 204 | return errorPage(title = "Bad Request", info = "Please enter a valid stock symbol", file="animated-400.svg") 205 | shares = int(request.form.get("shares")) 206 | if symbol == None: 207 | return errorPage(title="No Data", info = "Please enter number of shares", file = "no-data.svg") 208 | if shares < 0: 209 | return errorPage(title = "Bad Request", info = "Please enter a positive number", file="animated-400.svg") 210 | if shares == 0: 211 | return errorPage(title="No Data", info = "Transaction will not proceed", file = "no-data.svg") 212 | 213 | # Obtain user id 214 | user = session["user_id"] 215 | print("user:", user) 216 | 217 | # Obtain available cash 218 | available = (Users.query.filter_by(id = user).first()).cash 219 | print("available:", available) 220 | 221 | # Use IEX API to get price of stock 222 | price = lookup(symbol).get('price') 223 | print("price:", price) 224 | 225 | # Calculate total cost 226 | total = shares * price 227 | 228 | # User error handling: stop user if seeking to buy beyond cash balance 229 | if available < total: 230 | return errorPage(title="Forbidden", info = "Insufficient funds to complete transaction", file="animated-403.svg") 231 | 232 | # Continue with transaction and calculate remaining cash 233 | remaining = available - total 234 | 235 | # Obtain year, month, day, hour, minute, second 236 | now = datetime.now() 237 | time = now.strftime("%d/%m/%Y %H:%M:%S") 238 | 239 | # Update cash field in Users Table and create entry into Bought Table 240 | update_cash = Users.query.filter_by(id = user).first() 241 | update_cash.cash = remaining 242 | db.session.commit() 243 | #"UPDATE users SET cash = :remaining WHERE id = :id", remaining = remaining, id = user) 244 | 245 | # Log transaction history 246 | log_purchase = Bought(user, time, symbol, shares, price) 247 | db.session.add(log_purchase) 248 | db.session.commit() 249 | #("INSERT INTO bought (buyer_id, time, symbol, shares_bought, price_bought) VALUES (:buyer_id, :time, :symbol, :shares_bought, :price_bought)", time = datetime.datetime.now(), symbol = symbol, shares_bought = shares, price_bought = price, buyer_id = user) 250 | 251 | # If buyer never bought this stock before 252 | portfolio = Portfolio.query.filter(Portfolio.user_id == user, Portfolio.symbol == symbol).first() 253 | print("portfolio", portfolio) 254 | 255 | #("SELECT symbol FROM portfolio WHERE user_id = :id AND symbol = :symbol", id = user, symbol = symbol) 256 | if portfolio == None: 257 | db.session.add(Portfolio(user, symbol, shares)) 258 | db.session.commit() 259 | #("INSERT INTO portfolio (user_id, symbol, current_shares) VALUES (:user_id, :symbol, :current_shares)", user_id = user, symbol = symbol, current_shares = shares) 260 | else: 261 | stock_owned = portfolio.symbol 262 | print("stock_owned", stock_owned) 263 | # Obtain current number of shares from portfolio 264 | current_shares = portfolio.current_shares 265 | print("current shares", current_shares) 266 | #("SELECT current_shares FROM portfolio WHERE user_id = :id AND symbol = :symbol", id = user, symbol = symbol) 267 | 268 | # Calculate new amount of shares 269 | new_shares = shares + current_shares 270 | print("Total shares now:", new_shares) 271 | 272 | # Update portfolio table with new amount of shares 273 | portfolio.current_shares = new_shares 274 | print("Update db with new total:", portfolio.current_shares) 275 | db.session.commit() 276 | #("UPDATE portfolio SET current_shares = :new_shares WHERE user_id = :id", new_shares = new_shares, id = user) 277 | 278 | return render_template("bought.html", symbol = symbol, shares = shares, total = usd(total)) 279 | 280 | 281 | @application.route("/history") 282 | @login_required 283 | def history(): 284 | # Obtain user id 285 | user = session["user_id"] 286 | 287 | # Obtain purchase history 288 | bought_list = Bought.query.filter_by(buyer_id = user).all() 289 | print("bought_list:", bought_list) 290 | #("SELECT time, symbol, shares_bought, price_bought FROM bought WHERE buyer_id = :id", id = user) 291 | 292 | # If user didn't sell stocks, only query bought table, if didn't buy anything, return empty 293 | if bought_list == []: 294 | # Will return empty list if user didn't buy anything 295 | return render_template("history.html", bought_list_length = 0, bought_list = [], sold_list_length = 0, sold_list = []) 296 | 297 | # Else query sold table 298 | else: 299 | # Obtain sell history 300 | sold_list = Sold.query.filter_by(seller_id = user).all() 301 | print("sold_list:", sold_list) 302 | #("SELECT time, symbol, shares_sold, price_sold FROM sold WHERE seller_id = :id", id = user) 303 | 304 | # Calculate length of bought_list and sold_list 305 | bought_list_length = len(bought_list) 306 | sold_list_length = len(sold_list) 307 | 308 | return render_template("history.html", bought_list = bought_list, sold_list = sold_list, bought_list_length = bought_list_length, sold_list_length = sold_list_length) 309 | 310 | 311 | @application.route("/login", methods=["GET", "POST"]) 312 | def login(): 313 | """Log user in""" 314 | 315 | # Forget any user_id 316 | session.clear() 317 | 318 | # User reached route via POST (as by submitting a form via POST) 319 | if request.method == "POST": 320 | 321 | # Ensure username was submitted 322 | if not request.form.get("username"): 323 | return errorPage(title="No Data", info = "Must provide username", file = "no-data.svg") 324 | 325 | # Ensure password was submitted 326 | elif not request.form.get("password"): 327 | return errorPage(title="No Data", info = "Must provide password", file = "no-data.svg") 328 | 329 | # Query database for username 330 | rows = Users.query.filter_by(username=request.form.get("username")).first() 331 | #("SELECT * FROM users WHERE username = :username", username=request.form.get("username")) 332 | 333 | # Ensure user exists 334 | try: 335 | rows.username 336 | 337 | # NoneType is returned and therefore username does't exist in database 338 | except AttributeError: 339 | return errorPage(title="No Data", info = "User doesn't exist", file = "no-data.svg") 340 | 341 | # Finish logging user in 342 | else: 343 | # Ensure username and password is correct 344 | if rows.username != request.form.get("username") or not check_password_hash(rows.hash, request.form.get("password")): 345 | return errorPage(title = "Unauthorized", info = "invalid username and/or password", file="animated-401.svg") 346 | 347 | # Remember which user has logged in 348 | session["user_id"] = rows.id 349 | 350 | # Redirect user to home page 351 | return redirect("/home") 352 | 353 | # User reached route via GET (as by clicking a link or via redirect) 354 | else: 355 | return render_template("login.html") 356 | 357 | 358 | @application.route("/logout") 359 | def logout(): 360 | """Log user out""" 361 | 362 | # Forget any user_id 363 | session.clear() 364 | 365 | # Redirect user to login form 366 | return redirect("/") 367 | 368 | 369 | @application.route("/quote", methods=["GET", "POST"]) 370 | @login_required 371 | def quote(): 372 | if request.method == "GET": 373 | return render_template("quote.html") 374 | else: 375 | symbol = request.form.get("symbol") 376 | data = lookup(symbol) 377 | # User error handling: stop empty symbol and shares fields, stop invalid symbols, and negative share numbers 378 | if not symbol: 379 | return errorPage(title="No Data", info = "Please enter a stock symbol, i.e. AMZN", file = "no-data.svg") 380 | if data == None: 381 | return errorPage(title = "Bad Request", info = "Please enter a valid stock symbol", file="animated-400.svg") 382 | return render_template("quoted.html", data = data) 383 | 384 | 385 | @application.route("/register", methods=["GET", "POST"]) 386 | def register(): 387 | if request.method == "GET": 388 | return render_template("register.html") 389 | else: 390 | # Obtain username inputted 391 | username = request.form.get("username") 392 | 393 | # User error handling: stop empty username and password fields, stop usernames already taken, stop non-matching passwords 394 | if not username: 395 | return errorPage(title="No Data", info = "Please enter a username", file = "no-data.svg") 396 | 397 | existing = Users.query.filter_by(username=username) 398 | print("EXISTING USER: ", existing) 399 | #("SELECT * FROM users WHERE username = :username", username=username) 400 | if existing == username: 401 | print("EXISTING USER ALREADY!: ", existing) 402 | return errorPage(title="Forbidden", info = "Username already taken", file="animated-403.svg") 403 | password = request.form.get("password") 404 | if not password: 405 | return errorPage(title="No Data", info = "Please enter a password", file = "no-data.svg") 406 | confirmation = request.form.get("confirmation") 407 | if password != confirmation: 408 | return errorPage(title = "Unauthorized", info = "Passwords do not match", file="animated-401.svg") 409 | hashed = generate_password_hash(password, method='pbkdf2:sha256', salt_length=8) 410 | 411 | # All users automatically recieve $10,000 to start with 412 | cash = 10000 413 | 414 | # Add and commit the data into database 415 | db.session.add(Users(username, hashed, cash)) 416 | db.session.commit() 417 | #("INSERT INTO users (username, hash) VALUES (:username, :hash)", username=username, hash=hashed) 418 | 419 | # Automatically sign in after creating account 420 | rows = Users.query.filter_by(username=request.form.get("username")).first() 421 | session["user_id"] = rows.id 422 | 423 | # Redirect user to home page 424 | return redirect("/home") 425 | 426 | 427 | @application.route("/sell", methods=["GET", "POST"]) 428 | @login_required 429 | def sell(): 430 | # Obtain user id 431 | user = session["user_id"] 432 | 433 | if request.method == "GET": 434 | # Obtain stock symbols that the user possesses 435 | symbol_list = Portfolio.query.filter_by(user_id = user).all() 436 | #("SELECT symbol FROM portfolio WHERE user_id = :id", id = user) 437 | 438 | # If user never bought anaything, return empty values 439 | if symbol_list == []: 440 | return render_template("sell.html", symbol_list_length = 0) 441 | # Else display stock symbols in drop-down menu 442 | else: 443 | symbol_list_length = len(symbol_list) 444 | # Render sell page with list of stocks the user owns 445 | return render_template("sell.html", symbol_list = symbol_list, symbol_list_length = symbol_list_length) 446 | else: 447 | # Obtain stock symbol from user 448 | symbol = request.form.get("symbol") 449 | 450 | # If user doesn't own stock, render error 451 | if symbol == '': 452 | return errorPage(title="Forbidden", info = "Must own stock before selling", file="animated-403.svg") 453 | 454 | # Obtain number of shares from user 455 | shares = int(request.form.get("shares")) 456 | 457 | # Prevent user from submitting form with no number, negative number, or zero 458 | if not shares: 459 | return errorPage(title="No Data", info = "Please enter number of shares", file = "no-data.svg") 460 | if shares < 0: 461 | return errorPage(title = "Bad Request", info = "Please enter a positive number", file="animated-400.svg") 462 | if shares == 0: 463 | return errorPage(title="No Data", info = "Transaction will not proceed", file = "no-data.svg") 464 | 465 | # Obtain data for stock selected 466 | shares_held_list = Portfolio.query.filter(Portfolio.user_id == user, Portfolio.symbol == symbol).first() 467 | #("SELECT current_shares FROM portfolio WHERE user_id = :id AND symbol = :symbol", id = user, symbol = symbol) 468 | print("shares_held_list:", shares_held_list) 469 | 470 | # Obtain number of shares for stock selected 471 | shares_held = shares_held_list.current_shares 472 | print("shares_held:", shares_held) 473 | 474 | # Prevent user from selling more than they have 475 | if shares > shares_held: 476 | return errorPage(title="Forbidden", info = "Unable to sell more than you have", file="animated-403.svg") 477 | 478 | # Obtain available cash 479 | available = (Users.query.filter_by(id = user).first()).cash 480 | #("SELECT cash FROM users WHERE id = :id", id = user) 481 | 482 | # Obtain current price of stock 483 | price = lookup(symbol).get('price') 484 | 485 | # Calculate new number of shares 486 | updated_shares = shares_held - shares 487 | 488 | # Remove stocks from user's portfolio by number of shares indicated 489 | portfolio = Portfolio.query.filter(Portfolio.user_id == user, Portfolio.symbol == symbol).first() 490 | print("portfolio", portfolio) 491 | portfolio.current_shares = updated_shares 492 | print("Update db with new total:", portfolio.current_shares) 493 | db.session.commit() 494 | #("UPDATE portfolio SET current_shares = :updated_shares WHERE user_id = :id", updated_shares = updated_shares, id = user) 495 | 496 | # Calculate new amount of available cash 497 | total = available + (price * shares) 498 | 499 | # Update cash field in Users Table 500 | update_cash = Users.query.filter_by(id = user).first() 501 | update_cash.cash = total 502 | db.session.commit() 503 | #("UPDATE users SET cash = :total WHERE id = :id", total = total, id = user) 504 | 505 | # Obtain year, month, day, hour, minute, second 506 | now = datetime.now() 507 | time = now.strftime("%d/%m/%Y %H:%M:%S") 508 | 509 | # Log transaction history 510 | log_sale = Sold(user, time, symbol, shares, price) 511 | db.session.add(log_sale) 512 | db.session.commit() 513 | #("INSERT INTO sold (seller_id, time, symbol, shares_sold, price_sold) VALUES (:seller_id, :time, :symbol, :shares_sold, :price_sold)", time = datetime.datetime.now(), symbol = symbol, shares_sold = shares, price_sold = price, seller_id = user) 514 | 515 | # Render success page with infomation about transaction 516 | return render_template("sold.html", shares = shares, symbol = symbol.upper()) 517 | 518 | 519 | # def errorhandler(e): 520 | # """Handle error""" 521 | # if not isinstance(e, HTTPException): 522 | # e = InternalServerError() 523 | # return apology(e.name, e.code) 524 | 525 | 526 | # Listen for errors 527 | # for code in default_exceptions: 528 | # application.errorhandler(code)(errorhandler) 529 | 530 | @application.errorhandler(404) 531 | def page_not_found(e): 532 | # note that we set the 404 status explicitly 533 | return render_template('404.html'), 404 534 | 535 | # Run Server 536 | # Run the following in the command line: python application.py 537 | if __name__ == '__main__': 538 | application.run(host='0.0.0.0') -------------------------------------------------------------------------------- /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 errorPage(blockTitle, errorMessage, imageSource): 10 | return render_template("error.html", title = (blockTitle), info = (errorMessage), file = (imageSource)) 11 | 12 | 13 | def login_required(f): 14 | """ 15 | Decorate routes to require login. 16 | 17 | http://flask.pocoo.org/docs/1.0/patterns/viewdecorators/ 18 | """ 19 | @wraps(f) 20 | def decorated_function(*args, **kwargs): 21 | if session.get("user_id") is None: 22 | return redirect("/login") 23 | return f(*args, **kwargs) 24 | return decorated_function 25 | 26 | 27 | def lookup(symbol): 28 | """Look up quote for symbol.""" 29 | 30 | # Contact API 31 | try: 32 | api_key = os.environ.get("API_KEY") 33 | response = requests.get(f"https://cloud.iexapis.com/stable/stock/{urllib.parse.quote_plus(symbol)}/quote?token={api_key}") 34 | response.raise_for_status() 35 | except requests.RequestException: 36 | return None 37 | 38 | # Parse response 39 | try: 40 | quote = response.json() 41 | return { 42 | "name": quote["companyName"], 43 | "price": float(quote["latestPrice"]), 44 | "symbol": quote["symbol"] 45 | } 46 | except (KeyError, TypeError, ValueError): 47 | return None 48 | 49 | 50 | def usd(value): 51 | """Format value as USD.""" 52 | return f"${value:,.2f}" 53 | -------------------------------------------------------------------------------- /img/400.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /img/C$50 Finance - CS50x.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/28852b9a12fc32a788b48d31467c943c0379fb06/img/C$50 Finance - CS50x.pdf -------------------------------------------------------------------------------- /img/Web - CS50x.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/28852b9a12fc32a788b48d31467c943c0379fb06/img/Web - CS50x.pdf -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/28852b9a12fc32a788b48d31467c943c0379fb06/img/demo.gif -------------------------------------------------------------------------------- /static/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/28852b9a12fc32a788b48d31467c943c0379fb06/static/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/28852b9a12fc32a788b48d31467c943c0379fb06/static/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/28852b9a12fc32a788b48d31467c943c0379fb06/static/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /static/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/28852b9a12fc32a788b48d31467c943c0379fb06/static/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobGrisham/Finance-Full-Stack-Web-App-using-Flask-and-SQL/28852b9a12fc32a788b48d31467c943c0379fb06/static/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /static/images/animated-400.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /static/images/animated-login.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /static/images/animated-stats.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /static/images/animated-wallet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /static/images/hat-with-feather-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/images/hat-with-feather-green.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/images/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 54 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /static/images/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /static/images/landing.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 40 | 44 | 48 | 52 | 56 | 57 | 59 | 60 | 62 | image/svg+xml 63 | 65 | 66 | 67 | 68 | 69 | 73 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /static/images/no-data.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /static/images/sprite.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /static/index.js: -------------------------------------------------------------------------------- 1 | // -------------------------------------- 2 | // Google Analytics 3 | // -------------------------------------- 4 | window.dataLayer = window.dataLayer || []; 5 | function gtag(){dataLayer.push(arguments);} 6 | gtag('js', new Date()); 7 | 8 | gtag('config', 'G-36KQSD0BWH'); 9 | 10 | 11 | // -------------------------------------- 12 | // Disable form submissions if there are invalid fields 13 | // -------------------------------------- 14 | (function () { 15 | 'use strict' 16 | 17 | window.addEventListener('load', function() { 18 | // Fetch all the forms we want to apply custom validation styles 19 | var inputs = document.getElementsByClassName('form-control') 20 | 21 | // Loop over each input and watch blue event 22 | var validation = Array.prototype.filter.call(inputs, function(input) { 23 | 24 | input.addEventListener('blur', function(event) { 25 | // reset 26 | input.classList.remove('is-invalid') 27 | input.classList.remove('is-valid') 28 | 29 | if (input.checkValidity() === false) { 30 | input.classList.add('is-invalid') 31 | input.nextElementSibling.nextElementSibling.classList.remove('spaceholder') 32 | } 33 | else { 34 | input.classList.add('is-valid') 35 | input.nextElementSibling.nextElementSibling.classList.remove('spaceholder') 36 | } 37 | }, false); 38 | }); 39 | }, false); 40 | 41 | // Fetch all the forms we want to apply custom Bootstrap validation styles to 42 | var forms = document.querySelectorAll('.needs-validation'); 43 | 44 | // Loop over them and prevent submission 45 | Array.prototype.slice.call(forms) 46 | .forEach(function (form) { 47 | form.addEventListener('submit', function (event) { 48 | if (!form.checkValidity()) { 49 | event.preventDefault() 50 | event.stopPropagation() 51 | } 52 | 53 | form.classList.add('was-validated') 54 | }, false) 55 | }) 56 | 57 | })() 58 | 59 | // -------------------------------------- 60 | // Copyright Date 61 | // -------------------------------------- 62 | var date = new Date(); 63 | var year = date.getFullYear(); 64 | 65 | document.getElementById("date").innerHTML = year; -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | nav .navbar-brand 2 | { 3 | /* size for brand */ 4 | font-size: xx-large; 5 | } 6 | 7 | nav img 8 | { 9 | height: auto; 10 | width: 2rem; 11 | } 12 | 13 | .navbar-nav { 14 | flex-direction: row; 15 | justify-content: space-around; 16 | } 17 | 18 | .register 19 | { 20 | border-radius: 3rem; 21 | width: 7rem; 22 | } 23 | 24 | main .form-control 25 | { 26 | /* center form controls */ 27 | display: inline-block; 28 | 29 | /* override Bootstrap's 100% width for form controls */ 30 | width: auto; 31 | } 32 | 33 | main 34 | { 35 | /* scroll horizontally as needed */ 36 | overflow-x: auto; 37 | 38 | /* center contents */ 39 | text-align: center; 40 | } 41 | 42 | main img 43 | { 44 | /* constrain images on small screens */ 45 | max-width: 100%; 46 | } 47 | 48 | .jumbotron 49 | { 50 | background-color: transparent; 51 | } 52 | 53 | h1 { 54 | margin-top: 4rem; 55 | } 56 | 57 | #freepik_stories-investing { 58 | height: auto; 59 | width: auto; 60 | } 61 | 62 | @media only screen and (min-width: 767.98px) { 63 | #freepik_stories-investing { 64 | width: 25rem; 65 | } 66 | } 67 | @media only screen and (min-width: 991.98px) { 68 | #freepik_stories-investing { 69 | width: auto; 70 | } 71 | } 72 | 73 | .background 74 | { 75 | position: absolute; 76 | z-index: -10; 77 | right: 0; 78 | } 79 | 80 | #home 81 | { 82 | width: 100vw; 83 | } 84 | 85 | @media only screen and (max-width: 767.98px) { 86 | #home 87 | { 88 | top: auto; 89 | transform: rotate(180deg); 90 | } 91 | } 92 | 93 | .error { 94 | max-width: max-content; 95 | } 96 | 97 | .footer { 98 | display: block; 99 | } 100 | 101 | .footer p { 102 | margin: 0; 103 | line-height: 24px; 104 | } 105 | 106 | /* Heart icon in footer */ 107 | .footer p svg { 108 | height: 16px; 109 | width: 20px; 110 | } 111 | 112 | /* --------------------------------------------- */ 113 | /* Login/Register */ 114 | /* --------------------------------------------- */ 115 | 116 | :root { 117 | --input-padding-x: .75rem; 118 | --input-padding-y: .75rem; 119 | } 120 | 121 | .form-control { 122 | height: auto; 123 | width: 20rem!important; 124 | } 125 | 126 | .form-label-group { 127 | position: relative; 128 | margin-bottom: 0.5rem; 129 | } 130 | 131 | .form-label-group > input, 132 | .form-label-group > label { 133 | padding: var(--input-padding-y) var(--input-padding-x); 134 | } 135 | 136 | .form-label-group > label { 137 | position: absolute; 138 | top: 0; 139 | left: 0; 140 | display: block; 141 | width: 100%; 142 | margin-bottom: 0; 143 | /* Override default `