commit 375774594a48b79f5dddb71919c8bf2779409e57 Author: colin Date: Thu Jul 3 10:37:02 2025 -0400 Initial commit with version management diff --git a/.cursor/rules/docker-organization.mdc b/.cursor/rules/docker-organization.mdc new file mode 100644 index 0000000..0a4b114 --- /dev/null +++ b/.cursor/rules/docker-organization.mdc @@ -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 diff --git a/.cursor/rules/file-management.mdc b/.cursor/rules/file-management.mdc new file mode 100644 index 0000000..bd61a7c --- /dev/null +++ b/.cursor/rules/file-management.mdc @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..296cc5c --- /dev/null +++ b/README.md @@ -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://:5001 (Network access) + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2ab6a3d --- /dev/null +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/docker/ploughshares/Dockerfile b/docker/ploughshares/Dockerfile new file mode 100644 index 0000000..526c0cc --- /dev/null +++ b/docker/ploughshares/Dockerfile @@ -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"] \ No newline at end of file diff --git a/docker/ploughshares/app.py b/docker/ploughshares/app.py new file mode 100644 index 0000000..e7becda --- /dev/null +++ b/docker/ploughshares/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/') +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//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) \ No newline at end of file diff --git a/docker/ploughshares/data.json b/docker/ploughshares/data.json new file mode 100644 index 0000000..c8c367c --- /dev/null +++ b/docker/ploughshares/data.json @@ -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": [] +} \ No newline at end of file diff --git a/docker/ploughshares/generate_test_data.py b/docker/ploughshares/generate_test_data.py new file mode 100755 index 0000000..ab27515 --- /dev/null +++ b/docker/ploughshares/generate_test_data.py @@ -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)) \ No newline at end of file diff --git a/docker/ploughshares/init_db.py b/docker/ploughshares/init_db.py new file mode 100644 index 0000000..67a7666 --- /dev/null +++ b/docker/ploughshares/init_db.py @@ -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() \ No newline at end of file diff --git a/docker/ploughshares/requirements.txt b/docker/ploughshares/requirements.txt new file mode 100644 index 0000000..7a56188 --- /dev/null +++ b/docker/ploughshares/requirements.txt @@ -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 \ No newline at end of file diff --git a/docker/ploughshares/schema.sql b/docker/ploughshares/schema.sql new file mode 100644 index 0000000..2d20d12 --- /dev/null +++ b/docker/ploughshares/schema.sql @@ -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); \ No newline at end of file diff --git a/docker/ploughshares/templates/api_docs.html b/docker/ploughshares/templates/api_docs.html new file mode 100644 index 0000000..c539e88 --- /dev/null +++ b/docker/ploughshares/templates/api_docs.html @@ -0,0 +1,183 @@ +{% extends "base.html" %} + +{% block title %}API Documentation - Project Ploughshares{% endblock %} + +{% block content %} +
+
+

Project Ploughshares API Documentation

+

Base URL: http://{{ server_name }}

+ +
+
+

Endpoints

+
+
+

1. List All Transactions

+
+

GET /api/transactions

+ +
Complete Example:
+
curl -X GET "http://{{ server_name }}/api/transactions"
+ + +
Response:
+
[
+  {
+    "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"
+  }
+]
+
+ +

2. Get Transaction Details

+
+

GET /api/transaction/{id}

+ +
Complete Example:
+
curl -X GET "http://{{ server_name }}/api/transaction/1"
+ + +
Response:
+
{
+  "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"
+    }
+  ]
+}
+
+ +

3. Create New Transaction

+
+

POST /api/transaction

+ +
Complete Example:
+
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"
+  }'
+ + +
Response:
+
{
+  "message": "Transaction created successfully",
+  "transaction_id": 2
+}
+
+ +

4. Update Transaction

+
+

PUT /api/transaction/{id}

+ +
Complete Example:
+
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"
+  }'
+ + +
Response:
+
{
+  "message": "Transaction updated successfully"
+}
+
+ +

