diff --git a/.github/workflows/stage_unit_tests.yml b/.github/workflows/stage_unit_tests.yml index d8d4846..e5f8769 100644 --- a/.github/workflows/stage_unit_tests.yml +++ b/.github/workflows/stage_unit_tests.yml @@ -1,13 +1,15 @@ name: Unit Tests - Stage Branch on: - push: + pull_request: + types: [opened , reopene, edited] branches: - - stage + - 'stage' + workflow_dispatch: # Allows manual triggering from the UI jobs: - pydantic_tests: + Stage_Pull_Request_Unit_Tests: runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..a5b4256 --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,31 @@ +## Pull Request Type: + +**1. Title:** [Concise and Descriptive Title - Explain the Change] + + * Example: "Fix: Incorrect Calculation in User Profile" + * Example: "Feature: Add Support for Dark Mode" + * Keep titles short and informative. A good title should immediately convey the essence of the change. + +**2. Description:** + + * **Summary:** Briefly describe the purpose of this pull request in 1-2 sentences. + * **Motivation:** Explain *why* this change is needed. What problem does it solve? What opportunity does it address? Provide context. + * **Proposed Solution:** Describe your solution in detail. Explain *how* you implemented the change. Be specific. + * **Screenshots/GIFs (if applicable):** If your change involves UI changes, include screenshots or a GIF to demonstrate the impact. + * **Testing:** Describe the tests you've added or run to ensure the change is correct. (e.g., "Added unit tests for the new function," "Manually tested on Chrome, Firefox, and Safari"). + * **Known Limitations/Future Considerations:** Are there any known issues with this change? Are there any potential future improvements you envision? + +**3. Checklist:** + + [ ] I have followed the project's coding style and conventions. + [ ] I have written unit tests to cover my changes. + [ ] My code has been tested locally. + [ ] I have included screenshots/GIFs if applicable. + [ ] I have updated the documentation (if necessary). + [ ] I have labeled this PR appropriately (e.g., "bug," "feature," "refactor"). + + +**4. Reviewer Notes (Optional - Add if you have specific requests)** + + * "Please pay particular attention to the [specific section] of the code." + * "Would appreciate feedback on the design of this component." diff --git a/pyproject.toml b/pyproject.toml index 0d8897a..b2ff274 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,3 +16,4 @@ pythonpath = ["."] testspaths = ["tests"] python_files = "test_*.py" python_functions = "test_*" + diff --git a/src/config.py b/src/config.py index d39aacf..02db6f5 100644 --- a/src/config.py +++ b/src/config.py @@ -1,20 +1,20 @@ import os import yaml -class VerityConfig: +class VerityConfig: def __init__(self): - self.SECRET_KEY = os.environ.get('SECRET_KEY') or 'super_secure_secret_key' - self.DATABASE = 'Verity.db' - 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.SECRET_KEY = os.environ.get("SECRET_KEY") or "super_secret_key" + self.DATABASE = "Verity.db" + 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") def load_config_file(self, file): - config = '' - filepath = os.path.join(self.CONFIG_FILE_DIRECTORY,file) + config = "" + filepath = os.path.join(self.CONFIG_FILE_DIRECTORY, file) try: - with open(filepath, 'r') as f: + with open(filepath, "r") as f: try: config = yaml.safe_load(f) except yaml.YAMLError as e: diff --git a/src/config_files/logging_config.yaml b/src/config_files/logging_config.yaml index 3d8b5b4..6af3902 100644 --- a/src/config_files/logging_config.yaml +++ b/src/config_files/logging_config.yaml @@ -11,7 +11,7 @@ formatters: handlers: stderr: class: logging.StreamHandler - level: INFO + level: DEBUG formatter: nodate stream: ext://sys.stdout file: diff --git a/src/data_handler.py b/src/data_handler.py index 825c8da..16eb705 100644 --- a/src/data_handler.py +++ b/src/data_handler.py @@ -1,48 +1,98 @@ import sqlite3 import logging -from config import VerityConfig +from datetime import datetime logger = logging.getLogger(__name__) -class database(): - 'basic Database class to start some development' - def __init__(self, config: VerityConfig ) -> None: - self.database = config.DATABASE - self.schema = config.DATABASE_SCHEMA + +class database: + """basic Database class to start some development + Will need a proper refactor once basic functions are in and working + This is POC + """ + + def __init__(self, config) -> None: + self.verity_config = config + 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): + "send the query here, returns true if successful, false if fail" + logging.debug(f"received request to execute {sql_statement}") + new_id: int = 0 + is_success: bool = False + try: + connection = sqlite3.connect( + database=self.database, + timeout=10, # seconds i hope + ) + logging.debug("opened connection to database") + cursor = connection.cursor() + cursor.execute(sql_statement, {}) + connection.commit() + if return_id: + new_id = cursor.lastrowid + is_success = True + except Exception as e: # TODO: better Exception handling + logging.error(e) + is_success = False + finally: + connection.close() + if return_id: + return (is_success, new_id) + 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: - name = column['column_name'] - is_pk = column['is_pk'] - datatype = column['datatype'] - nullable = column['nullable'] - column_string = f'{name} {datatype}' + def _build_column(column: dict) -> str: + name = column["column_name"] + is_pk = column["is_pk"] + datatype = column["datatype"] + nullable = column["nullable"] + column_string = f"{name} {datatype}" if is_pk: - column_string += ' PRIMARY KEY AUTOINCREMENT' + column_string += " PRIMARY KEY AUTOINCREMENT" if not nullable: - column_string += ' NOT NULL' + column_string += " NOT NULL" return column_string @staticmethod - def _build_foreign_key(key:dict) -> str: - column = key['column'] - reference_table = key['references'] - reference_column = key['reference_column'] - return f'FOREIGN KEY ({column}) REFERENCES {reference_table} ({reference_column})' - - 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']} ( - ''' + def _build_foreign_key(key: dict) -> str: + column = key["column"] + ref_table = key["references"] + ref_column = key["reference_column"] + return f"FOREIGN KEY ({column}) REFERENCES {ref_table} ({ref_column})" + + 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"]} ( + """ columns = [] - for column in table['table_columns']: + for column in table["table_columns"]: columns.append(self._build_column(column)) - sql += ',\n'.join(columns) - if table.get('table_foreign_keys', None): - sql += ',\n' - for key in table['table_foreign_keys']: - sql += f'{self._build_foreign_key(key)}' - sql += '\n);' + sql += ",\n".join(columns) + if table.get("table_foreign_keys", None): + sql += ",\n" + for key in table["table_foreign_keys"]: + sql += f"{self._build_foreign_key(key)}" + sql += "\n);" success_status = False try: connection = sqlite3.connect(self.database) @@ -64,14 +114,15 @@ def _add_table_to_db(self, table:dict) -> bool: return success_status def build_database(self): - for table in self.schema['tables']: + for table in self.schema["tables"]: logger.info(f"Checking {table['table_name']}") # Add true/false handling here to gracefully handle errors self._add_table_to_db(table) def print_table_schema(self, table_name): """ - Connects to the sqlite3 database, retrieves the schema of a specified table, + Connects to the sqlite3 database, + retrieves the schema of a specified table, and prints it to the console. Args: @@ -87,7 +138,9 @@ 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}") @@ -96,4 +149,51 @@ def print_table_schema(self, table_name): try: connection.close() except Exception as e: - logger.error(e) \ No newline at end of file + logger.error(e) + + def add_budget_name(self, budget_name: str) -> int: + "takes budget name string, returns budget id" + now = datetime.now() + formatted_datetime = now.strftime("%Y-%m-%d %H:%M:%S") + logger.debug( + f"attempting to insert values into budget table {budget_name} | { + formatted_datetime + }" + ) + try: + connection = sqlite3.connect(self.database) + logger.debug("connection to db open") + cursor = connection.cursor() + logger.debug("cursor activated") + cursor.execute( + """INSERT INTO budget ( + name, + created_date + ) + VALUES (?,?) + """, + (budget_name, formatted_datetime), + ) + logger.debug("cursor executed") + connection.commit() + budget_id = cursor.lastrowid + logger.debug(f"insert attempt seems successful, budget id is {budget_id}") + + if budget_id is None: + budget_id = 0 + except Exception as e: + logger.error(f"Failed to insert budget name, error: {e}") + budget_id = 0 + finally: + try: + connection.close() + logger.debug("connection to db closed") + return budget_id + except Exception as e: + logger.error(f"failed to close connection message: {e}") + return 0 + + def get_budgets(self) -> list: + get_budget_sql = "SELECT name FROM budget" + budgets = self.read_database(get_budget_sql) + return budgets diff --git a/src/front/home.py b/src/front/home.py index e8da2eb..5f0a360 100644 --- a/src/front/home.py +++ b/src/front/home.py @@ -1,15 +1,40 @@ -from flask import Blueprint, render_template, flash, redirect, url_for, session -import os +from flask import Blueprint, render_template +from flask import flash, redirect, url_for, session, request +import logging -home_bp = Blueprint('home',__name__, template_folder='templates') +import data_handler +from config import VerityConfig -@home_bp.route('/') +logger = logging.getLogger(__name__) + +home_bp = Blueprint("home", __name__, template_folder="templates") +verity_config = VerityConfig() + + +@home_bp.route("/") def home_page(): - cwd = os.getcwd() - return render_template('home.html',cwd=cwd) - -@home_bp.route('/clear_session') -def clear_session(): - session.clear() - flash('All Clear!',category='success') - return redirect(url_for('home.home')) + logger.info("home page hit") + db_call = data_handler.database(verity_config) + budgets = db_call.get_budgets() + return render_template("home.html", budgets=budgets) + + +@home_bp.route("/submit", methods=["POST"]) +def submit_budget_name(): + budget_name = request.form.get("budgetName") + logger.debug(f"Request Received: {request.form}") + if budget_name: + logger.info(f"User submitted new budget name: {budget_name}") + session["budget_name"] = budget_name + db_call = data_handler.database(verity_config) + budget_id = db_call.add_budget_name(budget_name) + if budget_id == 0: + # db entry failed, throw error message + flash("Budget Name not saved, please check the logs", "error") + return redirect(url_for("home.home_page")) + session["budget_id"] = budget_id + flash("Budget name saved!", "success") + return redirect(url_for("home.home_page")) + else: + flash("Please enter a budget name.", "error") + return redirect(url_for("home.home_page")) diff --git a/src/front/static/style.css b/src/front/static/verity_style.css similarity index 100% rename from src/front/static/style.css rename to src/front/static/verity_style.css diff --git a/src/front/templates/base.html b/src/front/templates/base.html index 918789f..dffef7b 100644 --- a/src/front/templates/base.html +++ b/src/front/templates/base.html @@ -13,7 +13,11 @@ crossorigin="anonymous" /> - + diff --git a/src/front/templates/home.html b/src/front/templates/home.html index 5ddacd4..633fc07 100644 --- a/src/front/templates/home.html +++ b/src/front/templates/home.html @@ -1,5 +1,25 @@ {% extends "base.html" %} {% block title %} Home {% endblock %} {% block content %}

