From 764b05268b99c176048f83a6eb3d431f30e92f50 Mon Sep 17 00:00:00 2001 From: Eddy Harrington Date: Fri, 8 May 2020 18:14:45 -0700 Subject: [PATCH] added spend categories page and functionality --- .vscode/settings.json | 3 + app.py | 172 ++++++++++++++++++++++++++++++++- static/logo.png | Bin 0 -> 49347 bytes templates/categories.html | 198 ++++++++++++++++++++++++++++++++++++++ templates/login.html | 7 +- templates/register.html | 16 +-- tendie_budgets.py | 4 +- tendie_categories.py | 174 +++++++++++++++++++++++++++++++++ tendie_dashboard.py | 9 -- 9 files changed, 559 insertions(+), 24 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 static/logo.png create mode 100644 templates/categories.html create mode 100644 tendie_categories.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b5ffdd2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "env\\Scripts\\python.exe" +} \ No newline at end of file diff --git a/app.py b/app.py index 84e46b5..4c6751f 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,7 @@ import tendie_dashboard import tendie_expenses import tendie_budgets +import tendie_categories from cs50 import SQL from flask import Flask, flash, jsonify, redirect, render_template, request, session @@ -151,7 +152,7 @@ def index(): spending_month = [] # Get the users spend categories (for quick expense modal) - categories = tendie_dashboard.getSpendCategories(session["user_id"]) + categories = tendie_categories.getSpendCategories(session["user_id"]) # Get todays date (for quick expense modal) date = datetime.today().strftime('%Y-%m-%d') @@ -234,7 +235,7 @@ def addexpenses(): # User reached route via GET else: # Get the users spend categories - categories = tendie_dashboard.getSpendCategories(session["user_id"]) + categories = tendie_categories.getSpendCategories(session["user_id"]) # Render expense page date = datetime.today().strftime('%Y-%m-%d') @@ -252,7 +253,7 @@ def history(): history = tendie_expenses.getHistory(session["user_id"]) # Get the users spend categories - categories = tendie_dashboard.getSpendCategories(session["user_id"]) + categories = tendie_categories.getSpendCategories(session["user_id"]) return render_template("history.html", history=history, categories=categories, isDeleteAlert=False) @@ -288,7 +289,7 @@ def history(): # Get the users expense history, spend categories, and then render the history page w/ delete alert history = tendie_expenses.getHistory(session["user_id"]) - categories = tendie_dashboard.getSpendCategories( + categories = tendie_categories.getSpendCategories( session["user_id"]) return render_template("history.html", history=history, categories=categories, isDeleteAlert=True) @@ -381,7 +382,7 @@ def createbudget(): budgeted = tendie_budgets.getTotalBudgeted(session["user_id"]) # Get the users spend categories - categories = tendie_dashboard.getSpendCategories(session["user_id"]) + categories = tendie_categories.getSpendCategories(session["user_id"]) return render_template("createbudget.html", income=income, budgeted=budgeted, categories=categories) @@ -436,6 +437,167 @@ def updatebudget(urlvar_budgetname): return render_template("updatebudget.html", income=income, budgeted=budgeted, budget=budget) +@app.route("/categories", methods=["GET", "POST"]) +@login_required +def categories(): + """Manage spending categories""" + + # User reached route via POST + if request.method == "POST": + + # Initialize user's actions + userHasSelected_newCategory = False + userHasSelected_renameCategory = False + userHasSelected_deleteCategory = False + + # Initialize user alerts + alert_newCategory = None + alert_renameCategory = None + alert_deleteCategory = None + + # Determine what action was selected by the user (button/form trick from: https://stackoverflow.com/questions/26217779/how-to-get-the-name-of-a-submitted-form-in-flask) + if "btnCreateCategory" in request.form: + userHasSelected_newCategory = True + elif "btnRenameCategory" in request.form: + userHasSelected_renameCategory = True + elif "btnDeleteCategory" in request.form: + userHasSelected_deleteCategory = True + else: + return apology("Doh! Spend Categories is drunk. Try again!") + + # Get new category details and create a new record in the DB + if userHasSelected_newCategory: + + # Get the new name provided by user + newCategoryName = request.form.get("createName").strip() + + # Check to see if the new name already exists in the database (None == does not exist) + categoryID = tendie_categories.getCategoryID(newCategoryName) + + # Category exists in the database already + if categoryID: + + # Make sure the user isn't trying to add a category they already have by passing in the users ID now (None == does not exists) + existingID = tendie_categories.getCategoryID( + newCategoryName, session["user_id"]) + if (existingID): + return apology("You already have '" + newCategoryName + "' category") + # Add the category to the users account + else: + tendie_categories.addCategory_User( + categoryID, session["user_id"]) + + # Category does not exist in the DB already - create a new category and then add it to the users account + else: + # Creates a new category in the DB + newCategoryID = tendie_categories.addCategory_DB( + newCategoryName) + + # Adds the category to the users account + tendie_categories.addCategory_User( + newCategoryID, session["user_id"]) + + # Set the alert message for user + alert_newCategory = newCategoryName + + # Get renamed category details and update records in the DB + if userHasSelected_renameCategory: + + # Get the new/old names provided by user + oldCategoryName = request.form.get("oldname").strip() + newCategoryName = request.form.get("newname").strip() + + # Check to see if the *old* category actually exists in the database (None == does not exist) + oldCategoryID = tendie_categories.getCategoryID(oldCategoryName) + + # Old category does not exists in the database, throw error + if oldCategoryID is None: + return apology("The category you're trying to rename doesn't exist") + + # Check to see if the *new* name already exists in the database (None == does not exist) + newCategoryID = tendie_categories.getCategoryID(newCategoryName) + + # Category exists in the database already + if newCategoryID: + + # Make sure the user isn't trying to rename to a category they already have by passing in the users ID now (None == does not exists) + existingID = tendie_categories.getCategoryID( + newCategoryName, session["user_id"]) + if existingID: + return apology("You already have '" + newCategoryName + "' category") + + # Get the new category name from the DB (prevents string upper/lowercase inconsistencies that can result from using the users input from the form instead of the DB) + newCategoryNameFromDB = tendie_categories.getSpendCategoryName( + newCategoryID) + + # Rename the category + tendie_categories.renameCategory( + oldCategoryID, newCategoryID, oldCategoryName, newCategoryNameFromDB, session["user_id"]) + + # Category does not exist in the DB already - create a new category and then add it to the users account + else: + # Creates a new category in the DB + newCategoryID = tendie_categories.addCategory_DB( + newCategoryName) + + # Rename the category + tendie_categories.renameCategory( + oldCategoryID, newCategoryID, oldCategoryName, newCategoryName, session["user_id"]) + + # Set the alert message for user + alert_renameCategory = [oldCategoryName, newCategoryName] + + # Get deleted category details and update records in the DB + if userHasSelected_deleteCategory: + + # Get the name of the category the user wants to delete + deleteName = request.form.get("delete").strip() + + # Check to see if the category actually exists in the database (None == does not exist) + categoryID = tendie_categories.getCategoryID(deleteName) + + # Category does not exists in the database, throw error + if categoryID is None: + return apology("The category you're trying to delete doesn't exist") + + # Get budgets that are currently using the category they want to delete + budgets = tendie_categories.getBudgetsFromSpendCategory( + categoryID, session["user_id"]) + + # Delete categories from the users budgets + if budgets: + tendie_categories.deleteSpendCategoriesInBudgets( + budgets, categoryID) + + # Delete the category from the users account + # TODO what should happen when a user deletes a category, and a budget that was using it now has NO categories checked? + tendie_categories.deleteCategory_User( + categoryID, session["user_id"]) + + # Set the alert message for user + alert_deleteCategory = deleteName + + # Get the users spend categories + categories = tendie_categories.getSpendCategories(session["user_id"]) + + return render_template("categories.html", categories=categories, newCategory=alert_newCategory, renamedCategory=alert_renameCategory, deleteCategory=alert_deleteCategory) + + # User reached route via GET + else: + # Get the users spend categories + categories = tendie_categories.getSpendCategories(session["user_id"]) + + # Get the budgets associated with each spend category + categoryBudgets = tendie_categories.getBudgetsSpendCategories( + session["user_id"]) + + # Generate a single data structure for storing all categories and their associated budgets + categoriesWithBudgets = tendie_categories.generateSpendCategoriesWithBudgets( + categories, categoryBudgets) + + return render_template("categories.html", categories=categoriesWithBudgets, newCategory=None, renamedCategory=None, deleteCategory=None) + + # Handle errors by rendering apology def errorhandler(e): """Handle error""" diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1401c612240f9b23899ace859917afd748ae9214 GIT binary patch literal 49347 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 Spend Categories +
+ {% if newCategory != None %} +
+ + Success! You created a new category called '{{ newCategory }}'. +
+ {% elif renamedCategory != None %} +
+ + Cya later '{{ renamedCategory[0] }}' 👋 You renamed '{{ renamedCategory[0] }}' to '{{ renamedCategory[1] }}'. +
+ {% elif deleteCategory != None %} +
+ + POOF! You deleted the category '{{ deleteCategory }}'. +
+ {% endif %} +
+
+
+
+
Create a new spending category
+

