Initial commit with version management
This commit is contained in:
commit
375774594a
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Docker Organization
|
||||
|
||||
- Keep all source code adjacent to its corresponding Dockerfile
|
||||
- Avoid separating application code from its Docker configuration
|
||||
- Place related files in the same directory as the Dockerfile they support
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# File Management
|
||||
|
||||
- Avoid creating new files whenever possible
|
||||
- Modify existing files in place rather than creating copies
|
||||
- Use git for version control instead of creating backup files
|
||||
- Only create new files when absolutely necessary for new functionality
|
|
@ -0,0 +1,106 @@
|
|||
# Project Ploughshares
|
||||
|
||||
Transaction Management System for Project Ploughshares.
|
||||
|
||||
## Version
|
||||
|
||||
The current version is stored in the `VERSION` file. Use the `versionbump.sh` script to update the version number.
|
||||
|
||||
```bash
|
||||
# To bump the patch version (e.g., 1.0.0 -> 1.0.1)
|
||||
./versionbump.sh patch
|
||||
|
||||
# To bump the minor version (e.g., 1.0.0 -> 1.1.0)
|
||||
./versionbump.sh minor
|
||||
|
||||
# To bump the major version (e.g., 1.0.0 -> 2.0.0)
|
||||
./versionbump.sh major
|
||||
```
|
||||
|
||||
## Docker Setup
|
||||
|
||||
The application is containerized using Docker and can be run using docker-compose.
|
||||
|
||||
```bash
|
||||
# Build the containers
|
||||
docker-compose build
|
||||
|
||||
# Start the application
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
The application will be available at http://localhost:5001.
|
||||
|
||||
## Features
|
||||
|
||||
- Transaction management (create, view, edit)
|
||||
- Document uploads and attachments
|
||||
- API endpoints for programmatic access
|
||||
- PostgreSQL database for data storage
|
||||
|
||||
## Running the Application
|
||||
|
||||
### Using Docker (Recommended)
|
||||
|
||||
The application can be run using Docker:
|
||||
|
||||
```bash
|
||||
# Run with PostgreSQL database
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Build the Docker image
|
||||
2. Start PostgreSQL database
|
||||
3. Initialize the database schema
|
||||
4. Start the application on port 5001
|
||||
|
||||
#### Stopping the Application
|
||||
|
||||
```bash
|
||||
# Stop all containers
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Running Locally
|
||||
|
||||
1. Start PostgreSQL:
|
||||
```bash
|
||||
./start_postgres.sh
|
||||
```
|
||||
|
||||
2. Initialize the database:
|
||||
```bash
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
3. Start the application:
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
API documentation is available at:
|
||||
- http://localhost:5001/api-docs
|
||||
- http://localhost:5001/api/docs
|
||||
- http://localhost:5001/docs
|
||||
|
||||
## Testing
|
||||
|
||||
To generate test data:
|
||||
|
||||
```bash
|
||||
python generate_test_data.py
|
||||
```
|
||||
|
||||
## Accessing the Application
|
||||
|
||||
The application runs on all addresses (0.0.0.0) and is accessible via:
|
||||
- http://localhost:5001 (Docker)
|
||||
- http://localhost:5001 (Local)
|
||||
- http://<machine-ip>:5001 (Network access)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@ -0,0 +1,44 @@
|
|||
networks:
|
||||
ploughshares-network:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ./docker/ploughshares
|
||||
ports:
|
||||
- "5001:5001"
|
||||
environment:
|
||||
- FLASK_RUN_PORT=5001
|
||||
- POSTGRES_HOST=db
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=ploughshares
|
||||
- POSTGRES_USER=ploughshares
|
||||
- POSTGRES_PASSWORD=ploughshares_password
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- ploughshares-network
|
||||
|
||||
db:
|
||||
image: postgres:12
|
||||
environment:
|
||||
- POSTGRES_DB=ploughshares
|
||||
- POSTGRES_USER=ploughshares
|
||||
- POSTGRES_PASSWORD=ploughshares_password
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./docker/ploughshares/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ploughshares -d ploughshares"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- ploughshares-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
|
@ -0,0 +1,30 @@
|
|||
FROM python:3.9-bullseye
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY app.py .
|
||||
COPY generate_test_data.py .
|
||||
COPY schema.sql .
|
||||
COPY templates/ ./templates/
|
||||
|
||||
# Create uploads directory
|
||||
RUN mkdir -p uploads
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 5001
|
||||
|
||||
# Command to run the application
|
||||
CMD ["python", "app.py"]
|
|
@ -0,0 +1,233 @@
|
|||
import os
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory, abort
|
||||
from werkzeug.utils import secure_filename
|
||||
from datetime import datetime
|
||||
import generate_test_data
|
||||
import locale
|
||||
|
||||
# Get version from VERSION file
|
||||
with open(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'VERSION'), 'r') as f:
|
||||
VERSION = f.read().strip()
|
||||
|
||||
# Initialize the Flask app
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'supersecretkey'
|
||||
app.config['UPLOAD_FOLDER'] = 'uploads'
|
||||
app.config['VERSION'] = VERSION
|
||||
|
||||
# Set locale for currency formatting
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
||||
except locale.Error:
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '') # Use system default locale
|
||||
except locale.Error:
|
||||
pass # If all fails, we'll use the fallback in the filter
|
||||
|
||||
# Custom filter for currency formatting
|
||||
@app.template_filter('currency')
|
||||
def currency_filter(value):
|
||||
if value is None:
|
||||
return "$0.00"
|
||||
try:
|
||||
return locale.currency(float(value), grouping=True)
|
||||
except (ValueError, TypeError, locale.Error):
|
||||
# Fallback formatting if locale doesn't work
|
||||
return f"${float(value):,.2f}"
|
||||
|
||||
# --- Database Connection ---
|
||||
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')
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
dbname=dbname,
|
||||
user=user,
|
||||
password=password,
|
||||
cursor_factory=RealDictCursor
|
||||
)
|
||||
conn.autocommit = True
|
||||
print(f"Connected to PostgreSQL at {host}:{port}")
|
||||
return conn
|
||||
except psycopg2.OperationalError as e:
|
||||
print(f"Error connecting to PostgreSQL: {e}")
|
||||
return None
|
||||
|
||||
# --- Routes ---
|
||||
@app.route('/')
|
||||
def index():
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT * FROM transactions ORDER BY id DESC')
|
||||
transactions = cur.fetchall()
|
||||
conn.close()
|
||||
return render_template('index.html', transactions=transactions, version=VERSION)
|
||||
|
||||
@app.route('/api-docs')
|
||||
def api_docs():
|
||||
server_name = request.host
|
||||
return render_template('api_docs.html', server_name=server_name, version=VERSION)
|
||||
|
||||
@app.route('/transaction/<int:id>')
|
||||
def view_transaction(id):
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
||||
transaction = cur.fetchone()
|
||||
conn.close()
|
||||
if transaction is None:
|
||||
abort(404)
|
||||
return render_template('view_transaction.html', transaction=transaction, version=VERSION)
|
||||
|
||||
@app.route('/transaction/add', methods=['GET', 'POST'])
|
||||
def create_transaction():
|
||||
if request.method == 'POST':
|
||||
# Get form data and set defaults for missing fields
|
||||
data = request.form.to_dict()
|
||||
default_fields = {
|
||||
'transaction_type': '', 'company_division': '', 'address_1': '', 'address_2': '',
|
||||
'city': '', 'province': '', 'region': '', 'postal_code': '', 'source_date': None,
|
||||
'source_description': '', 'grant_type': '', 'description': '', 'amount': 0,
|
||||
'recipient': '', 'commodity_class': '', 'contract_number': '', 'comments': ''
|
||||
}
|
||||
|
||||
# Fill in missing fields with defaults
|
||||
for field, default in default_fields.items():
|
||||
if field not in data:
|
||||
data[field] = default
|
||||
|
||||
# Handle checkbox fields
|
||||
data['is_primary'] = 'is_primary' in data
|
||||
|
||||
conn = get_db_connection()
|
||||
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
|
||||
)
|
||||
new_transaction_id = cur.fetchone()['id']
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return redirect(url_for('view_transaction', id=new_transaction_id))
|
||||
|
||||
return render_template('transaction_form.html', version=VERSION)
|
||||
|
||||
@app.route('/transaction/<int:id>/edit', methods=['GET', 'POST'])
|
||||
def update_transaction(id):
|
||||
conn = get_db_connection()
|
||||
if request.method == 'POST':
|
||||
# Get form data and set defaults for missing fields
|
||||
data = request.form.to_dict()
|
||||
default_fields = {
|
||||
'transaction_type': '', 'company_division': '', 'address_1': '', 'address_2': '',
|
||||
'city': '', 'province': '', 'region': '', 'postal_code': '', 'source_date': None,
|
||||
'source_description': '', 'grant_type': '', 'description': '', 'amount': 0,
|
||||
'recipient': '', 'commodity_class': '', 'contract_number': '', 'comments': ''
|
||||
}
|
||||
|
||||
# Fill in missing fields with defaults
|
||||
for field, default in default_fields.items():
|
||||
if field not in data:
|
||||
data[field] = default
|
||||
|
||||
# Handle checkbox fields
|
||||
data['is_primary'] = 'is_primary' in data
|
||||
data['id'] = id
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE transactions
|
||||
SET transaction_type = %(transaction_type)s,
|
||||
company_division = %(company_division)s,
|
||||
address_1 = %(address_1)s,
|
||||
address_2 = %(address_2)s,
|
||||
city = %(city)s,
|
||||
province = %(province)s,
|
||||
region = %(region)s,
|
||||
postal_code = %(postal_code)s,
|
||||
is_primary = %(is_primary)s,
|
||||
source_date = %(source_date)s,
|
||||
source_description = %(source_description)s,
|
||||
grant_type = %(grant_type)s,
|
||||
description = %(description)s,
|
||||
amount = %(amount)s,
|
||||
recipient = %(recipient)s,
|
||||
commodity_class = %(commodity_class)s,
|
||||
contract_number = %(contract_number)s,
|
||||
comments = %(comments)s
|
||||
WHERE id = %(id)s
|
||||
""",
|
||||
data
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return redirect(url_for('view_transaction', id=id))
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
||||
transaction = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
if transaction is None:
|
||||
abort(404)
|
||||
return render_template('transaction_form.html', transaction=transaction, version=VERSION)
|
||||
|
||||
def bootstrap_database():
|
||||
"""
|
||||
Checks if the database is empty and populates it with test data if it is.
|
||||
"""
|
||||
print("Checking database for existing data...")
|
||||
conn = get_db_connection()
|
||||
if conn is None:
|
||||
print("Database connection failed. Exiting.")
|
||||
exit(1)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT COUNT(*) FROM transactions")
|
||||
count = cur.fetchone()['count']
|
||||
if count == 0:
|
||||
print("Database is empty. Populating with test data...")
|
||||
test_data = generate_test_data.get_test_transactions()
|
||||
for transaction in test_data:
|
||||
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
|
||||
)
|
||||
""",
|
||||
transaction
|
||||
)
|
||||
conn.commit()
|
||||
print("Successfully inserted 10 test transactions.")
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.environ.get('FLASK_RUN_PORT', 5001))
|
||||
app.run(host='0.0.0.0', port=port)
|
|
@ -0,0 +1,373 @@
|
|||
{
|
||||
"transactions": [
|
||||
{
|
||||
"transaction_id": 1,
|
||||
"transaction_no": "78708",
|
||||
"transaction_type": "Subcontract",
|
||||
"company_division": "C A E Inc",
|
||||
"address_1": "5585 Cote de Liesse",
|
||||
"address_2": "P O Box 1800",
|
||||
"city": "ST LAURENT",
|
||||
"province": "QC",
|
||||
"region": "Quebec",
|
||||
"postal_code": "H4T 1G6",
|
||||
"is_primary": true,
|
||||
"source_date": "2023-08-23",
|
||||
"source_description": "Source Description",
|
||||
"grant_type": "Grant Type",
|
||||
"description": "7000XR Full Flight Simulator (FFS) in Global 6000/6500 configuration (subc)",
|
||||
"amount": 0.0,
|
||||
"recipient": "US Army",
|
||||
"commodity_class": "Aerospace",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Subcontract with Leidos, US, through CAE Defense & Security. In support of the High Accuracy Detection and Exploitation System (HADES) program.",
|
||||
"created_at": "2025-07-02T19:30:43.640251"
|
||||
},
|
||||
{
|
||||
"transaction_id": 2,
|
||||
"transaction_no": "2021-11783",
|
||||
"transaction_type": "Invoice",
|
||||
"company_division": "L3Harris Technologies Communication Systems",
|
||||
"address_1": "100 N Riverside Plaza",
|
||||
"address_2": "",
|
||||
"city": "Chicago",
|
||||
"province": "IL",
|
||||
"region": "Illinois",
|
||||
"postal_code": "60606",
|
||||
"is_primary": false,
|
||||
"source_date": "2025-02-06",
|
||||
"source_description": "Source from Sole Source",
|
||||
"grant_type": "",
|
||||
"description": "Battlefield management software suite",
|
||||
"amount": 98787185.46,
|
||||
"recipient": "US Navy",
|
||||
"commodity_class": "Surveillance",
|
||||
"contract_number": "CONT-9738-C",
|
||||
"comments": "Includes technology transfer and local production",
|
||||
"created_at": "2025-07-02T19:31:13.337006"
|
||||
},
|
||||
{
|
||||
"transaction_id": 3,
|
||||
"transaction_no": "2020-28186",
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "Elbit Systems Land Systems",
|
||||
"address_1": "100 N Riverside Plaza",
|
||||
"address_2": "",
|
||||
"city": "Chicago",
|
||||
"province": "IL",
|
||||
"region": "Illinois",
|
||||
"postal_code": "60606",
|
||||
"is_primary": true,
|
||||
"source_date": "2024-08-04",
|
||||
"source_description": "Source from Direct Award",
|
||||
"grant_type": "Type IV",
|
||||
"description": "Satellite communications terminals",
|
||||
"amount": 40307275.77,
|
||||
"recipient": "Australian Defence Force",
|
||||
"commodity_class": "Logistics",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Urgent operational requirement for deployed forces",
|
||||
"created_at": "2025-07-02T19:31:13.339737"
|
||||
},
|
||||
{
|
||||
"transaction_id": 4,
|
||||
"transaction_no": "2024-46146",
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "Thales Group Defense & Security",
|
||||
"address_1": "100 N Riverside Plaza",
|
||||
"address_2": "",
|
||||
"city": "Chicago",
|
||||
"province": "IL",
|
||||
"region": "Illinois",
|
||||
"postal_code": "60606",
|
||||
"is_primary": true,
|
||||
"source_date": "2024-09-30",
|
||||
"source_description": "Source from Direct Award",
|
||||
"grant_type": "Type II",
|
||||
"description": "Naval vessel propulsion components",
|
||||
"amount": 42033336.08,
|
||||
"recipient": "UK Ministry of Defence",
|
||||
"commodity_class": "Cybersecurity",
|
||||
"contract_number": "CONT-4826-D",
|
||||
"comments": "Replaces aging legacy systems currently in service",
|
||||
"created_at": "2025-07-02T19:31:13.342318"
|
||||
},
|
||||
{
|
||||
"transaction_id": 5,
|
||||
"transaction_no": "2020-45049",
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "Northrop Grumman Mission Systems",
|
||||
"address_1": "1025 W NASA Boulevard",
|
||||
"address_2": "",
|
||||
"city": "Melbourne",
|
||||
"province": "FL",
|
||||
"region": "Florida",
|
||||
"postal_code": "32919",
|
||||
"is_primary": false,
|
||||
"source_date": "2024-11-27",
|
||||
"source_description": "Source from Framework Agreement",
|
||||
"grant_type": "",
|
||||
"description": "Aegis Combat System software updates",
|
||||
"amount": 64502031.29,
|
||||
"recipient": "UK Ministry of Defence",
|
||||
"commodity_class": "Protective Equipment",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Subcontract with Leidos, US, through CAE Defense & Security. In support of the High Accuracy Detection and Exploitation System (HADES) program.",
|
||||
"created_at": "2025-07-02T19:31:13.344968"
|
||||
},
|
||||
{
|
||||
"transaction_id": 6,
|
||||
"transaction_no": "2021-99601",
|
||||
"transaction_type": "Subcontract",
|
||||
"company_division": "MBDA Missile Systems",
|
||||
"address_1": "870 Winter Street",
|
||||
"address_2": "",
|
||||
"city": "Waltham",
|
||||
"province": "MA",
|
||||
"region": "Massachusetts",
|
||||
"postal_code": "02451",
|
||||
"is_primary": false,
|
||||
"source_date": "2022-10-08",
|
||||
"source_description": "Source from Competitive Bid",
|
||||
"grant_type": "Type I",
|
||||
"description": "Long-range precision fires development",
|
||||
"amount": 10778323.11,
|
||||
"recipient": "US Marine Corps",
|
||||
"commodity_class": "Software",
|
||||
"contract_number": "CONT-8144-A",
|
||||
"comments": "Replaces aging legacy systems currently in service",
|
||||
"created_at": "2025-07-02T19:31:13.347491"
|
||||
},
|
||||
{
|
||||
"transaction_id": 7,
|
||||
"transaction_no": "2021-99541",
|
||||
"transaction_type": "Subcontract",
|
||||
"company_division": "Northrop Grumman Mission Systems",
|
||||
"address_1": "1101 Wilson Boulevard",
|
||||
"address_2": "Suite 2000",
|
||||
"city": "Arlington",
|
||||
"province": "VA",
|
||||
"region": "Virginia",
|
||||
"postal_code": "22209",
|
||||
"is_primary": false,
|
||||
"source_date": "2023-02-22",
|
||||
"source_description": "Source from Direct Award",
|
||||
"grant_type": "Type III",
|
||||
"description": "THAAD missile defense interceptors",
|
||||
"amount": 53622255.95,
|
||||
"recipient": "German Armed Forces",
|
||||
"commodity_class": "Logistics",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Joint development program with international partners",
|
||||
"created_at": "2025-07-02T19:31:13.351123"
|
||||
},
|
||||
{
|
||||
"transaction_id": 8,
|
||||
"transaction_no": "2021-42294",
|
||||
"transaction_type": "Subcontract",
|
||||
"company_division": "Thales Group Defense & Security",
|
||||
"address_1": "100 N Riverside Plaza",
|
||||
"address_2": "",
|
||||
"city": "Chicago",
|
||||
"province": "IL",
|
||||
"region": "Illinois",
|
||||
"postal_code": "60606",
|
||||
"is_primary": false,
|
||||
"source_date": "2023-06-13",
|
||||
"source_description": "Source from Sole Source",
|
||||
"grant_type": "Type II",
|
||||
"description": "THAAD missile defense interceptors",
|
||||
"amount": 67639798.11,
|
||||
"recipient": "Australian Defence Force",
|
||||
"commodity_class": "Electronics",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Replaces aging legacy systems currently in service",
|
||||
"created_at": "2025-07-02T19:31:13.353980"
|
||||
},
|
||||
{
|
||||
"transaction_id": 9,
|
||||
"transaction_no": "2023-67502",
|
||||
"transaction_type": "Subcontract",
|
||||
"company_division": "BAE Systems Electronic Systems",
|
||||
"address_1": "5585 Cote de Liesse",
|
||||
"address_2": "P O Box 1800",
|
||||
"city": "ST LAURENT",
|
||||
"province": "QC",
|
||||
"region": "Quebec",
|
||||
"postal_code": "H4T 1G6",
|
||||
"is_primary": false,
|
||||
"source_date": "2023-09-07",
|
||||
"source_description": "Source from Framework Agreement",
|
||||
"grant_type": "Type III",
|
||||
"description": "Command and control software development",
|
||||
"amount": 59006974.62,
|
||||
"recipient": "Australian Defence Force",
|
||||
"commodity_class": "Logistics",
|
||||
"contract_number": "CONT-8954-C",
|
||||
"comments": "Follows successful completion of prototype testing phase",
|
||||
"created_at": "2025-07-02T19:31:13.355840"
|
||||
},
|
||||
{
|
||||
"transaction_id": 10,
|
||||
"transaction_no": "2025-37332",
|
||||
"transaction_type": "Contract",
|
||||
"company_division": "Leonardo S.p.A. Helicopters",
|
||||
"address_1": "Tour Carpe Diem",
|
||||
"address_2": "31 Place des Corolles",
|
||||
"city": "Courbevoie",
|
||||
"province": "",
|
||||
"region": "\u00cele-de-France",
|
||||
"postal_code": "92400",
|
||||
"is_primary": true,
|
||||
"source_date": "2023-09-22",
|
||||
"source_description": "Source from Framework Agreement",
|
||||
"grant_type": "Type I",
|
||||
"description": "F-35 Lightning II Joint Strike Fighter components",
|
||||
"amount": 99293527.27,
|
||||
"recipient": "US Space Force",
|
||||
"commodity_class": "Electronics",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Follows successful completion of prototype testing phase",
|
||||
"created_at": "2025-07-02T19:31:13.358113"
|
||||
},
|
||||
{
|
||||
"transaction_id": 11,
|
||||
"transaction_no": "2025-97753",
|
||||
"transaction_type": "Memorandum of Understanding",
|
||||
"company_division": "Saab AB Aeronautics",
|
||||
"address_1": "870 Winter Street",
|
||||
"address_2": "",
|
||||
"city": "Waltham",
|
||||
"province": "MA",
|
||||
"region": "Massachusetts",
|
||||
"postal_code": "02451",
|
||||
"is_primary": false,
|
||||
"source_date": "2023-12-31",
|
||||
"source_description": "Source from Framework Agreement",
|
||||
"grant_type": "Type II",
|
||||
"description": "F-35 Lightning II Joint Strike Fighter components",
|
||||
"amount": 80186777.36,
|
||||
"recipient": "German Armed Forces",
|
||||
"commodity_class": "Aerospace",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Follows successful completion of prototype testing phase",
|
||||
"created_at": "2025-07-02T19:31:13.360869"
|
||||
},
|
||||
{
|
||||
"transaction_id": 12,
|
||||
"transaction_no": "2020-21874",
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "Thales Group Defense & Security",
|
||||
"address_1": "Ottobrunn",
|
||||
"address_2": "Willy-Messerschmitt-Str. 1",
|
||||
"city": "Munich",
|
||||
"province": "",
|
||||
"region": "Bavaria",
|
||||
"postal_code": "85521",
|
||||
"is_primary": false,
|
||||
"source_date": "2022-10-22",
|
||||
"source_description": "Source from Framework Agreement",
|
||||
"grant_type": "Type III",
|
||||
"description": "THAAD missile defense interceptors",
|
||||
"amount": 56116899.6,
|
||||
"recipient": "French Armed Forces",
|
||||
"commodity_class": "Defense",
|
||||
"contract_number": "CONT-6498-B",
|
||||
"comments": "Joint development program with international partners",
|
||||
"created_at": "2025-07-02T19:31:13.363525"
|
||||
},
|
||||
{
|
||||
"transaction_id": 13,
|
||||
"transaction_no": "2021-77008",
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "CAE Inc Defense & Security",
|
||||
"address_1": "5585 Cote de Liesse",
|
||||
"address_2": "P O Box 1800",
|
||||
"city": "ST LAURENT",
|
||||
"province": "QC",
|
||||
"region": "Quebec",
|
||||
"postal_code": "H4T 1G6",
|
||||
"is_primary": true,
|
||||
"source_date": "2024-04-20",
|
||||
"source_description": "Source from Direct Award",
|
||||
"grant_type": "",
|
||||
"description": "Ballistic missile early warning radar",
|
||||
"amount": 68699920.25,
|
||||
"recipient": "Israeli Defense Forces",
|
||||
"commodity_class": "Naval Systems",
|
||||
"contract_number": "CONT-5837-D",
|
||||
"comments": "Includes technology transfer and local production",
|
||||
"created_at": "2025-07-02T19:31:13.366312"
|
||||
},
|
||||
{
|
||||
"transaction_id": 14,
|
||||
"transaction_no": "2025-18493",
|
||||
"transaction_type": "Grant",
|
||||
"company_division": "BAE Systems Electronic Systems",
|
||||
"address_1": "1025 W NASA Boulevard",
|
||||
"address_2": "",
|
||||
"city": "Melbourne",
|
||||
"province": "FL",
|
||||
"region": "Florida",
|
||||
"postal_code": "32919",
|
||||
"is_primary": true,
|
||||
"source_date": "2023-05-31",
|
||||
"source_description": "Source from RFP",
|
||||
"grant_type": "Type I",
|
||||
"description": "Aegis Combat System software updates",
|
||||
"amount": 37067541.29,
|
||||
"recipient": "NATO",
|
||||
"commodity_class": "Naval Systems",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Part of larger modernization initiative",
|
||||
"created_at": "2025-07-02T19:31:13.370023"
|
||||
},
|
||||
{
|
||||
"transaction_id": 15,
|
||||
"transaction_no": "2022-45237",
|
||||
"transaction_type": "Grant",
|
||||
"company_division": "Northrop Grumman Mission Systems",
|
||||
"address_1": "5585 Cote de Liesse",
|
||||
"address_2": "P O Box 1800",
|
||||
"city": "ST LAURENT",
|
||||
"province": "QC",
|
||||
"region": "Quebec",
|
||||
"postal_code": "H4T 1G6",
|
||||
"is_primary": true,
|
||||
"source_date": "2025-01-02",
|
||||
"source_description": "Source from Direct Award",
|
||||
"grant_type": "Type II",
|
||||
"description": "Counter-UAS detection and defeat systems",
|
||||
"amount": 17804484.99,
|
||||
"recipient": "German Armed Forces",
|
||||
"commodity_class": "Medical",
|
||||
"contract_number": "CONT-3919-A",
|
||||
"comments": "Follows successful completion of prototype testing phase",
|
||||
"created_at": "2025-07-02T19:31:13.373169"
|
||||
},
|
||||
{
|
||||
"transaction_id": 16,
|
||||
"transaction_no": "2022-68024",
|
||||
"transaction_type": "Memorandum of Understanding",
|
||||
"company_division": "Rheinmetall Vehicle Systems",
|
||||
"address_1": "5585 Cote de Liesse",
|
||||
"address_2": "P O Box 1800",
|
||||
"city": "ST LAURENT",
|
||||
"province": "QC",
|
||||
"region": "Quebec",
|
||||
"postal_code": "H4T 1G6",
|
||||
"is_primary": true,
|
||||
"source_date": "2025-04-29",
|
||||
"source_description": "Source from Sole Source",
|
||||
"grant_type": "",
|
||||
"description": "Tactical radio communication systems",
|
||||
"amount": 56484890.19,
|
||||
"recipient": "US Navy",
|
||||
"commodity_class": "Munitions",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Joint development program with international partners",
|
||||
"created_at": "2025-07-02T19:31:13.376183"
|
||||
}
|
||||
],
|
||||
"documents": []
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import json
|
||||
import random
|
||||
from faker import Faker
|
||||
|
||||
fake = Faker()
|
||||
|
||||
# Configuration
|
||||
BASE_URL = "http://localhost:5000"
|
||||
API_URL = f"{BASE_URL}/api/transaction"
|
||||
NUM_TRANSACTIONS = 10
|
||||
|
||||
# Sample data choices
|
||||
TRANSACTION_TYPES = ['Subcontract', 'Purchase Order', 'Invoice', 'Contract']
|
||||
COMPANY_DIVISIONS = ['Defense Systems', 'Aerospace Products', 'Technology Services', 'Corporate']
|
||||
COMMODITY_CLASSES = ['Aerospace', 'Defense', 'Electronics', 'Software', 'Services']
|
||||
|
||||
def generate_transaction():
|
||||
"""Generates a single fake transaction."""
|
||||
return {
|
||||
"transaction_type": random.choice(TRANSACTION_TYPES),
|
||||
"company_division": random.choice(COMPANY_DIVISIONS),
|
||||
"address_1": fake.street_address(),
|
||||
"address_2": fake.secondary_address(),
|
||||
"city": fake.city(),
|
||||
"province": fake.state_abbr(),
|
||||
"region": fake.state(),
|
||||
"postal_code": fake.zipcode(),
|
||||
"is_primary": fake.boolean(),
|
||||
"source_date": fake.date_between(start_date='-2y', end_date='today').isoformat(),
|
||||
"source_description": fake.sentence(nb_words=10),
|
||||
"grant_type": fake.word().capitalize(),
|
||||
"description": fake.paragraph(nb_sentences=3),
|
||||
"amount": round(random.uniform(1000.0, 500000.0), 2),
|
||||
"recipient": fake.company(),
|
||||
"commodity_class": random.choice(COMMODITY_CLASSES),
|
||||
"contract_number": fake.bothify(text='??-####-####'),
|
||||
"comments": fake.sentence()
|
||||
}
|
||||
|
||||
def get_test_transactions(count=10):
|
||||
"""Generates a list of fake transactions."""
|
||||
return [generate_transaction() for _ in range(count)]
|
||||
|
||||
if __name__ == '__main__':
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
for _ in range(NUM_TRANSACTIONS):
|
||||
transaction_data = generate_transaction()
|
||||
try:
|
||||
response = requests.post(API_URL, headers=headers, data=json.dumps(transaction_data))
|
||||
if response.status_code == 201:
|
||||
print(f"Successfully created transaction: {response.json().get('id')}")
|
||||
else:
|
||||
print(f"Failed to create transaction. Status code: {response.status_code}, Response: {response.text}")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"Connection to {API_URL} failed. Make sure the Flask application is running.")
|
||||
break
|
||||
print(json.dumps(get_test_transactions(), indent=4))
|
|
@ -0,0 +1,44 @@
|
|||
import psycopg2
|
||||
import os
|
||||
|
||||
def init_db():
|
||||
# Database connection parameters
|
||||
conn = psycopg2.connect(
|
||||
host="192.168.1.119",
|
||||
port=5433,
|
||||
dbname="testdb",
|
||||
user="testuser",
|
||||
password="testpass"
|
||||
)
|
||||
conn.autocommit = True
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Read schema file
|
||||
with open('schema.sql', 'r') as f:
|
||||
sql_script = f.read()
|
||||
|
||||
# Execute schema
|
||||
cursor.execute(sql_script)
|
||||
|
||||
# Insert sample data
|
||||
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 (
|
||||
'Subcontract', 'C A E Inc', '5585 Cote de Liesse', 'P O Box 1800',
|
||||
'ST LAURENT', 'QC', 'Quebec', 'H4T 1G6', true, '2023-08-23', 'Source Description',
|
||||
'Grant Type', '7000XR Full Flight Simulator (FFS) in Global 6000/6500 configuration (subc)',
|
||||
0.00, 'US Army', 'Aerospace', 'SUMMARY',
|
||||
'Subcontract with Leidos, US, through CAE Defense & Security. In support of the High Accuracy Detection and Exploitation System (HADES) program.'
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print("Database initialized successfully with sample data.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
|
@ -0,0 +1,10 @@
|
|||
Flask==2.2.2
|
||||
psycopg2-binary==2.9.3
|
||||
requests==2.28.1
|
||||
Faker==15.3.3
|
||||
gunicorn==20.1.0
|
||||
Werkzeug==2.3.7
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.3
|
||||
itsdangerous==2.1.2
|
||||
click==8.1.7
|
|
@ -0,0 +1,45 @@
|
|||
-- Drop tables if they exist
|
||||
DROP TABLE IF EXISTS transaction_documents;
|
||||
DROP TABLE IF EXISTS transactions;
|
||||
|
||||
-- Create transactions table
|
||||
CREATE TABLE transactions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
transaction_no SERIAL UNIQUE,
|
||||
transaction_type VARCHAR(50),
|
||||
company_division VARCHAR(100),
|
||||
address_1 VARCHAR(255),
|
||||
address_2 VARCHAR(255),
|
||||
city VARCHAR(100),
|
||||
province VARCHAR(100),
|
||||
region VARCHAR(100),
|
||||
postal_code VARCHAR(20),
|
||||
is_primary BOOLEAN,
|
||||
source_date DATE,
|
||||
source_description TEXT,
|
||||
grant_type VARCHAR(100),
|
||||
description TEXT,
|
||||
amount NUMERIC(15, 2),
|
||||
recipient VARCHAR(255),
|
||||
commodity_class VARCHAR(100),
|
||||
contract_number VARCHAR(100),
|
||||
comments TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create table for document attachments
|
||||
CREATE TABLE transaction_documents (
|
||||
document_id SERIAL PRIMARY KEY,
|
||||
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
document_type VARCHAR(100),
|
||||
description TEXT,
|
||||
note TEXT,
|
||||
upload_date TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX idx_transaction_no ON transactions(transaction_no);
|
||||
CREATE INDEX idx_transaction_date ON transactions(source_date);
|
||||
CREATE INDEX idx_transaction_documents ON transaction_documents(transaction_id);
|
|
@ -0,0 +1,183 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}API Documentation - Project Ploughshares{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Project Ploughshares API Documentation</h2>
|
||||
<p class="lead">Base URL: <code>http://{{ server_name }}</code></p>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3>Endpoints</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h4 class="mt-3">1. List All Transactions</h4>
|
||||
<div class="bg-light p-3 mb-3">
|
||||
<p><strong>GET</strong> <code>/api/transactions</code></p>
|
||||
|
||||
<h5>Complete Example:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code id="getAllTransactions">curl -X GET "http://{{ server_name }}/api/transactions"</code></pre>
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyToClipboard('getAllTransactions')">Copy</button>
|
||||
|
||||
<h5>Response:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code>[
|
||||
{
|
||||
"transaction_id": 1,
|
||||
"transaction_no": "78708",
|
||||
"transaction_type": "Subcontract",
|
||||
"company_division": "C A E Inc",
|
||||
"amount": 0.00,
|
||||
"recipient": "US Army",
|
||||
"created_at": "2023-07-02T12:34:56.789012"
|
||||
},
|
||||
{
|
||||
"transaction_id": 2,
|
||||
"transaction_no": "78709",
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "Example Corp",
|
||||
"amount": 1000.00,
|
||||
"recipient": "Test Recipient",
|
||||
"created_at": "2023-07-03T10:11:12.131415"
|
||||
}
|
||||
]</code></pre>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">2. Get Transaction Details</h4>
|
||||
<div class="bg-light p-3 mb-3">
|
||||
<p><strong>GET</strong> <code>/api/transaction/{id}</code></p>
|
||||
|
||||
<h5>Complete Example:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code id="getTransaction">curl -X GET "http://{{ server_name }}/api/transaction/1"</code></pre>
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyToClipboard('getTransaction')">Copy</button>
|
||||
|
||||
<h5>Response:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code>{
|
||||
"transaction": {
|
||||
"transaction_id": 1,
|
||||
"transaction_no": "78708",
|
||||
"transaction_type": "Subcontract",
|
||||
"company_division": "C A E Inc",
|
||||
"address_1": "5585 Cote de Liesse",
|
||||
"address_2": "P O Box 1800",
|
||||
"city": "ST LAURENT",
|
||||
"province": "QC",
|
||||
"region": "Quebec",
|
||||
"postal_code": "H4T 1G6",
|
||||
"is_primary": true,
|
||||
"source_date": "2023-08-23",
|
||||
"source_description": "Source Description",
|
||||
"description": "7000XR Full Flight Simulator (FFS) in Global 6000/6500 configuration (subc)",
|
||||
"amount": 0.00,
|
||||
"recipient": "US Army",
|
||||
"commodity_class": "Aerospace",
|
||||
"contract_number": "SUMMARY",
|
||||
"comments": "Subcontract with Leidos, US, through CAE Defense & Security...",
|
||||
"created_at": "2023-07-02T12:34:56.789012"
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"document_id": 1,
|
||||
"transaction_id": 1,
|
||||
"filename": "78708_20240501.pdf",
|
||||
"file_path": "1/78708_20240501.pdf",
|
||||
"document_type": "Contract",
|
||||
"description": "Contract document",
|
||||
"note": "Original contract",
|
||||
"upload_date": "2023-07-02T12:34:56.789012"
|
||||
}
|
||||
]
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">3. Create New Transaction</h4>
|
||||
<div class="bg-light p-3 mb-3">
|
||||
<p><strong>POST</strong> <code>/api/transaction</code></p>
|
||||
|
||||
<h5>Complete Example:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code id="createTransaction">curl -X POST "http://{{ server_name }}/api/transaction" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"transaction_no": "12345",
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "Example Corp",
|
||||
"description": "Test transaction",
|
||||
"amount": 1000.00,
|
||||
"recipient": "Test Recipient"
|
||||
}'</code></pre>
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyToClipboard('createTransaction')">Copy</button>
|
||||
|
||||
<h5>Response:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code>{
|
||||
"message": "Transaction created successfully",
|
||||
"transaction_id": 2
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">4. Update Transaction</h4>
|
||||
<div class="bg-light p-3 mb-3">
|
||||
<p><strong>PUT</strong> <code>/api/transaction/{id}</code></p>
|
||||
|
||||
<h5>Complete Example:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code id="updateTransaction">curl -X PUT "http://{{ server_name }}/api/transaction/2" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "Updated Corp",
|
||||
"description": "Updated transaction",
|
||||
"amount": 1500.00,
|
||||
"recipient": "Updated Recipient"
|
||||
}'</code></pre>
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyToClipboard('updateTransaction')">Copy</button>
|
||||
|
||||
<h5>Response:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code>{
|
||||
"message": "Transaction updated successfully"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">5. Delete Transaction</h4>
|
||||
<div class="bg-light p-3 mb-3">
|
||||
<p><strong>DELETE</strong> <code>/api/transaction/{id}</code></p>
|
||||
|
||||
<h5>Complete Example:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code id="deleteTransaction">curl -X DELETE "http://{{ server_name }}/api/transaction/3"</code></pre>
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyToClipboard('deleteTransaction')">Copy</button>
|
||||
|
||||
<h5>Response:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code>{
|
||||
"message": "Transaction deleted successfully"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function copyToClipboard(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
const text = element.textContent;
|
||||
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function() {
|
||||
// Show a temporary success message instead of an alert
|
||||
const button = document.querySelector(`button[onclick="copyToClipboard('${elementId}')"]`);
|
||||
const originalText = button.textContent;
|
||||
button.textContent = "Copied!";
|
||||
button.classList.remove("btn-secondary");
|
||||
button.classList.add("btn-success");
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove("btn-success");
|
||||
button.classList.add("btn-secondary");
|
||||
}, 1500);
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,109 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Project Ploughshares - Transaction Management System{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.header {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.header h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
line-height: 40px;
|
||||
}
|
||||
.footer {
|
||||
padding-top: 19px;
|
||||
color: #777;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.document-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.logo {
|
||||
max-height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.currency-value {
|
||||
font-weight: 600;
|
||||
color: #28a745;
|
||||
}
|
||||
.amount-cell {
|
||||
text-align: right;
|
||||
}
|
||||
td:has(.currency-value) {
|
||||
text-align: right;
|
||||
}
|
||||
.version {
|
||||
font-size: 0.8em;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header clearfix">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('index') }}">
|
||||
<span class="fw-bold text-primary">Project Ploughshares</span> - Transaction Management
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('index') }}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('create_transaction') }}">New Transaction</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('api_docs') }}">API Documentation</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
{% for message in messages %}
|
||||
{{ message }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2023 Project Ploughshares - Transaction Management System <span class="version">v{{ version }}</span></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,83 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Transactions - Project Ploughshares{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>Transactions</h2>
|
||||
<a href="{{ url_for('create_transaction') }}" class="btn btn-success">
|
||||
<i class="bi bi-plus-lg"></i> New Transaction
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Transaction No.</th>
|
||||
<th>Type</th>
|
||||
<th>Division</th>
|
||||
<th class="amount-cell">Amount</th>
|
||||
<th>Source Date</th>
|
||||
<th>Recipient</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transaction in transactions %}
|
||||
<tr>
|
||||
<td>{{ transaction['transaction_no'] }}</td>
|
||||
<td>{{ transaction['transaction_type'] }}</td>
|
||||
<td>{{ transaction['company_division'] }}</td>
|
||||
<td class="amount-cell"><span class="currency-value">{{ transaction['amount']|currency }}</span></td>
|
||||
<td>{{ transaction['source_date'].strftime('%Y-%m-%d') if transaction['source_date'] else 'N/A' }}</td>
|
||||
<td>{{ transaction['recipient'] }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('view_transaction', id=transaction['id']) }}" class="btn btn-sm btn-info">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{{ url_for('update_transaction', id=transaction['id']) }}" class="btn btn-sm btn-warning">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchButton = document.getElementById('searchButton');
|
||||
const tableRows = document.querySelectorAll('tbody tr');
|
||||
|
||||
function filterTable() {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
|
||||
tableRows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
if (text.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
searchButton.addEventListener('click', filterTable);
|
||||
searchInput.addEventListener('keyup', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
filterTable();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,102 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if transaction %}Edit{% else %}New{% endif %} Transaction - Project Ploughshares{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>{% if transaction %}Edit{% else %}New{% endif %} Transaction</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('create_transaction') if not transaction else url_for('update_transaction', id=transaction.id) }}" method="post" class="needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="transaction_type">Transaction Type</label>
|
||||
<input type="text" class="form-control" id="transaction_type" name="transaction_type" value="{{ transaction.transaction_type if transaction else '' }}" required>
|
||||
<div class="invalid-feedback">Please enter a transaction type.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="company_division">Company Division</label>
|
||||
<input type="text" class="form-control" id="company_division" name="company_division" value="{{ transaction.company_division if transaction else '' }}" required>
|
||||
<div class="invalid-feedback">Please enter a company division.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="address_1">Address 1</label>
|
||||
<input type="text" class="form-control" id="address_1" name="address_1" value="{{ transaction.address_1 if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="address_2">Address 2</label>
|
||||
<input type="text" class="form-control" id="address_2" name="address_2" value="{{ transaction.address_2 if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="city">City</label>
|
||||
<input type="text" class="form-control" id="city" name="city" value="{{ transaction.city if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="province">Province</label>
|
||||
<input type="text" class="form-control" id="province" name="province" value="{{ transaction.province if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="region">Region</label>
|
||||
<input type="text" class="form-control" id="region" name="region" value="{{ transaction.region if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="postal_code">Postal Code</label>
|
||||
<input type="text" class="form-control" id="postal_code" name="postal_code" value="{{ transaction.postal_code if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="is_primary" name="is_primary" {% if transaction and transaction.is_primary %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_primary">Is Primary</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="source_date">Source Date</label>
|
||||
<input type="date" class="form-control" id="source_date" name="source_date" value="{{ transaction.source_date.strftime('%Y-%m-%d') if transaction and transaction.source_date else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="source_description">Source Description</label>
|
||||
<textarea class="form-control" id="source_description" name="source_description" rows="3">{{ transaction.source_description if transaction else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="grant_type">Grant Type</label>
|
||||
<input type="text" class="form-control" id="grant_type" name="grant_type" value="{{ transaction.grant_type if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3">{{ transaction.description if transaction else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" step="0.01" min="0" class="form-control" id="amount" name="amount" value="{{ transaction.amount if transaction else '' }}" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="recipient">Recipient</label>
|
||||
<input type="text" class="form-control" id="recipient" name="recipient" value="{{ transaction.recipient if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="commodity_class">Commodity Class</label>
|
||||
<input type="text" class="form-control" id="commodity_class" name="commodity_class" value="{{ transaction.commodity_class if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="contract_number">Contract Number</label>
|
||||
<input type="text" class="form-control" id="contract_number" name="contract_number" value="{{ transaction.contract_number if transaction else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comments">Comments</label>
|
||||
<textarea class="form-control" id="comments" name="comments" rows="3">{{ transaction.comments if transaction else '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-3">Submit</button>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary mt-3">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,95 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Transaction {{ transaction.transaction_no }} - Project Ploughshares{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>Transaction #{{ transaction.transaction_no }} - Project Ploughshares</h2>
|
||||
<div>
|
||||
<a href="{{ url_for('update_transaction', id=transaction.id) }}" class="btn btn-warning">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="details" role="tabpanel" aria-labelledby="details-tab">
|
||||
<div class="p-3">
|
||||
<table class="table table-bordered">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style="width: 20%;">Transaction Type:</th>
|
||||
<td>{{ transaction.transaction_type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Company/Division:</th>
|
||||
<td>{{ transaction.company_division }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Address:</th>
|
||||
<td>
|
||||
{{ transaction.address_1 }}<br>
|
||||
{% if transaction.address_2 %}{{ transaction.address_2 }}<br>{% endif %}
|
||||
{{ transaction.city }}, {{ transaction.province }}, {{ transaction.postal_code }}<br>
|
||||
{{ transaction.region }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Primary:</th>
|
||||
<td>{{ 'Yes' if transaction.is_primary else 'No' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Source Date:</th>
|
||||
<td>{{ transaction.source_date.strftime('%Y-%m-%d') if transaction.source_date else 'N/A' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Source Description:</th>
|
||||
<td>{{ transaction.source_description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Grant Type:</th>
|
||||
<td>{{ transaction.grant_type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description:</th>
|
||||
<td>{{ transaction.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Amount:</th>
|
||||
<td class="amount-cell"><span class="currency-value">{{ transaction.amount|currency }}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Recipient:</th>
|
||||
<td>{{ transaction.recipient }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Commodity Class:</th>
|
||||
<td>{{ transaction.commodity_class }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Contract Number:</th>
|
||||
<td>{{ transaction.contract_number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Comments:</th>
|
||||
<td>{{ transaction.comments }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
|
||||
# versionbump.sh - Script to bump version numbers in the VERSION file
|
||||
# Usage: ./versionbump.sh [major|minor|patch]
|
||||
|
||||
set -e
|
||||
|
||||
VERSION_FILE="VERSION"
|
||||
|
||||
# Check if VERSION file exists
|
||||
if [ ! -f "$VERSION_FILE" ]; then
|
||||
echo "Error: $VERSION_FILE does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read current version
|
||||
CURRENT_VERSION=$(cat "$VERSION_FILE")
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
# Split version into components
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
|
||||
|
||||
# Check which part to bump
|
||||
case "$1" in
|
||||
major)
|
||||
MAJOR=$((MAJOR + 1))
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
;;
|
||||
minor)
|
||||
MINOR=$((MINOR + 1))
|
||||
PATCH=0
|
||||
;;
|
||||
patch)
|
||||
PATCH=$((PATCH + 1))
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [major|minor|patch]"
|
||||
echo "Example: $0 minor"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create new version string
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||
echo "New version: $NEW_VERSION"
|
||||
|
||||
# Update VERSION file
|
||||
echo "$NEW_VERSION" > "$VERSION_FILE"
|
||||
echo "Updated $VERSION_FILE to $NEW_VERSION"
|
||||
|
||||
# Optional: Update other files that might need version updates
|
||||
# Add commands here to update version in other files if needed
|
||||
|
||||
echo "Version bump complete!"
|
Loading…
Reference in New Issue