diff --git a/VERSION b/VERSION index 0c62199..0d91a54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 +0.3.0 diff --git a/docker/ploughshares/app.py b/docker/ploughshares/app.py index 92c9140..bf2a37e 100644 --- a/docker/ploughshares/app.py +++ b/docker/ploughshares/app.py @@ -302,6 +302,136 @@ def view_transaction(id): return render_template('view_transaction.html', transaction=transaction, prev_id=prev_id, next_id=next_id, version=VERSION) +@app.route('/pending-approval') +def pending_approval(): + """ + Display transactions pending approval + """ + conn = get_db_connection() + if conn is None: + flash("Database connection error", "error") + return render_template('pending_approval.html', transactions=[], version=VERSION) + + try: + with conn.cursor() as cur: + cur.execute('SELECT * FROM transactions WHERE approved = FALSE ORDER BY id DESC') + transactions = cur.fetchall() + except Exception as e: + logger.error(f"Database error: {e}") + flash(f"Database error: {e}", "error") + transactions = [] + finally: + conn.close() + + return render_template('pending_approval.html', transactions=transactions, version=VERSION) + +@app.route('/transaction//approve', methods=['POST']) +def approve_transaction(id): + """ + Approve a transaction + """ + conn = get_db_connection() + if conn is None: + flash("Database connection error", "error") + return redirect(url_for('pending_approval')) + + try: + with conn.cursor() as cur: + # Get the approver name from the form or use default + approver = request.form.get('approver', 'System') + + # Update the transaction + cur.execute( + """ + UPDATE transactions + SET approved = TRUE, + approved_at = CURRENT_TIMESTAMP, + approved_by = %s + WHERE id = %s + """, + (approver, id) + ) + conn.commit() + flash(f"Transaction #{id} approved successfully", "success") + except Exception as e: + logger.error(f"Error approving transaction: {e}") + flash(f"Error approving transaction: {e}", "error") + finally: + conn.close() + + return redirect(url_for('pending_approval')) + +@app.route('/api/transactions/pending', methods=['GET']) +def api_get_pending_transactions(): + """API endpoint to get all pending (unapproved) 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 WHERE approved = FALSE 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//approve', methods=['POST']) +def api_approve_transaction(id): + """API endpoint to approve a transaction""" + conn = get_db_connection() + 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') + + 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 + + # Update the transaction + cur.execute( + """ + UPDATE transactions + SET approved = TRUE, + approved_at = CURRENT_TIMESTAMP, + approved_by = %s + WHERE id = %s + """, + (approver, id) + ) + conn.commit() + return jsonify({"message": "Transaction approved successfully"}), 200 + except Exception as e: + logger.error(f"Error approving transaction via API: {e}") + return jsonify({"error": f"Error approving transaction: {str(e)}"}), 500 + finally: + conn.close() + @app.route('/transaction/add', methods=['GET', 'POST']) def create_transaction(): if request.method == 'POST': @@ -343,12 +473,13 @@ def create_transaction(): 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 + grant_type, description, amount, recipient, commodity_class, contract_number, comments, + approved ) 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 + %(commodity_class)s, %(contract_number)s, %(comments)s, FALSE ) RETURNING id """, data @@ -367,6 +498,7 @@ def create_transaction(): finally: conn.close() + flash("Transaction created successfully and is pending approval", "success") return redirect(url_for('view_transaction', id=new_transaction_id)) return render_template('transaction_form.html', version=VERSION) @@ -641,12 +773,13 @@ def api_create_transaction(): 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 + grant_type, description, amount, recipient, commodity_class, contract_number, comments, + approved ) 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 + %(commodity_class)s, %(contract_number)s, %(comments)s, FALSE ) RETURNING id """, data @@ -655,7 +788,7 @@ def api_create_transaction(): 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 + return jsonify({"message": "Transaction created successfully and is pending approval", "transaction_id": new_transaction_id}), 201 else: return jsonify({"error": "Could not create transaction"}), 500 except Exception as e: diff --git a/docker/ploughshares/migrate_approval.py b/docker/ploughshares/migrate_approval.py new file mode 100644 index 0000000..205fd80 --- /dev/null +++ b/docker/ploughshares/migrate_approval.py @@ -0,0 +1,72 @@ +import os +import psycopg2 +import logging + +# 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') + 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 migrate_database(): + """ + Adds approval fields to the transactions table if they don't exist + """ + conn = get_db_connection() + cursor = conn.cursor() + + try: + # Check if the approved column already exists + cursor.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'transactions' AND column_name = 'approved' + """) + + if cursor.fetchone() is None: + logger.info("Adding approval fields to transactions table...") + + # Add the new columns + cursor.execute(""" + ALTER TABLE transactions + ADD COLUMN IF NOT EXISTS approved BOOLEAN DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMP, + ADD COLUMN IF NOT EXISTS approved_by VARCHAR(255) + """) + + # Create an index on the approved column + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_transactions_approved + ON transactions(approved) + """) + + logger.info("Successfully added approval fields to transactions table") + else: + logger.info("Approval fields already exist in transactions table") + + except Exception as e: + logger.error(f"Error migrating database: {e}") + finally: + cursor.close() + conn.close() + +if __name__ == "__main__": + migrate_database() \ No newline at end of file diff --git a/docker/ploughshares/schema.sql b/docker/ploughshares/schema.sql index c8bdd1d..f329e3c 100644 --- a/docker/ploughshares/schema.sql +++ b/docker/ploughshares/schema.sql @@ -23,6 +23,9 @@ CREATE TABLE IF NOT EXISTS transactions ( commodity_class VARCHAR(255), contract_number VARCHAR(255), comments TEXT, + approved BOOLEAN DEFAULT FALSE, + approved_at TIMESTAMP, + approved_by VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -42,4 +45,5 @@ CREATE TABLE transaction_documents ( CREATE INDEX IF NOT EXISTS idx_transactions_type ON transactions(transaction_type); CREATE INDEX IF NOT EXISTS idx_transactions_division ON transactions(company_division); CREATE INDEX IF NOT EXISTS idx_transactions_recipient ON transactions(recipient); -CREATE INDEX IF NOT EXISTS idx_transaction_date ON transactions(source_date); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_transaction_date ON transactions(source_date); +CREATE INDEX IF NOT EXISTS idx_transactions_approved ON transactions(approved); \ No newline at end of file diff --git a/docker/ploughshares/templates/api_docs.html b/docker/ploughshares/templates/api_docs.html index fec1f5d..4637342 100644 --- a/docker/ploughshares/templates/api_docs.html +++ b/docker/ploughshares/templates/api_docs.html @@ -147,6 +147,55 @@
Response:
{
   "message": "Transaction deleted successfully"
+}
+ + +