Categorize your common expenses to help track spending.

+

+ +

+
+
+
+
+ + +
+

+
+
+
+
+
+
+
+
Update existing categories
+ {% if categories %} + {% for category in categories %} +
+ + +
+

+ {% endfor %} + {% else %} +

You have not created any spending categories yet 😢

+ {% endif %} +
+
+
+
+ + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index 9f00db2..1453614 100644 --- a/templates/login.html +++ b/templates/login.html @@ -5,7 +5,11 @@ {% endblock %} {% block main %} - +

Tendie Tracker

+Every tender counts
+Tendie Tracker logo + +

Please sign in

@@ -14,6 +18,7 @@

Please sign in

+
{% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html index 8a9d2f8..7c4371e 100644 --- a/templates/register.html +++ b/templates/register.html @@ -5,7 +5,11 @@ {% endblock %} {% block main %} - +

Tendie Tracker

+Every tender counts
+Tendie Tracker logo + +

Register an account

{% if username %}
@@ -16,23 +20,21 @@
Oh snap!
{% endif %}
-
- - Your password is safe with us. We do not store plain-text passwords in our database ❤
+
-
-

Coming soon: sign-up with your Facebook/Twitter/Google account!

+ {# TODO 1) Password validation helper: https://www.w3schools.com/howto/howto_js_password_validation.asp diff --git a/tendie_budgets.py b/tendie_budgets.py index ad98b6e..86b6c7e 100644 --- a/tendie_budgets.py +++ b/tendie_budgets.py @@ -1,5 +1,5 @@ import re -import tendie_dashboard +import tendie_categories from cs50 import SQL from flask import request, session @@ -228,7 +228,7 @@ def isUniqueBudgetName(budgetName, budgetID, userID): def getUpdatableBudget(budget, userID): # Get the users library of spend categories - categories = tendie_dashboard.getSpendCategories(userID) + categories = tendie_categories.getSpendCategories(userID) # Get the budget's spend categories and % amount for each category budgetCategories = db.execute("SELECT DISTINCT categories.name, budgetCategories.amount FROM budgetCategories INNER JOIN categories ON budgetCategories.category_id = categories.id INNER JOIN budgets ON budgetCategories.budgets_id = budgets.id WHERE budgets.id = :budgetsID", diff --git a/tendie_categories.py b/tendie_categories.py new file mode 100644 index 0000000..fc41098 --- /dev/null +++ b/tendie_categories.py @@ -0,0 +1,174 @@ +from cs50 import SQL +from flask import request, session +from flask_session import Session + +# Configure CS50 Library to use SQLite database +db = SQL("sqlite:///budget.db") + + +# Gets and return the users spend categories +def getSpendCategories(userID): + categories = db.execute( + "SELECT categories.id, categories.name FROM userCategories INNER JOIN categories ON userCategories.category_id = categories.id WHERE userCategories.user_id = :usersID", + usersID=userID) + + return categories + + +# Get and return all spend categories from the category library +def getSpendCategoryLibrary(): + categories = db.execute("SELECT id, name FROM categories") + return categories + + +# Get and return the name of a category from the library +def getSpendCategoryName(categoryID): + name = db.execute( + "SELECT name FROM categories WHERE id = :categoryID", categoryID=categoryID) + + return name[0]["name"] + + +# Gets and return the users budgets, and for each budget the categories they've selected +def getBudgetsSpendCategories(userID): + budgetsWithCategories = db.execute("SELECT budgets.name AS 'BudgetName', categories.id AS 'CategoryID', categories.name AS 'CategoryName' FROM budgetCategories INNER JOIN budgets on budgetCategories.budgets_id = budgets.id INNER JOIN categories on budgetCategories.category_id = categories.id WHERE budgets.user_id = :usersID ORDER BY budgets.name, categories.name", + usersID=userID) + + return budgetsWithCategories + + +# Gets and returns the users budgets for a specific category ID +def getBudgetsFromSpendCategory(categoryID, userID): + budgets = db.execute("SELECT budgets.id AS 'BudgetID', budgets.name AS 'BudgetName', categories.id AS 'CategoryID', categories.name AS 'CategoryName' FROM budgetCategories INNER JOIN budgets on budgetCategories.budgets_id = budgets.id INNER JOIN categories on budgetCategories.category_id = categories.id WHERE budgets.user_id = :usersID AND budgetCategories.category_id = :categoryID ORDER BY budgets.name, categories.name", usersID=userID, categoryID=categoryID) + + return budgets + + +# Updates budgets where an old category needs to be replaced with a new one (e.g. renaming a category) +def updateSpendCategoriesInBudgets(budgets, oldCategoryID, newCategoryID): + for budget in budgets: + # Update existing budget record with the new category ID + db.execute("UPDATE budgetCategories SET category_id = :newID WHERE budgets_id = :budgetID AND category_id = :oldID", + newID=newCategoryID, budgetID=budget["BudgetID"], oldID=oldCategoryID) + + +# Updates budgets where a category needs to be deleted +def deleteSpendCategoriesInBudgets(budgets, categoryID): + for budget in budgets: + # Delete existing budget record with the old category ID + db.execute("DELETE FROM budgetCategories WHERE budgets_id = :budgetID AND category_id = :categoryID", + budgetID=budget["BudgetID"], categoryID=categoryID) + + +# Generates a ditionary containing all spend categories and the budgets associated with each category +def generateSpendCategoriesWithBudgets(categories, categoryBudgets): + categoriesWithBudgets = [] + + # Loop through every category + for category in categories: + # Build a dictionary to hold category ID + Name, and a list holding all the budgets which have that category selected + categoryWithBudget = {"id": None, "name": None, "budgets": []} + categoryWithBudget["id"] = category["id"] + categoryWithBudget["name"] = category["name"] + + # Insert the budget for the spend category if it exists + for budget in categoryBudgets: + if category["name"] == budget["CategoryName"]: + categoryWithBudget["budgets"].append(budget["BudgetName"]) + + # Add the completed dict to the list + categoriesWithBudgets.append(categoryWithBudget) + + return categoriesWithBudgets + + +# Checks if the category name exists in the 'library' or 'registrar' (categories table) - if so, return the ID for it so it can be passed to below add +def existsInLibrary(newName): + # Query the library for a record that matches the name + row = db.execute( + "SELECT * FROM categories WHERE LOWER(name) = :name", name=newName.lower()) + + if row: + return True + else: + return False + + +# Get category ID from DB +def getCategoryID(categoryName, userID=None): + # If no userID is supplied, then it's searching the category library + if userID is None: + categoryID = db.execute( + "SELECT id FROM categories WHERE LOWER(name) = :name", name=categoryName.lower()) + + if not categoryID: + return None + else: + return categoryID[0]["id"] + + # Otherwise search the users selection of categories + else: + categoryID = db.execute( + "SELECT categories.id FROM userCategories INNER JOIN categories ON userCategories.category_id = categories.id WHERE userCategories.user_id = :usersID AND LOWER(categories.name) = :name", usersID=userID, name=categoryName.lower()) + + if not categoryID: + return None + else: + return categoryID[0]["id"] + + +# Checks if the category name exists in the users seleciton of categories (userCategories table) - if so, just return as False? +def existsForUser(newName, userID): + # Query the library for a record that matches the name + row = db.execute( + "SELECT categories.id FROM userCategories INNER JOIN categories ON userCategories.category_id = categories.id WHERE userCategories.user_id = :usersID AND LOWER(categories.name) = :name", usersID=userID, name=newName.lower()) + + if row: + return True + else: + return False + + +# Adds a category to the database (but not to any specific users account) +def addCategory_DB(newName): + # Create a new record in categories table + categoryID = db.execute( + "INSERT INTO categories (name) VALUES (:name)", name=newName) + + return categoryID + + +# Adds a category to the users account +def addCategory_User(categoryID, userID): + db.execute("INSERT INTO userCategories (user_id, category_id) VALUES (:usersID, :categoryID)", + usersID=userID, categoryID=categoryID) + + +# Deletes a category from the users account +def deleteCategory_User(categoryID, userID): + db.execute("DELETE FROM userCategories WHERE user_id = :usersID AND category_id = :categoryID", + usersID=userID, categoryID=categoryID) + + +# Update just the spend categories of expense records (used for category renaming) +def updateExpenseCategoryNames(oldCategoryName, newCategoryName, userID): + db.execute("UPDATE expenses SET category = :newName WHERE user_id = :usersID AND category = :oldName", + newName=newCategoryName, usersID=userID, oldName=oldCategoryName) + + +# Rename a category +def renameCategory(oldCategoryID, newCategoryID, oldCategoryName, newCategoryName, userID): + # Add the renamed category to the users account + addCategory_User(newCategoryID, userID) + + # Delete the old category from their account + deleteCategory_User(oldCategoryID, userID) + + # Update users budgets (if any exist) that are using the old category to the new one + budgets = getBudgetsFromSpendCategory(oldCategoryID, userID) + + if budgets: + updateSpendCategoriesInBudgets(budgets, oldCategoryID, newCategoryID) + + # Update users expense records that are using the old category to the new one + updateExpenseCategoryNames(oldCategoryName, newCategoryName, userID) diff --git a/tendie_dashboard.py b/tendie_dashboard.py index 325407d..44cfeae 100644 --- a/tendie_dashboard.py +++ b/tendie_dashboard.py @@ -9,15 +9,6 @@ db = SQL("sqlite:///budget.db") -# Gets and return the users spend categories -def getSpendCategories(userID): - categories = db.execute( - "SELECT categories.name FROM userCategories INNER JOIN categories ON userCategories.category_id = categories.id WHERE userCategories.user_id = :usersID", - usersID=userID) - - return categories - - # Get the users total income def getIncome(userID): income = db.execute(