Add API endpoints and prev/next navigation for transactions
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
colin 2025-07-04 20:09:47 -04:00
parent c85092e0fe
commit a64ac45046
5 changed files with 385 additions and 2 deletions

View File

@ -1 +1 @@
0.2.0
0.2.1

View File

@ -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/<int:id>', 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/<int:id>', 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/<int:id>', 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()

View File

@ -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()

View File

@ -17,6 +17,29 @@
</div>
</div>
<div class="card-body">
<!-- Navigation buttons -->
<div class="d-flex justify-content-between mb-3">
{% if prev_id %}
<a href="{{ url_for('view_transaction', id=prev_id) }}" class="btn btn-outline-primary" aria-label="Previous transaction">
<i class="bi bi-arrow-left"></i> Previous
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled aria-label="No previous transaction">
<i class="bi bi-arrow-left"></i> Previous
</button>
{% endif %}
{% if next_id %}
<a href="{{ url_for('view_transaction', id=next_id) }}" class="btn btn-outline-primary" aria-label="Next transaction">
Next <i class="bi bi-arrow-right"></i>
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled aria-label="No next transaction">
Next <i class="bi bi-arrow-right"></i>
</button>
{% endif %}
</div>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="details-tab" data-bs-toggle="tab" data-bs-target="#details" type="button" role="tab" aria-controls="details" aria-selected="true">Details</button>
@ -89,6 +112,33 @@
</div>
</div>
</div>
<!-- Bottom navigation buttons for easier access -->
<div class="d-flex justify-content-between mt-3">
{% if prev_id %}
<a href="{{ url_for('view_transaction', id=prev_id) }}" class="btn btn-outline-primary" aria-label="Previous transaction">
<i class="bi bi-arrow-left"></i> Previous
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled aria-label="No previous transaction">
<i class="bi bi-arrow-left"></i> Previous
</button>
{% endif %}
<a href="{{ url_for('index') }}" class="btn btn-secondary">
<i class="bi bi-list"></i> All Transactions
</a>
{% if next_id %}
<a href="{{ url_for('view_transaction', id=next_id) }}" class="btn btn-outline-primary" aria-label="Next transaction">
Next <i class="bi bi-arrow-right"></i>
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled aria-label="No next transaction">
Next <i class="bi bi-arrow-right"></i>
</button>
{% endif %}
</div>
</div>
</div>
</div>

View File

@ -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