Home of Verity

-

Current Directory: {{ cwd }}

+
+
+

New Budget Name:

+ + +
+
+
+

+ {% for budget in budgets %} +- {{ budget[0] }}
+ {% endfor %} +

+
{% endblock %} diff --git a/src/tests/test_config.py b/src/tests/test_config.py index a9d74c5..307da81 100644 --- a/src/tests/test_config.py +++ b/src/tests/test_config.py @@ -2,46 +2,54 @@ import os import yaml + def test_verity_config_default_secret_key(monkeypatch): - """Test that the default secret key is used if the environment variable is not set.""" + """Test that the default secret key is used + if the environment variable is not set.""" testing_config = config.VerityConfig() - assert testing_config.SECRET_KEY == 'super_secure_secret_key' + assert testing_config.SECRET_KEY == "super_secret_key" + def test_verity_config_secret_key_from_env(monkeypatch): """Test that the secret key is loaded from the environment variable.""" - os.environ['SECRET_KEY'] = 'a_real_secret' + os.environ["SECRET_KEY"] = "a_real_secret" testing_config = config.VerityConfig() - assert testing_config.SECRET_KEY == 'a_real_secret' - del os.environ['SECRET_KEY'] # Clean up the environment + assert testing_config.SECRET_KEY == "a_real_secret" + del os.environ["SECRET_KEY"] # Clean up the environment + 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 == "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 == "config_files" + def test_verity_config_load_config_file(monkeypatch): """Test that load_config_file handles successful YAML loading.""" # Mock the YAML file content - mock_config = {'logging_level': 'INFO'} + mock_config = {"logging_level": "INFO"} # Create a temporary file import tempfile + temp_dir = tempfile.mkdtemp() - mock_filepath = os.path.join(temp_dir, 'logging_config.yaml') - with open(mock_filepath, 'w') as f: + mock_filepath = os.path.join(temp_dir, "logging_config.yaml") + with open(mock_filepath, "w") as f: yaml.safe_dump(mock_config, f) # Configure monkeypatch to simulate file loading testing_config = config.VerityConfig() - monkeypatch.setattr(testing_config, 'CONFIG_FILE_DIRECTORY', temp_dir) - loaded_config = testing_config.load_config_file('logging_config.yaml') + monkeypatch.setattr(testing_config, "CONFIG_FILE_DIRECTORY", temp_dir) + loaded_config = testing_config.load_config_file("logging_config.yaml") assert loaded_config == mock_config # Clean up the temporary file import shutil + shutil.rmtree(temp_dir) diff --git a/src/tests/test_data_handler.py b/src/tests/test_data_handler.py new file mode 100644 index 0000000..379c6ff --- /dev/null +++ b/src/tests/test_data_handler.py @@ -0,0 +1,60 @@ +import pytest + +from src import data_handler +from src.config import VerityConfig + + +@pytest.fixture +def test_db_call(): + config = VerityConfig() + db_call = data_handler.database(config) + db_call.build_database() + yield db_call + + +def test_execute_sql_success(test_db_call): + sql_statement = """INSERT INTO budget ( + name, + created_date + ) + VALUES ('a_budget_name','2025-05-23 09:04:00') + """ + result = test_db_call.execute_sql(sql_statement) + assert result + + +def test_execute_sql_error(test_db_call): + 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 + + +def test_read_database(test_db_call): + # Test reading database data + sql_statement = "SELECT name FROM budget" + 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) + + +def test_add_budget_name(test_db_call): + # Test adding a new budget name + budget_name = "Test Budget" + budget_id = test_db_call.add_budget_name(budget_name) + assert budget_id is not None + # Check if the budget name was actually added + # You might need to query the database to verify + # self.assertEqual(budget_id, 1) + + +def test_get_budgets(test_db_call): + # Test getting budgets + budgets = test_db_call.get_budgets() + assert isinstance(budgets, list) + # Check the contents of the list + # self.assertIsNotNone(budgets) + # self.assertIsInstance(budgets[0], str) diff --git a/src/verity.py b/src/verity.py index 4234943..70baf24 100644 --- a/src/verity.py +++ b/src/verity.py @@ -9,29 +9,30 @@ from data_handler import database from front.home import home_bp + def set_up_logging(config): - os.makedirs('../logs', exist_ok=True) + os.makedirs("../logs", exist_ok=True) logging.config.dictConfig(config.LOGGING_CONFIG) - queue_handler = logging.getHandlerByName('queue_handler') + queue_handler = logging.getHandlerByName("queue_handler") if queue_handler is not None: queue_handler.listener.start() atexit.register(queue_handler.listener.stop) + if __name__ == "__main__": # config and logging verity_config = VerityConfig() logger = logging.getLogger(__name__) set_up_logging(verity_config) - logger.info('app starting') + logger.info("app starting") # database initialise verity = database(verity_config) verity.build_database() # app initialise - app = Flask(__name__) - app.config['SECRET_KEY'] = verity_config.SECRET_KEY - app.config['DEBUG'] = True - app.config['STATIC_FOLDER'] = 'front/static' + app = Flask("Verity", static_folder="./front/static/") + app.config["SECRET_KEY"] = verity_config.SECRET_KEY + app.config["DEBUG"] = True app.register_blueprint(home_bp) app.run()