diff --git a/VERSION b/VERSION index 9e11b32..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.1 +0.4.0 diff --git a/docker-compose.yml b/docker-compose.yml index 3b0a1d8..d4009ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: - POSTGRES_DB=ploughshares - POSTGRES_USER=ploughshares - POSTGRES_PASSWORD=ploughshares_password - - APP_VERSION=0.2.0 + - APP_VERSION=0.4.0 - APP_ENV=development depends_on: db: diff --git a/docker/ploughshares/Dockerfile b/docker/ploughshares/Dockerfile index 78cc2b6..bd33349 100644 --- a/docker/ploughshares/Dockerfile +++ b/docker/ploughshares/Dockerfile @@ -17,6 +17,7 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY app.py . COPY schema.sql . +COPY migrate_approval.py . COPY templates/ ./templates/ COPY static/ ./static/ # Tests directory is empty or doesn't contain required files diff --git a/docker/ploughshares/__pycache__/app.cpython-39.pyc b/docker/ploughshares/__pycache__/app.cpython-39.pyc new file mode 100644 index 0000000..89fa38d Binary files /dev/null and b/docker/ploughshares/__pycache__/app.cpython-39.pyc differ diff --git a/docker/ploughshares/__pycache__/migrate_approval.cpython-39.pyc b/docker/ploughshares/__pycache__/migrate_approval.cpython-39.pyc new file mode 100644 index 0000000..d06f8d4 Binary files /dev/null and b/docker/ploughshares/__pycache__/migrate_approval.cpython-39.pyc differ diff --git a/docker/ploughshares/app.py b/docker/ploughshares/app.py index bf2a37e..7c13237 100644 --- a/docker/ploughshares/app.py +++ b/docker/ploughshares/app.py @@ -8,6 +8,7 @@ import locale import logging from flask_talisman import Talisman import re +from migrate_approval import migrate_database # Configure logging logging.basicConfig( @@ -263,8 +264,36 @@ def api_docs(): server_name = request.host return render_template('api_docs.html', server_name=server_name, version=VERSION) +def get_transaction_documents(transaction_id): + """ + Get all documents for a transaction + """ + conn = get_db_connection() + if conn is None: + return [] + + documents = [] + try: + with conn.cursor() as cur: + cur.execute(''' + SELECT document_id, filename, file_path, document_type, description, note, upload_date + FROM transaction_documents + WHERE transaction_id = %s + ORDER BY upload_date DESC + ''', (transaction_id,)) + documents = cur.fetchall() + except Exception as e: + logger.error(f"Error fetching documents: {e}") + finally: + conn.close() + + return documents + @app.route('/transaction/') def view_transaction(id): + """ + View a transaction + """ conn = get_db_connection() if conn is None: flash("Database connection error", "error") @@ -279,28 +308,61 @@ def view_transaction(id): 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'] + if transaction is None: + abort(404) - # 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'] + # 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 except Exception as e: logger.error(f"Database error: {e}") - flash(f"Database error: {e}", "error") + abort(404) finally: conn.close() + # Get documents for this transaction + documents = get_transaction_documents(id) + if transaction is None: abort(404) - return render_template('view_transaction.html', transaction=transaction, prev_id=prev_id, next_id=next_id, version=VERSION) + return render_template('view_transaction.html', transaction=transaction, documents=documents, prev_id=prev_id, next_id=next_id, version=VERSION) + +@app.route('/document/') +def view_document(document_id): + """ + View a document + """ + conn = get_db_connection() + if conn is None: + flash("Database connection error", "error") + abort(404) + + document = None + try: + with conn.cursor() as cur: + cur.execute('SELECT * FROM transaction_documents WHERE document_id = %s', (document_id,)) + document = cur.fetchone() + + if document is None: + abort(404) + except Exception as e: + logger.error(f"Database error: {e}") + abort(404) + finally: + conn.close() + + if document is None: + abort(404) + + # Serve the file from the uploads directory + file_path = os.path.join(app.config['UPLOAD_FOLDER'], document['file_path']) + return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=False) @app.route('/pending-approval') def pending_approval(): @@ -312,10 +374,19 @@ def pending_approval(): flash("Database connection error", "error") return render_template('pending_approval.html', transactions=[], version=VERSION) + transactions = [] try: with conn.cursor() as cur: cur.execute('SELECT * FROM transactions WHERE approved = FALSE ORDER BY id DESC') transactions = cur.fetchall() + + # For each transaction, count the number of documents + for transaction in transactions: + cur.execute('SELECT COUNT(*) as doc_count FROM transaction_documents WHERE transaction_id = %s', + (transaction['id'],)) + doc_count_result = cur.fetchone() + transaction['document_count'] = doc_count_result['doc_count'] if doc_count_result else 0 + except Exception as e: logger.error(f"Database error: {e}") flash(f"Database error: {e}", "error") @@ -337,8 +408,8 @@ def approve_transaction(id): try: with conn.cursor() as cur: - # Get the approver name from the form or use default - approver = request.form.get('approver', 'System') + # Use a default approver name instead of requiring user input + approver = "System Administrator" # Update the transaction cur.execute( @@ -402,9 +473,8 @@ def api_approve_transaction(id): if conn is None: return jsonify({"error": "Database connection error"}), 500 - # Get the approver from the request data or use default - data = request.get_json() if request.is_json else {} - approver = data.get('approver', 'API') + # Use the same default approver name for consistency + approver = "System Administrator" try: with conn.cursor() as cur: @@ -644,6 +714,11 @@ def bootstrap_database(): logger.error("Could not get count from database") except Exception as count_error: logger.error(f"Error counting transactions: {count_error}") + + # Run database migrations to ensure all columns exist + logger.info(f"Ploughshares v{VERSION} - Running database migrations...") + migrate_database() + logger.info(f"Ploughshares v{VERSION} - Database migrations completed.") except Exception as e: logger.error(f"Error checking database: {e}") finally: diff --git a/docker/ploughshares/populate_test_data.py b/docker/ploughshares/populate_test_data.py index aa8d95c..a4a710f 100644 --- a/docker/ploughshares/populate_test_data.py +++ b/docker/ploughshares/populate_test_data.py @@ -1,8 +1,16 @@ import os import psycopg2 import json +import logging from datetime import datetime +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('ploughshares') + def get_db_connection(): host = os.environ.get('POSTGRES_HOST', 'db') port = os.environ.get('POSTGRES_PORT', '5432') @@ -31,15 +39,15 @@ def populate_test_data(): # Extract the transactions array if 'transactions' not in data_json: - print("Error: 'transactions' key not found in data.json") + logger.error("'transactions' key not found in data.json") return data = data_json['transactions'] except FileNotFoundError: - print("Error: data.json file not found") + logger.error("data.json file not found") return except json.JSONDecodeError: - print("Error: data.json is not valid JSON") + logger.error("data.json is not valid JSON") return # Insert sample transactions @@ -75,14 +83,14 @@ def populate_test_data(): item.get('contract_number', ''), item.get('comments', '') )) - print(f"Added transaction: {item.get('company_division', 'Unknown')} - {item.get('recipient', 'Unknown')}") + logger.info(f"Added transaction: {item.get('company_division', 'Unknown')} - {item.get('recipient', 'Unknown')}") except Exception as e: - print(f"Error inserting transaction: {e}") + logger.error(f"Error inserting transaction: {e}") cursor.close() conn.close() - print("Database populated with test data successfully.") + logger.info("Database populated with test data successfully.") if __name__ == "__main__": populate_test_data() \ No newline at end of file diff --git a/docker/ploughshares/run_migration.py b/docker/ploughshares/run_migration.py new file mode 100755 index 0000000..0297e4a --- /dev/null +++ b/docker/ploughshares/run_migration.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" +Run this script to manually apply database migrations to add the 'approved' column +to the transactions table if it doesn't exist. +""" + +import logging +from migrate_approval import migrate_database + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('ploughshares') + +if __name__ == "__main__": + logger.info("Starting manual database migration...") + migrate_database() + logger.info("Database migration completed.") \ No newline at end of file diff --git a/docker/ploughshares/static/css/hotkeys.css b/docker/ploughshares/static/css/hotkeys.css index 0214cf9..0dce9e3 100644 --- a/docker/ploughshares/static/css/hotkeys.css +++ b/docker/ploughshares/static/css/hotkeys.css @@ -16,20 +16,6 @@ kbd { box-shadow: inset 0 -0.1rem 0 rgba(0, 0, 0, 0.25); } -/* Hotkey reference button */ -.hotkey-help-btn { - position: fixed; - bottom: 20px; - right: 20px; - z-index: 1030; - opacity: 0.7; - transition: opacity 0.3s; -} - -.hotkey-help-btn:hover { - opacity: 1; -} - /* Tooltip for hotkeys */ [data-hotkey]::after { content: attr(data-hotkey); @@ -64,11 +50,6 @@ kbd { /* Responsive adjustments */ @media (max-width: 768px) { - .hotkey-help-btn { - bottom: 10px; - right: 10px; - } - [data-hotkey]::after { display: none; } diff --git a/docker/ploughshares/static/js/hotkeys.js b/docker/ploughshares/static/js/hotkeys.js index 98f3014..7cd0238 100644 --- a/docker/ploughshares/static/js/hotkeys.js +++ b/docker/ploughshares/static/js/hotkeys.js @@ -348,27 +348,6 @@ function addHotkeyTooltips() { // Get the current page based on URL const currentPath = window.location.pathname; - // Add tooltips for global elements - const helpLink = document.createElement('div'); - helpLink.className = 'position-fixed bottom-0 end-0 p-3'; - helpLink.innerHTML = ` - - `; - document.body.appendChild(helpLink); - - // Add click event listener directly - document.addEventListener('DOMContentLoaded', function() { - const hotkeyHelpButton = document.getElementById('hotkeyHelpButton'); - if (hotkeyHelpButton) { - hotkeyHelpButton.addEventListener('click', function() { - showHotkeyReference(); - }); - } - }); - // Page-specific tooltips if (currentPath === '/' || currentPath === '/index.html') { // Transaction list page diff --git a/docker/ploughshares/templates/api_docs.html b/docker/ploughshares/templates/api_docs.html index 390bd76..d748a0a 100644 --- a/docker/ploughshares/templates/api_docs.html +++ b/docker/ploughshares/templates/api_docs.html @@ -195,15 +195,11 @@

