From a64ac45046812ba6505fc77195d4686677df08a5 Mon Sep 17 00:00:00 2001 From: colin Date: Fri, 4 Jul 2025 20:09:47 -0400 Subject: [PATCH] Add API endpoints and prev/next navigation for transactions --- VERSION | 2 +- docker/ploughshares/app.py | 246 +++++++++++++++++- docker/ploughshares/populate_test_data.py | 88 +++++++ .../templates/view_transaction.html | 50 ++++ version_history.log | 1 + 5 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 docker/ploughshares/populate_test_data.py diff --git a/VERSION b/VERSION index 0ea3a94..0c62199 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 +0.2.1 diff --git a/docker/ploughshares/app.py b/docker/ploughshares/app.py index 47e2a0f..92c9140 100644 --- a/docker/ploughshares/app.py +++ b/docker/ploughshares/app.py @@ -271,10 +271,26 @@ def view_transaction(id): abort(404) transaction = None + prev_id = None + next_id = None + try: with conn.cursor() as cur: cur.execute('SELECT * FROM transactions WHERE id = %s', (id,)) transaction = cur.fetchone() + + if transaction: + # Get previous transaction ID + cur.execute('SELECT id FROM transactions WHERE id < %s ORDER BY id DESC LIMIT 1', (id,)) + prev_result = cur.fetchone() + if prev_result: + prev_id = prev_result['id'] + + # Get next transaction ID + cur.execute('SELECT id FROM transactions WHERE id > %s ORDER BY id ASC LIMIT 1', (id,)) + next_result = cur.fetchone() + if next_result: + next_id = next_result['id'] except Exception as e: logger.error(f"Database error: {e}") flash(f"Database error: {e}", "error") @@ -284,7 +300,7 @@ def view_transaction(id): if transaction is None: abort(404) - return render_template('view_transaction.html', transaction=transaction, version=VERSION) + return render_template('view_transaction.html', transaction=transaction, prev_id=prev_id, next_id=next_id, version=VERSION) @app.route('/transaction/add', methods=['GET', 'POST']) def create_transaction(): @@ -501,6 +517,234 @@ def bootstrap_database(): finally: conn.close() +@app.route('/api/transactions', methods=['GET']) +def api_get_transactions(): + """API endpoint to get all transactions""" + conn = get_db_connection() + if conn is None: + return jsonify({"error": "Database connection error"}), 500 + + transactions = [] + try: + with conn.cursor() as cur: + cur.execute('SELECT * FROM transactions ORDER BY id DESC') + transactions = cur.fetchall() + + # Convert transactions to a list of dictionaries + result = [] + for transaction in transactions: + # Format the date if it exists + if transaction.get('source_date'): + transaction['source_date'] = transaction['source_date'].strftime('%Y-%m-%d') + + # Format created_at if it exists + if transaction.get('created_at'): + transaction['created_at'] = transaction['created_at'].strftime('%Y-%m-%dT%H:%M:%S.%f') + + result.append(dict(transaction)) + + except Exception as e: + logger.error(f"Database error in API: {e}") + return jsonify({"error": f"Database error: {str(e)}"}), 500 + finally: + conn.close() + + return jsonify(result) + +@app.route('/api/transaction/', methods=['GET']) +def api_get_transaction(id): + """API endpoint to get a specific transaction""" + conn = get_db_connection() + if conn is None: + return jsonify({"error": "Database connection error"}), 500 + + transaction = None + try: + with conn.cursor() as cur: + cur.execute('SELECT * FROM transactions WHERE id = %s', (id,)) + transaction = cur.fetchone() + + if transaction is None: + return jsonify({"error": "Transaction not found"}), 404 + + # Format the date if it exists + if transaction.get('source_date'): + transaction['source_date'] = transaction['source_date'].strftime('%Y-%m-%d') + + # Format created_at if it exists + if transaction.get('created_at'): + transaction['created_at'] = transaction['created_at'].strftime('%Y-%m-%dT%H:%M:%S.%f') + + # Get previous and next transaction IDs + cur.execute('SELECT id FROM transactions WHERE id < %s ORDER BY id DESC LIMIT 1', (id,)) + prev_result = cur.fetchone() + prev_id = prev_result['id'] if prev_result else None + + cur.execute('SELECT id FROM transactions WHERE id > %s ORDER BY id ASC LIMIT 1', (id,)) + next_result = cur.fetchone() + next_id = next_result['id'] if next_result else None + + # Add navigation info to the response + result = { + "transaction": dict(transaction), + "navigation": { + "prev_id": prev_id, + "next_id": next_id + } + } + + except Exception as e: + logger.error(f"Database error in API: {e}") + return jsonify({"error": f"Database error: {str(e)}"}), 500 + finally: + conn.close() + + return jsonify(result) + +@app.route('/api/transaction', methods=['POST']) +def api_create_transaction(): + """API endpoint to create a transaction""" + if not request.is_json: + return jsonify({"error": "Request must be JSON"}), 400 + + data = request.get_json() + + # Validate required fields + required_fields = ['transaction_type', 'company_division', 'recipient'] + for field in required_fields: + if field not in data: + return jsonify({"error": f"Missing required field: {field}"}), 400 + + # Set defaults for missing fields + default_fields = { + 'address_1': '', 'address_2': '', 'city': '', 'province': '', + 'region': '', 'postal_code': '', 'source_date': None, + 'source_description': '', 'grant_type': '', 'description': '', + 'amount': 0, 'commodity_class': '', 'contract_number': '', 'comments': '' + } + + for field, default in default_fields.items(): + if field not in data: + data[field] = default + + # Set is_primary to False if not provided + data['is_primary'] = data.get('is_primary', False) + + conn = get_db_connection() + if conn is None: + return jsonify({"error": "Database connection error"}), 500 + + try: + with conn.cursor() as cur: + cur.execute( + """ + INSERT INTO transactions ( + transaction_type, company_division, address_1, address_2, + city, province, region, postal_code, is_primary, source_date, source_description, + grant_type, description, amount, recipient, commodity_class, contract_number, comments + ) VALUES ( + %(transaction_type)s, %(company_division)s, %(address_1)s, %(address_2)s, + %(city)s, %(province)s, %(region)s, %(postal_code)s, %(is_primary)s, %(source_date)s, + %(source_description)s, %(grant_type)s, %(description)s, %(amount)s, %(recipient)s, + %(commodity_class)s, %(contract_number)s, %(comments)s + ) RETURNING id + """, + data + ) + result = cur.fetchone() + if result and 'id' in result: + new_transaction_id = result['id'] + conn.commit() + return jsonify({"message": "Transaction created successfully", "transaction_id": new_transaction_id}), 201 + else: + return jsonify({"error": "Could not create transaction"}), 500 + except Exception as e: + logger.error(f"Error creating transaction via API: {e}") + return jsonify({"error": f"Error creating transaction: {str(e)}"}), 500 + finally: + conn.close() + +@app.route('/api/transaction/', methods=['PUT']) +def api_update_transaction(id): + """API endpoint to update a transaction""" + if not request.is_json: + return jsonify({"error": "Request must be JSON"}), 400 + + data = request.get_json() + data['id'] = id + + conn = get_db_connection() + if conn is None: + return jsonify({"error": "Database connection error"}), 500 + + # Check if transaction exists + try: + with conn.cursor() as cur: + cur.execute('SELECT id FROM transactions WHERE id = %s', (id,)) + if cur.fetchone() is None: + return jsonify({"error": "Transaction not found"}), 404 + except Exception as e: + logger.error(f"Error checking transaction existence via API: {e}") + return jsonify({"error": f"Database error: {str(e)}"}), 500 + + # Update fields that are provided + update_fields = [] + update_values = {} + + # Fields that can be updated + allowed_fields = [ + 'transaction_type', 'company_division', 'address_1', 'address_2', + 'city', 'province', 'region', 'postal_code', 'is_primary', 'source_date', + 'source_description', 'grant_type', 'description', 'amount', 'recipient', + 'commodity_class', 'contract_number', 'comments' + ] + + for field in allowed_fields: + if field in data: + update_fields.append(f"{field} = %({field})s") + update_values[field] = data[field] + + if not update_fields: + return jsonify({"error": "No fields to update"}), 400 + + update_values['id'] = id + + try: + with conn.cursor() as cur: + query = f"UPDATE transactions SET {', '.join(update_fields)} WHERE id = %(id)s" + cur.execute(query, update_values) + conn.commit() + return jsonify({"message": "Transaction updated successfully"}), 200 + except Exception as e: + logger.error(f"Error updating transaction via API: {e}") + return jsonify({"error": f"Error updating transaction: {str(e)}"}), 500 + finally: + conn.close() + +@app.route('/api/transaction/', methods=['DELETE']) +def api_delete_transaction(id): + """API endpoint to delete a transaction""" + conn = get_db_connection() + if conn is None: + return jsonify({"error": "Database connection error"}), 500 + + try: + with conn.cursor() as cur: + # Check if transaction exists + cur.execute('SELECT id FROM transactions WHERE id = %s', (id,)) + if cur.fetchone() is None: + return jsonify({"error": "Transaction not found"}), 404 + + # Delete the transaction + cur.execute('DELETE FROM transactions WHERE id = %s', (id,)) + conn.commit() + return jsonify({"message": "Transaction deleted successfully"}), 200 + except Exception as e: + logger.error(f"Error deleting transaction via API: {e}") + return jsonify({"error": f"Error deleting transaction: {str(e)}"}), 500 + finally: + conn.close() + if __name__ == '__main__': logger.info(f"Starting Ploughshares v{VERSION}") bootstrap_database() diff --git a/docker/ploughshares/populate_test_data.py b/docker/ploughshares/populate_test_data.py new file mode 100644 index 0000000..aa8d95c --- /dev/null +++ b/docker/ploughshares/populate_test_data.py @@ -0,0 +1,88 @@ +import os +import psycopg2 +import json +from datetime import datetime + +def get_db_connection(): + host = os.environ.get('POSTGRES_HOST', 'db') + port = os.environ.get('POSTGRES_PORT', '5432') + dbname = os.environ.get('POSTGRES_DB', 'ploughshares') + user = os.environ.get('POSTGRES_USER', 'ploughshares') + password = os.environ.get('POSTGRES_PASSWORD', 'ploughshares_password') + + conn = psycopg2.connect( + host=host, + port=port, + dbname=dbname, + user=user, + password=password + ) + conn.autocommit = True + return conn + +def populate_test_data(): + conn = get_db_connection() + cursor = conn.cursor() + + # Load sample data from data.json + try: + with open('data.json', 'r') as f: + data_json = json.load(f) + + # Extract the transactions array + if 'transactions' not in data_json: + print("Error: 'transactions' key not found in data.json") + return + + data = data_json['transactions'] + except FileNotFoundError: + print("Error: data.json file not found") + return + except json.JSONDecodeError: + print("Error: data.json is not valid JSON") + return + + # Insert sample transactions + for item in data: + try: + cursor.execute(''' + INSERT INTO transactions ( + transaction_type, company_division, address_1, address_2, + city, province, region, postal_code, is_primary, source_date, source_description, + grant_type, description, amount, recipient, commodity_class, contract_number, comments + ) VALUES ( + %s, %s, %s, %s, + %s, %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, %s, %s + ) + ''', ( + item.get('transaction_type', ''), + item.get('company_division', ''), + item.get('address_1', ''), + item.get('address_2', ''), + item.get('city', ''), + item.get('province', ''), + item.get('region', ''), + item.get('postal_code', ''), + item.get('is_primary', False), + datetime.strptime(item.get('source_date', '2023-01-01'), '%Y-%m-%d').date() if item.get('source_date') else None, + item.get('source_description', ''), + item.get('grant_type', ''), + item.get('description', ''), + float(item.get('amount', 0)), + item.get('recipient', ''), + item.get('commodity_class', ''), + item.get('contract_number', ''), + item.get('comments', '') + )) + print(f"Added transaction: {item.get('company_division', 'Unknown')} - {item.get('recipient', 'Unknown')}") + except Exception as e: + print(f"Error inserting transaction: {e}") + + cursor.close() + conn.close() + + print("Database populated with test data successfully.") + +if __name__ == "__main__": + populate_test_data() \ No newline at end of file diff --git a/docker/ploughshares/templates/view_transaction.html b/docker/ploughshares/templates/view_transaction.html index fe3984e..c3fb8c9 100644 --- a/docker/ploughshares/templates/view_transaction.html +++ b/docker/ploughshares/templates/view_transaction.html @@ -17,6 +17,29 @@
+ +
+ {% if prev_id %} + + Previous + + {% else %} + + {% endif %} + + {% if next_id %} + + Next + + {% else %} + + {% endif %} +
+
+ + +
+ {% if prev_id %} + + Previous + + {% else %} + + {% endif %} + + + All Transactions + + + {% if next_id %} + + Next + + {% else %} + + {% endif %} +
diff --git a/version_history.log b/version_history.log index 02ca1f6..8b07a1a 100644 --- a/version_history.log +++ b/version_history.log @@ -1,3 +1,4 @@ Thu Jul 3 13:33:04 EDT 2025: Version changed from 0.1.0 to 0.1.1 Thu Jul 3 13:40:53 EDT 2025: Version changed from 0.1.1 to 0.1.2 Fri Jul 4 16:04:29 EDT 2025: Version changed from 0.1.2 to 0.2.0 +Fri Jul 4 20:03:15 EDT 2025: Version changed from 0.2.0 to 0.2.1