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)
|
||||
|
||||
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()
|
||||
|
|
|
@ -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 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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue