From d8c8a858c4d7ebc0695ddee1c20fb33ac62b248e Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 16:09:34 +0100 Subject: [PATCH 01/32] =?UTF-8?q?feat:=20=F0=9F=86=95=20enhance=20user=20a?= =?UTF-8?q?nd=20category=20management=20with=20new=20routes=20and=20templa?= =?UTF-8?q?tes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_handler.py | 101 ++++++++++++++++++---------- src/front/home.py | 117 +++++++++++++++++++++++++++++---- src/front/templates/home.html | 56 +++++++++++++--- src/tests/test_data_handler.py | 49 ++++++++++++++ 4 files changed, 267 insertions(+), 56 deletions(-) diff --git a/src/data_handler.py b/src/data_handler.py index 1e2448c..573c540 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -42,23 +42,6 @@ def execute_sql(self, sql_statement: str, return_id: bool = False) -> (bool, int else: return is_success - def read_database(self, sql_statement: str): - "reads the database query and returns the results" - logging.debug(f"received request to read {sql_statement}") - results = 0 - try: - connection = sqlite3.connect(self.database) - logging.debug("opened connection to database") - cursor = connection.cursor() - results = cursor.execute(sql_statement, {}) - results = results.fetchall() - logging.debug(f"query returned: {results}") - except Exception as e: - logging.error(f"Read error occured: {e}") - finally: - connection.close() - return results - @staticmethod def _build_column(column: dict) -> str: logging.debug( @@ -147,28 +130,21 @@ def print_table_schema(self, table_name): logger.error(e) def add_user_name(self, user_name: str) -> int: - "takes user name string, returns user id" - logger.debug( - f"attempting to insert values into user table {user_name}" - ) + logger.debug(f"attempting to insert values into user table {user_name}") try: connection = sqlite3.connect(self.database) - logger.debug("connection to db open") cursor = connection.cursor() - logger.debug("cursor activated") cursor.execute( - """INSERT INTO user ( - name - ) - VALUES (?) - """, + """ + INSERT INTO user ( + name + ) + VALUES (?) + """, (user_name,), ) - logger.debug("cursor executed") connection.commit() user_id = cursor.lastrowid - logger.debug(f"insert attempt seems successful, user id is {user_id}") - if user_id is None: user_id = 0 except Exception as e: @@ -184,6 +160,63 @@ def add_user_name(self, user_name: str) -> int: return 0 def get_users(self) -> list: - get_user_sql = "SELECT name FROM user" - users = self.read_database(get_user_sql) - return users + get_user_sql = "SELECT id, name FROM user" + return self.read_database(get_user_sql) + + def add_category(self, user_id: int, category_name: str, budget_value=None, parent_id=None) -> int: + """Inserts a new category. Returns the category id.""" + logger.debug(f"attempting to insert category '{category_name}' for user {user_id}") + try: + connection = sqlite3.connect(self.database) + cursor = connection.cursor() + cursor.execute( + """ + INSERT INTO category (user_id, name, budget_value, parent_id) + VALUES (?, ?, ?, ?) + """, + (user_id, category_name, budget_value, parent_id), + ) + connection.commit() + category_id = cursor.lastrowid or 0 + except Exception as e: + logger.error(f"Failed to insert category name, error: {e}") + category_id = 0 + finally: + try: + connection.close() + except Exception as e: + logger.error(f"failed to close connection: {e}") + return category_id + + def get_categories(self, user_id: int) -> list: + """Returns all categories for a given user.""" + try: + connection = sqlite3.connect(self.database) + cursor = connection.cursor() + sql = "SELECT id, name, budget_value, parent_id FROM category WHERE user_id = ?" + cursor.execute(sql, (user_id,)) + categories = cursor.fetchall() + except Exception as e: + logger.error(f"Failed to fetch categories: {e}") + categories = [] + finally: + try: + connection.close() + except Exception as e: + logger.error(f"failed to close connection: {e}") + return categories + + def read_database(self, sql_statement: str, params=()) -> list: + logging.debug(f"received request to read {sql_statement}") + results = [] + try: + connection = sqlite3.connect(self.database) + cursor = connection.cursor() + results = cursor.execute(sql_statement, params) + results = results.fetchall() + logging.debug(f"query returned: {results}") + except Exception as e: + logging.error(f"Read error occurred: {e}") + finally: + connection.close() + return results \ No newline at end of file diff --git a/src/front/home.py b/src/front/home.py index f452074..8022e2c 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -16,25 +16,116 @@ def home_page(): logger.info("home page hit") db_call = data_handler.database(verity_config) users = db_call.get_users() - return render_template("home.html", users=users) - + # Get user info directly from session - we store both ID and name there + user_id = session.get("user_id") + selected_user_name = session.get("user_name") + + # Debug log to see what's being passed to the template + logger.debug(f"User ID from session: {user_id}, User Name from session: {selected_user_name}") + + categories = db_call.get_categories(user_id) if user_id else [] + return render_template( + "home.html", + users=users, + categories=categories, + selected_user_id=user_id, + selected_user_name=selected_user_name + ) @home_bp.route("/submit", methods=["POST"]) def submit_user_name(): user_name = request.form.get("userName") logger.debug(f"Request Received: {request.form}") - if user_name: - logger.info(f"User submitted new user name: {user_name}") - session["user_name"] = user_name - db_call = data_handler.database(verity_config) - user_id = db_call.add_user_name(user_name) - if user_id == 0: - # db entry failed, throw error message - flash("User Name not saved, please check the logs", "error") + if not user_name: + flash("Please enter a user name.", "error") + return redirect(url_for("home.home_page")) + + db_call = data_handler.database(verity_config) + # Check if user already exists + users = db_call.get_users() + for user in users: + # users is [(id, name)] or [(name,)] + if (len(user) > 1 and user[1] == user_name) or (user[0] == user_name): + flash("A user with that name already exists.", "error") return redirect(url_for("home.home_page")) - session["user_id"] = user_id - flash("user name saved!", "success") + + logger.info(f"User submitted new user name: {user_name}") + user_id = db_call.add_user_name(user_name) + if user_id == 0: + flash("User Name not saved, please check the logs", "error") + return redirect(url_for("home.home_page")) + flash("user name saved! Now select the user from the dropdown to continue.", "success") + return redirect(url_for("home.home_page")) + +@home_bp.route("/select_user", methods=["POST"]) +def select_user(): + selected_user_id = request.form.get("selectedUserId") + if not selected_user_id: + flash("No user selected", "error") + session.pop("user_id", None) + session.pop("user_name", None) return redirect(url_for("home.home_page")) + + # Get the user details directly from the database using the ID + db_call = data_handler.database(verity_config) + result = db_call.read_database("SELECT id, name FROM user WHERE id = ?", (selected_user_id,)) + + if result and len(result) > 0: + user_id = result[0][0] + selected_user_name = result[0][1] + logger.debug(f"Found user with ID {user_id} and name {selected_user_name}") + + # Store both ID and name in the session + session["user_id"] = int(user_id) + session["user_name"] = selected_user_name + logger.debug(f"User selected - ID: {user_id}, Name: {selected_user_name}") + flash("User selected!", "success") else: - flash("Please enter a user name.", "error") + session.pop("user_id", None) + session.pop("user_name", None) + flash("User not found!", "error") + + return redirect(url_for("home.home_page")) + +@home_bp.route("/submit_category", methods=["POST"]) +def submit_category(): + user_id = session.get("user_id") + if not user_id: + flash("No user selected!", "error") + return redirect(url_for("home.home_page")) + + # Get and validate category name (required) + category_name = request.form.get("categoryName") + if not category_name: + flash("Category name is required", "error") return redirect(url_for("home.home_page")) + + # Convert budget_value to float if provided, otherwise None + budget_value_str = request.form.get("budgetValue") + budget_value = None + if budget_value_str and budget_value_str.strip(): + try: + budget_value = float(budget_value_str) + except ValueError: + logger.error(f"Invalid budget value: {budget_value_str}") + flash("Invalid budget value - please enter a valid number", "error") + return redirect(url_for("home.home_page")) + + # Convert parent_id to int if provided, otherwise None + parent_id_str = request.form.get("parentId") + parent_id = None + if parent_id_str and parent_id_str.strip(): + try: + parent_id = int(parent_id_str) + except ValueError: + logger.error(f"Invalid parent ID: {parent_id_str}") + flash("Invalid parent category ID", "error") + return redirect(url_for("home.home_page")) + + db_call = data_handler.database(verity_config) + category_id = db_call.add_category(user_id, category_name, budget_value, parent_id) + if category_id == 0: + flash("Category not saved, please check the logs", "error") + else: + flash("Category saved!", "success") + return redirect(url_for("home.home_page")) \ No newline at end of file diff --git a/src/front/templates/home.html b/src/front/templates/home.html index 5f5d5ed..c7bbe2d 100644 --- a/src/front/templates/home.html +++ b/src/front/templates/home.html @@ -1,25 +1,63 @@ -{% extends "base.html" %} {% block title %} Home {% endblock %} {% block content -%} +{% extends "base.html" %} +{% block title %} Home {% endblock %} +{% block content %}

Home of Verity

-

New User:

+

Create New User:

- + +
+
+ +
+
+

Select User:

+ +
+
+ +
+
+

New Category:

+ + + +

- {% for user in users %} -- {{ user[0] }}
+ Your Categories:
+ {% for category in categories %} + - {{ category[1] }}{% if category[3] %} (Sub of ID {{ category[3] }}){% endif %}{% if category[2] %} [Budget: {{ category[2] }}]{% endif %}
{% endfor %}

-{% endblock %} +{% else %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/tests/test_data_handler.py b/src/tests/test_data_handler.py index 85f8f01..087fc4b 100644 --- a/src/tests/test_data_handler.py +++ b/src/tests/test_data_handler.py @@ -57,3 +57,52 @@ def test_get_users(test_db_call): # Check the contents of the list # self.assertIsNotNone(users) # self.assertIsInstance(users[0], str) + + +def test_add_category_success(test_db_call): + # Add a user first, since category requires a user_id + user_id = test_db_call.add_user_name("CategoryTestUser") + assert user_id != 0 + # Add a category with all fields + category_id = test_db_call.add_category(user_id, "Groceries", 200.0, None) + assert category_id != 0 + # Add a category with only required fields + category_id2 = test_db_call.add_category(user_id, "Utilities") + assert category_id2 != 0 + + +def test_add_category_null_budget_and_parent(test_db_call): + user_id = test_db_call.add_user_name("NullBudgetParentUser") + assert user_id != 0 + # Add a category with None for budget_value and parent_id + category_id = test_db_call.add_category(user_id, "NoBudgetOrParent", None, None) + assert category_id != 0 + + +def test_add_category_and_get_categories(test_db_call): + # Add a user + user_name = "CategoryTestUser" + user_id = test_db_call.add_user_name(user_name) + assert user_id != 0 + # Add a category for this user + category_name = "Groceries" + budget_value = 100.0 + parent_id = None + category_id = test_db_call.add_category(user_id, category_name, budget_value, parent_id) + assert category_id != 0 + # Add a subcategory + subcategory_name = "Supermarket" + subcategory_id = test_db_call.add_category(user_id, subcategory_name, 50.0, category_id) + assert subcategory_id != 0 + # Retrieve categories for this user + categories = test_db_call.get_categories(user_id) + assert isinstance(categories, list) + names = [cat[1] for cat in categories] + assert category_name in names + assert subcategory_name in names + + +def test_add_category_invalid_user(test_db_call): + # Try to add a category with a non-existent user_id (should still succeed in SQLite unless foreign keys are enforced) + category_id = test_db_call.add_category(99999, "InvalidUserCategory") + assert isinstance(category_id, int) From a3ebc8bd1fc0aa6aa9f3f8cd91b6bed33702409c Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 16:23:01 +0100 Subject: [PATCH 02/32] =?UTF-8?q?feat:=20=F0=9F=86=95=20enhance=20test=20c?= =?UTF-8?q?overage=20for=20database=20operations=20with=20new=20test=20cas?= =?UTF-8?q?es=20and=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tests/test_data_handler.py | 110 ++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/src/tests/test_data_handler.py b/src/tests/test_data_handler.py index 087fc4b..64bb86c 100644 --- a/src/tests/test_data_handler.py +++ b/src/tests/test_data_handler.py @@ -1,4 +1,5 @@ import pytest +import os from src import data_handler from src.config import VerityConfig @@ -6,57 +7,132 @@ @pytest.fixture def test_db_call(): + """Create a test database instance with a unique test database file""" config = VerityConfig() + # Use a test-specific database file + test_db_file = "test_verity.db" + # Remove the test DB if it already exists + if os.path.exists(test_db_file): + os.remove(test_db_file) + + # Create a config with the test DB + config.DATABASE = test_db_file db_call = data_handler.database(config) db_call.build_database() yield db_call + # Cleanup after tests + if os.path.exists(test_db_file): + os.remove(test_db_file) def test_execute_sql_success(test_db_call): + """Test executing valid SQL statements""" sql_statement = """INSERT INTO user ( name ) VALUES ('a_user_name') """ result = test_db_call.execute_sql(sql_statement) - assert result + assert result is True def test_execute_sql_error(test_db_call): + """Test executing invalid SQL statements""" sql_statement = "SELECT * FROM non_existent_table" result = test_db_call.execute_sql(sql_statement) - assert not result - # Check if an error message is logged - # You might need to add a logging assertion here + assert result is False + + +def test_execute_sql_with_return_id(test_db_call): + """Test executing SQL with return_id flag""" + sql_statement = """INSERT INTO user ( + name + ) + VALUES ('return_id_test') + """ + result, new_id = test_db_call.execute_sql(sql_statement, return_id=True) + assert result is True + assert new_id > 0 def test_read_database(test_db_call): - # Test reading database data - sql_statement = "SELECT name FROM user" + """Test reading database data""" + # First insert a user + test_db_call.add_user_name("read_test_user") + # Then read it back + sql_statement = "SELECT id, name FROM user WHERE name = 'read_test_user'" results = test_db_call.read_database(sql_statement) assert isinstance(results, list) - # Check the contents of the list - # self.assertIsNotNone(results) - # self.assertIsInstance(results[0], str) + assert len(results) > 0 + assert results[0][1] == "read_test_user" + + +def test_read_database_with_params(test_db_call): + """Test reading database with parameterized query""" + # First insert a user + test_db_call.add_user_name("param_test_user") + # Then read it back with params + sql_statement = "SELECT id, name FROM user WHERE name = ?" + params = ("param_test_user",) + results = test_db_call.read_database(sql_statement, params) + assert isinstance(results, list) + assert len(results) > 0 + assert results[0][1] == "param_test_user" + + +def test_build_database(test_db_call): + """Test that the database builds correctly with all required tables""" + # Check if all tables have been created + tables_to_check = ["user", "account", "account_type", "party", "transaction_type", + "category", "transaction_log"] + for table in tables_to_check: + sql = f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}';" + results = test_db_call.read_database(sql) + assert len(results) == 1, f"Table '{table}' should exist" def test_add_user_name(test_db_call): - # Test adding a new user name + """Test adding a new user name""" user_name = "Test user" user_id = test_db_call.add_user_name(user_name) - assert user_id is not None + assert user_id > 0 + # Check if the user name was actually added - # You might need to query the database to verify - # self.assertEqual(user_id, 1) + sql = "SELECT id, name FROM user WHERE name = ?" + results = test_db_call.read_database(sql, (user_name,)) + assert len(results) == 1 + assert results[0][1] == user_name + + +def test_add_duplicate_user_name(test_db_call): + """Test adding a duplicate user name (should succeed at DB level without constraints)""" + user_name = "Duplicate User" + # Add first user + user_id1 = test_db_call.add_user_name(user_name) + assert user_id1 > 0 + + # Add second user with same name + user_id2 = test_db_call.add_user_name(user_name) + assert user_id2 > 0 + assert user_id2 != user_id1 # They should have different IDs def test_get_users(test_db_call): - # Test getting users + """Test getting all users""" + # Add some test users first + test_db_call.add_user_name("User One") + test_db_call.add_user_name("User Two") + + # Get users users = test_db_call.get_users() assert isinstance(users, list) - # Check the contents of the list - # self.assertIsNotNone(users) - # self.assertIsInstance(users[0], str) + assert len(users) >= 2 + + # Check that users are returned as (id, name) tuples + for user in users: + assert len(user) == 2 + assert isinstance(user[0], int) # ID + assert isinstance(user[1], str) # Name def test_add_category_success(test_db_call): From e767f5ceec479f3b41a3ffac1cf7a1ff74e69189 Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 16:37:44 +0100 Subject: [PATCH 03/32] =?UTF-8?q?feat:=20=F0=9F=86=95=20add=20docstring=20?= =?UTF-8?q?to=20read=5Fdatabase=20method=20for=20clarity=20on=20functional?= =?UTF-8?q?ity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data_handler.py b/src/data_handler.py index 573c540..38e71fd 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -207,6 +207,7 @@ def get_categories(self, user_id: int) -> list: return categories def read_database(self, sql_statement: str, params=()) -> list: + "reads the database query and returns the results" logging.debug(f"received request to read {sql_statement}") results = [] try: From d58a68e9143b51b165a8cfa4c0331b7dd88daaf6 Mon Sep 17 00:00:00 2001 From: Alex Dimmock <50240483+AtomicAlexD@users.noreply.github.com> Date: Wed, 28 May 2025 16:40:58 +0100 Subject: [PATCH 04/32] Update src/front/home.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/front/home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/home.py b/src/front/home.py index 8022e2c..d9fd6a1 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -45,7 +45,7 @@ def submit_user_name(): users = db_call.get_users() for user in users: # users is [(id, name)] or [(name,)] - if (len(user) > 1 and user[1] == user_name) or (user[0] == user_name): + if len(user) > 1 and user[1] == user_name: flash("A user with that name already exists.", "error") return redirect(url_for("home.home_page")) From 1f0aa8d0b94eebcc654b793760044f8170e745ff Mon Sep 17 00:00:00 2001 From: Alex Dimmock <50240483+AtomicAlexD@users.noreply.github.com> Date: Wed, 28 May 2025 16:41:43 +0100 Subject: [PATCH 05/32] Update src/front/home.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/front/home.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/front/home.py b/src/front/home.py index d9fd6a1..3944958 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -65,7 +65,15 @@ def select_user(): session.pop("user_id", None) session.pop("user_name", None) return redirect(url_for("home.home_page")) - + + # Convert selected_user_id to int and handle invalid input + try: + selected_user_id = int(selected_user_id) + except ValueError: + logger.error(f"Invalid user ID: {selected_user_id}") + flash("Invalid user ID", "error") + return redirect(url_for("home.home_page")) + # Get the user details directly from the database using the ID db_call = data_handler.database(verity_config) result = db_call.read_database("SELECT id, name FROM user WHERE id = ?", (selected_user_id,)) From 4cc647b018060cd74a5365d99faea3ee48ad7f63 Mon Sep 17 00:00:00 2001 From: Alex Dimmock <50240483+AtomicAlexD@users.noreply.github.com> Date: Wed, 28 May 2025 16:45:50 +0100 Subject: [PATCH 06/32] Update src/tests/test_data_handler.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/tests/test_data_handler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tests/test_data_handler.py b/src/tests/test_data_handler.py index 64bb86c..772fba9 100644 --- a/src/tests/test_data_handler.py +++ b/src/tests/test_data_handler.py @@ -10,10 +10,8 @@ def test_db_call(): """Create a test database instance with a unique test database file""" config = VerityConfig() # Use a test-specific database file - test_db_file = "test_verity.db" - # Remove the test DB if it already exists - if os.path.exists(test_db_file): - os.remove(test_db_file) + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as temp_db: + test_db_file = temp_db.name # Create a config with the test DB config.DATABASE = test_db_file From f12371b7b919da221a96160d9ce1357e98f2fba1 Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 16:46:30 +0100 Subject: [PATCH 07/32] =?UTF-8?q?feat:=20=F0=9F=86=95=20add=20tempfile=20i?= =?UTF-8?q?mport=20for=20handling=20temporary=20files=20in=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tests/test_data_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/test_data_handler.py b/src/tests/test_data_handler.py index 772fba9..9a40d62 100644 --- a/src/tests/test_data_handler.py +++ b/src/tests/test_data_handler.py @@ -1,5 +1,6 @@ import pytest import os +import tempfile from src import data_handler from src.config import VerityConfig From 747d28175e608b1d2a373120691e955e9fc8a649 Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 16:55:31 +0100 Subject: [PATCH 08/32] =?UTF-8?q?fix:=20=F0=9F=90=9B=20add=20conditional?= =?UTF-8?q?=20check=20for=20selected=5Fuser=5Fid=20before=20displaying=20c?= =?UTF-8?q?ategory=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/templates/home.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/front/templates/home.html b/src/front/templates/home.html index c7bbe2d..667cec8 100644 --- a/src/front/templates/home.html +++ b/src/front/templates/home.html @@ -32,6 +32,8 @@

Home of Verity

+ +{% if selected_user_id %}
From 566d4162bdea595cc117702f946fddff66d9848c Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 16:56:30 +0100 Subject: [PATCH 09/32] =?UTF-8?q?fix:=20=F0=9F=90=9B=20reorder=20import=20?= =?UTF-8?q?statements=20for=20consistency=20in=20home.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/home.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/front/home.py b/src/front/home.py index 3944958..23703ac 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -1,5 +1,4 @@ -from flask import Blueprint, render_template -from flask import flash, redirect, url_for, session, request +from flask import flash, redirect, url_for, session, request, Blueprint, render_template import logging import data_handler From 5341640a748689698f01a26ee69dde4105a0bd87 Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 16:59:43 +0100 Subject: [PATCH 10/32] =?UTF-8?q?fix:=20=F0=9F=90=9B=20update=20route=20na?= =?UTF-8?q?me=20for=20user=20submission=20to=20be=20more=20descriptive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/home.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/front/home.py b/src/front/home.py index 23703ac..d9c8c7e 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -31,7 +31,7 @@ def home_page(): selected_user_name=selected_user_name ) -@home_bp.route("/submit", methods=["POST"]) +@home_bp.route("/submit_user", methods=["POST"]) def submit_user_name(): user_name = request.form.get("userName") logger.debug(f"Request Received: {request.form}") From 9544a6975d6b2a0f37c195f1aadcc5a801defd56 Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 17:11:06 +0100 Subject: [PATCH 11/32] =?UTF-8?q?fix:=20=F0=9F=90=9B=20streamline=20user?= =?UTF-8?q?=20existence=20check=20and=20improve=20session=20handling=20in?= =?UTF-8?q?=20home.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/front/home.py | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/front/home.py b/src/front/home.py index d9c8c7e..85bde40 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -15,7 +15,7 @@ def home_page(): logger.info("home page hit") db_call = data_handler.database(verity_config) users = db_call.get_users() - # Get user info directly from session - we store both ID and name there + # Get user info directly from session user_id = session.get("user_id") selected_user_name = session.get("user_name") @@ -40,19 +40,18 @@ def submit_user_name(): return redirect(url_for("home.home_page")) db_call = data_handler.database(verity_config) - # Check if user already exists - users = db_call.get_users() - for user in users: - # users is [(id, name)] or [(name,)] - if len(user) > 1 and user[1] == user_name: - flash("A user with that name already exists.", "error") - return redirect(url_for("home.home_page")) + # More efficient check for existing user + exists = db_call.read_database("SELECT 1 FROM user WHERE name = ?", (user_name,)) + if exists: + flash("A user with that name already exists.", "error") + return redirect(url_for("home.home_page")) logger.info(f"User submitted new user name: {user_name}") user_id = db_call.add_user_name(user_name) if user_id == 0: flash("User Name not saved, please check the logs", "error") return redirect(url_for("home.home_page")) + flash("user name saved! Now select the user from the dropdown to continue.", "success") return redirect(url_for("home.home_page")) @@ -75,17 +74,12 @@ def select_user(): # Get the user details directly from the database using the ID db_call = data_handler.database(verity_config) - result = db_call.read_database("SELECT id, name FROM user WHERE id = ?", (selected_user_id,)) + result = db_call.read_database("SELECT name FROM user WHERE id = ?", (selected_user_id,)) - if result and len(result) > 0: - user_id = result[0][0] - selected_user_name = result[0][1] - logger.debug(f"Found user with ID {user_id} and name {selected_user_name}") - + if result and result[0]: # Store both ID and name in the session - session["user_id"] = int(user_id) - session["user_name"] = selected_user_name - logger.debug(f"User selected - ID: {user_id}, Name: {selected_user_name}") + session["user_id"] = selected_user_id + session["user_name"] = result[0][0] flash("User selected!", "success") else: session.pop("user_id", None) @@ -107,10 +101,11 @@ def submit_category(): flash("Category name is required", "error") return redirect(url_for("home.home_page")) - # Convert budget_value to float if provided, otherwise None - budget_value_str = request.form.get("budgetValue") + # Handle form values more concisely + # Convert budget_value to float if provided budget_value = None - if budget_value_str and budget_value_str.strip(): + budget_value_str = request.form.get("budgetValue", "").strip() + if budget_value_str: try: budget_value = float(budget_value_str) except ValueError: @@ -118,10 +113,10 @@ def submit_category(): flash("Invalid budget value - please enter a valid number", "error") return redirect(url_for("home.home_page")) - # Convert parent_id to int if provided, otherwise None - parent_id_str = request.form.get("parentId") + # Convert parent_id to int if provided parent_id = None - if parent_id_str and parent_id_str.strip(): + parent_id_str = request.form.get("parentId", "").strip() + if parent_id_str: try: parent_id = int(parent_id_str) except ValueError: From 1fc7afcb7e1f8789b279efd7fe95073f68bb6e9c Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 17:18:04 +0100 Subject: [PATCH 12/32] =?UTF-8?q?fix:=20=F0=9F=90=9B=20put=20the=20read=5F?= =?UTF-8?q?database=20back=20where=20it=20belongs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_handler.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/data_handler.py b/src/data_handler.py index 38e71fd..85aad98 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -41,7 +41,23 @@ def execute_sql(self, sql_statement: str, return_id: bool = False) -> (bool, int return (is_success, new_id) else: return is_success - + + def read_database(self, sql_statement: str, params=()) -> list: + "reads the database query and returns the results" + logging.debug(f"received request to read {sql_statement}") + results = [] + try: + connection = sqlite3.connect(self.database) + cursor = connection.cursor() + results = cursor.execute(sql_statement, params) + results = results.fetchall() + logging.debug(f"query returned: {results}") + except Exception as e: + logging.error(f"Read error occurred: {e}") + finally: + connection.close() + return results + @staticmethod def _build_column(column: dict) -> str: logging.debug( @@ -204,20 +220,4 @@ def get_categories(self, user_id: int) -> list: connection.close() except Exception as e: logger.error(f"failed to close connection: {e}") - return categories - - def read_database(self, sql_statement: str, params=()) -> list: - "reads the database query and returns the results" - logging.debug(f"received request to read {sql_statement}") - results = [] - try: - connection = sqlite3.connect(self.database) - cursor = connection.cursor() - results = cursor.execute(sql_statement, params) - results = results.fetchall() - logging.debug(f"query returned: {results}") - except Exception as e: - logging.error(f"Read error occurred: {e}") - finally: - connection.close() - return results \ No newline at end of file + return categories \ No newline at end of file From 1527857777d3be9e3e9811224b6f9096904efd5f Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Wed, 28 May 2025 17:27:32 +0100 Subject: [PATCH 13/32] =?UTF-8?q?fix:=20=F0=9F=90=9B=20add=20docstrings=20?= =?UTF-8?q?for=20add=5Fuser=5Fname=20and=20get=5Fusers=20methods=20in=20da?= =?UTF-8?q?tabase=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_handler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data_handler.py b/src/data_handler.py index 85aad98..0e169b7 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -146,6 +146,7 @@ def print_table_schema(self, table_name): logger.error(e) def add_user_name(self, user_name: str) -> int: + "takes user name string, returns user id" logger.debug(f"attempting to insert values into user table {user_name}") try: connection = sqlite3.connect(self.database) @@ -176,6 +177,7 @@ def add_user_name(self, user_name: str) -> int: return 0 def get_users(self) -> list: + """Returns all users in the database.""" get_user_sql = "SELECT id, name FROM user" return self.read_database(get_user_sql) From 0007c26de1a44a029eaf7cc80b8278c6cf52a662 Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Sat, 31 May 2025 17:30:20 +0100 Subject: [PATCH 14/32] feat: add new task for fixing code with Ruff and update tasks.json; improve database methods and tests --- .vscode/tasks.json | 19 +++++++++++++++++-- pyproject.toml | 1 + src/data_handler.py | 13 +++++++------ src/front/home.py | 28 ++++++++++++++-------------- src/tests/test_data_handler.py | 18 ++++++++++-------- 5 files changed, 49 insertions(+), 30 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d4db1b4..2d57213 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -61,8 +61,7 @@ "clear": true, "focus": true }, "problemMatcher": [] - }, - { + }, { "label": "UV Sync ", "type": "shell", "command": "uv sync", @@ -77,6 +76,22 @@ "focus": true }, "problemMatcher": [] + }, + { + "label": "Fix Code with Ruff", + "type": "shell", + "command": "ruff check --fix .", + "options": { + "cwd": "${workspaceFolder}/src" + }, + "group": "none", + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true, + "focus": true + }, + "problemMatcher": [] } ] } diff --git a/pyproject.toml b/pyproject.toml index 4804f36..d178565 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ plugins.jedi_definition.enabled = true [tool.ruff] line-length = 100 target-version = "py313" +fix = true [tool.ruff.lint] select = ["E", "F", "I", "W"] # Error, F: PyFlakes, I: isort, W: pycodestyle warnings diff --git a/src/data_handler.py b/src/data_handler.py index ef6e14d..f3d9677 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -41,7 +41,7 @@ def execute_sql(self, sql_statement: str, return_id: bool = False) -> (bool, int return (is_success, new_id) else: return is_success - + def read_database(self, sql_statement: str, params=()) -> list: "reads the database query and returns the results" logging.debug(f"received request to read {sql_statement}") @@ -57,7 +57,7 @@ def read_database(self, sql_statement: str, params=()) -> list: finally: connection.close() return results - + @staticmethod def _build_column(column: dict) -> str: logging.debug( @@ -179,9 +179,10 @@ def add_user_name(self, user_name: str) -> int: def get_users(self) -> list: """Returns all users in the database.""" get_user_sql = "SELECT id, name FROM user" - return self.read_database(get_user_sql) - - def add_category(self, user_id: int, category_name: str, budget_value=None, parent_id=None) -> int: + return self.read_database(get_user_sql) + + def add_category(self, user_id: int, category_name: str,budget_value: int = 0, + parent_id = None) -> int: """Inserts a new category. Returns the category id.""" logger.debug(f"attempting to insert category '{category_name}' for user {user_id}") try: @@ -222,4 +223,4 @@ def get_categories(self, user_id: int) -> list: connection.close() except Exception as e: logger.error(f"failed to close connection: {e}") - return categories \ No newline at end of file + return categories diff --git a/src/front/home.py b/src/front/home.py index ce6d45d..9d75d1f 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -17,12 +17,12 @@ def home_page(): db_call = data_handler.database(verity_config) users = db_call.get_users() # Get user info directly from session - user_id = session.get("user_id") + user_id = session.get("user_id") selected_user_name = session.get("user_name") - + # Debug log to see what's being passed to the template logger.debug(f"User ID from session: {user_id}, User Name from session: {selected_user_name}") - + categories = db_call.get_categories(user_id) if user_id else [] return render_template( "home.html", @@ -52,10 +52,10 @@ def submit_user_name(): if user_id == 0: flash("User Name not saved, please check the logs", "error") return redirect(url_for("home.home_page")) - + flash("user name saved! Now select the user from the dropdown to continue.", "success") return redirect(url_for("home.home_page")) - + @home_bp.route("/select_user", methods=["POST"]) def select_user(): selected_user_id = request.form.get("selectedUserId") @@ -64,7 +64,7 @@ def select_user(): session.pop("user_id", None) session.pop("user_name", None) return redirect(url_for("home.home_page")) - + # Convert selected_user_id to int and handle invalid input try: selected_user_id = int(selected_user_id) @@ -72,11 +72,11 @@ def select_user(): logger.error(f"Invalid user ID: {selected_user_id}") flash("Invalid user ID", "error") return redirect(url_for("home.home_page")) - + # Get the user details directly from the database using the ID db_call = data_handler.database(verity_config) result = db_call.read_database("SELECT name FROM user WHERE id = ?", (selected_user_id,)) - + if result and result[0]: # Store both ID and name in the session session["user_id"] = selected_user_id @@ -86,7 +86,7 @@ def select_user(): session.pop("user_id", None) session.pop("user_name", None) flash("User not found!", "error") - + return redirect(url_for("home.home_page")) @home_bp.route("/submit_category", methods=["POST"]) @@ -95,13 +95,13 @@ def submit_category(): if not user_id: flash("No user selected!", "error") return redirect(url_for("home.home_page")) - + # Get and validate category name (required) category_name = request.form.get("categoryName") if not category_name: flash("Category name is required", "error") return redirect(url_for("home.home_page")) - + # Handle form values more concisely # Convert budget_value to float if provided budget_value = None @@ -113,7 +113,7 @@ def submit_category(): logger.error(f"Invalid budget value: {budget_value_str}") flash("Invalid budget value - please enter a valid number", "error") return redirect(url_for("home.home_page")) - + # Convert parent_id to int if provided parent_id = None parent_id_str = request.form.get("parentId", "").strip() @@ -124,11 +124,11 @@ def submit_category(): logger.error(f"Invalid parent ID: {parent_id_str}") flash("Invalid parent category ID", "error") return redirect(url_for("home.home_page")) - + db_call = data_handler.database(verity_config) category_id = db_call.add_category(user_id, category_name, budget_value, parent_id) if category_id == 0: flash("Category not saved, please check the logs", "error") else: flash("Category saved!", "success") - return redirect(url_for("home.home_page")) \ No newline at end of file + return redirect(url_for("home.home_page")) diff --git a/src/tests/test_data_handler.py b/src/tests/test_data_handler.py index 9a40d62..becf155 100644 --- a/src/tests/test_data_handler.py +++ b/src/tests/test_data_handler.py @@ -1,7 +1,8 @@ -import pytest import os import tempfile +import pytest + from src import data_handler from src.config import VerityConfig @@ -13,7 +14,7 @@ def test_db_call(): # Use a test-specific database file with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as temp_db: test_db_file = temp_db.name - + # Create a config with the test DB config.DATABASE = test_db_file db_call = data_handler.database(config) @@ -82,7 +83,7 @@ def test_read_database_with_params(test_db_call): def test_build_database(test_db_call): """Test that the database builds correctly with all required tables""" # Check if all tables have been created - tables_to_check = ["user", "account", "account_type", "party", "transaction_type", + tables_to_check = ["user", "account", "account_type", "party", "transaction_type", "category", "transaction_log"] for table in tables_to_check: sql = f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}';" @@ -95,7 +96,7 @@ def test_add_user_name(test_db_call): user_name = "Test user" user_id = test_db_call.add_user_name(user_name) assert user_id > 0 - + # Check if the user name was actually added sql = "SELECT id, name FROM user WHERE name = ?" results = test_db_call.read_database(sql, (user_name,)) @@ -109,7 +110,7 @@ def test_add_duplicate_user_name(test_db_call): # Add first user user_id1 = test_db_call.add_user_name(user_name) assert user_id1 > 0 - + # Add second user with same name user_id2 = test_db_call.add_user_name(user_name) assert user_id2 > 0 @@ -121,12 +122,12 @@ def test_get_users(test_db_call): # Add some test users first test_db_call.add_user_name("User One") test_db_call.add_user_name("User Two") - + # Get users users = test_db_call.get_users() assert isinstance(users, list) assert len(users) >= 2 - + # Check that users are returned as (id, name) tuples for user in users: assert len(user) == 2 @@ -178,6 +179,7 @@ def test_add_category_and_get_categories(test_db_call): def test_add_category_invalid_user(test_db_call): - # Try to add a category with a non-existent user_id (should still succeed in SQLite unless foreign keys are enforced) + # Try to add a category with a non-existent user_id + # (should still succeed in SQLite unless foreign keys are enforced) category_id = test_db_call.add_category(99999, "InvalidUserCategory") assert isinstance(category_id, int) From 26e156c0a89b2f3a293b140afd3f66760f21755d Mon Sep 17 00:00:00 2001 From: Alex Dimmock Date: Sat, 31 May 2025 17:31:13 +0100 Subject: [PATCH 15/32] =?UTF-8?q?fix:=20=F0=9F=90=9B=20add=20setting=20to?= =?UTF-8?q?=20render=20whitespace=20in=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a329fa..084a4a9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,4 +17,5 @@ "editor.defaultFormatter": "charliermarsh.ruff", "ruff.enable": true, "ruff.organizeImports": true, + "editor.renderWhitespace": "all", } From 95dffe5e4efe529c01d9cc328cbb5ef1837d8c41 Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Mon, 2 Jun 2025 13:55:24 +0100 Subject: [PATCH 16/32] Some initial changes based on feedback --- src/front/home.py | 76 ++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/src/front/home.py b/src/front/home.py index 9d75d1f..d92251f 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -22,6 +22,7 @@ def home_page(): # Debug log to see what's being passed to the template logger.debug(f"User ID from session: {user_id}, User Name from session: {selected_user_name}") + logger.info(f"{selected_user_name} is logged in") categories = db_call.get_categories(user_id) if user_id else [] return render_template( @@ -29,33 +30,36 @@ def home_page(): users=users, categories=categories, selected_user_id=user_id, - selected_user_name=selected_user_name + selected_user_name=selected_user_name, ) + @home_bp.route("/submit_user", methods=["POST"]) def submit_user_name(): user_name = request.form.get("userName") logger.debug(f"Request Received: {request.form}") if not user_name: - flash("Please enter a user name.", "error") + flash("Please enter a user name.", "danger") return redirect(url_for("home.home_page")) db_call = data_handler.database(verity_config) # More efficient check for existing user exists = db_call.read_database("SELECT 1 FROM user WHERE name = ?", (user_name,)) if exists: - flash("A user with that name already exists.", "error") + logger.warning(f"username already exists in the database: {user_name}") + flash("A user with that name already exists.", "danger") return redirect(url_for("home.home_page")) - logger.info(f"User submitted new user name: {user_name}") user_id = db_call.add_user_name(user_name) if user_id == 0: - flash("User Name not saved, please check the logs", "error") + flash("User Name not saved, please check the logs", "danger") return redirect(url_for("home.home_page")) - - flash("user name saved! Now select the user from the dropdown to continue.", "success") + session["user_id"] = user_id + session["user_name"] = user_name + flash("user name saved! Start adding categories.", "success") return redirect(url_for("home.home_page")) + @home_bp.route("/select_user", methods=["POST"]) def select_user(): selected_user_id = request.form.get("selectedUserId") @@ -65,14 +69,6 @@ def select_user(): session.pop("user_name", None) return redirect(url_for("home.home_page")) - # Convert selected_user_id to int and handle invalid input - try: - selected_user_id = int(selected_user_id) - except ValueError: - logger.error(f"Invalid user ID: {selected_user_id}") - flash("Invalid user ID", "error") - return redirect(url_for("home.home_page")) - # Get the user details directly from the database using the ID db_call = data_handler.database(verity_config) result = db_call.read_database("SELECT name FROM user WHERE id = ?", (selected_user_id,)) @@ -85,35 +81,47 @@ def select_user(): else: session.pop("user_id", None) session.pop("user_name", None) - flash("User not found!", "error") + flash("User not found!", "danger") return redirect(url_for("home.home_page")) + +def convert_to_universal_currency(input_value: float) -> int: + """ + Converts the the input value to remove all decimal places and return an int. + This will be the starting point for our universal currency, + (see docs/data_dictionary). + for now, we will just focus on making this an int. + it will need change later once we have the basics done + """ + logger.info(f"received {input_value} to convert to universal currency") + input_value = float(input_value) + while input_value % 1 != 0: + logger.debug(f"input value is not a whole number {input_value}") + input_value = input_value * 10 + logger.info(f"returning {int(input_value)}") + return int(input_value) + + @home_bp.route("/submit_category", methods=["POST"]) def submit_category(): user_id = session.get("user_id") if not user_id: - flash("No user selected!", "error") + logger.warning("No user selected and trying to add a category... how?") + flash("No user selected!", "danger") return redirect(url_for("home.home_page")) # Get and validate category name (required) category_name = request.form.get("categoryName") - if not category_name: - flash("Category name is required", "error") - return redirect(url_for("home.home_page")) - - # Handle form values more concisely - # Convert budget_value to float if provided - budget_value = None - budget_value_str = request.form.get("budgetValue", "").strip() - if budget_value_str: - try: - budget_value = float(budget_value_str) - except ValueError: - logger.error(f"Invalid budget value: {budget_value_str}") - flash("Invalid budget value - please enter a valid number", "error") - return redirect(url_for("home.home_page")) + # Convert budget_value to our universal currency + budget_value: int = 0 + budget_value_input = request.form.get("budgetValue", "0") + logger.debug(f"budget value input: {budget_value_input}") + if budget_value_input: + logger.info(f"Category:{category_name}, Assigned amount: {budget_value_input}") + # User might either add a whole currency or a decimal of. we need to handle both + budget_value = convert_to_universal_currency(budget_value_input) # Convert parent_id to int if provided parent_id = None parent_id_str = request.form.get("parentId", "").strip() @@ -122,13 +130,13 @@ def submit_category(): parent_id = int(parent_id_str) except ValueError: logger.error(f"Invalid parent ID: {parent_id_str}") - flash("Invalid parent category ID", "error") + flash("Invalid parent category ID", "danger") return redirect(url_for("home.home_page")) db_call = data_handler.database(verity_config) category_id = db_call.add_category(user_id, category_name, budget_value, parent_id) if category_id == 0: - flash("Category not saved, please check the logs", "error") + flash("Category not saved, please check the logs", "danger") else: flash("Category saved!", "success") return redirect(url_for("home.home_page")) From 3a8233ef7ae9edef65852765ed8c4082e3ff3fe3 Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Mon, 2 Jun 2025 19:26:26 +0100 Subject: [PATCH 17/32] More additions --- src/front/home.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/front/home.py b/src/front/home.py index d92251f..9dd6c57 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -118,20 +118,18 @@ def submit_category(): budget_value: int = 0 budget_value_input = request.form.get("budgetValue", "0") logger.debug(f"budget value input: {budget_value_input}") - if budget_value_input: + if not budget_value_input: + logger.info("no budget value assigned to category") + else: logger.info(f"Category:{category_name}, Assigned amount: {budget_value_input}") # User might either add a whole currency or a decimal of. we need to handle both + if int(budget_value_input) < 0: + logger.info("user is stupid and tried to assign a negative amount to the category") + flash("Negative amounts don`t really make sense here, removed budget amount", "danger") + budget_value_input = 0 budget_value = convert_to_universal_currency(budget_value_input) # Convert parent_id to int if provided - parent_id = None - parent_id_str = request.form.get("parentId", "").strip() - if parent_id_str: - try: - parent_id = int(parent_id_str) - except ValueError: - logger.error(f"Invalid parent ID: {parent_id_str}") - flash("Invalid parent category ID", "danger") - return redirect(url_for("home.home_page")) + parent_id = int(request.form.get("parentId", "").strip()) db_call = data_handler.database(verity_config) category_id = db_call.add_category(user_id, category_name, budget_value, parent_id) From 0e3443158324b57897d920cbd88c39fb59a62113 Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Tue, 3 Jun 2025 08:57:35 +0100 Subject: [PATCH 18/32] Resolved most comments from Pull Request 8 --- src/data_handler.py | 122 +++++++++++---------------------- src/tests/test_data_handler.py | 9 +-- 2 files changed, 46 insertions(+), 85 deletions(-) diff --git a/src/data_handler.py b/src/data_handler.py index f3d9677..7eb77cf 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -15,9 +15,11 @@ def __init__(self, config) -> None: self.schema = self.verity_config.DATABASE_SCHEMA self.database = self.verity_config.DATABASE - def execute_sql(self, sql_statement: str, return_id: bool = False) -> (bool, int): + def execute_sql( + self, sql_statement: str, params: tuple = (), return_id: bool = False + ) -> (bool, int): "send the query here, returns true if successful, false if fail" - logging.debug(f"received request to execute {sql_statement}") + logger.debug(f"received request to execute {sql_statement}") new_id: int = 0 is_success: bool = False try: @@ -25,44 +27,47 @@ def execute_sql(self, sql_statement: str, return_id: bool = False) -> (bool, int database=self.database, timeout=10, # seconds i hope ) - logging.debug("opened connection to database") + logger.info("opened connection to database") cursor = connection.cursor() - cursor.execute(sql_statement, {}) + cursor.execute("PRAGMA foreign_keys = ON;") + cursor.execute(sql_statement, params) connection.commit() + logger.debug(f"executed {sql_statement} with params: {params}") + logger.info("executed sql command") if return_id: new_id = cursor.lastrowid + logger.debug(f"New id created {new_id}") is_success = True except Exception as e: # TODO: better Exception handling - logging.error(e) + logger.error(e) is_success = False finally: connection.close() + logger.info("closed connection to database") if return_id: return (is_success, new_id) else: return is_success - def read_database(self, sql_statement: str, params=()) -> list: + def read_database(self, sql_statement: str, params: tuple = ()) -> list: "reads the database query and returns the results" - logging.debug(f"received request to read {sql_statement}") + logger.debug(f"received request to read {sql_statement}") results = [] try: connection = sqlite3.connect(self.database) cursor = connection.cursor() results = cursor.execute(sql_statement, params) results = results.fetchall() - logging.debug(f"query returned: {results}") + logger.debug(f"query returned: {results}") except Exception as e: - logging.error(f"Read error occurred: {e}") + logger.error(f"Read error occurred: {e}") finally: connection.close() return results @staticmethod def _build_column(column: dict) -> str: - logging.debug( - f"building column {column}" - ) + logging.debug(f"building column {column}") name = column["column_name"] is_pk = column["is_pk"] is_fk = column.get("is_fk") @@ -77,7 +82,6 @@ def _build_column(column: dict) -> str: column_string += " NOT NULL" return column_string - def _add_table_to_db(self, table: dict) -> bool: "Creates the table in the verity database, based on the schema yaml" sql = f"""CREATE TABLE IF NOT EXISTS {table["table_name"]} ( @@ -132,9 +136,7 @@ def print_table_schema(self, table_name): # Print the schema logger.debug(f"Schema for table: {table_name}") for row in cursor.fetchall(): - logger.debug( - f"Column Name: {row[1]}, Data Type: {row[2]}, Not Null: {row[3]}" - ) + logger.debug(f"Column Name: {row[1]}, Data Type: {row[2]}, Not Null: {row[3]}") except Exception as e: logger.error(f"An error occurred: {e}") @@ -148,79 +150,37 @@ def print_table_schema(self, table_name): def add_user_name(self, user_name: str) -> int: "takes user name string, returns user id" logger.debug(f"attempting to insert values into user table {user_name}") - try: - connection = sqlite3.connect(self.database) - cursor = connection.cursor() - cursor.execute( - """ - INSERT INTO user ( - name - ) - VALUES (?) - """, - (user_name,), - ) - connection.commit() - user_id = cursor.lastrowid - if user_id is None: - user_id = 0 - except Exception as e: - logger.error(f"Failed to insert user name, error: {e}") - user_id = 0 - finally: - try: - connection.close() - logger.debug("connection to db closed") - return user_id - except Exception as e: - logger.error(f"failed to close connection message: {e}") - return 0 + sql_statement = """ + INSERT INTO user (name) + VALUES (?) + """ + params = (user_name,) + success, user_id = self.execute_sql(sql_statement, params, True) + if not success: + logger.error("Failed to execute sql, check the logs") + return user_id def get_users(self) -> list: """Returns all users in the database.""" get_user_sql = "SELECT id, name FROM user" return self.read_database(get_user_sql) - def add_category(self, user_id: int, category_name: str,budget_value: int = 0, - parent_id = None) -> int: + def add_category( + self, user_id: int, category_name: str, budget_value: int = 0, parent_id=None + ) -> int: """Inserts a new category. Returns the category id.""" logger.debug(f"attempting to insert category '{category_name}' for user {user_id}") - try: - connection = sqlite3.connect(self.database) - cursor = connection.cursor() - cursor.execute( - """ - INSERT INTO category (user_id, name, budget_value, parent_id) - VALUES (?, ?, ?, ?) - """, - (user_id, category_name, budget_value, parent_id), - ) - connection.commit() - category_id = cursor.lastrowid or 0 - except Exception as e: - logger.error(f"Failed to insert category name, error: {e}") - category_id = 0 - finally: - try: - connection.close() - except Exception as e: - logger.error(f"failed to close connection: {e}") - return category_id + sql_statement = """ + INSERT INTO category (user_id, name, budget_value, parent_id) + VALUES (?, ?, ?, ?)""" + params = (user_id, category_name, budget_value, parent_id) + + success, category_id = self.execute_sql(sql_statement, params, True) + if not success: + logger.error("Failed to execute sql, check the logs") + return category_id def get_categories(self, user_id: int) -> list: """Returns all categories for a given user.""" - try: - connection = sqlite3.connect(self.database) - cursor = connection.cursor() - sql = "SELECT id, name, budget_value, parent_id FROM category WHERE user_id = ?" - cursor.execute(sql, (user_id,)) - categories = cursor.fetchall() - except Exception as e: - logger.error(f"Failed to fetch categories: {e}") - categories = [] - finally: - try: - connection.close() - except Exception as e: - logger.error(f"failed to close connection: {e}") - return categories + sql = "SELECT id, name, budget_value, parent_id FROM category WHERE user_id = ?" + return self.read_database(sql_statement=sql, params=(user_id,)) diff --git a/src/tests/test_data_handler.py b/src/tests/test_data_handler.py index becf155..283ae80 100644 --- a/src/tests/test_data_handler.py +++ b/src/tests/test_data_handler.py @@ -83,10 +83,10 @@ def test_read_database_with_params(test_db_call): def test_build_database(test_db_call): """Test that the database builds correctly with all required tables""" # Check if all tables have been created - tables_to_check = ["user", "account", "account_type", "party", "transaction_type", - "category", "transaction_log"] - for table in tables_to_check: - sql = f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}';" + config = VerityConfig() + for table in config.DATABASE_SCHEMA["tables"]: + table_name = table["table_name"] + sql = f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}';" results = test_db_call.read_database(sql) assert len(results) == 1, f"Table '{table}' should exist" @@ -182,4 +182,5 @@ def test_add_category_invalid_user(test_db_call): # Try to add a category with a non-existent user_id # (should still succeed in SQLite unless foreign keys are enforced) category_id = test_db_call.add_category(99999, "InvalidUserCategory") + assert category_id == 0 assert isinstance(category_id, int) From 574341b30d224a615cc19dca4ac0ed9778f085fb Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Tue, 3 Jun 2025 18:33:26 +0100 Subject: [PATCH 19/32] Moved convert to universal currency to its own module. --- src/currency_handler.py | 23 +++++++++++++++++++++++ src/front/home.py | 20 ++------------------ src/tests/test_currency_handler.py | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 src/currency_handler.py create mode 100644 src/tests/test_currency_handler.py diff --git a/src/currency_handler.py b/src/currency_handler.py new file mode 100644 index 0000000..cb92036 --- /dev/null +++ b/src/currency_handler.py @@ -0,0 +1,23 @@ +import logging + +logger = logging.getLogger(__name__) + +# TODO: Make this into a proper class so we can scale currrency handling + + +def convert_to_universal_currency(input_value: float) -> int: + """ + Converts the the input value to remove all decimal places and return an int. + This will be the starting point for our universal currency, + (see docs/data_dictionary). + for now, we will just focus on making this an int. + it will need change later once we have the basics done + """ + logger.info(f"received {input_value} to convert to universal currency") + input_value = float(input_value) + while input_value % 1 != 0: + logger.debug(f"input value is not a whole number {input_value}") + input_value = input_value * 10 + logger.info(f"returning {int(input_value)}") + return int(input_value) + diff --git a/src/front/home.py b/src/front/home.py index 9dd6c57..453a54a 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -3,6 +3,7 @@ from flask import Blueprint, flash, redirect, render_template, request, session, url_for import data_handler +import currency_handler from config import VerityConfig logger = logging.getLogger(__name__) @@ -86,23 +87,6 @@ def select_user(): return redirect(url_for("home.home_page")) -def convert_to_universal_currency(input_value: float) -> int: - """ - Converts the the input value to remove all decimal places and return an int. - This will be the starting point for our universal currency, - (see docs/data_dictionary). - for now, we will just focus on making this an int. - it will need change later once we have the basics done - """ - logger.info(f"received {input_value} to convert to universal currency") - input_value = float(input_value) - while input_value % 1 != 0: - logger.debug(f"input value is not a whole number {input_value}") - input_value = input_value * 10 - logger.info(f"returning {int(input_value)}") - return int(input_value) - - @home_bp.route("/submit_category", methods=["POST"]) def submit_category(): user_id = session.get("user_id") @@ -127,7 +111,7 @@ def submit_category(): logger.info("user is stupid and tried to assign a negative amount to the category") flash("Negative amounts don`t really make sense here, removed budget amount", "danger") budget_value_input = 0 - budget_value = convert_to_universal_currency(budget_value_input) + budget_value = currency_handler.convert_to_universal_currency(budget_value_input) # Convert parent_id to int if provided parent_id = int(request.form.get("parentId", "").strip()) diff --git a/src/tests/test_currency_handler.py b/src/tests/test_currency_handler.py new file mode 100644 index 0000000..a2b03db --- /dev/null +++ b/src/tests/test_currency_handler.py @@ -0,0 +1,22 @@ +import pytest + +from src import currency_handler + + +def test_convert_to_universal_currency(): + input = 87.82 + result = currency_handler.convert_to_universal_currency(input) + assert result == 8782 + + +def test_whole_number(): + input = 123 + result = currency_handler.convert_to_universal_currency(input) + assert result == input + + +def test_negative_number(): + input = -123.45 + result = currency_handler.convert_to_universal_currency(input) + print(result) + assert result == -12345 From c3003a5bf34ac3615671bfac7b76c7614f75c4f9 Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Tue, 3 Jun 2025 21:25:11 +0100 Subject: [PATCH 20/32] Working towards adding default rows, need to add system user? --- src/config.py | 1 + src/config_files/default_data.yaml | 4 ++++ src/data_handler.py | 32 +++++++++++++++++++++++++++--- src/front/home.py | 21 +++++++++++++++----- src/tests/test_currency_handler.py | 1 - src/verity.py | 1 + 6 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 src/config_files/default_data.yaml diff --git a/src/config.py b/src/config.py index 975c89e..851119d 100644 --- a/src/config.py +++ b/src/config.py @@ -10,6 +10,7 @@ def __init__(self): self.CONFIG_FILE_DIRECTORY = "config_files" self.LOGGING_CONFIG = self.load_config_file("logging_config.yaml") self.DATABASE_SCHEMA = self.load_config_file("verity_schema.yaml") + self.DEFAULT_DATA = self.load_config_file("default_data.yaml") def load_config_file(self, file): config = "" diff --git a/src/config_files/default_data.yaml b/src/config_files/default_data.yaml new file mode 100644 index 0000000..fb9cac2 --- /dev/null +++ b/src/config_files/default_data.yaml @@ -0,0 +1,4 @@ +category: + - name: 'internal_master_category' + - name: 'tesing more categories' + budget_value: '100' diff --git a/src/data_handler.py b/src/data_handler.py index 7eb77cf..20db8c3 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -14,12 +14,13 @@ def __init__(self, config) -> None: self.verity_config = config self.schema = self.verity_config.DATABASE_SCHEMA self.database = self.verity_config.DATABASE + self.default_data = self.verity_config.DEFAULT_DATA def execute_sql( - self, sql_statement: str, params: tuple = (), return_id: bool = False + self, sql_statement: str, params: tuple = (), return_id: bool = False, seed: bool = False ) -> (bool, int): "send the query here, returns true if successful, false if fail" - logger.debug(f"received request to execute {sql_statement}") + logger.debug(f"received request to execute {sql_statement} with params {params}") new_id: int = 0 is_success: bool = False try: @@ -29,7 +30,8 @@ def execute_sql( ) logger.info("opened connection to database") cursor = connection.cursor() - cursor.execute("PRAGMA foreign_keys = ON;") + if seed: + cursor.execute("PRAGMA foreign_keys = OFF;") cursor.execute(sql_statement, params) connection.commit() logger.debug(f"executed {sql_statement} with params: {params}") @@ -42,6 +44,7 @@ def execute_sql( logger.error(e) is_success = False finally: + cursor.execute("PRAGMA foreign_keys = ON;") connection.close() logger.info("closed connection to database") if return_id: @@ -116,6 +119,27 @@ def build_database(self): logger.info(f"Checking {table['table_name']}") # Add true/false handling here to gracefully handle errors self._add_table_to_db(table) + for table in self.default_data: + # check if default row is there + data = self.read_database(f"select * from {table}") + logger.info(f"data in {table} is {data}") + # if not there, make default row. + if data == []: + logger.info("no data, need to add default row(s)") + for row in self.default_data[table]: + logger.debug(row) + col_string = ", ".join(row.keys()) + logger.debug(col_string) + val_string = ", ".join(row.values()) + logger.debug(val_string) + logger.info(f"insert into {table} ({col_string}) values ({val_string})") + self.execute_sql( + f"insert into {table} ({col_string}) VALUES (?)", + (val_string,), + seed=True, + ) + # seed = true + exit() def print_table_schema(self, table_name): """ @@ -173,6 +197,8 @@ def add_category( sql_statement = """ INSERT INTO category (user_id, name, budget_value, parent_id) VALUES (?, ?, ?, ?)""" + if not parent_id: + parent_id = "NULL" params = (user_id, category_name, budget_value, parent_id) success, category_id = self.execute_sql(sql_statement, params, True) diff --git a/src/front/home.py b/src/front/home.py index 453a54a..0bf3eb0 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -2,8 +2,8 @@ from flask import Blueprint, flash, redirect, render_template, request, session, url_for -import data_handler import currency_handler +import data_handler from config import VerityConfig logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def submit_user_name(): if user_id == 0: flash("User Name not saved, please check the logs", "danger") return redirect(url_for("home.home_page")) - session["user_id"] = user_id + session["user_id"] = int(user_id) session["user_name"] = user_name flash("user name saved! Start adding categories.", "success") return redirect(url_for("home.home_page")) @@ -70,6 +70,14 @@ def select_user(): session.pop("user_name", None) return redirect(url_for("home.home_page")) + # Convert selected_user_id to int and handle invalid input + try: + selected_user_id = int(selected_user_id) + except ValueError: + logger.error(f"Invalid user ID: {selected_user_id}") + flash("Invalid user ID", "error") + return redirect(url_for("home.home_page")) + # Get the user details directly from the database using the ID db_call = data_handler.database(verity_config) result = db_call.read_database("SELECT name FROM user WHERE id = ?", (selected_user_id,)) @@ -107,16 +115,19 @@ def submit_category(): else: logger.info(f"Category:{category_name}, Assigned amount: {budget_value_input}") # User might either add a whole currency or a decimal of. we need to handle both - if int(budget_value_input) < 0: + if float(budget_value_input) < 0: logger.info("user is stupid and tried to assign a negative amount to the category") flash("Negative amounts don`t really make sense here, removed budget amount", "danger") budget_value_input = 0 budget_value = currency_handler.convert_to_universal_currency(budget_value_input) # Convert parent_id to int if provided - parent_id = int(request.form.get("parentId", "").strip()) + parent_id = request.form.get("parentId", "0").strip() db_call = data_handler.database(verity_config) - category_id = db_call.add_category(user_id, category_name, budget_value, parent_id) + if parent_id == 0: + category_id = db_call.add_category(user_id, category_name, budget_value) + else: + category_id = db_call.add_category(user_id, category_name, budget_value, parent_id) if category_id == 0: flash("Category not saved, please check the logs", "danger") else: diff --git a/src/tests/test_currency_handler.py b/src/tests/test_currency_handler.py index a2b03db..8111018 100644 --- a/src/tests/test_currency_handler.py +++ b/src/tests/test_currency_handler.py @@ -1,4 +1,3 @@ -import pytest from src import currency_handler diff --git a/src/verity.py b/src/verity.py index 69df55b..0bf866c 100644 --- a/src/verity.py +++ b/src/verity.py @@ -28,6 +28,7 @@ def set_up_logging(config): # database initialise verity = database(verity_config) + logger.debug(verity_config.DEFAULT_DATA) verity.build_database() # app initialise From fc61b98c1ded8ce38b0327f44896df2258a39b9f Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Mon, 9 Jun 2025 09:09:38 +0100 Subject: [PATCH 21/32] =?UTF-8?q?feat:=20=E2=9C=A8We=20now=20add=20an=20in?= =?UTF-8?q?ternal=20master=20category=20per=20user=20that=20is=20hidden=20?= =?UTF-8?q?Will=20be=20useful=20for=20various=20key=20restraints.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_handler.py | 44 ++++++++++++++++---------------------------- 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/data_handler.py b/src/data_handler.py index 20db8c3..bbd730a 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -30,6 +30,7 @@ def execute_sql( ) logger.info("opened connection to database") cursor = connection.cursor() + cursor.execute("PRAGMA foreign_keys = ON;") if seed: cursor.execute("PRAGMA foreign_keys = OFF;") cursor.execute(sql_statement, params) @@ -54,7 +55,7 @@ def execute_sql( def read_database(self, sql_statement: str, params: tuple = ()) -> list: "reads the database query and returns the results" - logger.debug(f"received request to read {sql_statement}") + logger.debug(f"received request to read {sql_statement} with params {params}") results = [] try: connection = sqlite3.connect(self.database) @@ -119,27 +120,6 @@ def build_database(self): logger.info(f"Checking {table['table_name']}") # Add true/false handling here to gracefully handle errors self._add_table_to_db(table) - for table in self.default_data: - # check if default row is there - data = self.read_database(f"select * from {table}") - logger.info(f"data in {table} is {data}") - # if not there, make default row. - if data == []: - logger.info("no data, need to add default row(s)") - for row in self.default_data[table]: - logger.debug(row) - col_string = ", ".join(row.keys()) - logger.debug(col_string) - val_string = ", ".join(row.values()) - logger.debug(val_string) - logger.info(f"insert into {table} ({col_string}) values ({val_string})") - self.execute_sql( - f"insert into {table} ({col_string}) VALUES (?)", - (val_string,), - seed=True, - ) - # seed = true - exit() def print_table_schema(self, table_name): """ @@ -182,6 +162,7 @@ def add_user_name(self, user_name: str) -> int: success, user_id = self.execute_sql(sql_statement, params, True) if not success: logger.error("Failed to execute sql, check the logs") + self.add_category(user_id, "internal_master_category", seed=True) return user_id def get_users(self) -> list: @@ -190,23 +171,30 @@ def get_users(self) -> list: return self.read_database(get_user_sql) def add_category( - self, user_id: int, category_name: str, budget_value: int = 0, parent_id=None + self, user_id: int, category_name: str, budget_value: int = 0, parent_id=None, seed=False ) -> int: """Inserts a new category. Returns the category id.""" logger.debug(f"attempting to insert category '{category_name}' for user {user_id}") sql_statement = """ INSERT INTO category (user_id, name, budget_value, parent_id) VALUES (?, ?, ?, ?)""" - if not parent_id: - parent_id = "NULL" + if not seed: + if not parent_id: + parent_id = self.read_database( + "SELECT id FROM category WHERE user_id = ? AND name = ?", + (user_id, "internal_master_category"), + ) + parent_id = parent_id[0][0] params = (user_id, category_name, budget_value, parent_id) - success, category_id = self.execute_sql(sql_statement, params, True) + success, category_id = self.execute_sql(sql_statement, params, True, seed) if not success: logger.error("Failed to execute sql, check the logs") return category_id def get_categories(self, user_id: int) -> list: """Returns all categories for a given user.""" - sql = "SELECT id, name, budget_value, parent_id FROM category WHERE user_id = ?" - return self.read_database(sql_statement=sql, params=(user_id,)) + sql = ( + "SELECT id, name, budget_value, parent_id FROM category WHERE user_id = ? and name != ?" + ) + return self.read_database(sql_statement=sql, params=(user_id, "internal_master_category")) From 7efc613c497638501d7ea86b94ef52a97bf603a5 Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Mon, 9 Jun 2025 09:16:03 +0100 Subject: [PATCH 22/32] =?UTF-8?q?fix:=20=F0=9F=A9=B9=20Fixed=20a=20test=20?= =?UTF-8?q?failure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data_handler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/data_handler.py b/src/data_handler.py index bbd730a..c1d970d 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -184,7 +184,10 @@ def add_category( "SELECT id FROM category WHERE user_id = ? AND name = ?", (user_id, "internal_master_category"), ) - parent_id = parent_id[0][0] + try: + parent_id = parent_id[0][0] + except IndexError: + parent_id = None params = (user_id, category_name, budget_value, parent_id) success, category_id = self.execute_sql(sql_statement, params, True, seed) From 0cbfc69ea379e2df6637859469fc2684123abf32 Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Fri, 13 Jun 2025 05:46:31 +0100 Subject: [PATCH 23/32] =?UTF-8?q?chore:=20=F0=9F=A7=B9=20Refactored=20fold?= =?UTF-8?q?er=20structure=20ready=20for=20class=20refactoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {src => api}/config_files/logging_config.yaml | 2 +- {src => api}/config_files/verity_schema.yaml | 0 {src => api/src}/__init__.py | 0 {src => api/src}/config.py | 5 ++--- {src => api/src}/currency_handler.py | 1 - {src => api/src}/data_handler.py | 1 - {src/front => front}/__init__.py | 0 {src/front => front}/home.py | 5 ++--- {src/front => front}/static/verity_style.css | 0 {src/front => front}/templates/base.html | 0 {src/front => front}/templates/home.html | 0 src/config_files/default_data.yaml | 4 ---- {src/tests => tests}/__init__.py | 0 {src/tests => tests}/test_config.py | 6 +++--- {src/tests => tests}/test_currency_handler.py | 3 +-- {src/tests => tests}/test_data_handler.py | 4 ++-- src/verity.py => verity.py | 6 +++--- 17 files changed, 14 insertions(+), 23 deletions(-) rename {src => api}/config_files/logging_config.yaml (95%) rename {src => api}/config_files/verity_schema.yaml (100%) rename {src => api/src}/__init__.py (100%) rename {src => api/src}/config.py (82%) rename {src => api/src}/currency_handler.py (99%) rename {src => api/src}/data_handler.py (99%) rename {src/front => front}/__init__.py (100%) rename {src/front => front}/home.py (98%) rename {src/front => front}/static/verity_style.css (100%) rename {src/front => front}/templates/base.html (100%) rename {src/front => front}/templates/home.html (100%) delete mode 100644 src/config_files/default_data.yaml rename {src/tests => tests}/__init__.py (100%) rename {src/tests => tests}/test_config.py (91%) rename {src/tests => tests}/test_currency_handler.py (92%) rename {src/tests => tests}/test_data_handler.py (98%) rename src/verity.py => verity.py (89%) diff --git a/src/config_files/logging_config.yaml b/api/config_files/logging_config.yaml similarity index 95% rename from src/config_files/logging_config.yaml rename to api/config_files/logging_config.yaml index 3d8b5b4..2a8783d 100644 --- a/src/config_files/logging_config.yaml +++ b/api/config_files/logging_config.yaml @@ -18,7 +18,7 @@ handlers: class: logging.handlers.RotatingFileHandler level: DEBUG formatter: withdate - filename: "../logs/verity.log" + filename: "./logs/verity.log" maxBytes: 10485760 # 10MB backupCount: 5 queue_handler: diff --git a/src/config_files/verity_schema.yaml b/api/config_files/verity_schema.yaml similarity index 100% rename from src/config_files/verity_schema.yaml rename to api/config_files/verity_schema.yaml diff --git a/src/__init__.py b/api/src/__init__.py similarity index 100% rename from src/__init__.py rename to api/src/__init__.py diff --git a/src/config.py b/api/src/config.py similarity index 82% rename from src/config.py rename to api/src/config.py index 851119d..8ab3f8d 100644 --- a/src/config.py +++ b/api/src/config.py @@ -6,11 +6,10 @@ class VerityConfig: def __init__(self): self.SECRET_KEY = os.environ.get("SECRET_KEY") or "super_secret_key" - self.DATABASE = "Verity.db" - self.CONFIG_FILE_DIRECTORY = "config_files" + self.DATABASE = "api/data/verity.db" + self.CONFIG_FILE_DIRECTORY = "api/config_files" self.LOGGING_CONFIG = self.load_config_file("logging_config.yaml") self.DATABASE_SCHEMA = self.load_config_file("verity_schema.yaml") - self.DEFAULT_DATA = self.load_config_file("default_data.yaml") def load_config_file(self, file): config = "" diff --git a/src/currency_handler.py b/api/src/currency_handler.py similarity index 99% rename from src/currency_handler.py rename to api/src/currency_handler.py index cb92036..399120d 100644 --- a/src/currency_handler.py +++ b/api/src/currency_handler.py @@ -20,4 +20,3 @@ def convert_to_universal_currency(input_value: float) -> int: input_value = input_value * 10 logger.info(f"returning {int(input_value)}") return int(input_value) - diff --git a/src/data_handler.py b/api/src/data_handler.py similarity index 99% rename from src/data_handler.py rename to api/src/data_handler.py index c1d970d..9d27695 100644 --- a/src/data_handler.py +++ b/api/src/data_handler.py @@ -14,7 +14,6 @@ def __init__(self, config) -> None: self.verity_config = config self.schema = self.verity_config.DATABASE_SCHEMA self.database = self.verity_config.DATABASE - self.default_data = self.verity_config.DEFAULT_DATA def execute_sql( self, sql_statement: str, params: tuple = (), return_id: bool = False, seed: bool = False diff --git a/src/front/__init__.py b/front/__init__.py similarity index 100% rename from src/front/__init__.py rename to front/__init__.py diff --git a/src/front/home.py b/front/home.py similarity index 98% rename from src/front/home.py rename to front/home.py index 0bf3eb0..ed5f60f 100644 --- a/src/front/home.py +++ b/front/home.py @@ -2,9 +2,8 @@ from flask import Blueprint, flash, redirect, render_template, request, session, url_for -import currency_handler -import data_handler -from config import VerityConfig +from api.src import currency_handler, data_handler +from api.src.config import VerityConfig logger = logging.getLogger(__name__) diff --git a/src/front/static/verity_style.css b/front/static/verity_style.css similarity index 100% rename from src/front/static/verity_style.css rename to front/static/verity_style.css diff --git a/src/front/templates/base.html b/front/templates/base.html similarity index 100% rename from src/front/templates/base.html rename to front/templates/base.html diff --git a/src/front/templates/home.html b/front/templates/home.html similarity index 100% rename from src/front/templates/home.html rename to front/templates/home.html diff --git a/src/config_files/default_data.yaml b/src/config_files/default_data.yaml deleted file mode 100644 index fb9cac2..0000000 --- a/src/config_files/default_data.yaml +++ /dev/null @@ -1,4 +0,0 @@ -category: - - name: 'internal_master_category' - - name: 'tesing more categories' - budget_value: '100' diff --git a/src/tests/__init__.py b/tests/__init__.py similarity index 100% rename from src/tests/__init__.py rename to tests/__init__.py diff --git a/src/tests/test_config.py b/tests/test_config.py similarity index 91% rename from src/tests/test_config.py rename to tests/test_config.py index 56d14bd..5e28183 100644 --- a/src/tests/test_config.py +++ b/tests/test_config.py @@ -2,7 +2,7 @@ import yaml -from src import config +from api.src import config def test_verity_config_default_secret_key(monkeypatch): @@ -23,13 +23,13 @@ def test_verity_config_secret_key_from_env(monkeypatch): def test_verity_config_database_name(monkeypatch): """Test that the database name is correctly set.""" testing_config = config.VerityConfig() - assert testing_config.DATABASE == "Verity.db" + assert testing_config.DATABASE == "api/data/verity.db" def test_verity_config_config_file_directory(monkeypatch): """Test that the config file directory is set.""" testing_config = config.VerityConfig() - assert testing_config.CONFIG_FILE_DIRECTORY == "config_files" + assert testing_config.CONFIG_FILE_DIRECTORY == "api/config_files" def test_verity_config_load_config_file(monkeypatch): diff --git a/src/tests/test_currency_handler.py b/tests/test_currency_handler.py similarity index 92% rename from src/tests/test_currency_handler.py rename to tests/test_currency_handler.py index 8111018..6aa755a 100644 --- a/src/tests/test_currency_handler.py +++ b/tests/test_currency_handler.py @@ -1,5 +1,4 @@ - -from src import currency_handler +from api.src import currency_handler def test_convert_to_universal_currency(): diff --git a/src/tests/test_data_handler.py b/tests/test_data_handler.py similarity index 98% rename from src/tests/test_data_handler.py rename to tests/test_data_handler.py index 283ae80..77540ae 100644 --- a/src/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -3,8 +3,8 @@ import pytest -from src import data_handler -from src.config import VerityConfig +from api.src import data_handler +from api.src.config import VerityConfig @pytest.fixture diff --git a/src/verity.py b/verity.py similarity index 89% rename from src/verity.py rename to verity.py index 0bf866c..64f133b 100644 --- a/src/verity.py +++ b/verity.py @@ -5,8 +5,8 @@ from flask import Flask -from config import VerityConfig -from data_handler import database +from api.src.config import VerityConfig +from api.src.data_handler import database from front.home import home_bp @@ -28,7 +28,7 @@ def set_up_logging(config): # database initialise verity = database(verity_config) - logger.debug(verity_config.DEFAULT_DATA) + logger.debug(verity_config) verity.build_database() # app initialise From a1592c85bc907bb7210f4034cf18e5e4f78cbeea Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Fri, 13 Jun 2025 07:29:45 +0100 Subject: [PATCH 24/32] =?UTF-8?q?feat:=20=F0=9F=94=92=20Working=20User=20C?= =?UTF-8?q?lass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/currency_handler.py | 31 ++--- api/src/data_handler.py | 18 +-- api/src/user.py | 51 ++++++++ front/home.py | 24 ++-- tests/test_currency_handler.py | 8 +- tests/test_data_handler.py | 207 +++++++++++++++++---------------- verity.py | 4 +- 7 files changed, 196 insertions(+), 147 deletions(-) create mode 100644 api/src/user.py diff --git a/api/src/currency_handler.py b/api/src/currency_handler.py index 399120d..6afbe12 100644 --- a/api/src/currency_handler.py +++ b/api/src/currency_handler.py @@ -5,18 +5,19 @@ # TODO: Make this into a proper class so we can scale currrency handling -def convert_to_universal_currency(input_value: float) -> int: - """ - Converts the the input value to remove all decimal places and return an int. - This will be the starting point for our universal currency, - (see docs/data_dictionary). - for now, we will just focus on making this an int. - it will need change later once we have the basics done - """ - logger.info(f"received {input_value} to convert to universal currency") - input_value = float(input_value) - while input_value % 1 != 0: - logger.debug(f"input value is not a whole number {input_value}") - input_value = input_value * 10 - logger.info(f"returning {int(input_value)}") - return int(input_value) +class CurrencyBrain: + def convert_to_universal_currency(input_value: float) -> int: + """ + Converts the the input value to remove all decimal places and return an int. + This will be the starting point for our universal currency, + (see docs/data_dictionary). + for now, we will just focus on making this an int. + it will need change later once we have the basics done + """ + logger.info(f"received {input_value} to convert to universal currency") + input_value = float(input_value) + while input_value % 1 != 0: + logger.debug(f"input value is not a whole number {input_value}") + input_value = input_value * 10 + logger.info(f"returning {int(input_value)}") + return int(input_value) diff --git a/api/src/data_handler.py b/api/src/data_handler.py index 9d27695..e922659 100644 --- a/api/src/data_handler.py +++ b/api/src/data_handler.py @@ -4,7 +4,7 @@ logger = logging.getLogger(__name__) -class database: +class Database: """basic Database class to start some development Will need a proper refactor once basic functions are in and working This is POC @@ -54,7 +54,7 @@ def execute_sql( def read_database(self, sql_statement: str, params: tuple = ()) -> list: "reads the database query and returns the results" - logger.debug(f"received request to read {sql_statement} with params {params}") + logger.info(f"received request to read {sql_statement} with params {params}") results = [] try: connection = sqlite3.connect(self.database) @@ -150,20 +150,6 @@ def print_table_schema(self, table_name): except Exception as e: logger.error(e) - def add_user_name(self, user_name: str) -> int: - "takes user name string, returns user id" - logger.debug(f"attempting to insert values into user table {user_name}") - sql_statement = """ - INSERT INTO user (name) - VALUES (?) - """ - params = (user_name,) - success, user_id = self.execute_sql(sql_statement, params, True) - if not success: - logger.error("Failed to execute sql, check the logs") - self.add_category(user_id, "internal_master_category", seed=True) - return user_id - def get_users(self) -> list: """Returns all users in the database.""" get_user_sql = "SELECT id, name FROM user" diff --git a/api/src/user.py b/api/src/user.py new file mode 100644 index 0000000..1b82e86 --- /dev/null +++ b/api/src/user.py @@ -0,0 +1,51 @@ +import logging + +from api.src.data_handler import Database + +logger = logging.getLogger(__name__) + + +class User: + "Main user class, for anything related to the user" + + def __init__(self, database: Database, user_name: str): + self.database: Database = database + self.user_name: str = user_name + self.user_id: int = 0 + self.categories: list = [] + logger.info(f"{self.user_name} initialised.") + + def add_user_name(self) -> int: + logger.info(f"adding user {self.user_name}") + sql = "INSERT INTO USER(name) VALUES (?)" + params = (self.user_name,) + success, self.user_id = self.database.execute_sql(sql, params, return_id=True) + if not success: + logger.error(f"Failed to insert user {self.user_name} Please check the logs") + return 0 + category_sql = """INSERT INTO category (user_id, name, budget_value, parent_id) + VALUES (?, ?, ?, ?)""" + category_params = (self.user_id, "internal_master_category", 0, None) + category_success, default_category_id = self.database.execute_sql( + category_sql, category_params, return_id=True, seed=True + ) + if not category_success: + logger.error( + f"Failed to add default category for {self.user_name} Please check the logs" + ) + self.categories.append(default_category_id) + # if category fails to add, + # we probably need to delete the user, so we dont lock the username + # change id to category object when we make category class + return self.user_id + + def user_exists(self) -> bool: + logger.info(f"Checking to see if {self.user_name} already exists!") + sql = "SELECT 1 FROM user where name = ?" + params = (self.user_name,) + user_exists = self.database.read_database(sql, params) + logger.info(user_exists) + if user_exists: + logger.warning(f"{self.user_name} already exists in the database") + return True + return False diff --git a/front/home.py b/front/home.py index ed5f60f..20fc035 100644 --- a/front/home.py +++ b/front/home.py @@ -2,19 +2,21 @@ from flask import Blueprint, flash, redirect, render_template, request, session, url_for -from api.src import currency_handler, data_handler +from api.src.data_handler import Database +from api.src.currency_handler import CurrencyBrain +from api.src.user import User from api.src.config import VerityConfig logger = logging.getLogger(__name__) home_bp = Blueprint("home", __name__, template_folder="templates") -verity_config = VerityConfig() +# verity_config = VerityConfig() @home_bp.route("/") def home_page(): logger.info("home page hit") - db_call = data_handler.database(verity_config) + db_call = Database(VerityConfig()) users = db_call.get_users() # Get user info directly from session user_id = session.get("user_id") @@ -42,15 +44,13 @@ def submit_user_name(): flash("Please enter a user name.", "danger") return redirect(url_for("home.home_page")) - db_call = data_handler.database(verity_config) - # More efficient check for existing user - exists = db_call.read_database("SELECT 1 FROM user WHERE name = ?", (user_name,)) - if exists: - logger.warning(f"username already exists in the database: {user_name}") + verity_user = User(Database(VerityConfig()), user_name) + logger.info(f"does user '{user_name}' exist?") + if verity_user.user_exists(): flash("A user with that name already exists.", "danger") return redirect(url_for("home.home_page")) logger.info(f"User submitted new user name: {user_name}") - user_id = db_call.add_user_name(user_name) + user_id = verity_user.add_user_name() if user_id == 0: flash("User Name not saved, please check the logs", "danger") return redirect(url_for("home.home_page")) @@ -78,7 +78,7 @@ def select_user(): return redirect(url_for("home.home_page")) # Get the user details directly from the database using the ID - db_call = data_handler.database(verity_config) + db_call = Database(VerityConfig()) result = db_call.read_database("SELECT name FROM user WHERE id = ?", (selected_user_id,)) if result and result[0]: @@ -118,11 +118,11 @@ def submit_category(): logger.info("user is stupid and tried to assign a negative amount to the category") flash("Negative amounts don`t really make sense here, removed budget amount", "danger") budget_value_input = 0 - budget_value = currency_handler.convert_to_universal_currency(budget_value_input) + budget_value = CurrencyBrain.convert_to_universal_currency(budget_value_input) # Convert parent_id to int if provided parent_id = request.form.get("parentId", "0").strip() - db_call = data_handler.database(verity_config) + db_call = Database(VerityConfig()) if parent_id == 0: category_id = db_call.add_category(user_id, category_name, budget_value) else: diff --git a/tests/test_currency_handler.py b/tests/test_currency_handler.py index 6aa755a..a5eaab5 100644 --- a/tests/test_currency_handler.py +++ b/tests/test_currency_handler.py @@ -1,20 +1,20 @@ -from api.src import currency_handler +from api.src.currency_handler import CurrencyBrain def test_convert_to_universal_currency(): input = 87.82 - result = currency_handler.convert_to_universal_currency(input) + result = CurrencyBrain.convert_to_universal_currency(input) assert result == 8782 def test_whole_number(): input = 123 - result = currency_handler.convert_to_universal_currency(input) + result = CurrencyBrain.convert_to_universal_currency(input) assert result == input def test_negative_number(): input = -123.45 - result = currency_handler.convert_to_universal_currency(input) + result = CurrencyBrain.convert_to_universal_currency(input) print(result) assert result == -12345 diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index 77540ae..9643613 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -3,7 +3,7 @@ import pytest -from api.src import data_handler +from api.src.data_handler import Database from api.src.config import VerityConfig @@ -17,7 +17,7 @@ def test_db_call(): # Create a config with the test DB config.DATABASE = test_db_file - db_call = data_handler.database(config) + db_call = Database(config) db_call.build_database() yield db_call # Cleanup after tests @@ -58,7 +58,12 @@ def test_execute_sql_with_return_id(test_db_call): def test_read_database(test_db_call): """Test reading database data""" # First insert a user - test_db_call.add_user_name("read_test_user") + insert_sql = """INSERT INTO user ( + name + ) + VALUES ('read_test_user') + """ + _ = test_db_call.execute_sql(insert_sql) # Then read it back sql_statement = "SELECT id, name FROM user WHERE name = 'read_test_user'" results = test_db_call.read_database(sql_statement) @@ -70,8 +75,12 @@ def test_read_database(test_db_call): def test_read_database_with_params(test_db_call): """Test reading database with parameterized query""" # First insert a user - test_db_call.add_user_name("param_test_user") - # Then read it back with params + insert_sql = """INSERT INTO user ( + name + ) + VALUES ('param_test_user') + """ + _ = test_db_call.execute_sql(insert_sql) # Then read it back with params sql_statement = "SELECT id, name FROM user WHERE name = ?" params = ("param_test_user",) results = test_db_call.read_database(sql_statement, params) @@ -91,96 +100,98 @@ def test_build_database(test_db_call): assert len(results) == 1, f"Table '{table}' should exist" -def test_add_user_name(test_db_call): - """Test adding a new user name""" - user_name = "Test user" - user_id = test_db_call.add_user_name(user_name) - assert user_id > 0 - - # Check if the user name was actually added - sql = "SELECT id, name FROM user WHERE name = ?" - results = test_db_call.read_database(sql, (user_name,)) - assert len(results) == 1 - assert results[0][1] == user_name - - -def test_add_duplicate_user_name(test_db_call): - """Test adding a duplicate user name (should succeed at DB level without constraints)""" - user_name = "Duplicate User" - # Add first user - user_id1 = test_db_call.add_user_name(user_name) - assert user_id1 > 0 - - # Add second user with same name - user_id2 = test_db_call.add_user_name(user_name) - assert user_id2 > 0 - assert user_id2 != user_id1 # They should have different IDs - - -def test_get_users(test_db_call): - """Test getting all users""" - # Add some test users first - test_db_call.add_user_name("User One") - test_db_call.add_user_name("User Two") - - # Get users - users = test_db_call.get_users() - assert isinstance(users, list) - assert len(users) >= 2 - - # Check that users are returned as (id, name) tuples - for user in users: - assert len(user) == 2 - assert isinstance(user[0], int) # ID - assert isinstance(user[1], str) # Name - - -def test_add_category_success(test_db_call): - # Add a user first, since category requires a user_id - user_id = test_db_call.add_user_name("CategoryTestUser") - assert user_id != 0 - # Add a category with all fields - category_id = test_db_call.add_category(user_id, "Groceries", 200.0, None) - assert category_id != 0 - # Add a category with only required fields - category_id2 = test_db_call.add_category(user_id, "Utilities") - assert category_id2 != 0 - - -def test_add_category_null_budget_and_parent(test_db_call): - user_id = test_db_call.add_user_name("NullBudgetParentUser") - assert user_id != 0 - # Add a category with None for budget_value and parent_id - category_id = test_db_call.add_category(user_id, "NoBudgetOrParent", None, None) - assert category_id != 0 - - -def test_add_category_and_get_categories(test_db_call): - # Add a user - user_name = "CategoryTestUser" - user_id = test_db_call.add_user_name(user_name) - assert user_id != 0 - # Add a category for this user - category_name = "Groceries" - budget_value = 100.0 - parent_id = None - category_id = test_db_call.add_category(user_id, category_name, budget_value, parent_id) - assert category_id != 0 - # Add a subcategory - subcategory_name = "Supermarket" - subcategory_id = test_db_call.add_category(user_id, subcategory_name, 50.0, category_id) - assert subcategory_id != 0 - # Retrieve categories for this user - categories = test_db_call.get_categories(user_id) - assert isinstance(categories, list) - names = [cat[1] for cat in categories] - assert category_name in names - assert subcategory_name in names - - -def test_add_category_invalid_user(test_db_call): - # Try to add a category with a non-existent user_id - # (should still succeed in SQLite unless foreign keys are enforced) - category_id = test_db_call.add_category(99999, "InvalidUserCategory") - assert category_id == 0 - assert isinstance(category_id, int) +# TODO: Put the below tests in their respective test files (user and category) + +# def test_add_user_name(test_db_call): +# """Test adding a new user name""" +# user_name = "Test user" +# user_id = test_db_call.add_user_name(user_name) +# assert user_id > 0 +# +# # Check if the user name was actually added +# sql = "SELECT id, name FROM user WHERE name = ?" +# results = test_db_call.read_database(sql, (user_name,)) +# assert len(results) == 1 +# assert results[0][1] == user_name +# +# +# def test_add_duplicate_user_name(test_db_call): +# """Test adding a duplicate user name (should succeed at DB level without constraints)""" +# user_name = "Duplicate User" +# # Add first user +# user_id1 = test_db_call.add_user_name(user_name) +# assert user_id1 > 0 +# +# # Add second user with same name +# user_id2 = test_db_call.add_user_name(user_name) +# assert user_id2 > 0 +# assert user_id2 != user_id1 # They should have different IDs +# +# +# def test_get_users(test_db_call): +# """Test getting all users""" +# # Add some test users first +# test_db_call.add_user_name("User One") +# test_db_call.add_user_name("User Two") +# +# # Get users +# users = test_db_call.get_users() +# assert isinstance(users, list) +# assert len(users) >= 2 +# +# # Check that users are returned as (id, name) tuples +# for user in users: +# assert len(user) == 2 +# assert isinstance(user[0], int) # ID +# assert isinstance(user[1], str) # Name +# +# +# def test_add_category_success(test_db_call): +# # Add a user first, since category requires a user_id +# user_id = test_db_call.add_user_name("CategoryTestUser") +# assert user_id != 0 +# # Add a category with all fields +# category_id = test_db_call.add_category(user_id, "Groceries", 200.0, None) +# assert category_id != 0 +# # Add a category with only required fields +# category_id2 = test_db_call.add_category(user_id, "Utilities") +# assert category_id2 != 0 +# +# +# def test_add_category_null_budget_and_parent(test_db_call): +# user_id = test_db_call.add_user_name("NullBudgetParentUser") +# assert user_id != 0 +# # Add a category with None for budget_value and parent_id +# category_id = test_db_call.add_category(user_id, "NoBudgetOrParent", None, None) +# assert category_id != 0 +# +# +# def test_add_category_and_get_categories(test_db_call): +# # Add a user +# user_name = "CategoryTestUser" +# user_id = test_db_call.add_user_name(user_name) +# assert user_id != 0 +# # Add a category for this user +# category_name = "Groceries" +# budget_value = 100.0 +# parent_id = None +# category_id = test_db_call.add_category(user_id, category_name, budget_value, parent_id) +# assert category_id != 0 +# # Add a subcategory +# subcategory_name = "Supermarket" +# subcategory_id = test_db_call.add_category(user_id, subcategory_name, 50.0, category_id) +# assert subcategory_id != 0 +# # Retrieve categories for this user +# categories = test_db_call.get_categories(user_id) +# assert isinstance(categories, list) +# names = [cat[1] for cat in categories] +# assert category_name in names +# assert subcategory_name in names +# +# +# def test_add_category_invalid_user(test_db_call): +# # Try to add a category with a non-existent user_id +# # (should still succeed in SQLite unless foreign keys are enforced) +# category_id = test_db_call.add_category(99999, "InvalidUserCategory") +# assert category_id == 0 +# assert isinstance(category_id, int) diff --git a/verity.py b/verity.py index 64f133b..c475100 100644 --- a/verity.py +++ b/verity.py @@ -6,7 +6,7 @@ from flask import Flask from api.src.config import VerityConfig -from api.src.data_handler import database +from api.src.data_handler import Database from front.home import home_bp @@ -27,7 +27,7 @@ def set_up_logging(config): logger.info("app starting") # database initialise - verity = database(verity_config) + verity = Database(verity_config) logger.debug(verity_config) verity.build_database() From 6fa527f9e15479f34e7a8530487a071fa86185bb Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Sat, 14 Jun 2025 08:50:11 +0100 Subject: [PATCH 25/32] =?UTF-8?q?chore:=20=F0=9F=A7=B9=20Adding=20Category?= =?UTF-8?q?=20Class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/category.py | 37 +++++++++++++++++++++++++++++++++++++ api/src/user.py | 36 +++++++++++++++++------------------- front/home.py | 4 ++-- 3 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 api/src/category.py diff --git a/api/src/category.py b/api/src/category.py new file mode 100644 index 0000000..30d9f1d --- /dev/null +++ b/api/src/category.py @@ -0,0 +1,37 @@ +import logging + +from api.src.data_handler import Database +from api.src.user import User + +logger = logging.getLogger(__name__) + + +class Category: + "Main category class, for anything related to the category" + + def __init__(self, database: Database, user: User, category_name: str, budget_value: int = 0): + self.database: Database = database + self.name: str = category_name + self.budget_value: int = budget_value + self.id: int = 0 + self.children: [Category] = [] + self.parent: Category = None + self.user: User = user + logger.info(f"{self.name} initialised") + + def add(self): + logger.info(f"Adding {self.name} to {self.user.name}") + sql_statement = """ + INSERT INTO category (user_id, name, budget_value, parent_id) + VALUES (?, ?, ?, ?) + """ + if not self.parent: + self.get_default_category() + params = (self.user.id, self.name, self.budget_value, self.parent.id) + success, self.id = self.database.execute_sql(sql_statement, params, return_id=True) + if not success: + logger.error(f"Failed to add {self.name} to {self.user.name} Check the logs") + return self.id + + def get_default_category(self): + pass diff --git a/api/src/user.py b/api/src/user.py index 1b82e86..b8b48b8 100644 --- a/api/src/user.py +++ b/api/src/user.py @@ -10,42 +10,40 @@ class User: def __init__(self, database: Database, user_name: str): self.database: Database = database - self.user_name: str = user_name - self.user_id: int = 0 + self.name: str = user_name + self.id: int = 0 self.categories: list = [] logger.info(f"{self.user_name} initialised.") - def add_user_name(self) -> int: - logger.info(f"adding user {self.user_name}") + def add(self) -> int: + logger.info(f"adding user {self.name}") sql = "INSERT INTO USER(name) VALUES (?)" - params = (self.user_name,) - success, self.user_id = self.database.execute_sql(sql, params, return_id=True) + params = (self.name,) + success, self.id = self.database.execute_sql(sql, params, return_id=True) if not success: - logger.error(f"Failed to insert user {self.user_name} Please check the logs") + logger.error(f"Failed to insert user {self.name} Please check the logs") return 0 category_sql = """INSERT INTO category (user_id, name, budget_value, parent_id) VALUES (?, ?, ?, ?)""" - category_params = (self.user_id, "internal_master_category", 0, None) + category_params = (self.id, "internal_master_category", 0, None) category_success, default_category_id = self.database.execute_sql( category_sql, category_params, return_id=True, seed=True ) if not category_success: - logger.error( - f"Failed to add default category for {self.user_name} Please check the logs" - ) + logger.error(f"Failed to add default category for {self.name} Please check the logs") self.categories.append(default_category_id) # if category fails to add, # we probably need to delete the user, so we dont lock the username # change id to category object when we make category class - return self.user_id + return self.id - def user_exists(self) -> bool: - logger.info(f"Checking to see if {self.user_name} already exists!") + def exists(self) -> bool: + logger.info(f"Checking to see if {self.name} already exists!") sql = "SELECT 1 FROM user where name = ?" - params = (self.user_name,) - user_exists = self.database.read_database(sql, params) - logger.info(user_exists) - if user_exists: - logger.warning(f"{self.user_name} already exists in the database") + params = (self.name,) + user_in_db = self.database.read_database(sql, params) + logger.info(user_in_db) + if user_in_db: + logger.warning(f"{self.name} already exists in the database") return True return False diff --git a/front/home.py b/front/home.py index 20fc035..6899b17 100644 --- a/front/home.py +++ b/front/home.py @@ -46,11 +46,11 @@ def submit_user_name(): verity_user = User(Database(VerityConfig()), user_name) logger.info(f"does user '{user_name}' exist?") - if verity_user.user_exists(): + if verity_user.exists(): flash("A user with that name already exists.", "danger") return redirect(url_for("home.home_page")) logger.info(f"User submitted new user name: {user_name}") - user_id = verity_user.add_user_name() + user_id = verity_user.add() if user_id == 0: flash("User Name not saved, please check the logs", "danger") return redirect(url_for("home.home_page")) From d3e8262e290575440ab2dce0879b3f1dc419e28a Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Fri, 20 Jun 2025 08:03:05 +0100 Subject: [PATCH 26/32] =?UTF-8?q?refactor:=20=F0=9F=94=A8=20More=20work=20?= =?UTF-8?q?to=20make=20classes=20handle=20things?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/category.py | 92 +++++++++++++++++++++++++++++++++----- api/src/data_handler.py | 70 ++++++++++++++--------------- api/src/user.py | 52 +++++++++++++++++---- front/home.py | 42 +++++++++++------ front/templates/home.html | 9 ++-- tests/test_data_handler.py | 2 +- 6 files changed, 194 insertions(+), 73 deletions(-) diff --git a/api/src/category.py b/api/src/category.py index 30d9f1d..edef1fb 100644 --- a/api/src/category.py +++ b/api/src/category.py @@ -1,7 +1,8 @@ import logging +from typing import Optional from api.src.data_handler import Database -from api.src.user import User +# from api.src.user import User logger = logging.getLogger(__name__) @@ -9,29 +10,96 @@ class Category: "Main category class, for anything related to the category" - def __init__(self, database: Database, user: User, category_name: str, budget_value: int = 0): + def __init__( + self, + database: Database, + user_id: int, + category_name: str = "", + budget_value: int = 0, + id: int = 0, + parent: Optional["Category"] = None, + ): self.database: Database = database self.name: str = category_name self.budget_value: int = budget_value - self.id: int = 0 - self.children: [Category] = [] - self.parent: Category = None - self.user: User = user - logger.info(f"{self.name} initialised") + self.id: int = id + self.children: list[Category] = [] + self.parent: Category = parent + self.user_id = user_id + if not self.name: + self.get_name() + logger.info(f"{self.name} / {self.id} initialised") + + def __repr__(self): + return f"""Category:( + Name: {self.name} + Id: {self.id} + Budget Value: {self.budget_value} + Number of Childen: {len(self.children)} + Parent:{self.parent.name if self.parent else "Top Level Category"} + )""" + + def __str__(self): + return f"Category: {self.name}" def add(self): - logger.info(f"Adding {self.name} to {self.user.name}") + logger.info(f"Adding {self.name} to user id {self.user_id}") sql_statement = """ INSERT INTO category (user_id, name, budget_value, parent_id) VALUES (?, ?, ?, ?) """ if not self.parent: self.get_default_category() - params = (self.user.id, self.name, self.budget_value, self.parent.id) - success, self.id = self.database.execute_sql(sql_statement, params, return_id=True) + params = (self.user_id, self.name, self.budget_value, int(self.parent.id)) + success, self.id = self.database.execute(sql_statement, params, return_id=True) if not success: - logger.error(f"Failed to add {self.name} to {self.user.name} Check the logs") + logger.error(f"Failed to add Category {self.name} Check the logs") return self.id def get_default_category(self): - pass + logger.info("Getting Default Category") + self.parent = Category(self.database, self.user_id, "internal_master_category") + self.parent.get_id() + + def get_id(self): + logger.info(f"Getting Category id for {self.name}") + category_id_sql = "SELECT id FROM category WHERE user_id = ? AND name = ?" + category_id_params = (self.user_id, self.name) + category_id = self.database.read(category_id_sql, category_id_params) + logger.info(f"Category id is {category_id}") + try: + self.id = category_id[0][0] + except IndexError: + self.id = 0 + + def get_name(self): + logger.info(f"Getting Name for Category {self.id}") + cat_name_sql = "SELECT name FROM category WHERE user_id = ? and id = ?" + cat_name_params = (self.user_id, self.id) + cat_name = self.database.read(cat_name_sql, cat_name_params) + logger.debug(f"Category name returned {cat_name}") + self.name = cat_name[0][0] + + def get_children(self): + logger.info(f"Getting Child Categories for {self.name}") + sql = """ + SELECT id, name, budget_value + FROM category + WHERE user_id = ? + AND parent_id = ? + """ + params = (self.user_id, self.id) + child_categories = self.database.read(sql, params) + for child in child_categories: + category = Category(self.database, self.user_id, child[1], child[2], child[0]) + self.children.append(category) + + def get(self): + logger.info(f"Getting details for {self.name} from database") + sql = """SELECT user_id, name, budget_value, id, parent_id + FROM category + WHERE id = ? + AND user_id = ? + """ + params = (self.id, self.user_id) + return self.database.read(sql, params) diff --git a/api/src/data_handler.py b/api/src/data_handler.py index e922659..7e7ad89 100644 --- a/api/src/data_handler.py +++ b/api/src/data_handler.py @@ -15,7 +15,7 @@ def __init__(self, config) -> None: self.schema = self.verity_config.DATABASE_SCHEMA self.database = self.verity_config.DATABASE - def execute_sql( + def execute( self, sql_statement: str, params: tuple = (), return_id: bool = False, seed: bool = False ) -> (bool, int): "send the query here, returns true if successful, false if fail" @@ -52,7 +52,7 @@ def execute_sql( else: return is_success - def read_database(self, sql_statement: str, params: tuple = ()) -> list: + def read(self, sql_statement: str, params: tuple = ()) -> list: "reads the database query and returns the results" logger.info(f"received request to read {sql_statement} with params {params}") results = [] @@ -153,36 +153,36 @@ def print_table_schema(self, table_name): def get_users(self) -> list: """Returns all users in the database.""" get_user_sql = "SELECT id, name FROM user" - return self.read_database(get_user_sql) - - def add_category( - self, user_id: int, category_name: str, budget_value: int = 0, parent_id=None, seed=False - ) -> int: - """Inserts a new category. Returns the category id.""" - logger.debug(f"attempting to insert category '{category_name}' for user {user_id}") - sql_statement = """ - INSERT INTO category (user_id, name, budget_value, parent_id) - VALUES (?, ?, ?, ?)""" - if not seed: - if not parent_id: - parent_id = self.read_database( - "SELECT id FROM category WHERE user_id = ? AND name = ?", - (user_id, "internal_master_category"), - ) - try: - parent_id = parent_id[0][0] - except IndexError: - parent_id = None - params = (user_id, category_name, budget_value, parent_id) - - success, category_id = self.execute_sql(sql_statement, params, True, seed) - if not success: - logger.error("Failed to execute sql, check the logs") - return category_id - - def get_categories(self, user_id: int) -> list: - """Returns all categories for a given user.""" - sql = ( - "SELECT id, name, budget_value, parent_id FROM category WHERE user_id = ? and name != ?" - ) - return self.read_database(sql_statement=sql, params=(user_id, "internal_master_category")) + return self.read(get_user_sql) + + # def add_category( + # self, user_id: int, category_name: str, budget_value: int = 0, parent_id=None, seed=False + # ) -> int: + # """Inserts a new category. Returns the category id.""" + # logger.debug(f"attempting to insert category '{category_name}' for user {user_id}") + # sql_statement = """ + # INSERT INTO category (user_id, name, budget_value, parent_id) + # VALUES (?, ?, ?, ?)""" + # if not seed: + # if not parent_id: + # parent_id = self.read_database( + # "SELECT id FROM category WHERE user_id = ? AND name = ?", + # (user_id, "internal_master_category"), + # ) + # try: + # parent_id = parent_id[0][0] + # except IndexError: + # parent_id = None + # params = (user_id, category_name, budget_value, parent_id) + # + # success, category_id = self.execute_sql(sql_statement, params, True, seed) + # if not success: + # logger.error("Failed to execute sql, check the logs") + # return category_id + # + # def get_categories(self, user_id: int) -> list: + # """Returns all categories for a given user.""" + # sql = ( + # "SELECT id, name, budget_value, parent_id FROM category WHERE user_id = ? and name != ?" + # ) + # return self.read_database(sql_statement=sql, params=(user_id, "internal_master_category")) diff --git a/api/src/user.py b/api/src/user.py index b8b48b8..782bee3 100644 --- a/api/src/user.py +++ b/api/src/user.py @@ -1,6 +1,8 @@ import logging +from typing import List from api.src.data_handler import Database +from api.src.category import Category logger = logging.getLogger(__name__) @@ -8,42 +10,74 @@ class User: "Main user class, for anything related to the user" - def __init__(self, database: Database, user_name: str): + def __init__(self, database: Database, user_name: str = "", id: int = 0): self.database: Database = database self.name: str = user_name - self.id: int = 0 - self.categories: list = [] - logger.info(f"{self.user_name} initialised.") + self.id: int = id + self.categories: List = [] + logger.info(f"{self.name} initialised.") + + def __str__(self): + return f"User is {self.name}" def add(self) -> int: logger.info(f"adding user {self.name}") sql = "INSERT INTO USER(name) VALUES (?)" params = (self.name,) - success, self.id = self.database.execute_sql(sql, params, return_id=True) + success, self.id = self.database.execute(sql, params, return_id=True) if not success: logger.error(f"Failed to insert user {self.name} Please check the logs") return 0 category_sql = """INSERT INTO category (user_id, name, budget_value, parent_id) VALUES (?, ?, ?, ?)""" category_params = (self.id, "internal_master_category", 0, None) - category_success, default_category_id = self.database.execute_sql( + category_success, default_category_id = self.database.execute( category_sql, category_params, return_id=True, seed=True ) if not category_success: logger.error(f"Failed to add default category for {self.name} Please check the logs") - self.categories.append(default_category_id) # if category fails to add, # we probably need to delete the user, so we dont lock the username - # change id to category object when we make category class return self.id def exists(self) -> bool: logger.info(f"Checking to see if {self.name} already exists!") sql = "SELECT 1 FROM user where name = ?" params = (self.name,) - user_in_db = self.database.read_database(sql, params) + user_in_db = self.database.read(sql, params) logger.info(user_in_db) if user_in_db: logger.warning(f"{self.name} already exists in the database") return True return False + + def get_categories(self): + "Gets all top level categories for the user" + logger.info(f"Getting Categories for {self.name}") + sql = """ + SELECT id, name, budget_value + FROM category + WHERE user_id = ? + AND parent_id = (select id from category where user_id = ? and name = ?) + """ + params = (self.id, self.id, "internal_master_category") + categories = self.database.read(sql, params) + logger.info(f"categories: {categories}") + for category in categories: + cat = Category(self.database, self.id, category[1], category[2], category[0]) + self.categories.append(cat) + return self.categories + + def get(self): + logger.info("Getting details for User") + sql = """ + SELECT name + FROM user + WHERE id = ? + """ + params = (self.id,) + name = self.database.read(sql, params) + logger.info(name) + name = name[0][0] + logger.info(name) + self.name = name diff --git a/front/home.py b/front/home.py index 6899b17..5145fc1 100644 --- a/front/home.py +++ b/front/home.py @@ -2,10 +2,11 @@ from flask import Blueprint, flash, redirect, render_template, request, session, url_for -from api.src.data_handler import Database +from api.src.config import VerityConfig from api.src.currency_handler import CurrencyBrain +from api.src.data_handler import Database from api.src.user import User -from api.src.config import VerityConfig +from api.src.category import Category logger = logging.getLogger(__name__) @@ -21,12 +22,17 @@ def home_page(): # Get user info directly from session user_id = session.get("user_id") selected_user_name = session.get("user_name") - # Debug log to see what's being passed to the template logger.debug(f"User ID from session: {user_id}, User Name from session: {selected_user_name}") logger.info(f"{selected_user_name} is logged in") - - categories = db_call.get_categories(user_id) if user_id else [] + verity_user = User(db_call, selected_user_name, user_id) + logger.info(f"user: {verity_user}") + if not verity_user: + categories = [] + else: + categories = verity_user.get_categories() + for category in categories: + category.get_children() return render_template( "home.html", users=users, @@ -78,13 +84,14 @@ def select_user(): return redirect(url_for("home.home_page")) # Get the user details directly from the database using the ID - db_call = Database(VerityConfig()) - result = db_call.read_database("SELECT name FROM user WHERE id = ?", (selected_user_id,)) + database = Database(VerityConfig()) + verity_user = User(database, id=selected_user_id) + verity_user.get() - if result and result[0]: + if verity_user.name and verity_user.id: # Store both ID and name in the session session["user_id"] = selected_user_id - session["user_name"] = result[0][0] + session["user_name"] = verity_user.name flash("User selected!", "success") else: session.pop("user_id", None) @@ -121,12 +128,21 @@ def submit_category(): budget_value = CurrencyBrain.convert_to_universal_currency(budget_value_input) # Convert parent_id to int if provided parent_id = request.form.get("parentId", "0").strip() - - db_call = Database(VerityConfig()) + database = Database(VerityConfig()) if parent_id == 0: - category_id = db_call.add_category(user_id, category_name, budget_value) + new_category = Category(database, user_id, category_name, budget_value) else: - category_id = db_call.add_category(user_id, category_name, budget_value, parent_id) + parent_category = Category(database=database, user_id=user_id, id=parent_id) + new_category = Category( + database=database, + user_id=user_id, + category_name=category_name, + budget_value=budget_value, + parent=parent_category, + ) + logger.info(repr(new_category)) + category_id = new_category.add() + logger.info(repr(new_category)) if category_id == 0: flash("Category not saved, please check the logs", "danger") else: diff --git a/front/templates/home.html b/front/templates/home.html index 667cec8..89223b6 100644 --- a/front/templates/home.html +++ b/front/templates/home.html @@ -43,7 +43,7 @@

Home of Verity

@@ -53,7 +53,10 @@

Home of Verity

Your Categories:
{% for category in categories %} - - {{ category[1] }}{% if category[3] %} (Sub of ID {{ category[3] }}){% endif %}{% if category[2] %} [Budget: {{ category[2] }}]{% endif %}
+ - {{ category.name }} - Budget: {{ category.budget }}
+{% if category.children|length > 0 %}{% for child in category.children %} + - {{ child.name}} - Budget: {{ child.budget }}
+{% endfor %}{% endif %} {% endfor %}

@@ -62,4 +65,4 @@

Home of Verity

Please select a user before adding a category. {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index 9643613..47b938a 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -3,8 +3,8 @@ import pytest -from api.src.data_handler import Database from api.src.config import VerityConfig +from api.src.data_handler import Database @pytest.fixture From 48f8c7fbda2ecc13e3c13f392ff8fc41eef6af9b Mon Sep 17 00:00:00 2001 From: Jake Pullen Date: Mon, 23 Jun 2025 07:51:41 +0100 Subject: [PATCH 27/32] =?UTF-8?q?refactor:=20=F0=9F=94=A8=20Almost=20done,?= =?UTF-8?q?=20need=20to=20fix=20and=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/src/category.py | 14 ++++++++++---- api/src/data_handler.py | 2 ++ api/src/user.py | 18 ++++++++++++++++-- front/home.py | 12 ++++++++++-- front/templates/home.html | 17 ++++++++--------- 5 files changed, 46 insertions(+), 17 deletions(-) diff --git a/api/src/category.py b/api/src/category.py index edef1fb..73e436a 100644 --- a/api/src/category.py +++ b/api/src/category.py @@ -49,6 +49,7 @@ def add(self): VALUES (?, ?, ?, ?) """ if not self.parent: + logger.debug("No parent linked to category, linking now.") self.get_default_category() params = (self.user_id, self.name, self.budget_value, int(self.parent.id)) success, self.id = self.database.execute(sql_statement, params, return_id=True) @@ -59,7 +60,9 @@ def add(self): def get_default_category(self): logger.info("Getting Default Category") self.parent = Category(self.database, self.user_id, "internal_master_category") + logger.info(f"master category = {self.parent}") self.parent.get_id() + logger.info(f"parent id = {self.parent.id}") def get_id(self): logger.info(f"Getting Category id for {self.name}") @@ -78,7 +81,10 @@ def get_name(self): cat_name_params = (self.user_id, self.id) cat_name = self.database.read(cat_name_sql, cat_name_params) logger.debug(f"Category name returned {cat_name}") - self.name = cat_name[0][0] + try: + self.name = cat_name[0][0] + except IndexError: + self.name = "Unknown" def get_children(self): logger.info(f"Getting Child Categories for {self.name}") @@ -96,9 +102,9 @@ def get_children(self): def get(self): logger.info(f"Getting details for {self.name} from database") - sql = """SELECT user_id, name, budget_value, id, parent_id - FROM category - WHERE id = ? + sql = """SELECT user_id, name, budget_value, id, parent_id + FROM category + WHERE id = ? AND user_id = ? """ params = (self.id, self.user_id) diff --git a/api/src/data_handler.py b/api/src/data_handler.py index 7e7ad89..09cd631 100644 --- a/api/src/data_handler.py +++ b/api/src/data_handler.py @@ -48,8 +48,10 @@ def execute( connection.close() logger.info("closed connection to database") if return_id: + logger.debug(f"Returning tuple (success), (new id) ({is_success},{new_id})") return (is_success, new_id) else: + logger.debug(f"Returning success value {is_success}") return is_success def read(self, sql_statement: str, params: tuple = ()) -> list: diff --git a/api/src/user.py b/api/src/user.py index 782bee3..d019653 100644 --- a/api/src/user.py +++ b/api/src/user.py @@ -15,6 +15,7 @@ def __init__(self, database: Database, user_name: str = "", id: int = 0): self.name: str = user_name self.id: int = id self.categories: List = [] + self.internal_category_id = 0 logger.info(f"{self.name} initialised.") def __str__(self): @@ -38,6 +39,8 @@ def add(self) -> int: logger.error(f"Failed to add default category for {self.name} Please check the logs") # if category fails to add, # we probably need to delete the user, so we dont lock the username + logger.info(f"master_category id = {default_category_id}") + self.internal_category_id = default_category_id return self.id def exists(self) -> bool: @@ -58,9 +61,11 @@ def get_categories(self): SELECT id, name, budget_value FROM category WHERE user_id = ? - AND parent_id = (select id from category where user_id = ? and name = ?) + AND parent_id = ? """ - params = (self.id, self.id, "internal_master_category") + if self.internal_category_id == 0: + self.get_internal_master_category() + params = (self.id, self.internal_category_id) categories = self.database.read(sql, params) logger.info(f"categories: {categories}") for category in categories: @@ -81,3 +86,12 @@ def get(self): name = name[0][0] logger.info(name) self.name = name + + def get_internal_master_category(self): + sql = """ + SELECT id FROM category WHERE name = ? and user_id = ? + """ + params = ("internal_master_category", self.id) + master_id = self.database.read(sql, params) + master_id = master_id[0][0] + self.internal_category_id = master_id diff --git a/front/home.py b/front/home.py index 5145fc1..14f589b 100644 --- a/front/home.py +++ b/front/home.py @@ -129,9 +129,17 @@ def submit_category(): # Convert parent_id to int if provided parent_id = request.form.get("parentId", "0").strip() database = Database(VerityConfig()) - if parent_id == 0: - new_category = Category(database, user_id, category_name, budget_value) + logger.debug(f"parent_id for new category is {parent_id}") + if int(parent_id) == int(0): + logger.info("user submited category with no parent, class should get default category") + new_category = Category( + database=database, + user_id=user_id, + category_name=category_name, + budget_value=budget_value, + ) else: + logger.info("user submited category with parent, class should use that id") parent_category = Category(database=database, user_id=user_id, id=parent_id) new_category = Category( database=database, diff --git a/front/templates/home.html b/front/templates/home.html index 89223b6..c16c850 100644 --- a/front/templates/home.html +++ b/front/templates/home.html @@ -41,7 +41,7 @@

Home of Verity