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 0000000..1401c61 Binary files /dev/null and b/static/logo.png differ diff --git a/templates/categories.html b/templates/categories.html new file mode 100644 index 0000000..ed2ba8f --- /dev/null +++ b/templates/categories.html @@ -0,0 +1,198 @@ +{% extends "layout.html" %} + +{% block title %} + Manage Spend Categories +{% endblock %} + +{% block main %} +

Manage 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(