6. Get Pending Transactions

+
+

GET /api/transactions/pending

+ +
Complete Example:
+
curl -X GET "http://{{ server_name }}/api/transactions/pending"
+ + +
Response:
+
[
+  {
+    "transaction_id": 1,
+    "transaction_type": "Subcontract",
+    "company_division": "C A E Inc",
+    "amount": 0.00,
+    "recipient": "US Army",
+    "created_at": "2023-07-02T12:34:56.789012",
+    "approved": false
+  },
+  {
+    "transaction_id": 2,
+    "transaction_type": "Purchase Order",
+    "company_division": "Example Corp",
+    "amount": 1000.00,
+    "recipient": "Test Recipient",
+    "created_at": "2023-07-03T10:11:12.131415",
+    "approved": false
+  }
+]
+
+ +

7. Approve Transaction

+
+

POST /api/transaction/{id}/approve

+ +
Complete Example:
+
curl -X POST "http://{{ server_name }}/api/transaction/2/approve" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "approver": "John Doe"
+  }'
+ + +
Response:
+
{
+  "message": "Transaction approved successfully"
 }
diff --git a/docker/ploughshares/templates/base.html b/docker/ploughshares/templates/base.html index de515df..f3a2405 100644 --- a/docker/ploughshares/templates/base.html +++ b/docker/ploughshares/templates/base.html @@ -50,6 +50,12 @@ New Transaction +