import os from cs50 import SQL 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, HTTPException, InternalServerError from werkzeug.security import check_password_hash, generate_password_hash from helpers import apology, deal_finder, login_required, passw_requirements, usd # Configure application app = Flask(__name__) # Ensure templates are auto-reloaded app.config["TEMPLATES_AUTO_RELOAD"] = True # Ensure responses aren't cached, allows smoother implementation of development-changes @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, to avoid multiple cookies in one browser) app.config["SESSION_FILE_DIR"] = mkdtemp() app.config["SESSION_PERMANENT"] = False app.config["SESSION_TYPE"] = "filesystem" Session(app) # Configure the SQLite database db = SQL("sqlite:///spending.db") @app.route("/") def home(): """Home page with different versions for visiting guests & logged-in users """ # When a non-registered guest visits the website: if not session: return render_template("welcome.html") # For a logged-in user: else: user_id = session["user_id"] # Getting username & budget user_info = db.execute("SELECT * FROM users WHERE id = ?", user_id) username = user_info[0]["username"] cash = user_info[0]["cash"] # Getting total amount of money spent in each category amounts = [] categories = ['%Rent', '%Maintenance', '%Food', '%Family', '%Pets', '%Clothes', '%Hobbies', '%Investing', '%Other'] for element in categories: result = db.execute("SELECT SUM(amount) FROM expenses WHERE user_id = ? AND category LIKE ?", user_id, element)[0]["SUM(amount)"] if result == None: result = 0 amounts.append(result) return render_template("home.html", name=username, cash=cash, rent=amounts[0], maintain=amounts[1], food=amounts[2], family=amounts[3], pets=amounts[4], clothes=amounts[5], hobbies=amounts[6], invest=amounts[7], other=amounts[8]) @app.route("/account", methods=["GET", "POST"]) @login_required def account(): """ View user data & replace passsword """ user_id = session["user_id"] user_info = db.execute("SELECT * FROM users WHERE id = ?", user_id) username = user_info[0]["username"] cash = user_info[0]["cash"] if request.method == "GET": return render_template("account.html", name=username, budget=cash) else: # allowing the user to set a new password for their account pw_new = request.form.get("pw_new") if not check_password_hash(user_info[0]["hash"], request.form.get("pw_old")): return apology("Your old password was not correct", 400) elif pw_new != request.form.get("confirmation"): return apology("New Password and Confirm New Password did not match", 400) elif passw_requirements(pw_new) != "valid": return apology("Password must be between 6 and 25 characters long and contain at least one number and one letter", 400) # replace password in the db if all requirements were met db.execute("UPDATE users SET hash = ? WHERE username = ?", generate_password_hash(pw_new), username) flash("You successfully changed your password!") return render_template("/account.html", name=username, budget=cash) @app.route("/bargain", methods=["GET", "POST"]) @login_required def bargain(): """Search for & display cheapest deals for a user-specified item """ # display search form if request.method == "GET": return render_template("bargain.html") # display results collected & sorted by web scraping helper function else: item = request.form.get("product-search") deals = deal_finder(item) return render_template("bargain.html", deals=deals) @app.route("/budget", methods=["GET", "POST"]) @login_required def budget(): """ View & set budget """ user_id = session["user_id"] budget = db.execute("SELECT cash FROM users WHERE id = ?", user_id)[0]["cash"] # view current budget if request.method == "GET": return render_template("budget.html", budget=budget) # update budget else: added_budget = request.form.get("add_budget") new_budget = float(budget) + float(added_budget) db.execute("UPDATE users SET cash = ? WHERE id = ?", new_budget, user_id) flash("Budget updated!") return render_template("budget.html", budget=new_budget) @app.route("/history") @login_required def history(): """ Show record of all previous expenses """ user_id = session["user_id"] record = db.execute("SELECT * FROM expenses WHERE user_id = ? ORDER BY date", user_id) return render_template("history.html", record=record) @app.route("/login", methods=["GET", "POST"]) def login(): """Log user in""" # Forget any user_id session.clear() # User reached the route via GET: if request.method == "GET": return render_template("login.html") # User submitted via POST to log in: else: # 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("/") @app.route("/logout") def logout(): """Log user out""" # Forget any user_id session.clear() # Redirect user to login form return redirect("/") @app.route("/register", methods=["GET", "POST"]) def register(): """ Register a new user account """ # If the user visits this route, display the form to sign up for a new account if request.method == "GET": return render_template("register.html") # If the user submits their registration with POST else: # Get the user's name & password from the form reg_name = request.form.get("username") reg_passw = request.form.get("password") # Checking if the user failed to provide a username & password, if the name is already taken or if the confirmed password was different if not reg_name: return apology("Please enter a username", 400) elif len(db.execute("SELECT * FROM users WHERE username = ?", reg_name)) != 0: return apology("Username is already taken", 400) elif not reg_passw: return apology("Please choose a unique password", 400) elif reg_passw != request.form.get("confirmation"): return apology("Password and Confirm Password did not match", 400) # Special check with a helper function for password safety elif passw_requirements(reg_passw) != "valid": return apology("Password must be between 6 and 25 characters long and contain at least one number and one letter", 400) # Otherwise register the new user into the database (with a hashed password for extra security) and redirect them to the main page db.execute("INSERT INTO users (username, hash) VALUES (?, ?)", reg_name, generate_password_hash(reg_passw)) return redirect("/login") @app.route("/tracker", methods=["POST", "GET"]) @login_required def tracker(): """ Add new expenses """ # Display form to add a new expense if request.method == "GET": return render_template("tracker.html") # When submitting a new expense: else: user_id = session["user_id"] budget = db.execute("SELECT cash FROM users WHERE id = ?", user_id)[0]["cash"] # ternary condition if the user does not give an optional comment on the expense comment = "Unspecified" if not request.form.get("comment") else request.form.get("comment") category, amount, date = request.form.get("category"), request.form.get("amount", type=float), request.form.get("date") # subtract expense from budget updated_budget = float(budget) - amount db.execute("UPDATE users SET cash = ? WHERE id = ?", updated_budget, user_id) # update table "expenses" in database for use in history.html db.execute("INSERT INTO expenses (user_id, comment, category, amount, date) VALUES (?, ?, ?, ?, ?)", user_id, comment, category, amount, date) return redirect("/") # Handle errors def errorhandler(e): if not isinstance(e, HTTPException): e = InternalServerError() return apology(e.name, e.code) # Listen for errors for code in default_exceptions: app.errorhandler(code)(errorhandler)