Add API endpoints and prev/next navigation for transactions
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
c85092e0fe
commit
a64ac45046
|
@ -271,10 +271,26 @@ def view_transaction(id):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
transaction = None
|
transaction = None
|
||||||
|
prev_id = None
|
||||||
|
next_id = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
||||||
transaction = cur.fetchone()
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Database error: {e}")
|
logger.error(f"Database error: {e}")
|
||||||
flash(f"Database error: {e}", "error")
|
flash(f"Database error: {e}", "error")
|
||||||
|
@ -284,7 +300,7 @@ def view_transaction(id):
|
||||||
if transaction is None:
|
if transaction is None:
|
||||||
abort(404)
|
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'])
|
@app.route('/transaction/add', methods=['GET', 'POST'])
|
||||||
def create_transaction():
|
def create_transaction():
|
||||||
|
@ -501,6 +517,234 @@ def bootstrap_database():
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
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__':
|
if __name__ == '__main__':
|
||||||
logger.info(f"Starting Ploughshares v{VERSION}")
|
logger.info(f"Starting Ploughshares v{VERSION}")
|
||||||
bootstrap_database()
|
bootstrap_database()
|
||||||
|
|
|
@ -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()
|
|
@ -17,6 +17,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<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">
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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: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
|
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 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
|
||||||
|
|
Loading…
Reference in New Issue