Important: This endpoint provides human-in-the-loop verification. All transactions (especially those created via API) require explicit approval before being considered valid. - The approver name is recorded for audit purposes. + The system automatically records the approval with a standard identifier.

Complete Example:
-
curl -X POST "http://{{ server_name }}/api/transaction/2/approve" \
-  -H "Content-Type: application/json" \
-  -d '{
-    "approver": "John Doe"
-  }'
+
curl -X POST "http://{{ server_name }}/api/transaction/2/approve"
Response:
diff --git a/docker/ploughshares/templates/pending_approval.html b/docker/ploughshares/templates/pending_approval.html index 3579d76..4e42114 100644 --- a/docker/ploughshares/templates/pending_approval.html +++ b/docker/ploughshares/templates/pending_approval.html @@ -29,6 +29,7 @@ Source Date Recipient Created At + Documents Actions @@ -42,6 +43,13 @@ {{ transaction['source_date'].strftime('%Y-%m-%d') if transaction['source_date'] else 'N/A' }} {{ transaction['recipient'] }} {{ transaction['created_at'].strftime('%Y-%m-%d %H:%M') if transaction['created_at'] else 'N/A' }} + + {% if transaction['document_count'] > 0 %} + {{ transaction['document_count'] }} + {% else %} + 0 + {% endif %} +
@@ -57,7 +65,7 @@