5. Delete Transaction

+
+

DELETE /api/transaction/{id}

+ +
Complete Example:
+
curl -X DELETE "http://{{ server_name }}/api/transaction/3"
+ + +
Response:
+
{
+  "message": "Transaction deleted successfully"
+}
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/docker/ploughshares/templates/base.html b/docker/ploughshares/templates/base.html new file mode 100644 index 0000000..e83d943 --- /dev/null +++ b/docker/ploughshares/templates/base.html @@ -0,0 +1,109 @@ + + + + + + {% block title %}Project Ploughshares - Transaction Management System{% endblock %} + + + + + +
+ + + {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} + +
+ {% block content %}{% endblock %} +
+ +
+

© 2023 Project Ploughshares - Transaction Management System v{{ version }}

+
+
+ + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/docker/ploughshares/templates/index.html b/docker/ploughshares/templates/index.html new file mode 100644 index 0000000..6cfaf98 --- /dev/null +++ b/docker/ploughshares/templates/index.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block title %}Transactions - Project Ploughshares{% endblock %} + +{% block content %} +
+
+
+

Transactions

+ + New Transaction + +
+
+
+ + + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + + + {% endfor %} + +
Transaction No.TypeDivisionAmountSource DateRecipientActions
{{ transaction['transaction_no'] }}{{ transaction['transaction_type'] }}{{ transaction['company_division'] }}{{ transaction['amount']|currency }}{{ transaction['source_date'].strftime('%Y-%m-%d') if transaction['source_date'] else 'N/A' }}{{ transaction['recipient'] }} + + + + + + +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/docker/ploughshares/templates/transaction_form.html b/docker/ploughshares/templates/transaction_form.html new file mode 100644 index 0000000..1118768 --- /dev/null +++ b/docker/ploughshares/templates/transaction_form.html @@ -0,0 +1,102 @@ +{% extends "base.html" %} + +{% block title %}{% if transaction %}Edit{% else %}New{% endif %} Transaction - Project Ploughshares{% endblock %} + +{% block content %} +
+
+
+

{% if transaction %}Edit{% else %}New{% endif %} Transaction

+
+
+
+
+
+
+ + +
Please enter a transaction type.
+
+
+ + +
Please enter a company division.
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ $ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Cancel +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/docker/ploughshares/templates/view_transaction.html b/docker/ploughshares/templates/view_transaction.html new file mode 100644 index 0000000..72441ea --- /dev/null +++ b/docker/ploughshares/templates/view_transaction.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} + +{% block title %}Transaction {{ transaction.transaction_no }} - Project Ploughshares{% endblock %} + +{% block content %} +
+
+
+

Transaction #{{ transaction.transaction_no }} - Project Ploughshares

+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Transaction Type:{{ transaction.transaction_type }}
Company/Division:{{ transaction.company_division }}
Address: + {{ transaction.address_1 }}
+ {% if transaction.address_2 %}{{ transaction.address_2 }}
{% endif %} + {{ transaction.city }}, {{ transaction.province }}, {{ transaction.postal_code }}
+ {{ transaction.region }} +
Primary:{{ 'Yes' if transaction.is_primary else 'No' }}
Source Date:{{ transaction.source_date.strftime('%Y-%m-%d') if transaction.source_date else 'N/A' }}
Source Description:{{ transaction.source_description }}
Grant Type:{{ transaction.grant_type }}
Description:{{ transaction.description }}
Amount:{{ transaction.amount|currency }}
Recipient:{{ transaction.recipient }}
Commodity Class:{{ transaction.commodity_class }}
Contract Number:{{ transaction.contract_number }}
Comments:{{ transaction.comments }}
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/versionbump.sh b/versionbump.sh new file mode 100755 index 0000000..b573225 --- /dev/null +++ b/versionbump.sh @@ -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!" \ No newline at end of file